Skip to content

Commit 4171fb4

Browse files
committed
Reference schema types from the fields of wired types so wirings can change self-referencing field types
Also drop the input objects parameter of determineType, which can no longer match.
1 parent 2b94fef commit 4171fb4

3 files changed

Lines changed: 90 additions & 20 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt‎

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ class SchemaParser internal constructor(
373373
.description(getDocumentation(fieldDefinition, options))
374374
.definition(fieldDefinition)
375375
.apply { getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
376-
.type(determineOutputType(fieldDefinition.type, inputObjects))
376+
.type(determineOutputType(fieldDefinition.type))
377377
.withAppliedDirectives(*buildAppliedDirectives(fieldDefinition.directives, FIELD_DEFINITION))
378378
.withDirectives(*buildDirectives(fieldDefinition.directives, FIELD_DEFINITION))
379379
.apply {
@@ -431,8 +431,8 @@ class SchemaParser internal constructor(
431431
.name(definition.name)
432432
.definition(definition)
433433
.description(getDocumentation(definition, options))
434-
// input objects are type references here, since they need the directives to be built first
435-
.type(determineType(GraphQLInputType::class, definition.type, permittedTypesForInputObject, listOf()) as GraphQLInputType)
434+
// type references, so that an input object used by a directive applied to it doesn't contain itself
435+
.type(determineType(GraphQLInputType::class, definition.type, permittedTypesForInputObject) as GraphQLInputType)
436436
.apply { getDeprecated(definition.directives)?.let { deprecate(it) } }
437437
.apply { definition.defaultValue?.let { defaultValueLiteral(it) } }
438438
.build()
@@ -591,22 +591,18 @@ class SchemaParser internal constructor(
591591
private fun missingArgumentsWithDefault(directive: Directive, graphQLDirective: GraphQLDirective): List<GraphQLArgument> =
592592
graphQLDirective.arguments.filter { it.hasSetDefaultValue() && directive.getArgument(it.name) == null }
593593

594-
private fun determineOutputType(typeDefinition: Type<*>, inputObjects: List<GraphQLInputObjectType>) =
595-
determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject, inputObjects) as GraphQLOutputType
594+
private fun determineOutputType(typeDefinition: Type<*>) =
595+
determineType(GraphQLOutputType::class, typeDefinition, permittedTypesForObject) as GraphQLOutputType
596596

597+
// named types other than scalars are type references
597598
private fun <T : Any> determineType(
598599
expectedType: KClass<T>,
599600
typeDefinition: Type<*>,
600-
allowedTypeReferences: Set<String>,
601-
inputObjects: List<GraphQLInputObjectType>
601+
allowedTypeReferences: Set<String>
602602
): GraphQLType =
603603
when (typeDefinition) {
604-
is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
605-
is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
606-
is InputObjectTypeDefinition -> {
607-
log.info("Create input object")
608-
createInputObject(typeDefinition, inputObjects, mutableSetOf())
609-
}
604+
is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
605+
is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
610606
is TypeName -> {
611607
val scalarType = customScalars[typeDefinition.name]
612608
?: GRAPHQL_SCALARS[typeDefinition.name]
@@ -616,7 +612,7 @@ class SchemaParser internal constructor(
616612
if (!allowedTypeReferences.contains(typeDefinition.name)) {
617613
throw unexpectedTypeError(typeDefinition, expectedType)
618614
}
619-
inputObjects.find { it.name == typeDefinition.name } ?: GraphQLTypeReference(typeDefinition.name)
615+
GraphQLTypeReference(typeDefinition.name)
620616
}
621617
}
622618
else -> throw SchemaError("Unknown type: $typeDefinition")

‎src/main/kotlin/graphql/kickstart/tools/directive/DirectiveWiringHelper.kt‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ internal class DirectiveWiringHelper(
2222
val isWiringRequired = runtimeWiring.registeredDirectiveWiring.isNotEmpty() || runtimeWiring.directiveWiring.isNotEmpty()
2323

2424
fun wireSchema(schema: GraphQLSchema): GraphQLSchema = SchemaTransformer.transformSchema(schema, object : GraphQLTypeVisitorStub() {
25+
private val references = TypeReferences(schema)
26+
2527
override fun visitGraphQLObjectType(node: GraphQLObjectType, context: TraverserContext<GraphQLSchemaElement>) =
2628
wire(node, node.definition, context, ::wireObject)
2729

@@ -41,12 +43,46 @@ internal class DirectiveWiringHelper(
4143
private fun <T : GraphQLNamedType> wire(node: T, definition: Node<*>?, context: TraverserContext<GraphQLSchemaElement>, wireType: (T) -> T): TraversalControl {
4244
if (definition != null) {
4345
val wired = wireType(node)
44-
if (wired !== node) changeNode(context, wired)
46+
if (wired !== node) changeNode(context, references.inFields(wired))
4547
}
4648
return TraversalControl.CONTINUE
4749
}
4850
})
4951

52+
/**
53+
* Swaps the schema's own types in the fields of a wired type for type references, like they were before the schema was built.
54+
* Otherwise a type whose fields refer to itself contains itself, which the schema transformer can't rebuild.
55+
*/
56+
private class TypeReferences(private val schema: GraphQLSchema) {
57+
// the types wirings build in code, rebuilt once each
58+
private val built = IdentityHashMap<GraphQLType, GraphQLType>()
59+
60+
@Suppress("UNCHECKED_CAST")
61+
fun <T : GraphQLNamedType> inFields(type: T): T = when (type) {
62+
is GraphQLObjectType -> type.transform { it.clearFields().fields(type.fields.map(::inField)) }
63+
is GraphQLInterfaceType -> type.transform { it.clearFields().fields(type.fields.map(::inField)) }
64+
is GraphQLInputObjectType -> type.transform { it.clearFields().fields(type.fields.map(::inInputField)) }
65+
else -> type
66+
} as T
67+
68+
private fun inField(field: GraphQLFieldDefinition) = field.transform {
69+
it.type(of(field.type) as GraphQLOutputType)
70+
.clearArguments()
71+
.arguments(field.arguments.map { argument -> argument.transform { a -> a.type(of(argument.type) as GraphQLInputType) } })
72+
}
73+
74+
private fun inInputField(field: GraphQLInputObjectField) = field.transform { it.type(of(field.type) as GraphQLInputType) }
75+
76+
private fun of(type: GraphQLType): GraphQLType = when (type) {
77+
is GraphQLNonNull -> GraphQLNonNull.nonNull(of(type.wrappedType))
78+
is GraphQLList -> GraphQLList.list(of(type.wrappedType))
79+
// only these can lead back to the wired type. Scalars stay, since a reference only finds a scalar another field uses
80+
is GraphQLFieldsContainer, is GraphQLInputObjectType ->
81+
if (schema.typeMap[type.name] === type) GraphQLTypeReference.typeRef(type.name) else built.getOrPut(type) { inFields(type) }
82+
else -> type
83+
}
84+
}
85+
5086
private fun wireObject(objectType: GraphQLObjectType): GraphQLObjectType {
5187
return wireFields(objectType)
5288
.let { fields -> if (objectType.fields != fields) objectType.transform { it.clearFields().fields(fields) } else objectType }

‎src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt‎

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,18 +256,40 @@ class DirectiveTest {
256256
}
257257

258258
@Test
259-
fun `should keep types a directive wiring builds in code`() {
259+
fun `should let directive wirings change the types of self-referencing fields`() {
260260
val schema = SchemaParser.newParser()
261261
.schemaString(
262262
"""
263+
directive @required on FIELD_DEFINITION
264+
directive @many on INPUT_FIELD_DEFINITION
263265
directive @wrap on FIELD_DEFINITION
264266
265267
type Query {
266-
name: String @wrap
268+
person(filter: PersonFilter): Person
269+
}
270+
271+
type Person {
272+
name: String
273+
age: Int
274+
friend: Person @required
275+
best: Person @wrap
276+
}
277+
278+
input PersonFilter {
279+
name: String
280+
parent: PersonFilter @many
267281
}
268282
""")
269283
.resolvers(object : GraphQLQueryResolver {
270-
fun name(): String? = null
284+
fun person(filter: PersonFilter?): Person = Person("a")
285+
})
286+
.directive("required", object : SchemaDirectiveWiring {
287+
override fun onField(environment: SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition>): GraphQLFieldDefinition =
288+
environment.element.transform { it.type(GraphQLNonNull.nonNull(environment.element.type)) }
289+
})
290+
.directive("many", object : SchemaDirectiveWiring {
291+
override fun onInputObjectField(environment: SchemaDirectiveWiringEnvironment<GraphQLInputObjectField>): GraphQLInputObjectField =
292+
environment.element.transform { it.type(GraphQLList.list(environment.element.type)) }
271293
})
272294
.directive("wrap", object : SchemaDirectiveWiring {
273295
override fun onField(environment: SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition>): GraphQLFieldDefinition {
@@ -280,9 +302,15 @@ class DirectiveTest {
280302
})
281303
.build()
282304
.makeExecutableSchema()
283-
.assertEveryFieldResolves()
284305

285-
assertEquals(GraphQLTypeUtil.simplePrint(schema.queryType.getField("name").type), "Wrapper")
306+
val person = schema.getObjectType("Person")!!
307+
val filter = schema.getTypeAs<GraphQLInputObjectType>("PersonFilter")!!
308+
val wrapper = schema.getObjectType("Wrapper")!!
309+
assertEquals(GraphQLTypeUtil.simplePrint(person.getField("friend").type), "Person!")
310+
assertEquals(GraphQLTypeUtil.simplePrint(person.getField("best").type), "Wrapper")
311+
assertEquals(GraphQLTypeUtil.simplePrint(filter.getField("parent").type), "[PersonFilter]")
312+
assert(GraphQLTypeUtil.unwrapAll(person.getField("friend").type) === person)
313+
assert(wrapper.getField("value").type === person)
286314
}
287315

288316
@Test
@@ -849,6 +877,16 @@ class DirectiveTest {
849877
val label: String?
850878
)
851879

880+
class Person(val name: String, val age: Int = 1) {
881+
val friend: Person get() = this
882+
val best: Person get() = this
883+
}
884+
885+
data class PersonFilter(
886+
val name: String?,
887+
val parent: PersonFilter?
888+
)
889+
852890
private data class BoundInput(
853891
val max: Int?
854892
)

0 commit comments

Comments
 (0)