Skip to content

Commit 6c42aad

Browse files
author
sjin
committed
feat: generate override directive based on fed version
1 parent 60ce9ae commit 6c42aad

File tree

5 files changed

+157
-27
lines changed

5 files changed

+157
-27
lines changed

examples/client/server/src/main/kotlin/com/expediagroup/graphql/examples/client/server/FederatedQuery.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ data class ProductVariation(
9696
data class User(
9797
@ExternalDirective
9898
val email: String,
99-
@OverrideDirective(from = "users", label = "Migrating name field")
99+
@OverrideDirective(from = "users")
100100
val name: String,
101101
@ExternalDirective
102102
val totalProductsCreated: Int? = null

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ import graphql.TypeResolutionEnvironment
7777
import graphql.schema.DataFetcher
7878
import graphql.schema.FieldCoordinates
7979
import graphql.schema.GraphQLAppliedDirective
80-
import graphql.schema.GraphQLAppliedDirectiveArgument
8180
import graphql.schema.GraphQLCodeRegistry
8281
import graphql.schema.GraphQLDirective
8382
import graphql.schema.GraphQLDirectiveContainer
@@ -98,8 +97,8 @@ open class FederatedSchemaGeneratorHooks(
9897
private val resolvers: List<FederatedTypeResolver>
9998
) : FlowSubscriptionSchemaGeneratorHooks() {
10099
private val validator: FederatedSchemaValidator = FederatedSchemaValidator()
101-
data class LinkSpec(val namespace: String, val imports: Map<String, String>)
102-
private val linkSpecs: MutableMap<String, LinkSpec> = HashMap()
100+
data class LinkSpec(val namespace: String, val imports: Map<String, String>, val url: String? = FEDERATION_SPEC_LATEST_URL)
101+
public val linkSpecs: MutableMap<String, LinkSpec> = HashMap()
103102

104103
// workaround to https://github.com/ExpediaGroup/graphql-kotlin/issues/1815
105104
// since those scalars can be renamed, we need to ensure we only generate those scalars just once
@@ -175,7 +174,7 @@ open class FederatedSchemaGeneratorHooks(
175174
normalizeImportName(import.name) to normalizeImportName(importedName)
176175
}
177176

178-
val linkSpec = LinkSpec(nameSpace, imports)
177+
val linkSpec = LinkSpec(nameSpace, imports, appliedDirectiveAnnotation.url)
179178
linkSpecs[spec] = linkSpec
180179
}
181180
}
@@ -218,19 +217,23 @@ open class FederatedSchemaGeneratorHooks(
218217
else -> super.willGenerateGraphQLType(type)
219218
}
220219

221-
override fun willGenerateDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? =
222-
when (directiveInfo.effectiveName) {
220+
override fun willGenerateDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? {
221+
val federationSpec = linkSpecs[FEDERATION_SPEC]
222+
val federationUrl = federationSpec?.url ?: FEDERATION_SPEC_LATEST_URL
223+
224+
return when (directiveInfo.effectiveName) {
223225
CONTACT_DIRECTIVE_NAME -> CONTACT_DIRECTIVE_TYPE
224226
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE
225227
KEY_DIRECTIVE_NAME -> keyDirectiveDefinition(fieldSetScalar)
226228
LINK_DIRECTIVE_NAME -> linkDirectiveDefinition(linkImportScalar)
227-
OVERRIDE_DIRECTIVE_NAME -> overrideDirectiveDefinition()
228-
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar)
229+
OVERRIDE_DIRECTIVE_NAME -> overrideDirectiveDefinition(federationUrl)
230+
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar, federationUrl)
229231
PROVIDES_DIRECTIVE_NAME -> providesDirectiveDefinition(fieldSetScalar)
230232
REQUIRES_DIRECTIVE_NAME -> requiresDirectiveDefinition(fieldSetScalar)
231233
REQUIRES_SCOPE_DIRECTIVE_NAME -> requiresScopesDirectiveType(scopesScalar)
232234
else -> super.willGenerateDirective(directiveInfo)
233235
}
236+
}
234237

