Skip to content

Commit aab055d

Browse files
fix: stop validating @requires field set contents (#2165) (#2166)
cherry-pick a955e0d Co-authored-by: David Glasser <glasser@apollographql.com>
1 parent 4080f89 commit aab055d

6 files changed

Lines changed: 80 additions & 85 deletions

File tree

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,10 @@ internal class FederatedSchemaValidator {
3737
* Validates target GraphQLType whether it is a valid federated object.
3838
*
3939
* Verifies:
40-
* - @key, @provides and @requires field sets reference existing fields
41-
* - @requires references @external fields
40+
* - @key, @provides and @requires field sets are non-empty
41+
* - @key and @provides field sets reference existing fields
4242
* - @provides references an object
43-
* - field sets cannot reference unions
44-
* - list and interfaces can only be referenced from `@requires` and `@provides`
43+
* - @key field sets cannot reference unions, lists, or interfaces
4544
*/
4645
internal fun validateGraphQLType(type: GraphQLType): List<String> {
4746
val unwrappedType = GraphQLTypeUtil.unwrapAll(type)
@@ -61,7 +60,9 @@ internal class FederatedSchemaValidator {
6160
errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directiveMap, fieldMap))
6261
for (field in fields) {
6362
if (field.getAppliedDirective(REQUIRES_DIRECTIVE_NAME) != null) {
64-
errors.addAll(validateDirective("$federatedType.${field.name}", REQUIRES_DIRECTIVE_NAME, field.allAppliedDirectivesByName, fieldMap))
63+
// Don't validate @requires field set contents — the naive parser does not
64+
// support inline fragments or __typename, which are valid in @requires.
65+
errors.addAll(validateDirective("$federatedType.${field.name}", REQUIRES_DIRECTIVE_NAME, field.allAppliedDirectivesByName, null))
6566
}
6667

6768
if (field.getAppliedDirective(PROVIDES_DIRECTIVE_NAME) != null) {

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ internal fun validateDirective(
2626
validatedType: String,
2727
targetDirective: String,
2828
directiveMap: Map<String, List<GraphQLAppliedDirective>>,
29-
fieldMap: Map<String, GraphQLFieldDefinition>
29+
fieldMap: Map<String, GraphQLFieldDefinition>?
3030
): List<String> {
3131
val validationErrors = mutableListOf<String>()
3232
val directives = directiveMap[targetDirective]
@@ -39,7 +39,7 @@ internal fun validateDirective(
3939
val fieldSet = fieldSetValue.split(" ").filter { it.isNotEmpty() }
4040
if (fieldSet.isEmpty()) {
4141
validationErrors.add("@$targetDirective directive on $validatedType is missing field information")
42-
} else {
42+
} else if (fieldMap != null) {
4343
val directiveInfo = DirectiveInfo(
4444
directiveName = targetDirective,
4545
fieldSet = fieldSetValue,

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/requires/failure/_2/RequiresNonExistentField.kt

Lines changed: 0 additions & 37 deletions
This file was deleted.

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/requires/failure/_1/RequiresLocalField.kt renamed to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/requires/success/_8/RequiresInlineFragmentOnUnion.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 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.
@@ -14,26 +14,44 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.expediagroup.graphql.generator.federation.data.integration.requires.failure._1
17+
package com.expediagroup.graphql.generator.federation.data.integration.requires.success._8
1818

19+
import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
1920
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2021
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
2122
import com.expediagroup.graphql.generator.federation.directives.RequiresDirective
23+
import kotlin.properties.Delegates
2224

2325
/*
24-
# example of invalid usage of @requires directive when it references local field
25-
type RequiresLocalField @key(fields : "id") {
26-
description: String!
27-
id: String!
28-
shippingCost: String! @requires(fields : "weight")
29-
weight: Float!
26+
# example of @requires directive with inline fragment on union type (issue #1939)
27+
type RequiresInlineFragmentOnUnion @key(fields : "id") {
28+
id: Int!
29+
shippingLabel: String! @requires(fields : "animal { ... on Dog { breed } }")
30+
animal: Animal! @external
31+
}
32+
33+
union Animal = Dog | Cat
34+
35+
type Dog {
36+
breed: String!
37+
}
38+
39+
type Cat {
40+
color: String!
3041
}
3142
*/
3243
@KeyDirective(fields = FieldSet("id"))
33-
class RequiresLocalField(val id: String, val description: String) {
44+
data class RequiresInlineFragmentOnUnion(val id: Int) {
3445

35-
var weight: Double = 0.0
46+
@ExternalDirective
47+
var animal: Animal by Delegates.notNull()
3648

37-
@RequiresDirective(FieldSet("weight"))
38-
fun shippingCost(): String = "$${weight * 9.99}"
49+
@RequiresDirective(FieldSet("animal { ... on Dog { breed } }"))
50+
fun shippingLabel(): String = "label"
3951
}
52+
53+
interface Animal
54+
55+
data class Dog(val breed: String) : Animal
56+
57+
data class Cat(val color: String) : Animal

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,29 @@ internal class ValidateDirectiveKtTest {
270270
actual = exception.message
271271
)
272272
}
273+
274+
/**
275+
* @foo(fields: "bar")
276+
* Validates that when fieldMap is null, field set contents are not validated.
277+
*/
278+
@Test
279+
fun `if fieldMap is null, non-empty field set is accepted without validation`() {
280+
val directive: GraphQLAppliedDirective = mockk {
281+
every { name } returns "foo"
282+
every { getArgument(eq("fields")) } returns mockk {
283+
every { argumentValue.value } returns mockk<FieldSet> {
284+
every { value } returns "bar"
285+
}
286+
}
287+
}
288+
289+
val validationErrors = validateDirective(
290+
validatedType = "",
291+
targetDirective = "foo",
292+
directiveMap = mapOf("foo" to listOf(directive)),
293+
fieldMap = null
294+
)
295+
296+
assertTrue(validationErrors.isEmpty())
297+
}
273298
}

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

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,11 @@ package com.expediagroup.graphql.generator.federation.validation.integration
1919
import com.expediagroup.graphql.generator.federation.directives.EXTERNAL_DIRECTIVE_NAME
2020
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
2121
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_DIRECTIVE_NAME
22-
import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema
2322
import com.expediagroup.graphql.generator.federation.toFederatedSchema
2423
import graphql.schema.GraphQLObjectType
2524
import graphql.schema.GraphQLTypeUtil
2625
import org.junit.jupiter.api.Test
2726
import org.junit.jupiter.api.assertDoesNotThrow
28-
import kotlin.test.assertEquals
29-
import kotlin.test.assertFailsWith
3027
import kotlin.test.assertFalse
3128
import kotlin.test.assertNotNull
3229
import kotlin.test.assertTrue
@@ -93,32 +90,6 @@ class FederatedRequiresDirectiveIT {
9390
}
9491
}
9592

96-
@Test
97-
fun `@requires directive cannot reference local fields`() {
98-
val exception = assertFailsWith<InvalidFederatedSchema> {
99-
toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.failure._1"))
100-
}
101-
val expected =
102-
"""
103-
Invalid federated schema:
104-
- @requires(fields = "weight") directive on RequiresLocalField.shippingCost specifies invalid field set - @requires should reference only @external fields, field=weight
105-
""".trimIndent()
106-
assertEquals(expected, exception.message)
107-
}
108-
109-
@Test
110-
fun `@requires directive should reference valid fields`() {
111-
val exception = assertFailsWith<InvalidFederatedSchema> {
112-
toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.failure._2"))
113-
}
114-
val expected =
115-
"""
116-
Invalid federated schema:
117-
- @requires(fields = "zipCode") directive on RequiresNonExistentField.shippingCost specifies invalid field set - field set specifies field that does not exist, field=zipCode
118-
""".trimIndent()
119-
assertEquals(expected, exception.message)
120-
}
121-
12293
@Test
12394
fun `verifies @requires needs @external leaf fields only`() {
12495
assertDoesNotThrow {
@@ -159,6 +130,23 @@ class FederatedRequiresDirectiveIT {
159130
}
160131
}
161132

133+
// Regression test for #1939: inline fragments in @requires field sets were
134+
// rejected by the naive space-splitting parser.
135+
@Test
136+
fun `verifies @requires directive can use inline fragments on union type`() {
137+
assertDoesNotThrow {
138+
val schema = toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.success._8"))
139+
val validatedType = schema.getObjectType("RequiresInlineFragmentOnUnion")
140+
assertTrue(validatedType.hasAppliedDirective(KEY_DIRECTIVE_NAME))
141+
val externalField = validatedType.getFieldDefinition("animal")
142+
assertNotNull(externalField)
143+
assertTrue(externalField.hasAppliedDirective(EXTERNAL_DIRECTIVE_NAME))
144+
val requiresField = validatedType.getFieldDefinition("shippingLabel")
145+
assertNotNull(requiresField)
146+
assertTrue(requiresField.hasAppliedDirective(REQUIRES_DIRECTIVE_NAME))
147+
}
148+
}
149+
162150
@Test
163151
fun `verifies @external is applied on all fields within external type`() {
164152
assertDoesNotThrow {

0 commit comments

Comments
 (0)