Skip to content

Commit f3e41f9

Browse files
committed
federation: add support for Apollo Federation subgraph spec v2.3
Updates `federation` module to support Apollo Federation subgraph spec v2.3 Changes: * v2.2 - update `@shareable` definition to be repeatable to allow annotating both types and their extensions (NOTE: this functionality is not applicable to `graphql-kotlin`) * v2.3 - adds new `@interfaceObject` directive that allows you to extend interface entity functionality in subgraphs, i.e. by applying `@interfaceObject` directive on a type we provide meta information to the composition logic that this entity type is actually an interface in the supergraph. This allows us to extend interface functionality without knowing any of its implementing types.
1 parent 1fbbd48 commit f3e41f9

File tree

9 files changed

+181
-9
lines changed

9 files changed

+181
-9
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ 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_NAME
2324
import com.expediagroup.graphql.generator.federation.directives.COMPOSE_DIRECTIVE_TYPE
2425
import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIVE_TYPE
2526
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE
2627
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL
2728
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2829
import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_NAME
2930
import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_TYPE
31+
import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_NAME
32+
import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_TYPE
3033
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
3134
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE
3235
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE_V2
@@ -75,7 +78,9 @@ open class FederatedSchemaGeneratorHooks(
7578
private val validator = FederatedSchemaValidator()
7679

7780
private val federationV2OnlyDirectiveNames: Set<String> = setOf(
81+
COMPOSE_DIRECTIVE_NAME,
7882
INACCESSIBLE_DIRECTIVE_NAME,
83+
INTERFACE_OBJECT_DIRECTIVE_NAME,
7984
LINK_DIRECTIVE_NAME,
8085
OVERRIDE_DIRECTIVE_NAME,
8186
SHAREABLE_DIRECTIVE_NAME
@@ -93,6 +98,7 @@ open class FederatedSchemaGeneratorHooks(
9398
EXTENDS_DIRECTIVE_TYPE,
9499
EXTERNAL_DIRECTIVE_TYPE,
95100
INACCESSIBLE_DIRECTIVE_TYPE,
101+
INTERFACE_OBJECT_DIRECTIVE_TYPE,
96102
KEY_DIRECTIVE_TYPE,
97103
LINK_DIRECTIVE_TYPE,
98104
OVERRIDE_DIRECTIVE_TYPE,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.generator.federation.directives
18+
19+
import com.expediagroup.graphql.generator.annotations.GraphQLDirective
20+
import graphql.introspection.Introspection
21+
22+
/**
23+
* ```graphql
24+
* directive @interfaceObject on OBJECT
25+
* ```
26+
*
27+
* This directive provides meta information to the router that this entity type defined within this subgraph is an interface in the supergraph. This allows you to extend functionality
28+
* of an interface across the supergraph without having to implement (or even be aware of) all its implementing types.
29+
*
30+
* Example:
31+
* Given an interface that is defined in another subgraph
32+
*
33+
* ```graphql
34+
* interface Product @key(fields: "id") {
35+
* id: ID!
36+
* description: String
37+
* }
38+
*
39+
* type Book implements Product @key(fields: "id") {
40+
* id: ID!
41+
* description: String
42+
* pages: Int!
43+
* }
44+
*
45+
* type Movie implements Product @key(fields: "id") {
46+
* id: ID!
47+
* description: String
48+
* duration: Int!
49+
* }
50+
* ```
51+
*
52+
* We can extend Product entity in our subgraph and a new field directly to it. This will result in making this new field available to ALL implementing types.
53+
*
54+
* ```kotlin
55+
* @InterfaceObjectDirective
56+
* data class Product(val id: ID) {
57+
* fun reviews(): List<Review> = TODO()
58+
* }
59+
* ```
60+
*
61+
* Which generates the following subgraph schema
62+
*
63+
* ```graphql
64+
* type Product @key(fields: "id") @interfaceObject {
65+
* id: ID!
66+
* reviews: [Review!]!
67+
* }
68+
* ```
69+
*/
70+
@GraphQLDirective(
71+
name = INTERFACE_OBJECT_DIRECTIVE_NAME,
72+
description = INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION,
73+
locations = [Introspection.DirectiveLocation.OBJECT]
74+
)
75+
annotation class InterfaceObjectDirective
76+
77+
internal const val INTERFACE_OBJECT_DIRECTIVE_NAME = "interfaceObject"
78+
private const val INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION = "Provides meta information to the router that this entity type is an interface in the supergraph."
79+
80+
internal val INTERFACE_OBJECT_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
81+
.name(INTERFACE_OBJECT_DIRECTIVE_NAME)
82+
.description(INTERFACE_OBJECT_DIRECTIVE_DESCRIPTION)
83+
.validLocations(Introspection.DirectiveLocation.OBJECT)
84+
.build()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2023 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -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.1"
27+
const val FEDERATION_SPEC_URL = "https://specs.apollo.dev/federation/v2.3"
2828

2929
/**
3030
* ```graphql

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2023 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
24-
* directive @shareable on FIELD_DEFINITION | OBJECT
24+
* directive @shareable repeatable on FIELD_DEFINITION | OBJECT
2525
* ```
2626
*
2727
* Shareable directive indicates that given object and/or field can be resolved by multiple subgraphs. If an object is marked as `@shareable` then all its fields are automatically shareable without the
@@ -44,6 +44,7 @@ import graphql.introspection.Introspection.DirectiveLocation
4444
* }
4545
* ```
4646
*/
47+
@Repeatable
4748
@GraphQLDirective(
4849
name = SHAREABLE_DIRECTIVE_NAME,
4950
description = SHAREABLE_DIRECTIVE_DESCRIPTION,

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,7 +30,7 @@ class FederatedSchemaV2GeneratorTest {
3030
fun `verify can generate federated schema`() {
3131
val expectedSchema =
3232
"""
33-
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
33+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
3434
query: Query
3535
}
3636
@@ -60,6 +60,9 @@ class FederatedSchemaV2GeneratorTest {
6060
if: Boolean!
6161
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
6262
63+
"Provides meta information to the router that this entity type is an interface in the supergraph."
64+
directive @interfaceObject on OBJECT
65+
6366
"Space separated list of primary keys needed to access federated object"
6467
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
6568
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.expediagroup.graphql.generator.federation.data.integration.intfObject
2+
3+
import com.expediagroup.graphql.generator.federation.data.queries.federated.v1.Review
4+
import com.expediagroup.graphql.generator.federation.directives.FieldSet
5+
import com.expediagroup.graphql.generator.federation.directives.InterfaceObjectDirective
6+
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
7+
import com.expediagroup.graphql.generator.scalars.ID
8+
9+
class IntfObjectQuery {
10+
11+
fun product(id: ID): Product = TODO()
12+
}
13+
14+
@InterfaceObjectDirective
15+
@KeyDirective(fields = FieldSet("id"))
16+
data class Product(val id: ID) {
17+
fun reviews(): List<Review> = TODO()
18+
}

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,7 +86,7 @@ scalar CustomScalar"""
8686

8787
const val BASE_SERVICE_SDL =
8888
"""
89-
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
89+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
9090
query: Query
9191
}
9292
@@ -102,6 +102,9 @@ directive @external on FIELD_DEFINITION
102102
"Marks location within schema as inaccessible from the GraphQL Gateway"
103103
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
104104
105+
"Provides meta information to the router that this entity type is an interface in the supergraph."
106+
directive @interfaceObject on OBJECT
107+
105108
"Space separated list of primary keys needed to access federated object"
106109
directive @key(fields: FieldSet!) repeatable on OBJECT | INTERFACE
107110
@@ -145,7 +148,7 @@ scalar FieldSet
145148

146149
const val FEDERATED_SERVICE_SDL_V2 =
147150
"""
148-
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.1"){
151+
schema @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
149152
query: Query
150153
}
151154
@@ -163,6 +166,9 @@ directive @external on FIELD_DEFINITION
163166
"Marks location within schema as inaccessible from the GraphQL Gateway"
164167
directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
165168
169+
"Provides meta information to the router that this entity type is an interface in the supergraph."
170+
directive @interfaceObject on OBJECT
171+
166172
"Space separated list of primary keys needed to access federated object"
167173
directive @key(fields: FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
168174

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/ComposeDirectiveIT.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ComposeDirectiveIT {
2121
)
2222

2323
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"){
24+
schema @composeDirective(name : "custom") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
2525
query: Query
2626
}
2727

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

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,60 @@ type Product {
211211
}
212212
```
213213

214+
## `@interfaceObject` directive
215+
216+
:::note
217+
Only available in Federation v2.
218+
:::
219+
220+
```graphql
221+
directive @interfaceObject on OBJECT
222+
```
223+
224+
This directive provides meta information to the router that this entity type defined within this subgraph is an interface in the supergraph. This allows you to extend functionality
225+
of an interface across the supergraph without having to implement (or even be aware of) all its implementing types.
226+
227+
Example:
228+
Given an interface that is defined somewhere in our supergraph
229+
230+
```graphql
231+
interface Product @key(fields: "id") {
232+
id: ID!
233+
description: String
234+
}
235+
236+
type Book implements Product @key(fields: "id") {
237+
id: ID!
238+
description: String
239+
pages: Int!
240+
}
241+
242+
type Movie implements Product @key(fields: "id") {
243+
id: ID!
244+
description: String
245+
duration: Int!
246+
}
247+
```
248+
249+
We can extend `Product` entity in our subgraph and a new field directly to it. This will result in making this new field available to ALL implementing types.
250+
251+
```kotlin
252+
@InterfaceObjectDirective
253+
@KeyDirective(fields = FieldSet("id"))
254+
data class Product(val id: ID) {
255+
fun reviews(): List<Review> = TODO()
256+
}
257+
```
258+
259+
Which generates the following subgraph schema
260+
261+
```graphql
262+
type Product @key(fields: "id") @interfaceObject {
263+
id: ID!
264+
reviews: [Review!]!
265+
}
266+
```
267+
214268
## `@key` directive
215269

216270
```graphql
@@ -470,7 +524,7 @@ Only available in Federation v2.
470524
:::
471525

472526
```graphql
473-
directive @shareable on FIELD_DEFINITION | OBJECT
527+
directive @shareable repeatable on FIELD_DEFINITION | OBJECT
474528
```
475529

476530
Shareable directive indicates that given object and/or field can be resolved by multiple subgraphs. If an object is marked as `@shareable` then all its fields are automatically shareable without the

0 commit comments

Comments
 (0)