Skip to content

Commit 2073c98

Browse files
authored
Merge pull request #4446 from graphql-java/codex/backport-4390-to-26.x
Backport 26.1: Fix schema rebuild after root type deletion
2 parents d81252f + 8d6eaba commit 2073c98

3 files changed

Lines changed: 183 additions & 9 deletions

File tree

‎build.gradle‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,11 +884,13 @@ tasks.register('markGeneratedEqualsHashCode') {
884884
def jacocoDir = layout.buildDirectory.dir('classes-jacoco/java/main')
885885

886886
inputs.dir(originalDir)
887+
inputs.property("generatedAnnotationTaskVersion", 2)
887888
outputs.dir(jacocoDir)
888889

889890
doLast {
890891
def src = originalDir.get().asFile
891892
def dest = jacocoDir.get().asFile
893+
project.delete(dest)
892894
if (!src.exists()) return
893895

894896
// Copy all class files to a separate directory for JaCoCo

‎src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java‎

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.List;
2828
import java.util.Map;
2929
import java.util.TreeMap;
30-
import java.util.function.Supplier;
3130

3231
import static graphql.schema.GraphQLTypeUtil.unwrapAllAs;
3332
import static graphql.util.TraversalControl.CONTINUE;
@@ -43,8 +42,9 @@
4342
* themselves are not collected - only concrete type instances are stored in the result map.
4443
* <p>
4544
* Because type references are not followed, this visitor also tracks "indirect strong references"
46-
* - types that are directly referenced (not via type reference) by fields, arguments, and input
47-
* fields. This handles edge cases where schema transformations replace type references with
45+
* - types that are directly referenced (not via type reference) by fields, arguments,
46+
* input fields, implemented interfaces, and union members. This handles edge cases where
47+
* schema transformations replace type references with
4848
* actual types, which would otherwise be missed during traversal.
4949
*
5050
* @see SchemaUtil#visitPartiallySchema
@@ -77,6 +77,7 @@ public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, Traverser
7777
public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
7878
assertTypeUniqueness(node, result);
7979
save(node.getName(), node);
80+
saveIndirectStrongReferences(node.getInterfaces());
8081
return CONTINUE;
8182
}
8283

@@ -91,47 +92,55 @@ public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType node,
9192
public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, TraverserContext<GraphQLSchemaElement> context) {
9293
assertTypeUniqueness(node, result);
9394
save(node.getName(), node);
95+
saveIndirectStrongReferences(node.getInterfaces());
9496
return CONTINUE;
9597
}
9698

9799
@Override
98100
public TraversalControl visitGraphQLUnionType(GraphQLUnionType node, TraverserContext<GraphQLSchemaElement> context) {
99101
assertTypeUniqueness(node, result);
100102
save(node.getName(), node);
103+
saveIndirectStrongReferences(node.getTypes());
101104
return CONTINUE;
102105
}
103106

104107
@Override
105108
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext<GraphQLSchemaElement> context) {
106-
saveIndirectStrongReference(node::getType);
109+
saveIndirectStrongReference(node.getType());
107110
return CONTINUE;
108111
}
109112

110113
@Override
111114
public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField node, TraverserContext<GraphQLSchemaElement> context) {
112-
saveIndirectStrongReference(node::getType);
115+
saveIndirectStrongReference(node.getType());
113116
return CONTINUE;
114117
}
115118

116119
@Override
117120
public TraversalControl visitGraphQLArgument(GraphQLArgument node, TraverserContext<GraphQLSchemaElement> context) {
118-
saveIndirectStrongReference(node::getType);
121+
saveIndirectStrongReference(node.getType());
119122
return CONTINUE;
120123
}
121124

122125
@Override
123126
public TraversalControl visitGraphQLAppliedDirectiveArgument(GraphQLAppliedDirectiveArgument node, TraverserContext<GraphQLSchemaElement> context) {
124-
saveIndirectStrongReference(node::getType);
127+
saveIndirectStrongReference(node.getType());
125128
return CONTINUE;
126129
}
127130

