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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ docs/_build/
\.settings/
/.nb-gradle/
gen
.DS_Store
.DS_Store
.vscode
115 changes: 115 additions & 0 deletions src/main/java/graphql/schema/GraphQLSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,65 @@ public GraphQLObjectType getIntrospectionSchemaType() {
return introspectionSchemaType;
}

/**
* Returns the set of "additional types" that were provided when building the schema.
* <p>
* During schema construction, types are discovered by traversing the schema from multiple roots:
* <ul>
* <li>Root operation types (Query, Mutation, Subscription)</li>
* <li>Directive argument types</li>
* <li>Introspection types</li>
* <li>Types explicitly added via {@link Builder#additionalType(GraphQLType)}</li>
* </ul>
* <p>
* Additional types are types that are not reachable via any of the automatic traversal paths
* but still need to be part of the schema. The most common use case is for interface
* implementations that are not directly referenced elsewhere.
* <p>
* <b>Types that do NOT need to be added as additional types:</b>
* <ul>
* <li>Types reachable from Query, Mutation, or Subscription fields</li>
* <li>Types used as directive arguments (these are discovered via directive traversal)</li>
* </ul>
* <p>
* <b>When additional types ARE typically needed:</b>
* <ul>
* <li><b>Interface implementations:</b> When an interface is used as a field's return type,
* implementing object types are not automatically discovered because interfaces do not
* reference their implementors. These need to be added so they can be resolved at runtime
* and appear in introspection.</li>
* <li><b>SDL-defined schemas:</b> When building from SDL, the {@link graphql.schema.idl.SchemaGenerator}
* automatically detects types not connected to any root and adds them as additional types.</li>
* <li><b>Programmatic schemas with type references:</b> When using {@link GraphQLTypeReference}
* to break circular dependencies, the actual type implementations may need to be provided
* as additional types.</li>
* </ul>
* <p>
* <b>Example - Interface implementation not directly referenced:</b>
* <pre>{@code
* // Given this schema:
* // type Query { node: Node }
* // interface Node { id: ID! }
* // type User implements Node { id: ID!, name: String }
* //
* // User is not directly referenced from Query, so it needs to be added:
* GraphQLSchema.newSchema()
* .query(queryType)
* .additionalType(GraphQLObjectType.newObject().name("User")...)
* .build();
* }</pre>
* <p>
* <b>Note:</b> There are no restrictions on what types can be added via this mechanism.
* Types that are already reachable from other roots can also be added without causing
* errors - they will simply be present in both the type map (via traversal) and this set.
* After schema construction, use {@link #getTypeMap()} or {@link #getAllTypesAsList()} to get
* all types in the schema regardless of how they were discovered.
*
* @return an immutable set of types that were explicitly added as additional types
*
* @see Builder#additionalType(GraphQLType)
* @see Builder#additionalTypes(Set)
*/
public Set<GraphQLType> getAdditionalTypes() {
return additionalTypes;
}
Expand Down Expand Up @@ -722,16 +781,72 @@ public Builder codeRegistry(GraphQLCodeRegistry codeRegistry) {
return this;
}

/**
* Adds multiple types to the set of additional types.
* <p>
* Additional types are types that may not be directly reachable by traversing the schema
* from the root operation types (Query, Mutation, Subscription), but still need to be
* included in the schema. The most common use case is for object types that implement
* an interface but are not directly referenced as field return types.
* <p>
* <b>Example - Adding interface implementations:</b>
* <pre>{@code
* // If Node interface is used but User/Post types aren't directly referenced:
* builder.additionalTypes(Set.of(
* GraphQLObjectType.newObject().name("User").withInterface(nodeInterface)...,
* GraphQLObjectType.newObject().name("Post").withInterface(nodeInterface)...
* ));
* }</pre>
* <p>
* <b>Note:</b> There are no restrictions on what types can be added. Types already
* reachable from root operations can be added without causing errors - they will
* simply exist in both the traversed type map and this set.
*
* @param additionalTypes the types to add
*
* @return this builder
*
* @see GraphQLSchema#getAdditionalTypes()
*/
public Builder additionalTypes(Set<GraphQLType> additionalTypes) {
this.additionalTypes.addAll(additionalTypes);
return this;
}

