Skip to content

Commit 870fa09

Browse files
author
Dariusz Kuc
authored
[generator] support repeatable directives (ExpediaGroup#1360)
Kotlin 1.6 provides support for repeatable annotations with `RUNTIME` retention. This allows to finally support repeatable directives. In order to make your directives repeatable, you need to annotate it with `kotlin.annotation.Repeatable` annotation. ```kotlin @repeatable @GraphQLDirective annotation class MyRepeatableDirective(val value: String) ``` Generates the above directive as ```graphql directive @myRepeatableDirective(value: String!) repeatable on OBJECT | INTERFACE ``` Related Issues: * resolves ExpediaGroup#590 * depends on ExpediaGroup#1358
1 parent 569982b commit 870fa09

File tree

35 files changed

+288
-126
lines changed

35 files changed

+288
-126
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -147,7 +147,7 @@ open class FederatedSchemaGeneratorHooks(private val resolvers: List<FederatedTy
147147
return originalSchema.allTypesAsList
148148
.asSequence()
149149
.filterIsInstance<GraphQLObjectType>()
150-
.filter { type -> type.getDirective(KEY_DIRECTIVE_NAME) != null }
150+
.filter { type -> type.hasDirective(KEY_DIRECTIVE_NAME) }
151151
.map { it.name }
152152
.toSet()
153153
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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,16 +21,13 @@ import graphql.introspection.Introspection.DirectiveLocation
2121

2222
/**
2323
* ```graphql
24-
* directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
24+
* directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
2525
* ```
2626
*
2727
* The @key directive is used to indicate a combination of fields that can be used to uniquely identify and fetch an object or interface. Key directive should be specified on the root base type as
2828
* well as all the corresponding federated (i.e. extended) types. Key fields specified in the directive field set should correspond to a valid field on the underlying GraphQL interface/object.
2929
* Federated extended types should also instrument all the referenced key fields with @external directive.
3030
*
31-
* NOTE: The Federation spec specifies that multiple @key directives can be applied on the field. The GraphQL spec has been recently changed to allow this behavior,
32-
* but we are currently blocked and are tracking progress in [this issue](https://github.com/ExpediaGroup/graphql-kotlin/issues/590).
33-
*
3431
* Example:
3532
* Given
3633
*
@@ -55,6 +52,7 @@ import graphql.introspection.Introspection.DirectiveLocation
5552
* @see ExtendsDirective
5653
* @see ExternalDirective
5754
*/
55+
@Repeatable
5856
@GraphQLDirective(
5957
name = KEY_DIRECTIVE_NAME,
6058
description = KEY_DIRECTIVE_DESCRIPTION,
@@ -70,4 +68,5 @@ internal val KEY_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schem
7068
.description(KEY_DIRECTIVE_DESCRIPTION)
7169
.validLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE)
7270
.argument(FIELD_SET_ARGUMENT)
71+
.repeatable(true)
7372
.build()

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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,6 +20,6 @@ import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIV
2020
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
2121
import graphql.schema.GraphQLDirectiveContainer
2222

23-
internal fun GraphQLDirectiveContainer.isFederatedType() = this.getDirective(KEY_DIRECTIVE_NAME) != null || isExtendedType()
23+
internal fun GraphQLDirectiveContainer.isFederatedType() = this.getDirectives(KEY_DIRECTIVE_NAME).isNotEmpty() || isExtendedType()
2424

2525
internal fun GraphQLDirectiveContainer.isExtendedType() = this.getDirective(EXTENDS_DIRECTIVE_NAME) != null

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020 Expedia, Inc
2+
* Copyright 2022 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.
@@ -27,7 +27,7 @@ internal fun GraphQLSchema.addDirectivesIfNotPresent(directives: List<GraphQLDir
2727
val newBuilder = GraphQLSchema.newSchema(this)
2828

2929
directives.forEach {
30-
if (this.getDirective(it.name) == null) {
30+
if (!this.allDirectivesByName.containsKey(it.name)) {
3131
newBuilder.additionalDirective(it)
3232
}
3333
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -48,24 +48,24 @@ internal class FederatedSchemaValidator {
4848
internal fun validateGraphQLType(type: GraphQLType) {
4949
val unwrappedType = GraphQLTypeUtil.unwrapAll(type)
5050
if (unwrappedType is GraphQLObjectType && unwrappedType.isFederatedType()) {
51-
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.directivesByName)
51+
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.allDirectivesByName)
5252
} else if (unwrappedType is GraphQLInterfaceType && unwrappedType.isFederatedType()) {
53-
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.directivesByName)
53+
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.allDirectivesByName)
5454
}
5555
}
5656

57-
private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directives: Map<String, GraphQLDirective>) {
57+
private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directiveMap: Map<String, List<GraphQLDirective>>) {
5858
val errors = mutableListOf<String>()
5959
val fieldMap = fields.associateBy { it.name }
60-
val extendedType = directives.containsKey(EXTENDS_DIRECTIVE_NAME)
60+
val extendedType = directiveMap.containsKey(EXTENDS_DIRECTIVE_NAME)
6161

6262
// [OK] @key directive is specified
6363
// [OK] @key references valid existing fields
6464
// [OK] @key on @extended type references @external fields
6565
// [ERROR] @key references fields resulting in list
6666
// [ERROR] @key references fields resulting in union
6767
// [ERROR] @key references fields resulting in interface
68-
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directives, fieldMap, extendedType))
68+
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directiveMap, fieldMap, extendedType))
6969

