Skip to content

Commit 0464f19

Browse files
committed
add specific transform method for deletion cases
1 parent 2766087 commit 0464f19

2 files changed

Lines changed: 193 additions & 5 deletions

File tree

src/main/java/graphql/schema/SchemaTransformer.java

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.google.common.collect.Multimap;
55
import graphql.PublicApi;
66
import graphql.collect.ImmutableKit;
7+
import graphql.introspection.Introspection;
78
import graphql.util.Breadcrumb;
89
import graphql.util.NodeAdapter;
910
import graphql.util.NodeLocation;
@@ -102,6 +103,73 @@ public static GraphQLSchema transformSchema(GraphQLSchema schema, GraphQLTypeVis
102103
return schemaTransformer.transform(schema, visitor, postTransformation);
103104
}
104105

106+
/**
107+
* Transforms a GraphQLSchema with support for delete operations.
108+
* <p>
109+
* When a visitor uses {@link GraphQLTypeVisitor#deleteNode(TraverserContext)} to delete schema elements,
110+
* the traversal does not continue to the children of deleted nodes. This can cause issues when types
111+
* are only reachable through fields that get deleted, as those types won't be visited and transformed.
112+
* <p>
113+
* This method ensures all types in the schema are visited by adding them to the schema's additional types
114+
* before transformation. This guarantees that even types only reachable through deleted fields will be
115+
* properly visited and transformed.
116+
* <p>
117+
* Use this method instead of {@link #transformSchema(GraphQLSchema, GraphQLTypeVisitor)} when your
118+
* visitor deletes fields or types that may reference other types via circular references.
119+
*
120+
* @param schema the schema to transform
121+
* @param visitor the visitor call back
122+
*
123+
* @return a new GraphQLSchema instance.
124+
*
125+
* @see GraphQLTypeVisitor#deleteNode(TraverserContext)
126+
*/
127+
public static GraphQLSchema transformSchemaWithDeletes(GraphQLSchema schema, GraphQLTypeVisitor visitor) {
128+
return transformSchemaWithDeletes(schema, visitor, null);
129+
}
130+
131+
/**
132+
* Transforms a GraphQLSchema with support for delete operations.
133+
* <p>
134+
* When a visitor uses {@link GraphQLTypeVisitor#deleteNode(TraverserContext)} to delete schema elements,
135+
* the traversal does not continue to the children of deleted nodes. This can cause issues when types
136+
* are only reachable through fields that get deleted, as those types won't be visited and transformed.
137+
* <p>
138+
* This method ensures all types in the schema are visited by adding them to the schema's additional types
139+
* before transformation. This guarantees that even types only reachable through deleted fields will be
140+
* properly visited and transformed.
141+
* <p>
142+
* Use this method instead of {@link #transformSchema(GraphQLSchema, GraphQLTypeVisitor, Consumer)} when your
143+
* visitor deletes fields or types that may reference other types via circular references.
144+
*
145+
* @param schema the schema to transform
146+
* @param visitor the visitor call back
147+
* @param postTransformation a callback that can be used as a final step to the schema (can be null)
148+
*
149+
* @return a new GraphQLSchema instance.
150+
*
151+
* @see GraphQLTypeVisitor#deleteNode(TraverserContext)
152+
*/
153+
public static GraphQLSchema transformSchemaWithDeletes(GraphQLSchema schema, GraphQLTypeVisitor visitor, Consumer<GraphQLSchema.Builder> postTransformation) {
154+
// Add all types to additionalTypes to ensure they are all visited during transformation.
155+
// This is necessary because when a node is deleted, its children are not traversed.
156+
// Types that are only reachable through deleted fields would otherwise not be visited.
157+
GraphQLSchema schemaWithAllTypes = schema.transform(builder -> {
158+
for (GraphQLNamedType type : schema.getTypeMap().values()) {
159+
if (!isRootType(schema, type) && !Introspection.isIntrospectionTypes(type)) {
160+
builder.additionalType(type);
161+
}
162+
}
163+
});
164+
return transformSchema(schemaWithAllTypes, visitor, postTransformation);
165+
}
166+
167+
private static boolean isRootType(GraphQLSchema schema, GraphQLNamedType type) {
168+
return type == schema.getQueryType()
169+
|| type == schema.getMutationType()
170+
|| type == schema.getSubscriptionType();
171+
}
172+
105173
/**
106174
* Transforms a {@link GraphQLSchemaElement} and returns a new element.
107175
*

src/test/groovy/graphql/schema/SchemaTransformerTest.groovy

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package graphql.schema
22

3+
import graphql.AssertException
34
import graphql.GraphQL
45
import graphql.Scalars
56
import graphql.TestUtil
@@ -1056,14 +1057,75 @@ type Query {
10561057
"""
10571058

10581059
def schema = TestUtil.schema(sdl)
1059-
schema = schema.transform { builder ->
1060-
for (def type : schema.getTypeMap().values()) {
1061-
if (type != schema.getQueryType() && type != schema.getMutationType() && type != schema.getSubscriptionType()) {
1062-
builder.additionalType(type)
1060+
1061+
def visitor = new GraphQLTypeVisitorStub() {
1062+
1063+
@Override
1064+
TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext<GraphQLSchemaElement> context) {
1065+
if (node.hasAppliedDirective("remove")) {
1066+
return deleteNode(context)
1067+
}
1068+
return TraversalControl.CONTINUE
1069+
}
1070+
1071+
@Override
1072+
TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
1073+
if (node.getFields().stream().allMatch(field -> field.hasAppliedDirective("remove"))) {
1074+
return deleteNode(context)
10631075
}
1076+
1077+
return TraversalControl.CONTINUE
10641078
}
10651079
}
1080+
when:
1081+
def newSchema = SchemaTransformer.transformSchemaWithDeletes(schema, visitor)
1082+
def printer = new SchemaPrinter(SchemaPrinter.Options.defaultOptions().includeDirectives(false))
1083+
def newSdl = printer.print(newSchema)
10661084

1085+
then:
1086+
newSdl.trim() == """type Customer {
1087+
rental: Rental
1088+
}
1089+
1090+
type Query {
1091+
customer: Customer
1092+
}
1093+
1094+
type Rental {
1095+
id: ID
1096+
}""".trim()
1097+
}
1098+
1099+
def "issue 4133 simplified reproduction - demonstrates the bug"() {
1100+
def sdl = """
1101+
directive @remove on FIELD_DEFINITION
1102+
1103+
type Query {
1104+
rental: Rental @remove
1105+
customer: Customer
1106+
}
1107+
1108+
type Customer {
1109+
rental: Rental
1110+
payment: Payment @remove
1111+
}
1112+
1113+
type Rental {
1114+
id: ID
1115+
customer: Customer @remove
1116+
}
1117+
1118+
type Payment {
1119+
inventory: Inventory @remove
1120+
}
1121+
1122+
type Inventory {
1123+
payment: Payment @remove
1124+
}
1125+
"""
1126+
1127+
def schema = TestUtil.schema(sdl)
1128+
// NO WORKAROUND - this should fail with regular transformSchema
10671129

10681130
def visitor = new GraphQLTypeVisitorStub() {
10691131

@@ -1085,7 +1147,65 @@ type Query {
10851147
}
10861148
}
10871149
when:
1088-
def newSchema = SchemaTransformer.transformSchema(schema, visitor)
1150+
SchemaTransformer.transformSchema(schema, visitor)
1151+
1152+
then:
1153+
def e = thrown(AssertException)
1154+
e.message.contains("not found in schema")
1155+
}
1156+
1157+
def "issue 4133 simplified - fixed with transformSchemaWithDeletes"() {
1158+
def sdl = """
1159+
directive @remove on FIELD_DEFINITION
1160+
1161+
type Query {
1162+
rental: Rental @remove
1163+
customer: Customer
1164+
}
1165+
1166+
type Customer {
1167+
rental: Rental
1168+
payment: Payment @remove
1169+
}
1170+
1171+
type Rental {
1172+
id: ID
1173+
customer: Customer @remove
1174+
}
1175+
1176+
type Payment {
1177+
inventory: Inventory @remove
1178+
}
1179+
1180+
type Inventory {
1181+
payment: Payment @remove
1182+
}
1183+
"""
1184+
1185+
def schema = TestUtil.schema(sdl)
1186+
1187+
def visitor = new GraphQLTypeVisitorStub() {
1188+
1189+
@Override
1190+
TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition node, TraverserContext<GraphQLSchemaElement> context) {
1191+
if (node.hasAppliedDirective("remove")) {
1192+
return deleteNode(context)
1193+
}
1194+
return TraversalControl.CONTINUE
1195+
}
1196+
1197+
@Override
1198+
TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
1199+
if (node.getFields().stream().allMatch(field -> field.hasAppliedDirective("remove"))) {
1200+
return deleteNode(context)
1201+
}
1202+
1203+
return TraversalControl.CONTINUE
1204+
}
1205+
}
1206+
when:
1207+
// Use the new transformSchemaWithDeletes method - this should work!
1208+
def newSchema = SchemaTransformer.transformSchemaWithDeletes(schema, visitor)
10891209
def printer = new SchemaPrinter(SchemaPrinter.Options.defaultOptions().includeDirectives(false))
10901210
def newSdl = printer.print(newSchema)
10911211

0 commit comments

Comments
 (0)