/**
* Adds a single type to the set of additional types.
* <p>
* Additional types are types that may not be directly reachable by traversing the schema
* from the root operation types (Query, Mutation, Subscription), but still need to be
* included in the schema. The most common use case is for object types that implement
* an interface but are not directly referenced as field return types.
* <p>
* <b>Note:</b> There are no restrictions on what types can be added. Types already
* reachable from root operations can be added without causing errors.
*
* @param additionalType the type to add
*
* @return this builder
*
* @see GraphQLSchema#getAdditionalTypes()
* @see #additionalTypes(Set)
*/
public Builder additionalType(GraphQLType additionalType) {
this.additionalTypes.add(additionalType);
return this;
}

/**
* Clears all additional types that have been added to this builder.
* <p>
* This is useful when transforming an existing schema and you want to
* rebuild the additional types set from scratch.
*
* @return this builder
*
* @see #additionalType(GraphQLType)
* @see #additionalTypes(Set)
*/
public Builder clearAdditionalTypes() {
this.additionalTypes.clear();
return this;
Expand Down
112 changes: 112 additions & 0 deletions src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -563,4 +563,116 @@ class GraphQLSchemaTest extends Specification {

}

def "additionalTypes can contain any type when building programmatically - not restricted to detached types"() {
given: "types that will be directly reachable from Query"
def simpleType = newObject()
.name("SimpleType")
.field(newFieldDefinition()
.name("name")
.type(GraphQLString))
.build()

def simpleInputType = newInputObject()
.name("SimpleInput")
.field(newInputObjectField()
.name("value")
.type(GraphQLString))
.build()

def simpleInterface = GraphQLInterfaceType.newInterface()
.name("SimpleInterface")
.field(newFieldDefinition()
.name("id")
.type(GraphQLString))
.build()

def simpleUnion = GraphQLUnionType.newUnionType()
.name("SimpleUnion")
.possibleType(simpleType)
.build()

def simpleEnum = GraphQLEnumType.newEnum()
.name("SimpleEnum")
.value("VALUE_A")
.value("VALUE_B")
.build()

def simpleScalar = GraphQLScalarType.newScalar()
.name("SimpleScalar")
.coercing(new Coercing() {
@Override
Object serialize(Object dataFetcherResult) { return dataFetcherResult }

@Override
Object parseValue(Object input) { return input }

@Override
Object parseLiteral(Object input) { return input }
})
.build()

and: "a query type that references all these types directly"
def queryType = newObject()
.name("Query")
.field(newFieldDefinition()
.name("simpleField")
.type(simpleType))
.field(newFieldDefinition()
.name("interfaceField")
.type(simpleInterface))
.field(newFieldDefinition()
.name("unionField")
.type(simpleUnion))
.field(newFieldDefinition()
.name("enumField")
.type(simpleEnum))
.field(newFieldDefinition()
.name("scalarField")
.type(simpleScalar))
.field(newFieldDefinition()
.name("inputField")
.type(GraphQLString)
.argument(newArgument()
.name("input")
.type(simpleInputType)))
.build()

def codeRegistry = GraphQLCodeRegistry.newCodeRegistry()
.typeResolver(simpleInterface, { env -> simpleType })
.typeResolver(simpleUnion, { env -> simpleType })
.build()

when: "we add ALL types (including already reachable ones) as additionalTypes"
def schema = GraphQLSchema.newSchema()
.query(queryType)
.codeRegistry(codeRegistry)
.additionalType(simpleType) // already reachable via Query.simpleField
.additionalType(simpleInputType) // already reachable via Query.inputField argument
.additionalType(simpleInterface) // already reachable via Query.interfaceField
.additionalType(simpleUnion) // already reachable via Query.unionField
.additionalType(simpleEnum) // already reachable via Query.enumField
.additionalType(simpleScalar) // already reachable via Query.scalarField
.build()

then: "schema builds successfully - no restriction on what can be in additionalTypes"
schema != null

and: "all types are in the type map (as expected)"
schema.getType("SimpleType") == simpleType
schema.getType("SimpleInput") == simpleInputType
schema.getType("SimpleInterface") == simpleInterface
schema.getType("SimpleUnion") == simpleUnion
schema.getType("SimpleEnum") == simpleEnum
schema.getType("SimpleScalar") == simpleScalar

and: "additionalTypes contains all types we added - even though they were already reachable"
schema.getAdditionalTypes().size() == 6
schema.getAdditionalTypes().contains(simpleType)
schema.getAdditionalTypes().contains(simpleInputType)
schema.getAdditionalTypes().contains(simpleInterface)
schema.getAdditionalTypes().contains(simpleUnion)
schema.getAdditionalTypes().contains(simpleEnum)
schema.getAdditionalTypes().contains(simpleScalar)
}

}
Loading