7070
for (field in fields) {
7171
if (field.getDirective(REQUIRES_DIRECTIVE_NAME) != null) {

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

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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,28 +24,30 @@ import graphql.schema.GraphQLFieldDefinition
2424
internal fun validateDirective(
2525
validatedType: String,
2626
targetDirective: String,
27-
directives: Map<String, GraphQLDirective>,
27+
directiveMap: Map<String, List<GraphQLDirective>>,
2828
fieldMap: Map<String, GraphQLFieldDefinition>,
2929
extendedType: Boolean
3030
): List<String> {
3131
val validationErrors = mutableListOf<String>()
32-
val directive = directives[targetDirective]
32+
val directives = directiveMap[targetDirective]
3333

34-
if (directive == null) {
34+
if (directives == null) {
3535
validationErrors.add("@$targetDirective directive is missing on federated $validatedType type")
3636
} else {
37-
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value
38-
val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty()
39-
if (fieldSet.isEmpty()) {
40-
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")
41-
} else {
42-
// validate directive field set selection
43-
val directiveInfo = DirectiveInfo(
44-
directiveName = targetDirective,
45-
fieldSet = fieldSet.joinToString(" "),
46-
typeName = validatedType
47-
)
48-
validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors)
37+
for (directive in directives) {
38+
val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value
39+
val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty()
40+
if (fieldSet.isEmpty()) {
41+
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")
42+
} else {
43+
// validate directive field set selection
44+
val directiveInfo = DirectiveInfo(
45+
directiveName = targetDirective,
46+
fieldSet = fieldSet.joinToString(" "),
47+
typeName = validatedType
48+
)
49+
validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors)
50+
}
4951
}
5052
}
5153
return validationErrors

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -40,7 +40,7 @@ internal fun validateProvidesDirective(federatedType: String, field: GraphQLFiel
4040
validateDirective(
4141
"$federatedType.${field.name}",
4242
PROVIDES_DIRECTIVE_NAME,
43-
field.directivesByName,
43+
field.allDirectivesByName,
4444
returnTypeFields,
4545
true
4646
)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -25,7 +25,7 @@ import graphql.schema.GraphQLFieldDefinition
2525
internal fun validateRequiresDirective(validatedType: String, validatedField: GraphQLFieldDefinition, fieldMap: Map<String, GraphQLFieldDefinition>, extendedType: Boolean): List<String> {
2626
val errors = mutableListOf<String>()
2727
if (extendedType) {
28-
errors.addAll(validateDirective("$validatedType.${validatedField.name}", REQUIRES_DIRECTIVE_NAME, validatedField.directivesByName, fieldMap, extendedType))
28+
errors.addAll(validateDirective("$validatedType.${validatedField.name}", REQUIRES_DIRECTIVE_NAME, validatedField.allDirectivesByName, fieldMap, extendedType))
2929
} else {
3030
errors.add("base $validatedType type has fields marked with @requires directive, validatedField=${validatedField.name}")
3131
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -58,7 +58,7 @@ private val FEDERATED_SDL =
5858
directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
5959
6060
"Space separated list of primary keys needed to access federated object"
61-
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
61+
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
6262
6363
"Specifies required input field set from the base type for a resolver"
6464
directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
@@ -75,18 +75,20 @@ private val FEDERATED_SDL =
7575
url: String!
7676
) on SCALAR
7777
78-
interface Product @extends @key(fields : "id") {
78+
interface Product @extends @key(fields : "id") @key(fields : "upc") {
7979
id: String! @external
8080
reviews: [Review!]!
81+
upc: String! @external
8182
}
8283
8384
union _Entity = Book | User
8485
85-
type Book implements Product @extends @key(fields : "id") {
86+
type Book implements Product @extends @key(fields : "id") @key(fields : "upc") {
8687
author: User! @provides(fields : "name")
8788
id: String! @external
8889
reviews: [Review!]!
8990
shippingCost: String! @requires(fields : "weight")
91+
upc: String! @external
9092
weight: Float! @external
9193
}
9294
@@ -136,7 +138,7 @@ class FederatedSchemaGeneratorTest {
136138
assertEquals(FEDERATED_SDL, schema.print().trim())
137139
val productType = schema.getObjectType("Book")
138140
assertNotNull(productType)
139-
assertNotNull(productType.getDirective(KEY_DIRECTIVE_NAME))
141+
assertNotNull(productType.hasDirective(KEY_DIRECTIVE_NAME))
140142

141143
val entityUnion = schema.getType(ENTITY_UNION_NAME) as? GraphQLUnionType
142144
assertNotNull(entityUnion)
@@ -185,7 +187,7 @@ class FederatedSchemaGeneratorTest {
185187
directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
186188
187189
"Space separated list of primary keys needed to access federated object"
188-
directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
190+
directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE
189191
190192
"Marks target object as extending part of the federated schema"
191193
directive @extends on OBJECT | INTERFACE
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._1
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._01
1818

1919
import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective
2020
import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._2
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._02
1818

1919
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2020
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._3
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._03
1818

1919
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2020
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._4
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._04
1818

1919
import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
2020
import com.expediagroup.graphql.generator.federation.directives.FieldSet
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._5
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._05
1818

1919
import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective
2020
import com.expediagroup.graphql.generator.federation.directives.FieldSet
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._6
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._06
1818

1919
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2020
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._7
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._07
1818

1919
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2020
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2021 Expedia, Inc
2+
* Copyright 2022 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.
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._8
17+
package com.expediagroup.graphql.generator.federation.data.integration.key.failure._08
1818

1919
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2020
import com.expediagroup.graphql.generator.federation.directives.KeyDirective

0 commit comments

Comments
 (0)