Skip to content

Commit 10e9d84

Browse files
authored
federation: add support for Apollo Federation subgraph spec v2.3 (#1661)
* 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. * fix integration tests * update remaining fed2.1 references with fed2.3 * update composition test to use latest router * update test schema to fed 2.3 * fix directive definitions * fix tests * fix definitions * add missing intf object IT
1 parent be7d676 commit 10e9d84

File tree

26 files changed

+449
-101
lines changed

26 files changed

+449
-101
lines changed

examples/federation/compose-router.yaml

Lines changed: 0 additions & 10 deletions
This file was deleted.

examples/federation/compose-subgraphs.yaml

Lines changed: 0 additions & 27 deletions
This file was deleted.

examples/federation/docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
router:
3-
image: ghcr.io/apollographql/router:v1.2.1
3+
image: ghcr.io/apollographql/router:v1.10.1
44
volumes:
55
- ./router.yaml:/dist/config/router.yaml
66
- ./supergraph.graphql:/dist/config/supergraph.graphql

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

Lines changed: 23 additions & 19 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.
@@ -20,13 +20,18 @@ 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
26+
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_NAME
2527
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE
28+
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_TYPE_V2
2629
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL
2730
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2831
import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_NAME
2932
import com.expediagroup.graphql.generator.federation.directives.INACCESSIBLE_DIRECTIVE_TYPE
33+
import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_NAME
34+
import com.expediagroup.graphql.generator.federation.directives.INTERFACE_OBJECT_DIRECTIVE_TYPE
3035
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
3136
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE
3237
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_TYPE_V2
@@ -75,7 +80,9 @@ open class FederatedSchemaGeneratorHooks(
7580
private val validator = FederatedSchemaValidator()
7681

7782
private val federationV2OnlyDirectiveNames: Set<String> = setOf(
83+
COMPOSE_DIRECTIVE_NAME,
7884
INACCESSIBLE_DIRECTIVE_NAME,
85+
INTERFACE_OBJECT_DIRECTIVE_NAME,
7986
LINK_DIRECTIVE_NAME,
8087
OVERRIDE_DIRECTIVE_NAME,
8188
SHAREABLE_DIRECTIVE_NAME
@@ -91,9 +98,10 @@ open class FederatedSchemaGeneratorHooks(
9198
private val federatedDirectiveV2List: List<GraphQLDirective> = listOf(
9299
COMPOSE_DIRECTIVE_TYPE,
93100
EXTENDS_DIRECTIVE_TYPE,
94-
EXTERNAL_DIRECTIVE_TYPE,
101+
EXTERNAL_DIRECTIVE_TYPE_V2,
95102
INACCESSIBLE_DIRECTIVE_TYPE,
96-
KEY_DIRECTIVE_TYPE,
103+
INTERFACE_OBJECT_DIRECTIVE_TYPE,
104+
KEY_DIRECTIVE_TYPE_V2,
97105
LINK_DIRECTIVE_TYPE,
98106
OVERRIDE_DIRECTIVE_TYPE,
99107
PROVIDES_DIRECTIVE_TYPE,
@@ -117,23 +125,19 @@ open class FederatedSchemaGeneratorHooks(
117125
willGenerateFederatedDirective(directiveInfo)
118126
}
119127

120-
private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation) =
121-
if (federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName)) {
122-
throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName)
123-
} else if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) {
124-
KEY_DIRECTIVE_TYPE
125-
} else {
126-
super.willGenerateDirective(directiveInfo)
127-
}
128+
private fun willGenerateFederatedDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when {
129+
federationV2OnlyDirectiveNames.contains(directiveInfo.effectiveName) -> throw IncorrectFederatedDirectiveUsage(directiveInfo.effectiveName)
130+
EXTERNAL_DIRECTIVE_NAME == directiveInfo.effectiveName -> EXTERNAL_DIRECTIVE_TYPE
131+
KEY_DIRECTIVE_NAME == directiveInfo.effectiveName -> KEY_DIRECTIVE_TYPE
132+
else -> super.willGenerateDirective(directiveInfo)
133+
}
128134

129-
private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation) =
130-
if (KEY_DIRECTIVE_NAME == directiveInfo.effectiveName) {
131-
KEY_DIRECTIVE_TYPE_V2
132-
} else if (LINK_DIRECTIVE_NAME == directiveInfo.effectiveName) {
133-
LINK_DIRECTIVE_TYPE
134-
} else {
135-
super.willGenerateDirective(directiveInfo)
136-
}
135+
private fun willGenerateFederatedDirectiveV2(directiveInfo: DirectiveMetaInformation): GraphQLDirective? = when (directiveInfo.effectiveName) {
136+
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE_V2
137+
KEY_DIRECTIVE_NAME -> KEY_DIRECTIVE_TYPE_V2
138+
LINK_DIRECTIVE_NAME -> LINK_DIRECTIVE_TYPE
139+
else -> super.willGenerateDirective(directiveInfo)
140+
}
137141

138142
override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType {
139143
validator.validateGraphQLType(generatedType)

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
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+
117
package com.expediagroup.graphql.generator.federation.directives
218

319
import com.expediagroup.graphql.generator.annotations.GraphQLDirective
@@ -32,7 +48,7 @@ import graphql.schema.GraphQLNonNull
3248
* it will generate following schema
3349
*
3450
* ```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") {
51+
* schema @composeDirective(name: "@myDirective") @link(import : ["@composeDirective", "@extends", "@external", "@inaccessible", "@interfaceObject", "@key", "@override", "@provides", "@requires", "@shareable", "@tag", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.3"){
3652
* query: Query
3753
* }
3854
*

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

Lines changed: 12 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,11 @@ import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
24+
* # federation v1 definition
2425
* directive @external on FIELD_DEFINITION
26+
*
27+
* # federation v2 definition
28+
* directive @external on OBJECT | FIELD_DEFINITION
2529
* ```
2630
*
2731
* The @external directive is used to mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. @external
@@ -60,7 +64,7 @@ import graphql.introspection.Introspection.DirectiveLocation
6064
@GraphQLDirective(
6165
name = EXTERNAL_DIRECTIVE_NAME,
6266
description = EXTERNAL_DIRECTIVE_DESCRIPTION,
63-
locations = [DirectiveLocation.FIELD_DEFINITION]
67+
locations = [DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION]
6468
)
6569
annotation class ExternalDirective
6670

@@ -72,3 +76,9 @@ internal val EXTERNAL_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.
7276
.description(EXTERNAL_DIRECTIVE_DESCRIPTION)
7377
.validLocations(DirectiveLocation.FIELD_DEFINITION)
7478
.build()
79+
80+
internal val EXTERNAL_DIRECTIVE_TYPE_V2: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
81+
.name(EXTERNAL_DIRECTIVE_NAME)
82+
.description(EXTERNAL_DIRECTIVE_DESCRIPTION)
83+
.validLocations(DirectiveLocation.OBJECT, DirectiveLocation.FIELD_DEFINITION)
84+
.build()
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: 4 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,
@@ -58,4 +59,5 @@ internal val SHAREABLE_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql
5859
.name(SHAREABLE_DIRECTIVE_NAME)
5960
.description(SHAREABLE_DIRECTIVE_DESCRIPTION)
6061
.validLocations(DirectiveLocation.FIELD_DEFINITION, DirectiveLocation.OBJECT)
62+
.repeatable(true)
6163
.build()

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

Lines changed: 1 addition & 1 deletion
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.

0 commit comments

Comments
 (0)