Skip to content

Commit e4a4aac

Browse files
smyrickShane Myrick
andauthored
Add extends directive if not defined (#702)
* Add extends directive if not defined Since the extends directive is always used in federation on the Query object we should always have the directive definition in the schema. However it may already be added if the generated schema uses it, so only add the definition if it is not yet defined * Add all directive definitions even if not used Co-authored-by: Shane Myrick <[email protected]>
1 parent 2f3564e commit e4a4aac

File tree

18 files changed

+274
-51
lines changed

18 files changed

+274
-51
lines changed

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

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,16 @@ package com.expediagroup.graphql.federation
1919
import com.expediagroup.graphql.annotations.GraphQLName
2020
import com.expediagroup.graphql.directives.DEPRECATED_DIRECTIVE_NAME
2121
import com.expediagroup.graphql.extensions.print
22-
import com.expediagroup.graphql.federation.directives.EXTENDS_DIRECTIVE_NAME
23-
import com.expediagroup.graphql.federation.directives.EXTERNAL_DIRECTIVE_NAME
22+
import com.expediagroup.graphql.federation.directives.EXTENDS_DIRECTIVE_TYPE
23+
import com.expediagroup.graphql.federation.directives.EXTERNAL_DIRECTIVE_TYPE
2424
import com.expediagroup.graphql.federation.directives.FieldSet
2525
import com.expediagroup.graphql.federation.directives.KEY_DIRECTIVE_NAME
26-
import com.expediagroup.graphql.federation.directives.PROVIDES_DIRECTIVE_NAME
27-
import com.expediagroup.graphql.federation.directives.REQUIRES_DIRECTIVE_NAME
28-
import com.expediagroup.graphql.federation.directives.extendsDirectiveType
26+
import com.expediagroup.graphql.federation.directives.KEY_DIRECTIVE_TYPE
27+
import com.expediagroup.graphql.federation.directives.PROVIDES_DIRECTIVE_TYPE
28+
import com.expediagroup.graphql.federation.directives.REQUIRES_DIRECTIVE_TYPE
2929
import com.expediagroup.graphql.federation.execution.EntityResolver
3030
import com.expediagroup.graphql.federation.execution.FederatedTypeRegistry
31+
import com.expediagroup.graphql.federation.extensions.addDirectivesIfNotPresent
3132
import com.expediagroup.graphql.federation.types.ANY_SCALAR_TYPE
3233
import com.expediagroup.graphql.federation.types.FIELD_SET_SCALAR_TYPE
3334
import com.expediagroup.graphql.federation.types.SERVICE_FIELD_DEFINITION
@@ -56,7 +57,8 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede
5657
private val emptyQueryRegex = "^type Query \\{$\\s+^\\}$\\s+".toRegex(setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
5758
private val validator = FederatedSchemaValidator()
5859

59-
private val directivesToInclude = listOf(EXTENDS_DIRECTIVE_NAME, EXTERNAL_DIRECTIVE_NAME, KEY_DIRECTIVE_NAME, PROVIDES_DIRECTIVE_NAME, REQUIRES_DIRECTIVE_NAME, DEPRECATED_DIRECTIVE_NAME)
60+
private val federatedDirectiveTypes: List<GraphQLDirective> = listOf(EXTERNAL_DIRECTIVE_TYPE, REQUIRES_DIRECTIVE_TYPE, PROVIDES_DIRECTIVE_TYPE, KEY_DIRECTIVE_TYPE, EXTENDS_DIRECTIVE_TYPE)
61+
private val directivesToInclude: List<String> = federatedDirectiveTypes.map { it.name }.plus(DEPRECATED_DIRECTIVE_NAME)
6062
private val customDirectivePredicate: Predicate<GraphQLDirective> = Predicate { directivesToInclude.contains(it.name) }
6163

6264
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
@@ -72,20 +74,25 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede
7274
override fun willBuildSchema(builder: GraphQLSchema.Builder): GraphQLSchema.Builder {
7375
val originalSchema = builder.build()
7476
val originalQuery = originalSchema.queryType
75-
7677
val federatedCodeRegistry = GraphQLCodeRegistry.newCodeRegistry(originalSchema.codeRegistry)
77-
val federatedSchema = GraphQLSchema.newSchema(originalSchema)
78+
79+
// Add all the federation directives if they are not present
80+
val federatedSchemaBuilder = originalSchema.addDirectivesIfNotPresent(federatedDirectiveTypes)
81+
82+
// Modify the query type to have the service field and extends directive
7883
val federatedQuery = GraphQLObjectType.newObject(originalQuery)
7984
.field(SERVICE_FIELD_DEFINITION)
80-
.withDirective(extendsDirectiveType)
85+
.withDirective(EXTENDS_DIRECTIVE_TYPE)
8186

8287
/**
83-
* SDL returned by _service query should NOT contain
84-
* - default schema definition
85-
* - empty Query type
86-
* - any directive definitions
87-
* - any custom directives
88-
* - new federated scalars
88+
* Register the data fetcher for the SDL returned by _service field.
89+
*
90+
* It should NOT contain:
91+
* - default schema definition
92+
* - empty Query type
93+
* - any directive definitions
94+
* - any custom directives
95+
* - new federated scalars
8996
*/
9097
val sdl = originalSchema.print(includeDefaultSchemaDefinition = false, includeDirectivesFilter = customDirectivePredicate)
9198
.replace(directiveDefinitionRegex, "")
@@ -101,17 +108,17 @@ open class FederatedSchemaGeneratorHooks(private val federatedTypeRegistry: Fede
101108
.map { it.name }
102109
.toSet()
103110

104-
// register new federated queries
111+
// Add the _entities field to the query
105112
if (entityTypeNames.isNotEmpty()) {
106113
val entityField = generateEntityFieldDefinition(entityTypeNames)
107114
federatedQuery.field(entityField)
108115

109116
federatedCodeRegistry.dataFetcher(FieldCoordinates.coordinates(originalQuery.name, entityField.name), EntityResolver(federatedTypeRegistry))
110117
federatedCodeRegistry.typeResolver("_Entity") { env: TypeResolutionEnvironment -> env.schema.getObjectType(env.getObjectName()) }
111-
federatedSchema.additionalType(ANY_SCALAR_TYPE)
118+
federatedSchemaBuilder.additionalType(ANY_SCALAR_TYPE)
112119
}
113120

114-
return federatedSchema.query(federatedQuery.build())
121+
return federatedSchemaBuilder.query(federatedQuery.build())
115122
.codeRegistry(federatedCodeRegistry.build())
116123
}
117124

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717
package com.expediagroup.graphql.federation.directives
1818

1919
import com.expediagroup.graphql.annotations.GraphQLDirective
20-
import graphql.introspection.Introspection
21-
22-
internal const val EXTENDS_DIRECTIVE_NAME = "extends"
23-
private const val DESCRIPTION = "Marks target object as extending part of the federated schema"
20+
import graphql.introspection.Introspection.DirectiveLocation
2421

2522
/**
2623
* ```graphql
@@ -55,12 +52,15 @@ private const val DESCRIPTION = "Marks target object as extending part of the fe
5552
@GraphQLDirective(
5653
name = EXTENDS_DIRECTIVE_NAME,
5754
description = DESCRIPTION,
58-
locations = [Introspection.DirectiveLocation.OBJECT, Introspection.DirectiveLocation.INTERFACE]
55+
locations = [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE]
5956
)
6057
annotation class ExtendsDirective
6158

62-
internal val extendsDirectiveType: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
59+
internal const val EXTENDS_DIRECTIVE_NAME = "extends"
60+
private const val DESCRIPTION = "Marks target object as extending part of the federated schema"
61+
62+
internal val EXTENDS_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
6363
.name(EXTENDS_DIRECTIVE_NAME)
6464
.description(DESCRIPTION)
65-
.validLocations(Introspection.DirectiveLocation.OBJECT, Introspection.DirectiveLocation.INTERFACE)
65+
.validLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE)
6666
.build()

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.federation.directives
1818

1919
import com.expediagroup.graphql.annotations.GraphQLDirective
20-
import graphql.introspection.Introspection
20+
import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
@@ -53,9 +53,16 @@ import graphql.introspection.Introspection
5353
*/
5454
@GraphQLDirective(
5555
name = EXTERNAL_DIRECTIVE_NAME,
56-
description = "Marks target field as external meaning it will be resolved by federated schema",
57-
locations = [Introspection.DirectiveLocation.FIELD_DEFINITION]
56+
description = EXTERNAL_DIRECTIVE_DESCRIPTION,
57+
locations = [DirectiveLocation.FIELD_DEFINITION]
5858
)
5959
annotation class ExternalDirective
6060

6161
internal const val EXTERNAL_DIRECTIVE_NAME = "external"
62+
private const val EXTERNAL_DIRECTIVE_DESCRIPTION = "Marks target field as external meaning it will be resolved by federated schema"
63+
64+
internal val EXTERNAL_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
65+
.name(EXTERNAL_DIRECTIVE_NAME)
66+
.description(EXTERNAL_DIRECTIVE_DESCRIPTION)
67+
.validLocations(DirectiveLocation.FIELD_DEFINITION)
68+
.build()

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package com.expediagroup.graphql.federation.directives
1818

19+
import com.expediagroup.graphql.federation.types.FIELD_SET_SCALAR_TYPE
20+
import graphql.schema.GraphQLArgument
21+
1922
/**
2023
* Annotation representing _FieldSet scalar type that is used to represent a set of fields.
2124
*
@@ -29,3 +32,10 @@ package com.expediagroup.graphql.federation.directives
2932
* @see com.expediagroup.graphql.federation.types.FIELD_SET_SCALAR_TYPE
3033
*/
3134
annotation class FieldSet(val value: String)
35+
36+
internal const val FIELD_SET_ARGUMENT_NAME = "fields"
37+
38+
internal val FIELD_SET_ARGUMENT = GraphQLArgument.newArgument()
39+
.name(FIELD_SET_ARGUMENT_NAME)
40+
.type(FIELD_SET_SCALAR_TYPE)
41+
.build()

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.federation.directives
1818

1919
import com.expediagroup.graphql.annotations.GraphQLDirective
20-
import graphql.introspection.Introspection
20+
import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
@@ -56,9 +56,17 @@ import graphql.introspection.Introspection
5656
*/
5757
@GraphQLDirective(
5858
name = KEY_DIRECTIVE_NAME,
59-
description = "Space separated list of primary keys needed to access federated object",
60-
locations = [Introspection.DirectiveLocation.OBJECT, Introspection.DirectiveLocation.INTERFACE]
59+
description = KEY_DIRECTIVE_DESCRIPTION,
60+
locations = [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE]
6161
)
6262
annotation class KeyDirective(val fields: FieldSet)
6363

6464
internal const val KEY_DIRECTIVE_NAME = "key"
65+
private const val KEY_DIRECTIVE_DESCRIPTION = "Space separated list of primary keys needed to access federated object"
66+
67+
internal val KEY_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
68+
.name(KEY_DIRECTIVE_NAME)
69+
.description(KEY_DIRECTIVE_DESCRIPTION)
70+
.validLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE)
71+
.argument(FIELD_SET_ARGUMENT)
72+
.build()

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.federation.directives
1818

1919
import com.expediagroup.graphql.annotations.GraphQLDirective
20-
import graphql.introspection.Introspection
20+
import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
@@ -69,9 +69,17 @@ import graphql.introspection.Introspection
6969
*/
7070
@GraphQLDirective(
7171
name = PROVIDES_DIRECTIVE_NAME,
72-
description = "Specifies the base type field set that will be selectable by the gateway",
73-
locations = [Introspection.DirectiveLocation.FIELD_DEFINITION]
72+
description = PROVIDES_DIRECTIVE_DESCRIPTION,
73+
locations = [DirectiveLocation.FIELD_DEFINITION]
7474
)
7575
annotation class ProvidesDirective(val fields: FieldSet)
7676

7777
internal const val PROVIDES_DIRECTIVE_NAME = "provides"
78+
private const val PROVIDES_DIRECTIVE_DESCRIPTION = "Specifies the base type field set that will be selectable by the gateway"
79+
80+
internal val PROVIDES_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
81+
.name(PROVIDES_DIRECTIVE_NAME)
82+
.description(PROVIDES_DIRECTIVE_DESCRIPTION)
83+
.validLocations(DirectiveLocation.FIELD_DEFINITION)
84+
.argument(FIELD_SET_ARGUMENT)
85+
.build()

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package com.expediagroup.graphql.federation.directives
1818

1919
import com.expediagroup.graphql.annotations.GraphQLDirective
20-
import graphql.introspection.Introspection
20+
import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
@@ -74,9 +74,17 @@ import graphql.introspection.Introspection
7474
*/
7575
@GraphQLDirective(
7676
name = REQUIRES_DIRECTIVE_NAME,
77-
description = "Specifies required input field set from the base type for a resolver",
78-
locations = [Introspection.DirectiveLocation.FIELD_DEFINITION]
77+
description = REQUIRES_DIRECTIVE_DESCRIPTION,
78+
locations = [DirectiveLocation.FIELD_DEFINITION]
7979
)
8080
annotation class RequiresDirective(val fields: FieldSet)
8181

8282
internal const val REQUIRES_DIRECTIVE_NAME = "requires"
83+
private const val REQUIRES_DIRECTIVE_DESCRIPTION = "Specifies required input field set from the base type for a resolver"
84+
85+
internal val REQUIRES_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schema.GraphQLDirective.newDirective()
86+
.name(REQUIRES_DIRECTIVE_NAME)
87+
.description(REQUIRES_DIRECTIVE_DESCRIPTION)
88+
.validLocations(DirectiveLocation.FIELD_DEFINITION)
89+
.argument(FIELD_SET_ARGUMENT)
90+
.build()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2020 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.federation.extensions
18+
19+
import graphql.schema.GraphQLDirective
20+
import graphql.schema.GraphQLSchema
21+
22+
/**
23+
* Add all the directives to the schema if they are not present.
24+
* Returns a new schema builder so you can continue adding more types if needed.
25+
*/
26+
internal fun GraphQLSchema.addDirectivesIfNotPresent(directives: List<GraphQLDirective>): GraphQLSchema.Builder {
27+
val newBuilder = GraphQLSchema.newSchema(this)
28+
29+
directives.forEach {
30+
if (this.getDirective(it.name) == null) {
31+
newBuilder.additionalDirective(it)
32+
}
33+
}
34+
35+
return newBuilder
36+
}

graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/federation/validation/validateDirective.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.expediagroup.graphql.federation.validation
1818

19+
import com.expediagroup.graphql.federation.directives.FIELD_SET_ARGUMENT_NAME
1920
import com.expediagroup.graphql.federation.directives.FieldSet
2021
import graphql.schema.GraphQLDirective
2122
import graphql.schema.GraphQLFieldDefinition
@@ -33,7 +34,7 @@ internal fun validateDirective(
3334
if (directive == null) {
3435
validationErrors.add("@$targetDirective directive is missing on federated $validatedType type")
3536
} else {
36-
val fieldSetValue = (directive.getArgument("fields")?.value as? FieldSet)?.value
37+
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.value as? FieldSet)?.value
3738
val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty()
3839
if (fieldSet.isEmpty()) {
3940
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")

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

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,50 @@ class FederatedSchemaGeneratorTest {
141141
query: Query
142142
}
143143
144-
type Query {
144+
"Directs the executor to include this field or fragment only when the `if` argument is true"
145+
directive @include(
146+
"Included when true."
147+
if: Boolean!
148+
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
149+
150+
"Directs the executor to skip this field or fragment when the `if`'argument is true."
151+
directive @skip(
152+
"Skipped when true."
153+
if: Boolean!
154+
) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
155+
156+
"Marks the field or enum value as deprecated"
157+
directive @deprecated(
158+
"The reason for the deprecation"
159+
reason: String! = "No longer supported"
160+
) on FIELD_DEFINITION | ENUM_VALUE
161+
162+
"Marks target field as external meaning it will be resolved by federated schema"
163+
directive @external on FIELD_DEFINITION
164+
165+
"Specifies required input field set from the base type for a resolver"
166+
directive @requires(fields: _FieldSet) on FIELD_DEFINITION
167+
168+
"Specifies the base type field set that will be selectable by the gateway"
169+
directive @provides(fields: _FieldSet) on FIELD_DEFINITION
170+
171+
"Space separated list of primary keys needed to access federated object"
172+
directive @key(fields: _FieldSet) on OBJECT | INTERFACE
173+
174+
"Marks target object as extending part of the federated schema"
175+
directive @extends on OBJECT | INTERFACE
176+
177+
type Query @extends {
145178
_service: _Service
146179
hello(name: String!): String!
147180
}
148181
149182
type _Service {
150183
sdl: String!
151184
}
185+
186+
"Federation type representing set of fields"
187+
scalar _FieldSet
152188
""".trimIndent()
153189

154190
val config = FederatedSchemaGeneratorConfig(
@@ -157,7 +193,7 @@ class FederatedSchemaGeneratorTest {
157193
)
158194

159195
val schema = toFederatedSchema(config, listOf(TopLevelObject(SimpleQuery())))
160-
assertEquals(expectedSchema, schema.print(includeDirectives = false).trim())
196+
assertEquals(expectedSchema, schema.print().trim())
161197
}
162198

163199
@Test
@@ -181,6 +217,9 @@ class FederatedSchemaGeneratorTest {
181217
type _Service {
182218
sdl: String!
183219
}
220+
221+
"Federation type representing set of fields"
222+
scalar _FieldSet
184223
""".trimIndent()
185224

186225
val config = FederatedSchemaGeneratorConfig(

0 commit comments

Comments
 (0)