235238
override fun willApplyDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLAppliedDirective? {
236239
return when (directiveInfo.effectiveName) {
@@ -300,7 +303,8 @@ open class FederatedSchemaGeneratorHooks(
300303
// only add @link directive definition if it doesn't exist yet
301304
builder.additionalDirective(linkDirective)
302305
}
303-
builder.withSchemaAppliedDirective(linkDirective.toAppliedLinkDirective(FEDERATION_SPEC_LATEST_URL, null, fed2Imports))
306+
val federationUrl = linkSpecs[FEDERATION_SPEC]?.url ?: FEDERATION_SPEC_LATEST_URL
307+
builder.withSchemaAppliedDirective(linkDirective.toAppliedLinkDirective(federationUrl, null, fed2Imports))
304308
}
305309

306310
val federatedCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(originalSchema.codeRegistry)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright 2025 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
18+
19+
/**
20+
* Checks if the federation version from the URL meets or exceeds the specified version.
21+
*
22+
* @param federationUrl The federation specification URL (e.g., "https://specs.apollo.dev/federation/v2.7")
23+
* @param major The major version to check against
24+
* @param minor The minor version to check against
25+
* @return True if the URL's version is at least the specified major.minor version
26+
*/
27+
internal fun isFederationVersionAtLeast(federationUrl: String, major: Int, minor: Int): Boolean {
28+
val versionRegex = """.*?/v?(\d+)\.(\d+).*""".toRegex()
29+
val matchResult = versionRegex.find(federationUrl)
30+
31+
return if (matchResult != null) {
32+
val (majorStr, minorStr) = matchResult.destructured
33+
val fedMajor = majorStr.toIntOrNull() ?: 0
34+
val fedMinor = minorStr.toIntOrNull() ?: 0
35+
36+
fedMajor > major || (fedMajor == major && fedMinor >= minor)
37+
} else {
38+
false
39+
}
40+
}

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

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.expediagroup.graphql.generator.federation.directives
1818

1919
import com.expediagroup.graphql.generator.annotations.GraphQLDirective
2020
import com.expediagroup.graphql.generator.directives.DirectiveMetaInformation
21+
import com.expediagroup.graphql.generator.federation.isFederationVersionAtLeast
2122
import graphql.Scalars
2223
import graphql.introspection.Introspection.DirectiveLocation
2324
import graphql.schema.GraphQLAppliedDirective
@@ -56,7 +57,7 @@ private const val OVERRIDE_DIRECTIVE_DESCRIPTION = "Overrides fields resolution
5657
/**
5758
* Creates the override directive definition
5859
*/
59-
internal fun overrideDirectiveDefinition(): graphql.schema.GraphQLDirective {
60+
internal fun overrideDirectiveDefinition(federationVersion: String = FEDERATION_SPEC_LATEST_URL): graphql.schema.GraphQLDirective {
6061
val builder = graphql.schema.GraphQLDirective.newDirective()
6162
.name(OVERRIDE_DIRECTIVE_NAME)
6263
.description(OVERRIDE_DIRECTIVE_DESCRIPTION)
@@ -66,16 +67,16 @@ internal fun overrideDirectiveDefinition(): graphql.schema.GraphQLDirective {
6667
.name(OVERRIDE_DIRECTIVE_FROM_PARAM)
6768
.description("Name of the subgraph to override field resolution")
6869
.type(GraphQLNonNull(Scalars.GraphQLString))
69-
.build()
7070
)
7171

72-
builder.argument(
73-
GraphQLArgument.newArgument()
74-
.name(OVERRIDE_DIRECTIVE_LABEL_PARAM)
75-
.description("The value must follow the format of 'percent(number)'")
76-
.type(Scalars.GraphQLString)
77-
.build()
78-
)
72+
if (isFederationVersionAtLeast(federationVersion, 2, 7)) {
73+
builder.argument(
74+
GraphQLArgument.newArgument()
75+
.name(OVERRIDE_DIRECTIVE_LABEL_PARAM)
76+
.description("The value must follow the format of 'percent(number)'")
77+
.type(Scalars.GraphQLString)
78+
)
79+
}
7980

8081
return builder.build()
8182
}
@@ -94,18 +95,22 @@ internal fun graphql.schema.GraphQLDirective.toAppliedOverrideDirective(directiv
9495

9596
val builder = GraphQLAppliedDirective.newDirective()
9697
.name(this.name)
97-
.argument(GraphQLAppliedDirectiveArgument.newArgument()
98+
.argument(
99+
GraphQLAppliedDirectiveArgument.newArgument()
98100
.name(OVERRIDE_DIRECTIVE_FROM_PARAM)
99101
.type(GraphQLNonNull(Scalars.GraphQLString))
100102
.valueProgrammatic(overrideDirective.from)
101-
.build())
103+
.build()
104+
)
102105

103106
if (!label.isNullOrEmpty()) {
104-
builder.argument(GraphQLAppliedDirectiveArgument.newArgument()
107+
builder.argument(
108+
GraphQLAppliedDirectiveArgument.newArgument()
105109
.name(OVERRIDE_DIRECTIVE_LABEL_PARAM)
106110
.type(Scalars.GraphQLString)
107111
.valueProgrammatic(label)
108-
.build())
112+
.build()
113+
)
109114
}
110115

111116
return builder.build()

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/override/OverrideDirectiveTest.kt

Lines changed: 85 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.expediagroup.graphql.generator.TopLevelObject
2020
import com.expediagroup.graphql.generator.extensions.print
2121
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig
2222
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
23+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC
2324
import com.expediagroup.graphql.generator.federation.directives.OVERRIDE_DIRECTIVE_NAME
2425
import com.expediagroup.graphql.generator.federation.toFederatedSchema
2526
import com.expediagroup.graphql.generator.federation.directives.OverrideDirective
@@ -30,7 +31,7 @@ import kotlin.test.assertNotNull
3031
class OverrideDirectiveTest {
3132

3233
@Test
33-
fun `verify override directive definition`() {
34+
fun `verify override directive definition for fed27`() {
3435
val expectedSchema =
3536
"""
3637
schema @link(import : ["@override"], url : "https://specs.apollo.dev/federation/v2.7"){
@@ -59,7 +60,7 @@ class OverrideDirectiveTest {
5960
directive @override(
6061
"Name of the subgraph to override field resolution"
6162
from: String!,
62-
"The value must follow the format of 'percent(number)'. Enterprise feature available in Federation 2.7+."
63+
"The value must follow the format of 'percent(number)'"
6364
label: String
6465
) on FIELD_DEFINITION
6566
@@ -92,7 +93,7 @@ class OverrideDirectiveTest {
9293
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.override"),
9394
hooks = FederatedSchemaGeneratorHooks(emptyList())
9495
)
95-
val schema = toFederatedSchema(config, listOf(TopLevelObject(Query())))
96+
val schema = toFederatedSchema(config, listOf(TopLevelObject(Fed27Query())))
9697
Assertions.assertEquals(expectedSchema, schema.print().trim())
9798

9899
val query = schema.getObjectType("Query")
@@ -105,12 +106,92 @@ class OverrideDirectiveTest {
105106
assertNotNull(barQuery.hasAppliedDirective(OVERRIDE_DIRECTIVE_NAME))
106107
}
107108

109+
@Test
110+
fun `verify override directive definition for fed20`() {
111+
val expectedSchema =
112+
"""
113+
schema @link(import : ["@override"], url : "https://specs.apollo.dev/federation/v2.0"){
114+
query: Query
115+
}
116+
117+
"Marks the field, argument, input field or enum value as deprecated"
118+
directive @deprecated(
119+
"The reason for the deprecation"
120+
reason: String = "No longer supported"
121+
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM_VALUE | INPUT_FIELD_DEFINITION
122+
123+
"Directs the executor to include this field or fragment only when the `if` argument is true"
124+
directive @include(
125+
"Included when true."
126+
if: Boolean!
127+
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
128+
129+
"Links definitions within the document to external schemas."
130+
directive @link(as: String, import: [link__Import], url: String!) repeatable on SCHEMA
131+
132+
"Indicates an Input Object is a OneOf Input Object."
133+
directive @oneOf on INPUT_OBJECT
134+
135+
"Overrides fields resolution logic from other subgraph. Used for migrating fields from one subgraph to another."
136+
directive @override(
137+
"Name of the subgraph to override field resolution"
138+
from: String!
139+
) on FIELD_DEFINITION
140+
141+
"Directs the executor to skip this field or fragment when the `if` argument is true."
142+
directive @skip(
143+
"Skipped when true."
144+
if: Boolean!
145+
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
146+
147+
"Exposes a URL that specifies the behaviour of this scalar."
148+
directive @specifiedBy(
149+
"The URL that specifies the behaviour of this scalar."
150+
url: String!
151+
) on SCALAR
152+
153+
type Query {
154+
_service: _Service!
155+
foo: String! @override(from : "products")
156+
}
157+
158+
type _Service {
159+
sdl: String!
160+
}
161+
162+
scalar link__Import
163+
""".trimIndent()
164+
165+
val config = FederatedSchemaGeneratorConfig(
166+
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.override"),
167+
hooks = FederatedSchemaGeneratorHooks(emptyList()).apply {
168+
this.linkSpecs[FEDERATION_SPEC] = FederatedSchemaGeneratorHooks.LinkSpec(
169+
namespace = "federation",
170+
imports = mapOf("override" to "override"),
171+
url = "https://specs.apollo.dev/federation/v2.0"
172+
)
173+
}
174+
)
175+
val schema = toFederatedSchema(config, listOf(TopLevelObject(Query())))
176+
Assertions.assertEquals(expectedSchema, schema.print().trim())
177+
178+
val query = schema.getObjectType("Query")
179+
assertNotNull(query)
180+
val fooQuery = query.getField("foo")
181+
assertNotNull(fooQuery)
182+
assertNotNull(fooQuery.hasAppliedDirective(OVERRIDE_DIRECTIVE_NAME))
183+
}
184+
108185
class Query {
109186
@OverrideDirective(from = "products")
110187
fun foo(): String = "test"
188+
}
189+
190+
class Fed27Query {
191+
@OverrideDirective(from = "products")
192+
fun foo(): String = "test"
111193

112194
@OverrideDirective(from = "products", label = "percent(50)")
113195
fun bar(): String = "test"
114196
}
115-
116197
}

0 commit comments

Comments
 (0)