Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# AI Agent Context for graphql-java

This file provides context for AI assistants working with this codebase.

## Test Execution

When running tests, exclude the Java version-specific test tasks to avoid failures:

```bash
./gradlew test -x testWithJava17 -x testWithJava11 -x testng
```
14 changes: 7 additions & 7 deletions src/main/java/graphql/schema/GraphQLSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class GraphQLSchema {
private final GraphQLObjectType mutationType;
private final GraphQLObjectType subscriptionType;
private final GraphQLObjectType introspectionSchemaType;
private final ImmutableSet<GraphQLType> additionalTypes;
private final ImmutableSet<GraphQLNamedType> additionalTypes;
private final GraphQLFieldDefinition introspectionSchemaField;
private final GraphQLFieldDefinition introspectionTypeField;
// we don't allow modification of "__typename" - it's a scalar
Expand Down Expand Up @@ -275,10 +275,10 @@ public GraphQLObjectType getIntrospectionSchemaType() {
*
* @return an immutable set of types that were explicitly added as additional types
*
* @see Builder#additionalType(GraphQLType)
* @see Builder#additionalType(GraphQLNamedType)
* @see Builder#additionalTypes(Set)
*/
public Set<GraphQLType> getAdditionalTypes() {
public Set<GraphQLNamedType> getAdditionalTypes() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change. Just in case you didnt realise.

return additionalTypes;
}

Expand Down Expand Up @@ -745,7 +745,7 @@ public static class Builder {
private final Set<GraphQLDirective> additionalDirectives = new LinkedHashSet<>(
asList(Directives.IncludeDirective, Directives.SkipDirective)
);
private final Set<GraphQLType> additionalTypes = new LinkedHashSet<>();
private final Set<GraphQLNamedType> additionalTypes = new LinkedHashSet<>();
private final List<GraphQLDirective> schemaDirectives = new ArrayList<>();
private final List<GraphQLAppliedDirective> schemaAppliedDirectives = new ArrayList<>();

Expand Down Expand Up @@ -808,7 +808,7 @@ public Builder codeRegistry(GraphQLCodeRegistry codeRegistry) {
*
* @see GraphQLSchema#getAdditionalTypes()
*/
public Builder additionalTypes(Set<GraphQLType> additionalTypes) {
public Builder additionalTypes(Set<? extends GraphQLNamedType> additionalTypes) {
this.additionalTypes.addAll(additionalTypes);
return this;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also a breaking change

Expand All @@ -831,7 +831,7 @@ public Builder additionalTypes(Set<GraphQLType> additionalTypes) {
* @see GraphQLSchema#getAdditionalTypes()
* @see #additionalTypes(Set)
*/
public Builder additionalType(GraphQLType additionalType) {
public Builder additionalType(GraphQLNamedType additionalType) {
this.additionalTypes.add(additionalType);
return this;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and again - thats ok - just pointing it out

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well .. they never contained not named types so far. Technically breaking, but I think nobody ever used it to add [Foo] instead of Foo

Expand All @@ -844,7 +844,7 @@ public Builder additionalType(GraphQLType additionalType) {
*
* @return this builder
*
* @see #additionalType(GraphQLType)
* @see #additionalType(GraphQLNamedType)
* @see #additionalTypes(Set)
*/
public Builder clearAdditionalTypes() {
Expand Down
155 changes: 134 additions & 21 deletions src/main/java/graphql/schema/SchemaTransformer.java

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion src/main/java/graphql/schema/idl/SchemaGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import graphql.language.OperationTypeDefinition;
import graphql.schema.GraphQLCodeRegistry;
import graphql.schema.GraphQLDirective;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLSchema;
import graphql.schema.GraphQLType;
import graphql.schema.idl.errors.SchemaProblem;
Expand Down Expand Up @@ -130,7 +131,7 @@ private GraphQLSchema makeExecutableSchemaImpl(ImmutableTypeDefinitionRegistry t

schemaGeneratorHelper.buildOperations(buildCtx, schemaBuilder);

Set<GraphQLType> additionalTypes = schemaGeneratorHelper.buildAdditionalTypes(buildCtx);
Set<GraphQLNamedType> additionalTypes = schemaGeneratorHelper.buildAdditionalTypes(buildCtx);
schemaBuilder.additionalTypes(additionalTypes);

buildCtx.getCodeRegistry().fieldVisibility(buildCtx.getWiring().getFieldVisibility());
Expand Down
13 changes: 7 additions & 6 deletions src/main/java/graphql/schema/idl/SchemaGeneratorHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import graphql.schema.GraphQLInterfaceType;
import graphql.schema.GraphQLNamedInputType;
import graphql.schema.GraphQLNamedOutputType;
import graphql.schema.GraphQLNamedType;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLOutputType;
import graphql.schema.GraphQLScalarType;
Expand Down Expand Up @@ -115,8 +116,8 @@ static class BuildContext {
private final RuntimeWiring wiring;
private final Deque<String> typeStack = new ArrayDeque<>();

private final Map<String, GraphQLOutputType> outputGTypes = new LinkedHashMap<>();
private final Map<String, GraphQLInputType> inputGTypes = new LinkedHashMap<>();
private final Map<String, GraphQLNamedOutputType> outputGTypes = new LinkedHashMap<>();
private final Map<String, GraphQLNamedInputType> inputGTypes = new LinkedHashMap<>();
private final Set<GraphQLDirective> directives = new LinkedHashSet<>();
private final GraphQLCodeRegistry.Builder codeRegistry;
public final Map<String, OperationTypeDefinition> operationTypeDefs;
Expand Down Expand Up @@ -173,15 +174,15 @@ void putOutputType(GraphQLNamedOutputType outputType) {
outputGTypes.put(outputType.getName(), outputType);
// certain types can be both input and output types, for example enums and scalars
if (outputType instanceof GraphQLInputType) {
inputGTypes.put(outputType.getName(), (GraphQLInputType) outputType);
inputGTypes.put(outputType.getName(), (GraphQLNamedInputType) outputType);
}
}

void putInputType(GraphQLNamedInputType inputType) {
inputGTypes.put(inputType.getName(), inputType);
// certain types can be both input and output types, for example enums and scalars
if (inputType instanceof GraphQLOutputType) {
outputGTypes.put(inputType.getName(), (GraphQLOutputType) inputType);
outputGTypes.put(inputType.getName(), (GraphQLNamedOutputType) inputType);
}
}

Expand Down Expand Up @@ -996,12 +997,12 @@ List<UnionTypeExtensionDefinition> unionTypeExtensions(UnionTypeDefinition typeD
*
* @return the additional types not referenced from the top level operations
*/
Set<GraphQLType> buildAdditionalTypes(BuildContext buildCtx) {
Set<GraphQLNamedType> buildAdditionalTypes(BuildContext buildCtx) {
TypeDefinitionRegistry typeRegistry = buildCtx.getTypeRegistry();

Set<String> detachedTypeNames = getDetachedTypeNames(buildCtx);

Set<GraphQLType> additionalTypes = new LinkedHashSet<>();
Set<GraphQLNamedType> additionalTypes = new LinkedHashSet<>();
// recursively record detached types on the ctx and add them to the additionalTypes set
typeRegistry.types().values().stream()
.filter(typeDefinition -> detachedTypeNames.contains(typeDefinition.getName()))
Expand Down
93 changes: 49 additions & 44 deletions src/main/java/graphql/schema/impl/GraphQLTypeCollectingVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@
import static graphql.util.TraversalControl.CONTINUE;
import static java.lang.String.format;

/**
* A visitor that collects all {@link GraphQLNamedType}s during schema traversal.
* <p>
* This visitor must be used with a traverser that calls {@code getChildrenWithTypeReferences()}
* to get children (see {@link SchemaUtil#visitPartiallySchema}). This means that when a field,
* argument, or input field references a type via {@link GraphQLTypeReference}, the traverser
* will see the type reference as a child, not the actual type it points to. Type references
* themselves are not collected - only concrete type instances are stored in the result map.
* <p>
* Because type references are not followed, this visitor also tracks "indirect strong references"
* - types that are directly referenced (not via type reference) by fields, arguments, and input
* fields. This handles edge cases where schema transformations replace type references with
* actual types, which would otherwise be missed during traversal.
*
* @see SchemaUtil#visitPartiallySchema
* @see #fixDanglingReplacedTypes
*/
@Internal
public class GraphQLTypeCollectingVisitor extends GraphQLTypeVisitorStub {

Expand All @@ -55,31 +72,22 @@ public TraversalControl visitGraphQLScalarType(GraphQLScalarType node, Traverser

@Override
public TraversalControl visitGraphQLObjectType(GraphQLObjectType node, TraverserContext<GraphQLSchemaElement> context) {
if (isNotTypeReference(node.getName())) {
assertTypeUniqueness(node, result);
} else {
save(node.getName(), node);
}
assertTypeUniqueness(node, result);
save(node.getName(), node);
return CONTINUE;
}

@Override
public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType node, TraverserContext<GraphQLSchemaElement> context) {
if (isNotTypeReference(node.getName())) {
assertTypeUniqueness(node, result);
} else {
save(node.getName(), node);
}
assertTypeUniqueness(node, result);
save(node.getName(), node);
return CONTINUE;
}

@Override
public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType node, TraverserContext<GraphQLSchemaElement> context) {
if (isNotTypeReference(node.getName())) {
assertTypeUniqueness(node, result);
} else {
save(node.getName(), node);
}
assertTypeUniqueness(node, result);
save(node.getName(), node);
return CONTINUE;
}

Expand Down Expand Up @@ -114,43 +122,25 @@ public TraversalControl visitGraphQLAppliedDirectiveArgument(GraphQLAppliedDirec
return CONTINUE;
}

private <T> void saveIndirectStrongReference(Supplier<GraphQLType> typeSupplier) {
private void saveIndirectStrongReference(Supplier<GraphQLType> typeSupplier) {
GraphQLNamedType type = unwrapAllAs(typeSupplier.get());
if (!(type instanceof GraphQLTypeReference)) {
indirectStrongReferences.put(type.getName(), type);
}
}


private boolean isNotTypeReference(String name) {
return result.containsKey(name) && !(result.get(name) instanceof GraphQLTypeReference);
}

private void save(String name, GraphQLNamedType type) {
result.put(name, type);
}


/*
From https://spec.graphql.org/October2021/#sec-Type-System

All types within a GraphQL schema must have unique names. No two provided types may have the same name.
No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types).

Enforcing this helps avoid problems later down the track fo example https://github.com/graphql-java/graphql-java/issues/373
*/
private void assertTypeUniqueness(GraphQLNamedType type, Map<String, GraphQLNamedType> result) {
GraphQLType existingType = result.get(type.getName());
// do we have an existing definition
GraphQLNamedType existingType = result.get(type.getName());
if (existingType != null) {
// type references are ok
if (!(existingType instanceof GraphQLTypeReference || type instanceof GraphQLTypeReference)) {
assertUniqueTypeObjects(type, existingType);
}
assertUniqueTypeObjects(type, existingType);
}
}

private void assertUniqueTypeObjects(GraphQLNamedType type, GraphQLType existingType) {
private void assertUniqueTypeObjects(GraphQLNamedType type, GraphQLNamedType existingType) {
// object comparison here is deliberate
if (existingType != type) {
throw new AssertException(format("All types within a GraphQL schema must have unique names. No two provided types may have the same name.\n" +
Expand All @@ -166,15 +156,30 @@ public ImmutableMap<String, GraphQLNamedType> getResult() {
}

/**
* It's possible for certain schema edits to create a situation where a field / arg / input field had a type reference, then
* it got replaced with an actual strong reference and then the schema gets edited such that the only reference
* to that type is the replaced strong reference. This edge case means that the replaced reference can be
* missed if it's the only way to get to that type because this visitor asks for the children as original type,
* e.g. the type reference and not the replaced reference.
* Fixes an edge case where types might be missed during traversal due to replaced type references.
* <p>
* The problem: During schema construction or transformation, a field's type might initially
* be a {@link GraphQLTypeReference} (a placeholder). Later, this reference gets replaced with the
* actual type instance. However, the schema traverser uses {@code getChildrenWithTypeReferences()}
* to discover children, which returns the original type references, not the replaced strong references.
* <p>
* The scenario:
* <ol>
* <li>Field {@code foo} has type reference to {@code Bar}</li>
* <li>During schema editing, the reference is replaced with the actual {@code Bar} type</li>
* <li>Further edits remove all other paths to {@code Bar}</li>
* <li>Now the only way to reach {@code Bar} is via the replaced reference in {@code foo}</li>
* <li>But the traverser still sees the original type reference as the child, so {@code Bar}
* is never visited as a child node</li>
* </ol>
* <p>
* The fix: During traversal, we also capture types directly from fields/arguments/inputs
* (in {@link #indirectStrongReferences}). After traversal, we merge any types that were captured
* this way but weren't found through normal traversal.
*
* @param visitedTypes the types collected by this visitor
* @param visitedTypes the types collected through normal traversal
*
* @return a fixed up map where the only
* @return the fixed map including any dangling replaced types
*/
private Map<String, GraphQLNamedType> fixDanglingReplacedTypes(Map<String, GraphQLNamedType> visitedTypes) {
for (GraphQLNamedType indirectStrongReference : indirectStrongReferences.values()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.stream.Stream;

import static graphql.schema.SchemaTransformer.transformSchema;
import static graphql.schema.SchemaTransformer.transformSchemaWithDeletes;

/**
* Transforms a schema by applying a visibility predicate to every field.
Expand Down Expand Up @@ -75,7 +76,7 @@ public final GraphQLSchema apply(GraphQLSchema schema) {
new SchemaTraverser(getChildrenFn(schema)).depthFirst(new TypeObservingVisitor(observedBeforeTransform), getRootTypes(schema));

// remove fields
GraphQLSchema interimSchema = transformSchema(schema,
GraphQLSchema interimSchema = transformSchemaWithDeletes(schema,
new FieldRemovalVisitor(visibleFieldPredicate, markedForRemovalTypes));

new SchemaTraverser(getChildrenFn(interimSchema)).depthFirst(new TypeObservingVisitor(observedAfterTransform), getRootTypes(interimSchema));
Expand Down Expand Up @@ -112,7 +113,7 @@ private Function<GraphQLSchemaElement, List<GraphQLSchemaElement>> getChildrenFn

private GraphQLSchema removeUnreferencedTypes(Set<GraphQLType> markedForRemovalTypes, GraphQLSchema connectedSchema) {
GraphQLSchema withoutAdditionalTypes = connectedSchema.transform(builder -> {
Set<GraphQLType> additionalTypes = new HashSet<>(connectedSchema.getAdditionalTypes());
Set<GraphQLNamedType> additionalTypes = new HashSet<>(connectedSchema.getAdditionalTypes());
additionalTypes.removeAll(markedForRemovalTypes);
builder.clearAdditionalTypes();
builder.additionalTypes(additionalTypes);
Expand Down
Loading
Loading