Skip to content

Commit ea770ef

Browse files
authored
fix: move federation validations to didBuildSchema hook (#1883)
### 📝 Description We were previously validating usage of `@key`/`@requires`/`@provides` directives when constructing the schema. While it generally worked fine, it is possible to craft a schema when generated types still reference types under construction (see: #1858) and we would have to skip those validations. By moving the validation to the last phase of schema generation, we always have a valid GraphQL schema at that point with resolved references and we can apply validations against all entities at once so we can have single error message with all validations (vs failing the build for each invalid entity). ### 🔗 Related Issues #1858
1 parent 716809e commit ea770ef

File tree

5 files changed

+43
-80
lines changed

5 files changed

+43
-80
lines changed

generator/graphql-kotlin-federation/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ plugins {
77
dependencies {
88
api(projects.graphqlKotlinSchemaGenerator)
99
api(libs.federation)
10-
implementation(libs.slf4j)
1110
testImplementation(libs.reactor.core)
1211
testImplementation(libs.reactor.extensions)
1312
testImplementation(libs.junit.params)

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import com.expediagroup.graphql.generator.federation.directives.toAppliedLinkDir
5959
import com.expediagroup.graphql.generator.federation.directives.toAppliedRequiresScopesDirective
6060
import com.expediagroup.graphql.generator.federation.exception.DuplicateSpecificationLinkImport
6161
import com.expediagroup.graphql.generator.federation.exception.IncorrectFederatedDirectiveUsage
62+
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema
6263
import com.expediagroup.graphql.generator.federation.exception.UnknownSpecificationException
6364
import com.expediagroup.graphql.generator.federation.execution.EntitiesDataFetcher
6465
import com.expediagroup.graphql.generator.federation.execution.FederatedTypeResolver
@@ -80,6 +81,7 @@ import graphql.schema.FieldCoordinates
8081
import graphql.schema.GraphQLAppliedDirective
8182
import graphql.schema.GraphQLCodeRegistry
8283
import graphql.schema.GraphQLDirective
84+
import graphql.schema.GraphQLDirectiveContainer
8385
import graphql.schema.GraphQLNamedType
8486
import graphql.schema.GraphQLObjectType
8587
import graphql.schema.GraphQLScalarType
@@ -264,11 +266,6 @@ open class FederatedSchemaGeneratorHooks(
264266
}
265267
}
266268

267-
override fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType): GraphQLType {
268-
validator.validateGraphQLType(generatedType)
269-
return super.didGenerateGraphQLType(type, generatedType)
270-
}
271-
272269
override fun didGenerateDirective(directiveInfo: DirectiveMetaInformation, directive: GraphQLDirective): GraphQLDirective {
273270
if (optInFederationV2) {
274271
// namespace generated directive if needed
@@ -412,10 +409,19 @@ open class FederatedSchemaGeneratorHooks(
412409
} else {
413410
KEY_DIRECTIVE_NAME
414411
}
415-
return originalSchema.allTypesAsList
416-
.asSequence()
417-
.filterIsInstance<GraphQLObjectType>()
412+
val entities = originalSchema.allTypesAsList
413+
.filterIsInstance<GraphQLDirectiveContainer>()
418414
.filter { type -> type.hasAppliedDirective(keyDirectiveName) }
415+
416+
val errors = entities
417+
.filterIsInstance<GraphQLNamedType>()
418+
.map { type -> validator.validateGraphQLType(type) }
419+
.flatten()
420+
if (errors.isNotEmpty()) {
421+
throw InvalidFederatedSchema(errors)
422+
}
423+
424+
return entities.filterIsInstance<GraphQLObjectType>()
419425
.map { it.name }
420426
.toSet()
421427
}

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

Lines changed: 7 additions & 9 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.
@@ -19,7 +19,6 @@ package com.expediagroup.graphql.generator.federation.validation
1919
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
2020
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_NAME
2121
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
22-
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema
2322
import graphql.schema.GraphQLAppliedDirective
2423
import graphql.schema.GraphQLDirectiveContainer
2524
import graphql.schema.GraphQLFieldDefinition
@@ -44,16 +43,18 @@ internal class FederatedSchemaValidator {
4443
* - field sets cannot reference unions
4544
* - list and interfaces can only be referenced from `@requires` and `@provides`
4645
*/
47-
internal fun validateGraphQLType(type: GraphQLType) {
46+
internal fun validateGraphQLType(type: GraphQLType): List<String> {
4847
val unwrappedType = GraphQLTypeUtil.unwrapAll(type)
49-
if (unwrappedType is GraphQLObjectType && unwrappedType.isFederatedType()) {
48+
return if (unwrappedType is GraphQLObjectType && unwrappedType.isFederatedType()) {
5049
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.allAppliedDirectivesByName)
5150
} else if (unwrappedType is GraphQLInterfaceType && unwrappedType.isFederatedType()) {
5251
validate(unwrappedType.name, unwrappedType.fieldDefinitions, unwrappedType.allAppliedDirectivesByName)
52+
} else {
53+
emptyList()
5354
}
5455
}
5556

56-
private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directiveMap: Map<String, List<GraphQLAppliedDirective>>) {
57+
private fun validate(federatedType: String, fields: List<GraphQLFieldDefinition>, directiveMap: Map<String, List<GraphQLAppliedDirective>>): List<String> {
5758
val errors = mutableListOf<String>()
5859
val fieldMap = fields.associateBy { it.name }
5960

@@ -82,10 +83,7 @@ internal class FederatedSchemaValidator {
8283
}
8384
}
8485
}
85-
86-
if (errors.isNotEmpty()) {
87-
throw InvalidFederatedSchema(errors)
88-
}
86+
return errors
8987
}
9088

9189
private fun GraphQLDirectiveContainer.isFederatedType() = this.getAppliedDirectives(KEY_DIRECTIVE_NAME).isNotEmpty()

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

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,7 @@ import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTI
2121
import graphql.schema.GraphQLDirectiveContainer
2222
import graphql.schema.GraphQLFieldDefinition
2323
import graphql.schema.GraphQLNamedType
24-
import graphql.schema.GraphQLType
25-
import graphql.schema.GraphQLTypeReference
2624
import graphql.schema.GraphQLTypeUtil
27-
import org.slf4j.Logger
28-
import org.slf4j.LoggerFactory
29-
30-
private val logger: Logger = LoggerFactory.getLogger("ValidateFieldSetSelection")
3125

3226
internal fun validateFieldSetSelection(
3327
validatedDirective: DirectiveInfo,
@@ -42,27 +36,14 @@ internal fun validateFieldSetSelection(
4236
errors.add("$validatedDirective specifies invalid field set - field set specifies field that does not exist, field=${selection.field}")
4337
} else {
4438
val currentFieldType = currentField.type
45-
if (currentFieldType.isReferenceType()) {
46-
logger.warn("Unable to validate field set selection as one of the fields is a type reference.")
47-
} else {
48-
val isExternal = isExternalPath || GraphQLTypeUtil.unwrapAll(currentFieldType).isExternalPath() || currentField.isExternalType()
49-
if (REQUIRES_DIRECTIVE_NAME == validatedDirective.directiveName && GraphQLTypeUtil.isLeaf(currentFieldType) && !isExternal) {
50-
errors.add("$validatedDirective specifies invalid field set - @requires should reference only @external fields, field=${selection.field}")
51-
}
52-
validateFieldSelection(validatedDirective, selection, currentFieldType, errors, isExternal)
39+
val isExternal = isExternalPath || GraphQLTypeUtil.unwrapAll(currentFieldType).isExternalPath() || currentField.isExternalType()
40+
if (REQUIRES_DIRECTIVE_NAME == validatedDirective.directiveName && GraphQLTypeUtil.isLeaf(currentFieldType) && !isExternal) {
41+
errors.add("$validatedDirective specifies invalid field set - @requires should reference only @external fields, field=${selection.field}")
5342
}
43+
validateFieldSelection(validatedDirective, selection, currentFieldType, errors, isExternal)
5444
}
5545
}
5646
}
5747

5848
private fun GraphQLDirectiveContainer.isExternalType(): Boolean = this.getAppliedDirectives(EXTERNAL_DIRECTIVE_NAME).isNotEmpty()
5949
private fun GraphQLNamedType.isExternalPath(): Boolean = this is GraphQLDirectiveContainer && this.isExternalType()
60-
61-
// workaround to GraphQLType.unwrapAll() which tries to cast GraphQLTypeReference to GraphQLUnmodifiedType
62-
private fun GraphQLType.isReferenceType(): Boolean {
63-
var type = this
64-
while (GraphQLTypeUtil.isWrapped(type)) {
65-
type = GraphQLTypeUtil.unwrapOne(type)
66-
}
67-
return type is GraphQLTypeReference
68-
}

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

Lines changed: 18 additions & 39 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,6 @@ import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NA
2121
import com.expediagroup.graphql.generator.federation.directives.PROVIDES_DIRECTIVE_NAME
2222
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
2323
import com.expediagroup.graphql.generator.federation.directives.keyDirectiveDefinition
24-
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema
2524
import com.expediagroup.graphql.generator.federation.externalDirective
2625
import com.expediagroup.graphql.generator.federation.getKeyDirective
2726
import com.expediagroup.graphql.generator.federation.types.FIELD_SET_ARGUMENT
@@ -34,7 +33,7 @@ import graphql.schema.GraphQLObjectType
3433
import org.junit.jupiter.api.Test
3534
import org.junit.jupiter.api.assertDoesNotThrow
3635
import kotlin.test.assertEquals
37-
import kotlin.test.assertFailsWith
36+
import kotlin.test.assertTrue
3837

3938
class FederatedSchemaValidatorTest {
4039

@@ -89,9 +88,10 @@ class FederatedSchemaValidatorTest {
8988
)
9089
.build()
9190

92-
assertFailsWith<InvalidFederatedSchema> {
93-
FederatedSchemaValidator().validateGraphQLType(typeToValidate)
94-
}
91+
val errors = FederatedSchemaValidator().validateGraphQLType(typeToValidate)
92+
assertTrue(errors.isNotEmpty())
93+
assertEquals(1, errors.size)
94+
assertEquals("@key directive on Foo is missing field information", errors[0])
9595
}
9696

9797
/**
@@ -114,17 +114,10 @@ class FederatedSchemaValidatorTest {
114114
.withAppliedDirective(keyDirectiveType)
115115
.build()
116116

117-
val result = kotlin.runCatching {
118-
FederatedSchemaValidator().validateGraphQLType(typeToValidate)
119-
}
120-
121-
val expectedError =
122-
"""
123-
Invalid federated schema:
124-
- @key directive on Foo is missing field information
125-
""".trimIndent()
126-
127-
assertEquals(expectedError, result.exceptionOrNull()?.message)
117+
val errors = FederatedSchemaValidator().validateGraphQLType(typeToValidate)
118+
assertTrue(errors.isNotEmpty())
119+
assertEquals(1, errors.size)
120+
assertEquals("@key directive on Foo is missing field information", errors[0])
128121
}
129122

130123
/**
@@ -155,17 +148,10 @@ class FederatedSchemaValidatorTest {
155148
.withAppliedDirective(EXTENDS_DIRECTIVE_TYPE.toAppliedDirective())
156149
.build()
157150

158-
val result = kotlin.runCatching {
159-
FederatedSchemaValidator().validateGraphQLType(typeToValidate)
160-
}
161-
162-
val expectedError =
163-
"""
164-
Invalid federated schema:
165-
- @provides directive is specified on a Foo.bar field but it does not return an object type
166-
""".trimIndent()
167-
168-
assertEquals(expectedError, result.exceptionOrNull()?.message)
151+
val errors = FederatedSchemaValidator().validateGraphQLType(typeToValidate)
152+
assertTrue(errors.isNotEmpty())
153+
assertEquals(1, errors.size)
154+
assertEquals("@provides directive is specified on a Foo.bar field but it does not return an object type", errors[0])
169155
}
170156

171157
/**
@@ -196,16 +182,9 @@ class FederatedSchemaValidatorTest {
196182
.withAppliedDirective(EXTENDS_DIRECTIVE_TYPE.toAppliedDirective())
197183
.build()
198184

199-
val result = kotlin.runCatching {
200-
FederatedSchemaValidator().validateGraphQLType(typeToValidate)
201-
}
202-
203-
val expectedError =
204-
"""
205-
Invalid federated schema:
206-
- @requires directive on Foo.bar is missing field information
207-
""".trimIndent()
208-
209-
assertEquals(expectedError, result.exceptionOrNull()?.message)
185+
val errors = FederatedSchemaValidator().validateGraphQLType(typeToValidate)
186+
assertTrue(errors.isNotEmpty())
187+
assertEquals(1, errors.size)
188+
assertEquals("@requires directive on Foo.bar is missing field information", errors[0])
210189
}
211190
}

0 commit comments

Comments
 (0)