Skip to content

Commit 7b40e7d

Browse files
authored
[federation] add support for Fed 2.1 (ExpediaGroup#1591)
By default, Supergraph schema excludes all custom directives. Federation 2.1 adds new `@composeDirective` that can be used to instruct composition logic to preserve custom directives in the Supergraph schema. ```graphql directive @composeDirective(name: String!) repeatable on SCHEMA ```
1 parent 533dc03 commit 7b40e7d

File tree

14 files changed

+232
-10
lines changed

14 files changed

+232
-10
lines changed

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.genera
2020
import com.apollographql.federation.graphqljava.printer.ServiceSDLPrinter.generateServiceSDLV2
2121
import com.expediagroup.graphql.generator.annotations.GraphQLName
2222
import com.expediagroup.graphql.generator.directives.DirectiveMetaInformation
23+
import com.expediagroup.graphql.generator.federation.directives.COMPOSE_DIRECTIVE_TYPE
2324
import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIVE_TYPE
2425
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE
2526
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL
@@ -88,6 +89,7 @@ open class FederatedSchemaGeneratorHooks(
8889
REQUIRES_DIRECTIVE_TYPE
8990
)
9091
private val federatedDirectiveV2List: List<GraphQLDirective> = listOf(
92+
COMPOSE_DIRECTIVE_TYPE,
9193
EXTENDS_DIRECTIVE_TYPE,
9294
EXTERNAL_DIRECTIVE_TYPE,
9395
INACCESSIBLE_DIRECTIVE_TYPE,
@@ -127,6 +129,8 @@ open class FederatedSchemaGeneratorHooks(
127129
private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation) =
128130
if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) {
129131
KEY_DIRECTIVE_TYPE_V2
132+
} else if (LINK_DIRECTIVE_NAME == directiveInfo.effectiveName) {
133+
LINK_DIRECTIVE_TYPE
130134
} else {
131135
super.willGenerateDirective(directiveInfo)
132136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.expediagroup.graphql.generator.federation.directives
2+
3+
import com.expediagroup.graphql.generator.annotations.GraphQLDirective
4+
import graphql.Scalars
5+
import graphql.introspection.Introspection
6+
import graphql.schema.GraphQLArgument
7+
import graphql.schema.GraphQLNonNull
8+
9+
/**
10+
* ```graphql
11+
* directive @composeDirective(name: String!) repeatable on SCHEMA
12+
* ```
13+
*
14+
* By default, Supergraph schema excludes all custom directives. The `@composeDirective` is used to specify custom directives that should be exposed in the Supergraph schema.
15+
*
16+
* Example:
17+
* Given `@custom` directive we can preserve it in the Supergraph schema
18+
*
19+
* ```kotlin
20+
* @GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
21+
* annotation class CustomDirective
22+
*
23+
* @ComposeDirective(name = "custom")
24+
* class CustomSchema
25+
*
26+
* class SimpleQuery {
27+
* @CustomDirective
28+
* fun helloWorld(): String = "Hello World"
29+
* }
30+
* ```
31+
*
32+
* it will generate following schema
33+
*
34+
* ```graphql
35+
* schema @composeDirective(name: "@myDirective") @link(import : ["composeDirective", "extends", "external", "inaccessible", "key", "override", "provides", "requires", "shareable", "tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1") {
36+
* query: Query
37+
* }
38+
*
39+
* directive @custom on FIELD_DEFINITION
40+
*
41+
* type Query {
42+
* helloWorld: String! @custom
43+
* }
44+
* ```
45+
*
46+
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective">@composeDirective definition</a>
47+
*/
48+
@Repeatable
49+
@GraphQLDirective(
50+
name = COMPOSE_DIRECTIVE_NAME,
51+
description = COMPOSE_DIRECTIVE_DESCRIPTION,
52+
locations = [Introspection.DirectiveLocation.SCHEMA]
53+
)
54+
annotation class ComposeDirective(val name: String)
55+
56+
internal const val COMPOSE_DIRECTIVE_NAME = "composeDirective"
57+
private const val COMPOSE_DIRECTIVE_DESCRIPTION = "Marks underlying custom directive to be included in the Supergraph schema"
58+
59+
internal val COMPOSE_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
60+
.name(COMPOSE_DIRECTIVE_NAME)
61+
.description(COMPOSE_DIRECTIVE_DESCRIPTION)
62+
.validLocations(Introspection.DirectiveLocation.SCHEMA)
63+
.argument(
64+
GraphQLArgument.newArgument()
65+
.name("name")
66+
.type(GraphQLNonNull.nonNull(Scalars.GraphQLString))
67+
)
68+
.repeatable(true)
69+
.build()

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/LinkDirective.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import graphql.schema.GraphQLList
2424
import graphql.schema.GraphQLNonNull
2525

2626
const val LINK_SPEC_URL = "https://specs.apollo.dev/link/v1.0/"
27-
const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.0"
27+
const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.1"
2828

2929
/**
3030
* ```graphql

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaV2GeneratorTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,13 @@ class FederatedSchemaV2GeneratorTest {
3030
fun `verify can generate federated schema`() {
3131
val expectedSchema =
3232
"""
33-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
33+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
3434
query: Query
3535
}
3636
37+
"Marks underlying custom directive to be included in the Supergraph schema"
38+
directive @composeDirective(name: String!) repeatable on SCHEMA
39+
3740
directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
3841
3942
"Marks the field, argument, input field or enum value as deprecated"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.expediagroup.graphql.generator.federation.data.integration.composeDirective
2+
3+
import com.expediagroup.graphql.generator.annotations.GraphQLDirective
4+
import com.expediagroup.graphql.generator.federation.directives.ComposeDirective
5+
import graphql.introspection.Introspection
6+
7+
@ComposeDirective(name = "custom")
8+
class CustomSchema
9+
10+
@GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
11+
annotation class CustomDirective
12+
13+
class SimpleQuery {
14+
@CustomDirective
15+
fun helloWorld(): String = "Hello World"
16+
}

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,13 @@ scalar CustomScalar"""
8686

8787
const val BASE_SERVICE_SDL =
8888
"""
89-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
89+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
9090
query: Query
9191
}
9292
93+
"Marks underlying custom directive to be included in the Supergraph schema"
94+
directive @composeDirective(name: String!) repeatable on SCHEMA
95+
9396
"Marks target object as extending part of the federated schema"
9497
directive @extends on OBJECT | INTERFACE
9598
@@ -142,10 +145,13 @@ scalar FieldSet
142145

143146
const val FEDERATED_SERVICE_SDL_V2 =
144147
"""
145-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
148+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
146149
query: Query
147150
}
148151
152+
"Marks underlying custom directive to be included in the Supergraph schema"
153+
directive @composeDirective(name: String!) repeatable on SCHEMA
154+
149155
directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
150156
151157
"Marks target object as extending part of the federated schema"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.expediagroup.graphql.generator.federation.validation.integration
2+
3+
import com.expediagroup.graphql.generator.TopLevelObject
4+
import com.expediagroup.graphql.generator.extensions.print
5+
import com.expediagroup.graphql.generator.federation.data.integration.composeDirective.CustomSchema
6+
import com.expediagroup.graphql.generator.federation.data.integration.composeDirective.SimpleQuery
7+
import com.expediagroup.graphql.generator.federation.toFederatedSchema
8+
import org.junit.jupiter.api.Assertions
9+
import org.junit.jupiter.api.Test
10+
import org.junit.jupiter.api.assertDoesNotThrow
11+
12+
class ComposeDirectiveIT {
13+
14+
@Test
15+
fun `verifies applying @composeDirective generates valid schema`() {
16+
assertDoesNotThrow {
17+
val schema = toFederatedSchema(
18+
config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.composeDirective"),
19+
queries = listOf(TopLevelObject(SimpleQuery())),
20+
schemaObject = TopLevelObject(CustomSchema())
21+
)
22+
23+
val expected = """
24+
schema @composeDirective(name : "custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
25+
query: Query
26+
}
27+
28+
"Marks underlying custom directive to be included in the Supergraph schema"
29+
directive @composeDirective(name: String!) repeatable on SCHEMA
30+
31+
directive @custom on FIELD_DEFINITION
32+
33+
"Links definitions within the document to external schemas."
34+
directive @link(import: [String], url: String!) repeatable on SCHEMA
35+
36+
type Query {
37+
_service: _Service!
38+
helloWorld: String! @custom
39+
}
40+
41+
type _Service {
42+
sdl: String!
43+
}
44+
""".trimIndent()
45+
val actual = schema.print(
46+
includeDirectivesFilter = { directive -> "link" == directive || "composeDirective" == directive || "custom" == directive },
47+
includeScalarTypes = false
48+
).trim()
49+
Assertions.assertEquals(expected, actual)
50+
}
51+
}
52+
}

plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePluginIT.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -516,10 +516,13 @@ class GraphQLGradlePluginIT : GraphQLGradlePluginAbstractIT() {
516516

517517
val expectedFederatedSchemaWithCustomScalar =
518518
"""
519-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
519+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
520520
query: Query
521521
}
522522
523+
"Marks underlying custom directive to be included in the Supergraph schema"
524+
directive @composeDirective(name: String!) repeatable on SCHEMA
525+
523526
"Marks the field, argument, input field or enum value as deprecated"
524527
directive @deprecated(
525528
"The reason for the deprecation"

plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLGenerateSDLTaskIT.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,13 @@ internal val DEFAULT_SCHEMA =
6565

6666
internal val FEDERATED_SCHEMA =
6767
"""
68-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
68+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
6969
query: Query
7070
}
7171
72+
"Marks underlying custom directive to be included in the Supergraph schema"
73+
directive @composeDirective(name: String!) repeatable on SCHEMA
74+
7275
"Marks the field, argument, input field or enum value as deprecated"
7376
directive @deprecated(
7477
"The reason for the deprecation"

plugins/graphql-kotlin-maven-plugin/src/integration/generate-sdl-federated/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ class GenerateSDLMojoTest {
3636
assertTrue(schemaFile.exists(), "schema file was generated")
3737

3838
val expectedSchema = """
39-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
39+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
4040
query: Query
4141
}
4242
43+
"Marks underlying custom directive to be included in the Supergraph schema"
44+
directive @composeDirective(name: String!) repeatable on SCHEMA
45+
4346
"Marks the field, argument, input field or enum value as deprecated"
4447
directive @deprecated(
4548
"The reason for the deprecation"

plugins/graphql-kotlin-maven-plugin/src/integration/generate-sdl-hooks/src/test/kotlin/com/expediagroup/graphql/plugin/maven/GenerateSDLMojoTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ class GenerateSDLMojoTest {
3636
assertTrue(schemaFile.exists(), "schema file was generated")
3737

3838
val expectedSchema = """
39-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
39+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
4040
query: Query
4141
}
4242
43+
"Marks underlying custom directive to be included in the Supergraph schema"
44+
directive @composeDirective(name: String!) repeatable on SCHEMA
45+
4346
"Marks the field, argument, input field or enum value as deprecated"
4447
directive @deprecated(
4548
"The reason for the deprecation"

plugins/schema/graphql-kotlin-sdl-generator/src/integrationTest/kotlin/com/expediagroup/graphql/plugin/schema/GenerateCustomSDLTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ class GenerateCustomSDLTest {
2525
fun `verify we can generate SDL using custom hooks provider`() {
2626
val expectedSchema =
2727
"""
28-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
28+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
2929
query: Query
3030
}
3131
32+
"Marks underlying custom directive to be included in the Supergraph schema"
33+
directive @composeDirective(name: String!) repeatable on SCHEMA
34+
3235
"Marks the field, argument, input field or enum value as deprecated"
3336
directive @deprecated(
3437
"The reason for the deprecation"

website/docs/schema-generator/federation/apollo-federation.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,11 @@ toFederatedSchema(
119119
will generate
120120

121121
```graphql
122-
schema @link(import : ["@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.0"){
122+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
123123
query: Query
124124
}
125125

126+
directive @composeDirective(name: String!) repeatable on SCHEMA
126127
directive @extends on OBJECT | INTERFACE
127128
directive @external on FIELD_DEFINITION
128129
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

website/docs/schema-generator/federation/federated-directives.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,49 @@ title: Federated Directives
66

77
For more details, see the [Apollo Federation Specification](https://www.apollographql.com/docs/federation/federation-spec/).
88

9+
## `@composeDirective` directive
10+
11+
```graphql
12+
directive @composeDirective(name: String!) repeatable on SCHEMA
13+
```
14+
15+
By default, Supergraph schema excludes all custom directives. The `@composeDirective` is used to specify custom directives that should be exposed in the Supergraph schema.
16+
17+
Example:
18+
Given `@custom` directive we can preserve it in the Supergraph schema
19+
20+
```kotlin
21+
@GraphQLDirective(name = "custom", locations = [Introspection.DirectiveLocation.FIELD_DEFINITION])
22+
annotation class CustomDirective
23+
24+
@ComposeDirective(name = "custom")
25+
class CustomSchema
26+
27+
class SimpleQuery {
28+
@CustomDirective
29+
fun helloWorld(): String = "Hello World"
30+
}
31+
```
32+
33+
it will generate following schema
34+
35+
```graphql
36+
schema
37+
@composeDirective(name: "@myDirective")
38+
@link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1")
39+
{
40+
query: Query
41+
}
42+
43+
directive @custom on FIELD_DEFINITION
44+
45+
type Query {
46+
helloWorld: String! @custom
47+
}
48+
```
49+
50+
See [@composeDirective definition](https://www.apollographql.com/docs/federation/federated-types/federated-directives/#composedirective) for more information.
51+
952
## `@contact` directive
1053

1154
```graphql
@@ -251,6 +294,19 @@ External schemas are identified by their `url`, which optionally ends with a nam
251294
By default, external types should be namespaced (prefixed with `<namespace>__`, e.g. `key` directive should be namespaced as `federation__key`) unless they are explicitly imported.
252295
`graphql-kotlin` automatically imports ALL federation directives to avoid the need for namespacing.
253296

297+
```kotlin
298+
@LinkDirective(url = "https://myspecs.company.dev/foo/v1.0", imports = ["@foo", "bar"])
299+
class MySchema
300+
```
301+
302+
This will generate following schema:
303+
304+
```graphql
305+
schema @link(import : ["@foo", "bar"], url : "https://myspecs.company.dev/foo/v1.0") {
306+
query: Query
307+
}
308+
```
309+
254310
:::danger
255311
We currently DO NOT support full `@link` directive capability as it requires support for namespacing and renaming imports. This functionality may be added in the future releases. See
256312
[@link specification](https://specs.apollo.dev/link/v1.0) for details.

0 commit comments

Comments
 (0)