128-
private void saveIndirectStrongReference(Supplier<GraphQLType> typeSupplier) {
129-
GraphQLNamedType type = unwrapAllAs(typeSupplier.get());
131+
private void saveIndirectStrongReference(GraphQLType graphQLType) {
132+
GraphQLNamedType type = unwrapAllAs(graphQLType);
130133
if (!(type instanceof GraphQLTypeReference)) {
131134
indirectStrongReferences.put(type.getName(), type);
132135
}
133136
}
134137

138+
private void saveIndirectStrongReferences(List<? extends GraphQLType> types) {
139+
for (GraphQLType type : types) {
140+
saveIndirectStrongReference(type);
141+
}
142+
}
143+
135144
private void save(String name, GraphQLNamedType type) {
136145
result.put(name, type);
137146
}

‎src/test/groovy/graphql/schema/impl/SchemaUtilTest.groovy‎

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,19 @@ import graphql.NestedInputSchema
66
import graphql.introspection.Introspection
77
import graphql.schema.GraphQLAppliedDirectiveArgument
88
import graphql.schema.GraphQLArgument
9+
import graphql.schema.GraphQLCodeRegistry
910
import graphql.schema.GraphQLFieldDefinition
1011
import graphql.schema.GraphQLInputObjectType
1112
import graphql.schema.GraphQLObjectType
13+
import graphql.schema.GraphQLSchema
14+
import graphql.schema.GraphQLSchemaElement
1215
import graphql.schema.GraphQLType
16+
import graphql.schema.GraphQLTypeVisitorStub
1317
import graphql.schema.GraphQLTypeReference
1418
import graphql.schema.GraphQLUnionType
19+
import graphql.schema.SchemaTransformer
20+
import graphql.util.TraversalControl
21+
import graphql.util.TraverserContext
1522
import spock.lang.Specification
1623

1724
import static graphql.Scalars.GraphQLBoolean
@@ -41,6 +48,7 @@ import static graphql.schema.GraphQLArgument.newArgument
4148
import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition
4249
import static graphql.schema.GraphQLInputObjectField.newInputObjectField
4350
import static graphql.schema.GraphQLInputObjectType.newInputObject
51+
import static graphql.schema.GraphQLInterfaceType.newInterface
4452
import static graphql.schema.GraphQLList.list
4553
import static graphql.schema.GraphQLObjectType.newObject
4654
import static graphql.schema.GraphQLSchema.newSchema
@@ -190,6 +198,161 @@ class SchemaUtilTest extends Specification {
190198
!(cacheEnabled.getType() instanceof GraphQLTypeReference)
191199
}
192200

201+
def "can rebuild schema after removing root type that made an implemented interface reachable"() {
202+
given:
203+
def schema = schemaWithObjectImplementingInterfaceThroughTypeReference()
204+
def node = schema.getType("Node")
205+
206+
when:
207+
def rebuiltSchema = newSchema(schema)
208+
.mutation((GraphQLObjectType) null)
209+
.build()
210+
211+
then:
212+
rebuiltSchema.getMutationType() == null
213+
rebuiltSchema.getType("Node") == node
214+
rebuiltSchema.getObjectType("Person").interfaces == [node]
215+
}
216+
217+
def "can transform schema after deleting root type that made an implemented interface reachable"() {
218+
given:
219+
def schema = schemaWithObjectImplementingInterfaceThroughTypeReference()
220+
def node = schema.getType("Node")
221+
222+
when:
223+
def transformedSchema = SchemaTransformer.transformSchemaWithDeletes(schema, new GraphQLTypeVisitorStub() {
224+
@Override
225+
TraversalControl visitGraphQLObjectType(GraphQLObjectType graphQLObjectType, TraverserContext<GraphQLSchemaElement> context) {
226+
if (graphQLObjectType.name == "Mutation") {
227+
return deleteNode(context)
228+
}
229+
return TraversalControl.CONTINUE
230+
}
231+
})
232+
233+
then:
234+
transformedSchema.getMutationType() == null
235+
transformedSchema.getType("Node") == node
236+
transformedSchema.getObjectType("Person").interfaces == [node]
237+
}
238+
239+
def "can rebuild schema after removing root type that made a union member reachable"() {
240+
given:
241+
def schema = schemaWithUnionMemberThroughTypeReference()
242+
def cat = schema.getObjectType("Cat")
243+
244+
when:
245+
def rebuiltSchema = newSchema(schema)
246+
.mutation((GraphQLObjectType) null)
247+
.build()
248+
249+
then:
250+
rebuiltSchema.getMutationType() == null
251+
rebuiltSchema.getType("Cat") == cat
252+
rebuiltSchema.getType("Pet").types == [cat]
253+
}
254+
255+
def "can rebuild schema after removing root type that made an interface implemented by another interface reachable"() {
256+
given:
257+
def schema = schemaWithInterfaceImplementingInterfaceThroughTypeReference()
258+
def node = schema.getType("Node")
259+
260+
when:
261+
def rebuiltSchema = newSchema(schema)
262+
.mutation((GraphQLObjectType) null)
263+
.build()
264+
265+
then:
266+
rebuiltSchema.getMutationType() == null
267+
rebuiltSchema.getType("Node") == node
268+
rebuiltSchema.getType("NamedNode").interfaces == [node]
269+
}
270+
271+
private GraphQLSchema schemaWithObjectImplementingInterfaceThroughTypeReference() {
272+
def node = newInterface()
273+
.name("Node")
274+
.field(newFieldDefinition().name("id").type(GraphQLString))
275+
.build()
276+
def person = newObject()
277+
.name("Person")
278+
.withInterface(typeRef("Node"))
279+
.field(newFieldDefinition().name("id").type(GraphQLString))
280+
.build()
281+
def query = newObject()
282+
.name("Query")
283+
.field(newFieldDefinition().name("person").type(person))
284+
.build()
285+
def mutation = newObject()
286+
.name("Mutation")
287+
.field(newFieldDefinition().name("node").type(node))
288+
.build()
289+
def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
290+
.typeResolver(node, { env -> person })
291+
.build()
292+
return newSchema()
293+
.query(query)
294+
.mutation(mutation)
295+
.codeRegistry(codeRegistry)
296+
.build()
297+
}
298+
299+
private GraphQLSchema schemaWithUnionMemberThroughTypeReference() {
300+
def cat = newObject()
301+
.name("Cat")
302+
.field(newFieldDefinition().name("name").type(GraphQLString))
303+
.build()
304+
def pet = GraphQLUnionType.newUnionType()
305+
.name("Pet")
306+
.possibleType(typeRef("Cat"))
307+
.build()
308+
def query = newObject()
309+
.name("Query")
310+
.field(newFieldDefinition().name("pet").type(pet))
311+
.build()
312+
def mutation = newObject()
313+
.name("Mutation")
314+
.field(newFieldDefinition().name("cat").type(cat))
315+
.build()
316+
def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
317+
.typeResolver(pet, { env -> cat })
318+
.build()
319+
return newSchema()
320+
.query(query)
321+
.mutation(mutation)
322+
.codeRegistry(codeRegistry)
323+
.build()
324+
}
325+
326+
private GraphQLSchema schemaWithInterfaceImplementingInterfaceThroughTypeReference() {
327+
def node = newInterface()
328+
.name("Node")
329+
.field(newFieldDefinition().name("id").type(GraphQLString))
330+
.build()
331+
def namedNode = newInterface()
332+
.name("NamedNode")
333+
.withInterface(typeRef("Node"))
334+
.field(newFieldDefinition().name("id").type(GraphQLString))
335+
.field(newFieldDefinition().name("name").type(GraphQLString))
336+
.build()
337+
def query = newObject()
338+
.name("Query")
339+
.field(newFieldDefinition().name("node").type(namedNode))
340+
.build()
341+
def mutation = newObject()
342+
.name("Mutation")
343+
.field(newFieldDefinition().name("node").type(node))
344+
.build()
345+
def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
346+
.typeResolver(node, { env -> null })
347+
.typeResolver(namedNode, { env -> null })
348+
.build()
349+
return newSchema()
350+
.query(query)
351+
.mutation(mutation)
352+
.codeRegistry(codeRegistry)
353+
.build()
354+
}
355+
193356
def "redefined types are caught"() {
194357
when:
195358
final GraphQLInputObjectType attributeListInputObjectType = newInputObject().name("attributes")

0 commit comments

Comments
 (0)