Skip to content

Commit d47bac5

Browse files
authored
Merge pull request #895 from graphql-java-kickstart/bugfix/875
Run directive wirings on the assembled schema
2 parents 21f3796 + e7b7d46 commit d47bac5

5 files changed

Lines changed: 282 additions & 73 deletions

File tree

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

Lines changed: 52 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ class SchemaParser internal constructor(
7171
private val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
7272
private val directiveWiringHelper = DirectiveWiringHelper(runtimeWiring, codeRegistryBuilder)
7373

74-
private lateinit var schemaDirectives: SchemaDirectives
74+
// by name, in the order they're declared
75+
private lateinit var schemaDirectives: MutableMap<String, GraphQLDirective>
7576

7677
/**
7778
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
@@ -88,21 +89,14 @@ class SchemaParser internal constructor(
8889

8990
// Create GraphQL objects
9091
val inputObjects: MutableList<GraphQLInputObjectType> = mutableListOf()
91-
createDirectives(inputObjects)
92+
createDirectives()
9293
inputObjectDefinitions.forEach {
9394
if (inputObjects.none { io -> io.name == it.name }) {
9495
inputObjects.add(createInputObject(it, inputObjects, mutableSetOf()))
9596
}
9697
}
9798
val interfaces = interfaceDefinitions.map { createInterfaceObject(it, inputObjects) }
9899
val objects = objectDefinitions.map { createObject(it, interfaces, inputObjects) }
99-
if (options.unmappedInputFields != UnmappedInputFields.IGNORE) {
100-
val checker = UnmappedInputFieldChecker(inputObjects.associateBy { it.name }, options.unmappedInputFields)
101-
objectDefinitions.forEach { type ->
102-
fieldResolversByType[type].orEmpty().values.filterIsInstance<MethodFieldResolver>().forEach { checker.check(type.name, it) }
103-
}
104-
checker.report()
105-
}
106100
val unions = unionDefinitions.map { createUnionObject(it, objects) }
107101
val enums = enumDefinitions.map { createEnumObject(it) }
108102

@@ -128,7 +122,33 @@ class SchemaParser internal constructor(
128122
// Directives applied to the schema itself, e.g. `extend schema @link(...)`. Only GraphQLAppliedDirectives are built here, not
129123
// the deprecated GraphQLDirective ones: graphql-java doesn't resolve type references in those, so enum arguments would fail.
130124
val schemaAppliedDirectives = buildAppliedDirectives(rootInfo.getDirectives(), SCHEMA).toList()
131-
return SchemaObjects(query, mutation, subscription, types, schemaDirectives.all, codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives)
125+
val schemaObjects = SchemaObjects(query, mutation, subscription, types, schemaDirectives.values.toSet(), codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives)
126+
// in the order the types were created
127+
val wiredObjects = if (directiveWiringHelper.isWiringRequired) wire(schemaObjects, inputObjects + interfaces + objects + unions + enums) else schemaObjects
128+
if (options.unmappedInputFields != UnmappedInputFields.IGNORE) {
129+
val wiredInputObjects = wiredObjects.dictionary.filterIsInstance<GraphQLInputObjectType>().associateBy { it.name }
130+
val checker = UnmappedInputFieldChecker(wiredInputObjects, options.unmappedInputFields)
131+
objectDefinitions.forEach { type ->
132+
fieldResolversByType[type].orEmpty().values.filterIsInstance<MethodFieldResolver>().forEach { checker.check(type.name, it) }
133+
}
134+
checker.report()
135+
}
136+
return wiredObjects
137+
}
138+
139+
/**
140+
* Runs the directive wirings on the types once the schema is assembled, so they see resolved types, and swaps in the types they change.
141+
*/
142+
private fun wire(schemaObjects: SchemaObjects, types: List<GraphQLNamedType>): SchemaObjects {
143+
// building the schema resolves the type references in these types
144+
val wired = directiveWiringHelper.wireTypes(schemaObjects.toSchema(), types)
145+
if (wired.isEmpty()) return schemaObjects
146+
return schemaObjects.copy(
147+
query = wired[schemaObjects.query.name] as GraphQLObjectType? ?: schemaObjects.query,
148+
mutation = schemaObjects.mutation?.let { wired[it.name] as GraphQLObjectType? ?: it },
149+
subscription = schemaObjects.subscription?.let { wired[it.name] as GraphQLObjectType? ?: it },
150+
dictionary = schemaObjects.dictionary.mapTo(linkedSetOf()) { wired[(it as GraphQLNamedType).name] ?: it }
151+
)
132152
}
133153

134154
/**
@@ -178,7 +198,7 @@ class SchemaParser internal constructor(
178198
}
179199
}
180200

181-
return directiveWiringHelper.wireObject(builder.build())
201+
return builder.build()
182202
}
183203

184204
private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List<GraphQLInputObjectType>,
@@ -214,7 +234,7 @@ class SchemaParser internal constructor(
214234
}
215235
}
216236

217-
return directiveWiringHelper.wireInputObject(builder.build())
237+
return builder.build()
218238
}
219239

220240
private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
@@ -259,7 +279,7 @@ class SchemaParser internal constructor(
259279
}
260280
}
261281

262-
return directiveWiringHelper.wireEnum(builder.build())
282+
return builder.build()
263283
}
264284

265285
private fun createInterfaceObject(interfaceDefinition: InterfaceTypeDefinition, inputObjects: List<GraphQLInputObjectType>): GraphQLInterfaceType {
@@ -286,7 +306,7 @@ class SchemaParser internal constructor(
286306
}
287307
}
288308

289-
return directiveWiringHelper.wireInterFace(builder.build())
309+
return builder.build()
290310
}
291311

292312
private fun createUnionObject(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): GraphQLUnionType {
@@ -304,7 +324,7 @@ class SchemaParser internal constructor(
304324
getLeafUnionObjects(definition, types).forEach { possibleType(it) }
305325
}
306326

307-
return directiveWiringHelper.wireUnion(builder.build())
327+
return builder.build()
308328
}
309329

310330
private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): List<GraphQLObjectType> {
@@ -353,7 +373,7 @@ class SchemaParser internal constructor(
353373
.description(getDocumentation(fieldDefinition, options))
354374
.definition(fieldDefinition)
355375
.apply { getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
356-
.type(determineOutputType(fieldDefinition.type, inputObjects))
376+
.type(determineOutputType(fieldDefinition.type))
357377
.withAppliedDirectives(*buildAppliedDirectives(fieldDefinition.directives, FIELD_DEFINITION))
358378
.withDirectives(*buildDirectives(fieldDefinition.directives, FIELD_DEFINITION))
359379
.apply {
@@ -376,11 +396,10 @@ class SchemaParser internal constructor(
376396
.build()
377397
}
378398

379-
private fun createDirectives(inputObjects: MutableList<GraphQLInputObjectType>) {
380-
schemaDirectives = SchemaDirectives(inputObjects)
381-
directiveDefinitions.forEach { schemaDirectives.find(it.name) }
399+
private fun createDirectives() {
400+
schemaDirectives = directiveDefinitions.associateTo(linkedMapOf()) { it.name to createDirective(it) }
382401
// because the arguments can have directives too, we attach them only after the directives themselves are created
383-
schemaDirectives.update { d ->
402+
schemaDirectives.replaceAll { _, d ->
384403
val arguments = d.arguments.map { a -> a.transform {
385404
it.withAppliedDirectives(*buildAppliedDirectives(a.definition!!.directives, ARGUMENT_DEFINITION))
386405
.withDirectives(*buildDirectives(a.definition!!.directives, ARGUMENT_DEFINITION))
@@ -389,33 +408,7 @@ class SchemaParser internal constructor(
389408
}
390409
}
391410

392-
/**
393-
* The directive definitions. The input objects used by their arguments need the directives applied to them, so each directive
394-
* is first built with type references to input objects, then rebuilt with the input objects the first time it's looked up.
395-
* Only a directive applied within its own argument types gets its first build there.
396-
*/
397-
private inner class SchemaDirectives(private val inputObjects: MutableList<GraphQLInputObjectType>) {
398-
// by name, in the order they're declared
399-
private val byName = directiveDefinitions.associateTo(linkedMapOf()) { definition ->
400-
definition.name to createDirective(definition) { determineType(GraphQLInputType::class, it, permittedTypesForInputObject, inputObjects) as GraphQLInputType }
401-
}
402-
private val unfinished = directiveDefinitions.associateByTo(mutableMapOf()) { it.name }
403-
// shared, so that an input object that's still being built isn't built again for another directive
404-
private val referencingInputObjects = mutableSetOf<String>()
405-
406-
val all: Set<GraphQLDirective> get() = byName.values.toSet()
407-
408-
fun find(name: String): GraphQLDirective? {
409-
unfinished.remove(name)?.let { definition ->
410-
byName[name] = createDirective(definition) { determineInputType(it, inputObjects, referencingInputObjects) }
411-
}
412-
return byName[name]
413-
}
414-
415-
fun update(transform: (GraphQLDirective) -> GraphQLDirective) = byName.replaceAll { _, directive -> transform(directive) }
416-
}
417-
418-
private fun createDirective(definition: DirectiveDefinition, determineArgumentType: (Type<*>) -> GraphQLInputType): GraphQLDirective {
411+
private fun createDirective(definition: DirectiveDefinition): GraphQLDirective {
419412
val locations = definition.directiveLocations.map { Introspection.DirectiveLocation.valueOf(it.name) }.toTypedArray()
420413

421414
return GraphQLDirective.newDirective()
@@ -427,18 +420,19 @@ class SchemaParser internal constructor(
427420
.repeatable(definition.isRepeatable)
428421
.apply {
429422
definition.inputValueDefinitions.forEach { argumentDefinition ->
430-
argument(createDirectiveArgument(argumentDefinition, determineArgumentType))
423+
argument(createDirectiveArgument(argumentDefinition))
431424
}
432425
}
433426
.build()
434427
}
435428

436-
private fun createDirectiveArgument(definition: InputValueDefinition, determineArgumentType: (Type<*>) -> GraphQLInputType): GraphQLArgument {
429+
private fun createDirectiveArgument(definition: InputValueDefinition): GraphQLArgument {
437430
return GraphQLArgument.newArgument()
438431
.name(definition.name)
439432
.definition(definition)
440433
.description(getDocumentation(definition, options))
441-
.type(determineArgumentType(definition.type))
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)
442436
.apply { getDeprecated(definition.directives)?.let { deprecate(it) } }
443437
.apply { definition.defaultValue?.let { defaultValueLiteral(it) } }
444438
.build()
@@ -508,7 +502,7 @@ class SchemaParser internal constructor(
508502
}
509503
}
510504

511-
private fun findDirective(name: String): GraphQLDirective? = schemaDirectives.find(name) ?: BUILT_IN_DIRECTIVES[name]
505+
private fun findDirective(name: String): GraphQLDirective? = schemaDirectives[name] ?: BUILT_IN_DIRECTIVES[name]
512506

513507
private fun buildUndeclaredAppliedDirective(directive: Directive): GraphQLAppliedDirective {
514508
return GraphQLAppliedDirective.newDirective()
@@ -597,22 +591,18 @@ class SchemaParser internal constructor(
597591
private fun missingArgumentsWithDefault(directive: Directive, graphQLDirective: GraphQLDirective): List<GraphQLArgument> =
598592
graphQLDirective.arguments.filter { it.hasSetDefaultValue() && directive.getArgument(it.name) == null }
599593

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

597+
// named types other than scalars are type references
603598
private fun <T : Any> determineType(
604599
expectedType: KClass<T>,
605600
typeDefinition: Type<*>,
606-
allowedTypeReferences: Set<String>,
607-
inputObjects: List<GraphQLInputObjectType>
601+
allowedTypeReferences: Set<String>
608602
): GraphQLType =
609603
when (typeDefinition) {
610-
is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
611-
is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences, inputObjects))
612-
is InputObjectTypeDefinition -> {
613-
log.info("Create input object")
614-
createInputObject(typeDefinition, inputObjects, mutableSetOf())
615-
}
604+
is ListType -> GraphQLList(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
605+
is NonNullType -> GraphQLNonNull(determineType(expectedType, typeDefinition.type, allowedTypeReferences))
616606
is TypeName -> {
617607
val scalarType = customScalars[typeDefinition.name]
618608
?: GRAPHQL_SCALARS[typeDefinition.name]
@@ -622,7 +612,7 @@ class SchemaParser internal constructor(
622612
if (!allowedTypeReferences.contains(typeDefinition.name)) {
623613
throw unexpectedTypeError(typeDefinition, expectedType)
624614
}
625-
inputObjects.find { it.name == typeDefinition.name } ?: GraphQLTypeReference(typeDefinition.name)
615+
GraphQLTypeReference(typeDefinition.name)
626616
}
627617
}
628618
else -> throw SchemaError("Unknown type: $typeDefinition")

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

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,91 @@ internal class DirectiveWiringHelper(
1616
) {
1717
private val schemaDirectiveParameters = Parameters(runtimeWiring, codeRegistryBuilder)
1818

19-
fun wireObject(objectType: GraphQLObjectType): GraphQLObjectType {
19+
val isWiringRequired = runtimeWiring.registeredDirectiveWiring.isNotEmpty() || runtimeWiring.directiveWiring.isNotEmpty()
20+
21+
/**
22+
* Runs the wirings on the given types of the assembled schema. If they change any, returns every type by name with type references to
23+
* the others, so that the next build resolves them to the wired types.
24+
*/
25+
fun wireTypes(schema: GraphQLSchema, types: List<GraphQLNamedType>): Map<String, GraphQLNamedType> {
26+
val changed = linkedMapOf<String, GraphQLNamedType>()
27+
types.forEach { type ->
28+
val wired = when (type) {
29+
is GraphQLObjectType -> wireObject(type)
30+
is GraphQLInterfaceType -> wireInterFace(type)
31+
is GraphQLInputObjectType -> wireInputObject(type)
32+
is GraphQLEnumType -> wireEnum(type)
33+
is GraphQLUnionType -> wireUnion(type)
34+
else -> type
35+
}
36+
if (wired !== type) changed[type.name] = wired
37+
}
38+
if (changed.isEmpty()) return changed
39+
val references = TypeReferences(schema)
40+
return types.associate { it.name to references.inType(changed[it.name] ?: it) }
41+
}
42+
43+
/**
44+
* Swaps the schema's own types that a type refers to for type references, like they were before the schema was built.
45+
*/
46+
private class TypeReferences(private val schema: GraphQLSchema) {
47+
// the types wirings build in code, rebuilt once each
48+
private val built = IdentityHashMap<GraphQLType, GraphQLType>()
49+
50+
@Suppress("UNCHECKED_CAST")
51+
fun <T : GraphQLNamedType> inType(type: T): T = when (type) {
52+
is GraphQLObjectType -> type.transform { it.clearFields().fields(type.fields.map(::inField)).replaceInterfaces(of(type.interfaces)) }
53+
is GraphQLInterfaceType -> type.transform { it.clearFields().fields(type.fields.map(::inField)).replaceInterfacesOrReferences(of(type.interfaces)) }
54+
is GraphQLUnionType -> type.transform { it.replacePossibleTypes(of(type.types)) }
55+
is GraphQLInputObjectType -> type.transform { it.clearFields().fields(type.fields.map(::inInputField)) }
56+
else -> type
57+
} as T
58+
59+
private fun inField(field: GraphQLFieldDefinition) = field.transform {
60+
it.type(of(field.type) as GraphQLOutputType)
61+
.clearArguments()
62+
.arguments(field.arguments.map { argument -> argument.transform { a -> a.type(of(argument.type) as GraphQLInputType) } })
63+
}
64+
65+
private fun inInputField(field: GraphQLInputObjectField) = field.transform { it.type(of(field.type) as GraphQLInputType) }
66+
67+
private fun of(type: GraphQLType): GraphQLType = when (type) {
68+
is GraphQLNonNull -> GraphQLNonNull.nonNull(of(type.wrappedType))
69+
is GraphQLList -> GraphQLList.list(of(type.wrappedType))
70+
// scalars aren't wired, and a reference only finds a scalar another field uses
71+
is GraphQLScalarType -> type
72+
is GraphQLNamedType -> if (schema.typeMap[type.name] === type) GraphQLTypeReference.typeRef(type.name) else built.getOrPut(type) { inType(type) }
73+
else -> type
74+
}
75+
76+
private fun of(types: List<GraphQLNamedOutputType>) = types.map { of(it) as GraphQLNamedOutputType }
77+
}
78+
79+
private fun wireObject(objectType: GraphQLObjectType): GraphQLObjectType {
2080
return wireFields(objectType)
2181
.let { fields -> if (objectType.fields != fields) objectType.transform { it.clearFields().fields(fields) } else objectType }
2282
.let { wireDirectives(WiringWrapper(it, OBJECT, SchemaDirectiveWiring::onObject)) }
2383
}
2484

25-
fun wireInterFace(interfaceType: GraphQLInterfaceType): GraphQLInterfaceType {
85+
private fun wireInterFace(interfaceType: GraphQLInterfaceType): GraphQLInterfaceType {
2686
return wireFields(interfaceType)
2787
.let { fields -> if (interfaceType.fields != fields) interfaceType.transform { it.clearFields().fields(fields) } else interfaceType }
2888
.let { wireDirectives(WiringWrapper(it, INTERFACE, SchemaDirectiveWiring::onInterface)) }
2989
}
3090

31-
fun wireInputObject(inputObjectType: GraphQLInputObjectType): GraphQLInputObjectType {
91+
private fun wireInputObject(inputObjectType: GraphQLInputObjectType): GraphQLInputObjectType {
3292
return wireInputFields(inputObjectType)
3393
.let { fields -> if (inputObjectType.fields != fields) inputObjectType.transform { it.clearFields().fields(fields) } else inputObjectType }
3494
.let { wireDirectives(WiringWrapper(it, INPUT_OBJECT, SchemaDirectiveWiring::onInputObjectType)) }
3595
}
3696

37-
fun wireEnum(enumType: GraphQLEnumType): GraphQLEnumType {
97+
private fun wireEnum(enumType: GraphQLEnumType): GraphQLEnumType {
3898
return wireEnumValues(enumType)
3999
.let { values -> if (enumType.values != values) enumType.transform { it.clearValues().values(values) } else enumType }
40100
.let { wireDirectives(WiringWrapper(it, ENUM, SchemaDirectiveWiring::onEnum)) }
41101
}
42102

43-
fun wireUnion(unionType: GraphQLUnionType): GraphQLUnionType {
103+
private fun wireUnion(unionType: GraphQLUnionType): GraphQLUnionType {
44104
return wireDirectives(WiringWrapper(unionType, UNION, SchemaDirectiveWiring::onUnion))
45105
}
46106

0 commit comments

Comments
 (0)