22
33import com .google .common .collect .ImmutableList ;
44import graphql .PublicApi ;
5+ import graphql .introspection .Introspection ;
56import graphql .schema .GraphQLEnumType ;
67import graphql .schema .GraphQLFieldDefinition ;
78import graphql .schema .GraphQLFieldsContainer ;
8- import graphql .schema .GraphQLImplementingType ;
99import graphql .schema .GraphQLInputObjectField ;
1010import graphql .schema .GraphQLInputObjectType ;
1111import graphql .schema .GraphQLInterfaceType ;
1212import graphql .schema .GraphQLNamedType ;
1313import graphql .schema .GraphQLObjectType ;
14+ import graphql .schema .GraphQLScalarType ;
1415import graphql .schema .GraphQLSchema ;
1516import graphql .schema .GraphQLSchemaElement ;
16- import graphql .schema .GraphQLType ;
1717import graphql .schema .GraphQLTypeVisitorStub ;
1818import graphql .schema .GraphQLUnionType ;
1919import graphql .schema .SchemaTraverser ;
20- import graphql .schema .impl .SchemaUtil ;
2120import graphql .schema .transform .VisibleFieldPredicateEnvironment .VisibleFieldPredicateEnvironmentImpl ;
2221import graphql .util .TraversalControl ;
2322import graphql .util .TraverserContext ;
2423
2524import java .util .ArrayList ;
26- import java .util .HashSet ;
2725import java .util .LinkedHashSet ;
2826import java .util .List ;
29- import java .util .Map ;
3027import java .util .Objects ;
3128import java .util .Set ;
3229import java .util .function .Function ;
3330import java .util .stream .Collectors ;
3431import java .util .stream .Stream ;
3532
36- import static graphql .schema .SchemaTransformer .transformSchema ;
3733import static graphql .schema .SchemaTransformer .transformSchemaWithDeletes ;
3834
3935/**
@@ -61,84 +57,103 @@ public FieldVisibilitySchemaTransformation(VisibleFieldPredicate visibleFieldPre
6157 }
6258
6359 public final GraphQLSchema apply (GraphQLSchema schema ) {
64- Set <String > observedBeforeTransform = new LinkedHashSet <>();
65- Set <String > observedAfterTransform = new LinkedHashSet <>();
66- Set <GraphQLType > markedForRemovalTypes = new HashSet <>();
67-
68- // query, mutation, and subscription types should not be removed
69- final Set <String > protectedTypeNames = new HashSet <>();
70- for (GraphQLObjectType graphQLObjectType : getOperationTypes (schema )) {
71- protectedTypeNames .add (graphQLObjectType .getName ());
72- }
7360
7461 beforeTransformationHook .run ();
7562
76- new SchemaTraverser (getChildrenFn (schema )).depthFirst (new TypeObservingVisitor (observedBeforeTransform ), getRootTypes (schema ));
63+ // Find root unused types BEFORE transformation
64+ // These are types that exist in the schema but are NOT reachable from operation types + directives
65+ Set <String > rootUnusedTypes = findRootUnusedTypes (schema );
7766
78- // remove fields
67+ // we delete all fields that should be deleted
68+ // this assumes the field remove itself is semantically valid
7969 GraphQLSchema interimSchema = transformSchemaWithDeletes (schema ,
80- new FieldRemovalVisitor (visibleFieldPredicate , markedForRemovalTypes ));
70+ new FieldRemovalVisitor (visibleFieldPredicate ));
71+
72+
73+ // cleanup schema
74+ // now we want to remove all types which are not reachable via root types, directives and the interface implements relationship
75+ SchemaTraverser schemaTraverser = new SchemaTraverser (childrenWithInterfaceImplementations (interimSchema ));
76+
77+ // first we observe all types we don't want to delete
78+ Set <String > observedTypes = new LinkedHashSet <>();
79+ TypeObservingVisitor typeObservingVisitor = new TypeObservingVisitor (observedTypes );
80+ schemaTraverser .depthFirst (typeObservingVisitor , getRootTypes (interimSchema ));
81+
82+ // Traverse from root unused types that still exist after transformation
83+ // This preserves originally unused types and their dependencies
84+ List <GraphQLSchemaElement > existingRootUnusedTypes = rootUnusedTypes .stream ()
85+ .map (interimSchema ::getType )
86+ .filter (Objects ::nonNull )
87+ .map (type -> (GraphQLSchemaElement ) type )
88+ .collect (Collectors .toList ());
8189
82- new SchemaTraverser (getChildrenFn (interimSchema )).depthFirst (new TypeObservingVisitor (observedAfterTransform ), getRootTypes (interimSchema ));
90+ if (!existingRootUnusedTypes .isEmpty ()) {
91+ schemaTraverser .depthFirst (typeObservingVisitor , existingRootUnusedTypes );
92+ }
8393
84- // remove types that are not used after removing fields - (connected schema only)
85- GraphQLSchema connectedSchema = transformSchema (interimSchema ,
86- new TypeVisibilityVisitor ( protectedTypeNames , observedBeforeTransform , observedAfterTransform ));
94+ // then we delete all the types which are not used anymore
95+ GraphQLSchema finalSchema = transformSchemaWithDeletes (interimSchema ,
96+ new TypeRemovalVisitor ( observedTypes ));
8797
88- // ensure markedForRemovalTypes are not referenced by other schema elements, and delete from the schema
89- // the ones that aren't.
90- GraphQLSchema finalSchema = removeUnreferencedTypes (markedForRemovalTypes , connectedSchema );
9198
9299 afterTransformationHook .run ();
93100
94101 return finalSchema ;
95102 }
96103
97- // Creates a getChildrenFn that includes interface
98- private Function <GraphQLSchemaElement , List <GraphQLSchemaElement >> getChildrenFn (GraphQLSchema schema ) {
99- Map <String , List <GraphQLImplementingType >> interfaceImplementations = new SchemaUtil ().groupImplementationsForInterfacesAndObjects (schema );
100-
101- return graphQLSchemaElement -> {
102- if (!(graphQLSchemaElement instanceof GraphQLInterfaceType )) {
103- return graphQLSchemaElement .getChildren ();
104- }
105- ArrayList <GraphQLSchemaElement > children = new ArrayList <>(graphQLSchemaElement .getChildren ());
106- List <GraphQLImplementingType > implementations = interfaceImplementations .get (((GraphQLInterfaceType ) graphQLSchemaElement ).getName ());
107- if (implementations != null ) {
108- children .addAll (implementations );
104+ /**
105+ * Finds root unused types - types that exist in additional types but are NOT reachable
106+ * from operation types (Query, Mutation, Subscription) and directives.
107+ */
108+ private Set <String > findRootUnusedTypes (GraphQLSchema schema ) {
109+ // Collect all types reachable from operation roots + directives
110+ // Use a traverser that includes interface implementations
111+ Set <String > typesReachableFromRoots = new LinkedHashSet <>();
112+ SchemaTraverser traverser = new SchemaTraverser (childrenWithInterfaceImplementations (schema ));
113+ TypeObservingVisitor visitor = new TypeObservingVisitor (typesReachableFromRoots );
114+ traverser .depthFirst (visitor , getRootTypes (schema ));
115+
116+ // Root unused types are additional types that are NOT reachable from roots
117+ Set <String > rootUnusedTypes = new LinkedHashSet <>();
118+ for (GraphQLNamedType type : schema .getAdditionalTypes ()) {
119+ String typeName = type .getName ();
120+ if (!typesReachableFromRoots .contains (typeName ) && !isIntrospectionType (typeName )) {
121+ rootUnusedTypes .add (typeName );
109122 }
110- return children ;
111- } ;
123+ }
124+ return rootUnusedTypes ;
112125 }
113126
114- private GraphQLSchema removeUnreferencedTypes (Set <GraphQLType > markedForRemovalTypes , GraphQLSchema connectedSchema ) {
115- GraphQLSchema withoutAdditionalTypes = connectedSchema .transform (builder -> {
116- Set <GraphQLNamedType > additionalTypes = new HashSet <>(connectedSchema .getAdditionalTypes ());
117- additionalTypes .removeAll (markedForRemovalTypes );
118- builder .clearAdditionalTypes ();
119- builder .additionalTypes (additionalTypes );
120- });
127+ /**
128+ * Checks if a type is an introspection type that should be protected from removal.
129+ * This includes standard introspection types (starting with "__") and special types
130+ * like _AppliedDirective (starting with "_") added by IntrospectionWithDirectivesSupport.
131+ */
132+ private static boolean isIntrospectionType (String typeName ) {
133+ return Introspection .isIntrospectionTypes (typeName ) || typeName .startsWith ("_" );
134+ }
121135
122- // remove from markedForRemovalTypes any type that might still be referenced by other schema elements
123- transformSchema (withoutAdditionalTypes , new AdditionalTypeVisibilityVisitor (markedForRemovalTypes ));
136+ /**
137+ * Creates a function that returns children of a schema element, including interface implementations.
138+ * This ensures that when traversing from an interface, we also visit all types that implement it.
139+ */
140+ private Function <GraphQLSchemaElement , List <GraphQLSchemaElement >> childrenWithInterfaceImplementations (GraphQLSchema schema ) {
124141
125- // finally remove the types on the schema we are certain aren't referenced by any other node.
126- return transformSchema (connectedSchema , new GraphQLTypeVisitorStub () {
127- @ Override
128- protected TraversalControl visitGraphQLType (GraphQLSchemaElement node , TraverserContext <GraphQLSchemaElement > context ) {
129- if (node instanceof GraphQLType && markedForRemovalTypes .contains (node )) {
130- return deleteNode (context );
131- }
132- return super .visitGraphQLType (node , context );
142+ return schemaElement -> {
143+ if (!(schemaElement instanceof GraphQLInterfaceType )) {
144+ return schemaElement .getChildren ();
133145 }
134- });
146+ ArrayList <GraphQLSchemaElement > children = new ArrayList <>(schemaElement .getChildren ());
147+ List <GraphQLObjectType > implementations = schema .getImplementations ((GraphQLInterfaceType ) schemaElement );
148+ children .addAll (implementations );
149+ return children ;
150+ };
135151 }
136152
137153 private static class TypeObservingVisitor extends GraphQLTypeVisitorStub {
138154
139155 private final Set <String > observedTypes ;
140156
141-
142157 private TypeObservingVisitor (Set <String > observedTypes ) {
143158 this .observedTypes = observedTypes ;
144159 }
@@ -150,7 +165,8 @@ protected TraversalControl visitGraphQLType(GraphQLSchemaElement node,
150165 node instanceof GraphQLEnumType ||
151166 node instanceof GraphQLInputObjectType ||
152167 node instanceof GraphQLInterfaceType ||
153- node instanceof GraphQLUnionType ) {
168+ node instanceof GraphQLUnionType ||
169+ node instanceof GraphQLScalarType ) {
154170 observedTypes .add (((GraphQLNamedType ) node ).getName ());
155171 }
156172
@@ -161,15 +177,12 @@ protected TraversalControl visitGraphQLType(GraphQLSchemaElement node,
161177 private static class FieldRemovalVisitor extends GraphQLTypeVisitorStub {
162178
163179 private final VisibleFieldPredicate visibilityPredicate ;
164- private final Set <GraphQLType > removedTypes ;
165180
166181 private final Set <GraphQLFieldDefinition > fieldDefinitionsToActuallyRemove = new LinkedHashSet <>();
167182 private final Set <GraphQLInputObjectField > inputObjectFieldsToDelete = new LinkedHashSet <>();
168183
169- private FieldRemovalVisitor (VisibleFieldPredicate visibilityPredicate ,
170- Set <GraphQLType > removedTypes ) {
184+ private FieldRemovalVisitor (VisibleFieldPredicate visibilityPredicate ) {
171185 this .visibilityPredicate = visibilityPredicate ;
172- this .removedTypes = removedTypes ;
173186 }
174187
175188 @ Override
@@ -189,7 +202,6 @@ private TraversalControl visitFieldsContainer(GraphQLFieldsContainer fieldsConta
189202 fieldDefinition , fieldsContainer );
190203 if (!visibilityPredicate .isVisible (environment )) {
191204 fieldDefinitionsToActuallyRemove .add (fieldDefinition );
192- removedTypes .add (fieldDefinition .getType ());
193205 } else {
194206 allFieldsDeleted = false ;
195207 }
@@ -210,7 +222,6 @@ public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType input
210222 inputField , inputObjectType );
211223 if (!visibilityPredicate .isVisible (environment )) {
212224 inputObjectFieldsToDelete .add (inputField );
213- removedTypes .add (inputField .getType ());
214225 } else {
215226 allFieldsDeleted = false ;
216227 }
@@ -245,76 +256,43 @@ public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField def
245256 }
246257 }
247258
248- private static class TypeVisibilityVisitor extends GraphQLTypeVisitorStub {
259+ private static class TypeRemovalVisitor extends GraphQLTypeVisitorStub {
249260
250261 private final Set <String > protectedTypeNames ;
251- private final Set <String > observedBeforeTransform ;
252- private final Set <String > observedAfterTransform ;
253262
254- private TypeVisibilityVisitor (Set <String > protectedTypeNames ,
255- Set <String > observedTypes ,
256- Set <String > observedAfterTransform ) {
263+ private TypeRemovalVisitor (Set <String > protectedTypeNames ) {
257264 this .protectedTypeNames = protectedTypeNames ;
258- this .observedBeforeTransform = observedTypes ;
259- this .observedAfterTransform = observedAfterTransform ;
260265 }
261266
262- @ Override
263- public TraversalControl visitGraphQLInterfaceType (GraphQLInterfaceType node ,
264- TraverserContext <GraphQLSchemaElement > context ) {
265- return super .visitGraphQLInterfaceType (node , context );
266- }
267267
268268 @ Override
269269 public TraversalControl visitGraphQLType (GraphQLSchemaElement node ,
270270 TraverserContext <GraphQLSchemaElement > context ) {
271+ if (node instanceof GraphQLNamedType ) {
272+ String name = ((GraphQLNamedType ) node ).getName ();
273+ if (isIntrospectionType (name )) {
274+ return TraversalControl .CONTINUE ;
275+ }
276+ }
271277 if (node instanceof GraphQLObjectType ||
272278 node instanceof GraphQLEnumType ||
273279 node instanceof GraphQLInputObjectType ||
274280 node instanceof GraphQLInterfaceType ||
275- node instanceof GraphQLUnionType ) {
281+ node instanceof GraphQLUnionType ||
282+ node instanceof GraphQLScalarType ) {
276283 String name = ((GraphQLNamedType ) node ).getName ();
277- if (observedBeforeTransform .contains (name ) &&
278- !observedAfterTransform .contains (name )
279- && !protectedTypeNames .contains (name )
280- ) {
284+ if (!protectedTypeNames .contains (name )) {
281285 return deleteNode (context );
282286 }
283287 }
284288 return TraversalControl .CONTINUE ;
285289 }
286290 }
287291
288- private static class AdditionalTypeVisibilityVisitor extends GraphQLTypeVisitorStub {
289-
290- private final Set <GraphQLType > markedForRemovalTypes ;
291-
292- private AdditionalTypeVisibilityVisitor (Set <GraphQLType > markedForRemovalTypes ) {
293- this .markedForRemovalTypes = markedForRemovalTypes ;
294- }
295-
296- @ Override
297- public TraversalControl visitGraphQLType (GraphQLSchemaElement node ,
298- TraverserContext <GraphQLSchemaElement > context ) {
299-
300- if (node instanceof GraphQLNamedType ) {
301- GraphQLNamedType namedType = (GraphQLNamedType ) node ;
302- // we encountered a node referencing one of the marked types, so it should not be removed.
303- if (markedForRemovalTypes .contains (node )) {
304- markedForRemovalTypes .remove (namedType );
305- }
306- }
307-
308- return TraversalControl .CONTINUE ;
309- }
310- }
311292
312293 private List <GraphQLSchemaElement > getRootTypes (GraphQLSchema schema ) {
313294 return ImmutableList .<GraphQLSchemaElement >builder ()
314295 .addAll (getOperationTypes (schema ))
315- // Include directive definitions as roots, since they won't be removed in the filtering process.
316- // Some types (enums, input types, etc.) might be reachable only by directive definitions (and
317- // not by other types or fields).
318296 .addAll (schema .getDirectives ())
319297 .build ();
320298 }
0 commit comments