Skip to content

Commit 1172da1

Browse files
authored
feat(federation): federation v2.6 support (#1928)
### 📝 Description Adds Federation v2.6 support ```graphql directive @Policy(policies: [[federation__Policy!]!]!) on | FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM scalar federation__Policy ``` `@policy` directive indicates to composition that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor. Refer to the [Apollo Router article](https://www.apollographql.com/docs/router/configuration/authorization#policy) for additional details. ### 🔗 Related Issues N/A
1 parent 0aea13e commit 1172da1

File tree

22 files changed

+387
-27
lines changed

22 files changed

+387
-27
lines changed

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.29.1
3+
image: ghcr.io/apollographql/router:v1.40.0
44
volumes:
55
- ./router.yaml:/dist/config/router.yaml
66
- ./supergraph.graphql:/dist/config/supergraph.graphql

examples/federation/supergraph.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
federation_version: =2.5.4
1+
federation_version: =2.6.3
22
subgraphs:
33
products:
44
routing_url: http://products:8080/graphql

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

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Expedia, Inc
2+
* Copyright 2024 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.
@@ -43,6 +43,7 @@ import com.expediagroup.graphql.generator.federation.directives.LinkDirective
4343
import com.expediagroup.graphql.generator.federation.directives.LinkImport
4444
import com.expediagroup.graphql.generator.federation.directives.LinkedSpec
4545
import com.expediagroup.graphql.generator.federation.directives.OVERRIDE_DIRECTIVE_NAME
46+
import com.expediagroup.graphql.generator.federation.directives.POLICY_DIRECTIVE_NAME
4647
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_NAME
4748
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_TYPE
4849
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
@@ -52,10 +53,12 @@ import com.expediagroup.graphql.generator.federation.directives.SHAREABLE_DIRECT
5253
import com.expediagroup.graphql.generator.federation.directives.TAG_DIRECTIVE_NAME
5354
import com.expediagroup.graphql.generator.federation.directives.keyDirectiveDefinition
5455
import com.expediagroup.graphql.generator.federation.directives.linkDirectiveDefinition
56+
import com.expediagroup.graphql.generator.federation.directives.policyDirectiveDefinition
5557
import com.expediagroup.graphql.generator.federation.directives.providesDirectiveDefinition
5658
import com.expediagroup.graphql.generator.federation.directives.requiresDirectiveDefinition
5759
import com.expediagroup.graphql.generator.federation.directives.requiresScopesDirectiveType
5860
import com.expediagroup.graphql.generator.federation.directives.toAppliedLinkDirective
61+
import com.expediagroup.graphql.generator.federation.directives.toAppliedPolicyDirective
5962
import com.expediagroup.graphql.generator.federation.directives.toAppliedRequiresScopesDirective
6063
import com.expediagroup.graphql.generator.federation.exception.DuplicateSpecificationLinkImport
6164
import com.expediagroup.graphql.generator.federation.exception.IncorrectFederatedDirectiveUsage
@@ -69,6 +72,7 @@ import com.expediagroup.graphql.generator.federation.types.FIELD_SET_SCALAR_NAME
6972
import com.expediagroup.graphql.generator.federation.types.FIELD_SET_SCALAR_TYPE
7073
import com.expediagroup.graphql.generator.federation.types.FieldSetTransformer
7174
import com.expediagroup.graphql.generator.federation.types.LINK_IMPORT_SCALAR_TYPE
75+
import com.expediagroup.graphql.generator.federation.types.POLICY_SCALAR_TYPE
7276
import com.expediagroup.graphql.generator.federation.types.SCOPE_SCALAR_TYPE
7377
import com.expediagroup.graphql.generator.federation.types.SERVICE_FIELD_DEFINITION
7478
import com.expediagroup.graphql.generator.federation.types._Service
@@ -149,6 +153,18 @@ open class FederatedSchemaGeneratorHooks(
149153
}
150154
}
151155
}
156+
private val policiesScalar: GraphQLScalarType by lazy {
157+
POLICY_SCALAR_TYPE.run {
158+
val policyScalarName = namespacedTypeName(FEDERATION_SPEC, this.name)
159+
if (policyScalarName != this.name) {
160+
this.transform {
161+
it.name(policyScalarName)
162+
}
163+
} else {
164+
this
165+
}
166+
}
167+
}
152168
private val scopesScalar: GraphQLScalarType by lazy {
153169
SCOPE_SCALAR_TYPE.run {
154170
val scopesScalarName = namespacedTypeName(FEDERATION_SPEC, this.name)
@@ -252,17 +268,24 @@ open class FederatedSchemaGeneratorHooks(
252268
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE_V2
253269
KEY_DIRECTIVE_NAME -> keyDirectiveDefinition(fieldSetScalar)
254270
LINK_DIRECTIVE_NAME -> linkDirectiveDefinition(linkImportScalar)
271+
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar)
255272
PROVIDES_DIRECTIVE_NAME -> providesDirectiveDefinition(fieldSetScalar)
256273
REQUIRES_DIRECTIVE_NAME -> requiresDirectiveDefinition(fieldSetScalar)
257274
REQUIRES_SCOPE_DIRECTIVE_NAME -> requiresScopesDirectiveType(scopesScalar)
258275
else -> super.willGenerateDirective(directiveInfo)
259276
}
260277

261278
override fun willApplyDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLAppliedDirective? {
262-
return if (directiveInfo.effectiveName == REQUIRES_SCOPE_DIRECTIVE_NAME) {
263-
directive.toAppliedRequiresScopesDirective(directiveInfo)
264-
} else {
265-
super.willApplyDirective(directiveInfo, directive)
279+
return when (directiveInfo.effectiveName) {
280+
REQUIRES_SCOPE_DIRECTIVE_NAME -> {
281+
directive.toAppliedRequiresScopesDirective(directiveInfo)
282+
}
283+
POLICY_DIRECTIVE_NAME -> {
284+
directive.toAppliedPolicyDirective(directiveInfo)
285+
}
286+
else -> {
287+
super.willApplyDirective(directiveInfo, directive)
288+
}
266289
}
267290
}
268291

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 2023 Expedia, Inc
2+
* Copyright 2024 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.
@@ -32,7 +32,7 @@ const val LINK_SPEC_URL_PREFIX = "$APOLLO_SPEC_URL/$LINK_SPEC"
3232
const val LINK_SPEC_LATEST_URL = "$LINK_SPEC_URL_PREFIX/v$LINK_SPEC_LATEST_VERSION"
3333

3434
const val FEDERATION_SPEC = "federation"
35-
const val FEDERATION_SPEC_LATEST_VERSION = "2.5"
35+
const val FEDERATION_SPEC_LATEST_VERSION = "2.6"
3636
const val FEDERATION_SPEC_URL_PREFIX = "$APOLLO_SPEC_URL/$FEDERATION_SPEC"
3737
const val FEDERATION_SPEC_LATEST_URL = "$FEDERATION_SPEC_URL_PREFIX/v$FEDERATION_SPEC_LATEST_VERSION"
3838

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024 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.GraphQLIgnore
20+
21+
/**
22+
* Annotation representing authorization policy scalar type that is used by the `@policy directive.
23+
*
24+
* @param value required authorization policy
25+
* @see [com.expediagroup.graphql.generator.federation.types.POLICY_SCALAR_TYPE]
26+
*/
27+
@LinkedSpec(FEDERATION_SPEC)
28+
annotation class Policy(val value: String)
29+
30+
// this is a workaround for JVM lack of support nested arrays as annotation values
31+
@GraphQLIgnore
32+
annotation class Policies(val value: Array<Policy>)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2024 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 com.expediagroup.graphql.generator.directives.DirectiveMetaInformation
21+
import graphql.introspection.Introspection
22+
import graphql.schema.GraphQLAppliedDirective
23+
import graphql.schema.GraphQLArgument
24+
import graphql.schema.GraphQLList
25+
import graphql.schema.GraphQLNonNull
26+
import graphql.schema.GraphQLScalarType
27+
import kotlin.reflect.full.memberProperties
28+
29+
/**
30+
* ```graphql
31+
* directive @policy(scopes: [[Policy!]!]!) on
32+
* ENUM
33+
* | FIELD_DEFINITION
34+
* | INTERFACE
35+
* | OBJECT
36+
* | SCALAR
37+
* ```
38+
*
39+
*
40+
* Directive that is used to indicate that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor.
41+
* Refer to the <a href="https://www.apollographql.com/docs/router/configuration/authorization#policy">Apollo Router article</a> for additional details.
42+
*
43+
* @see <a href="https://www.apollographql.com/docs/federation/federated-types/federated-directives#policy">@policy definition</a>
44+
* @see <a href="https://www.apollographql.com/docs/router/configuration/authorization#policy">Apollo Router @policy documentation</a>
45+
*/
46+
@LinkedSpec(FEDERATION_SPEC)
47+
@Repeatable
48+
@GraphQLDirective(
49+
name = POLICY_DIRECTIVE_NAME,
50+
description = POLICY_DIRECTIVE_DESCRIPTION,
51+
locations = [
52+
Introspection.DirectiveLocation.ENUM,
53+
Introspection.DirectiveLocation.FIELD_DEFINITION,
54+
Introspection.DirectiveLocation.INTERFACE,
55+
Introspection.DirectiveLocation.OBJECT,
56+
Introspection.DirectiveLocation.SCALAR,
57+
]
58+
)
59+
annotation class PolicyDirective(val policies: Array<Policies>)
60+
61+
internal const val POLICY_DIRECTIVE_NAME = "policy"
62+
private const val POLICY_DIRECTIVE_DESCRIPTION = "Indicates to composition that the target element is restricted based on authorization policies that are evaluated in a Rhai script or coprocessor"
63+
private const val POLICIES_ARGUMENT = "policies"
64+
65+
internal fun policyDirectiveDefinition(policies: GraphQLScalarType): graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
66+
.name(POLICY_DIRECTIVE_NAME)
67+
.description(POLICY_DIRECTIVE_DESCRIPTION)
68+
.validLocations(
69+
Introspection.DirectiveLocation.ENUM,
70+
Introspection.DirectiveLocation.FIELD_DEFINITION,
71+
Introspection.DirectiveLocation.INTERFACE,
72+
Introspection.DirectiveLocation.OBJECT,
73+
Introspection.DirectiveLocation.SCALAR
74+
)
75+
.argument(
76+
GraphQLArgument.newArgument()
77+
.name(POLICIES_ARGUMENT)
78+
.type(
79+
GraphQLNonNull.nonNull(
80+
GraphQLList.list(
81+
GraphQLNonNull(
82+
GraphQLList.list(
83+
policies
84+
)
85+
)
86+
)
87+
)
88+
)
89+
)
90+
.build()
91+
92+
@Suppress("UNCHECKED_CAST")
93+
internal fun graphql.schema.GraphQLDirective.toAppliedPolicyDirective(directiveInfo: DirectiveMetaInformation): GraphQLAppliedDirective {
94+
// we need to manually transform @policy directive definition as JVM does not support nested array as annotation arguments
95+
val annotationPolicies = directiveInfo.directive.annotationClass.memberProperties
96+
.first { it.name == POLICIES_ARGUMENT }
97+
.call(directiveInfo.directive) as? Array<Policies> ?: emptyArray()
98+
val policies = annotationPolicies.map { policiesAnnotation -> policiesAnnotation.value.toList() }
99+
100+
return this.toAppliedDirective()
101+
.transform { appliedDirectiveBuilder ->
102+
this.getArgument(POLICIES_ARGUMENT)
103+
.toAppliedArgument()
104+
.transform { argumentBuilder ->
105+
argumentBuilder.valueProgrammatic(policies)
106+
}
107+
.let {
108+
appliedDirectiveBuilder.argument(it)
109+
}
110+
}
111+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Expedia, Inc
2+
* Copyright 2024 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.
@@ -73,7 +73,7 @@ internal fun requiresScopesDirectiveType(scopes: GraphQLScalarType): graphql.sch
7373
)
7474
.argument(
7575
GraphQLArgument.newArgument()
76-
.name("scopes")
76+
.name(SCOPES_ARGUMENT)
7777
.type(
7878
GraphQLNonNull.nonNull(
7979
GraphQLList.list(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2024 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.types
18+
19+
import com.expediagroup.graphql.generator.federation.directives.Policy
20+
import com.expediagroup.graphql.generator.federation.exception.CoercingValueToLiteralException
21+
import graphql.GraphQLContext
22+
import graphql.Scalars
23+
import graphql.execution.CoercedVariables
24+
import graphql.language.StringValue
25+
import graphql.language.Value
26+
import graphql.schema.Coercing
27+
import graphql.schema.CoercingParseLiteralException
28+
import graphql.schema.CoercingSerializeException
29+
import graphql.schema.GraphQLScalarType
30+
import java.util.Locale
31+
32+
internal const val POLICY_SCALAR_NAME = "Policy"
33+
34+
/**
35+
* Custom scalar type that is used to represent authentication policy which serializes as a String.
36+
*/
37+
internal val POLICY_SCALAR_TYPE: GraphQLScalarType = GraphQLScalarType.newScalar(Scalars.GraphQLString)
38+
.name(POLICY_SCALAR_NAME)
39+
.description("Federation type representing authorization policy")
40+
.coercing(PolicyCoercing)
41+
.build()
42+
43+
private object PolicyCoercing : Coercing<Policy, String> {
44+
override fun serialize(dataFetcherResult: Any, graphQLContext: GraphQLContext, locale: Locale): String =
45+
when (dataFetcherResult) {
46+
is Policy -> dataFetcherResult.value
47+
else -> throw CoercingSerializeException(
48+
"Cannot serialize $dataFetcherResult. Expected type 'Policy' but was '${dataFetcherResult.javaClass.simpleName}'."
49+
)
50+
}
51+
52+
override fun parseValue(input: Any, graphQLContext: GraphQLContext, locale: Locale): Policy =
53+
when (input) {
54+
is Policy -> input
55+
is StringValue -> Policy::class.constructors.first().call(input.value)
56+
else -> throw CoercingParseLiteralException(
57+
"Cannot parse $input to Policy. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'."
58+
)
59+
}
60+
61+
override fun parseLiteral(input: Value<*>, variables: CoercedVariables, graphQLContext: GraphQLContext, locale: Locale): Policy =
62+
when (input) {
63+
is StringValue -> Policy::class.constructors.first().call(input.value)
64+
else -> throw CoercingParseLiteralException(
65+
"Cannot parse $input to Policy. Expected AST type 'StringValue' but was '${input.javaClass.simpleName}'."
66+
)
67+
}
68+
69+
override fun valueToLiteral(input: Any, graphQLContext: GraphQLContext, locale: Locale): Value<*> =
70+
when (input) {
71+
is Policy -> StringValue.newStringValue(input.value).build()
72+
else -> throw CoercingValueToLiteralException(Policy::class, input)
73+
}
74+
}

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

Lines changed: 1 addition & 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 : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){
33+
schema @link(import : ["@external", "@key", "@provides", "@requires", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
3434
query: Query
3535
}
3636

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/compose/ComposeDirectiveTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023 Expedia, Inc
2+
* Copyright 2024 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.
@@ -43,7 +43,7 @@ class ComposeDirectiveTest {
4343
fun `verify we can generate valid schema with @composeDirective`() {
4444
val expectedSchema =
4545
"""
46-
schema @composeDirective(name : "custom") @link(as : "myspec", import : ["@custom"], url : "https://www.myspecs.dev/myspec/v1.0") @link(import : ["@composeDirective", "@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.5"){
46+
schema @composeDirective(name : "custom") @link(as : "myspec", import : ["@custom"], url : "https://www.myspecs.dev/myspec/v1.0") @link(import : ["@composeDirective", "@key", "FieldSet"], url : "https://specs.apollo.dev/federation/v2.6"){
4747
query: Query
4848
}
4949

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/contact/ContactDirectiveTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ class ContactDirectiveTest {
3030
fun `verify we can import federation spec using custom @link`() {
3131
val expectedSchema =
3232
"""
33-
schema @contact(description : "Send emails to foo@myteamname.com", name : "My Team Name", url : "https://myteam.slack.com/room") @link(url : "https://specs.apollo.dev/federation/v2.5"){
33+
schema @contact(description : "Send emails to foo@myteamname.com", name : "My Team Name", url : "https://myteam.slack.com/room") @link(url : "https://specs.apollo.dev/federation/v2.6"){
3434
query: Query
3535
}
3636

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/link/LinkDirectiveTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ class LinkDirectiveTest {
4545
fun `verify we can import federation spec using custom @link`() {
4646
val expectedSchema =
4747
"""
48-
schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.5"){
48+
schema @link(as : "fed", import : [{name : "@key", as : "@myKey"}], url : "https://specs.apollo.dev/federation/v2.6"){
4949
query: Query
5050
}
5151

0 commit comments

Comments
 (0)