Skip to content

Commit e7b7d46

Browse files
committed
Wire the types directly instead of transforming the schema, since the transform is slow on big schemas
1 parent 3a1a5f6 commit e7b7d46

3 files changed

Lines changed: 65 additions & 50 deletions

File tree

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ class SchemaParser internal constructor(
123123
// the deprecated GraphQLDirective ones: graphql-java doesn't resolve type references in those, so enum arguments would fail.
124124
val schemaAppliedDirectives = buildAppliedDirectives(rootInfo.getDirectives(), SCHEMA).toList()
125125
val schemaObjects = SchemaObjects(query, mutation, subscription, types, schemaDirectives.values.toSet(), codeRegistryBuilder, rootInfo.getDescription(), schemaAppliedDirectives)
126-
val wiredObjects = if (directiveWiringHelper.isWiringRequired) wire(schemaObjects) else schemaObjects
126+
// in the order the types were created
127+
val wiredObjects = if (directiveWiringHelper.isWiringRequired) wire(schemaObjects, inputObjects + interfaces + objects + unions + enums) else schemaObjects
127128
if (options.unmappedInputFields != UnmappedInputFields.IGNORE) {
128129
val wiredInputObjects = wiredObjects.dictionary.filterIsInstance<GraphQLInputObjectType>().associateBy { it.name }
129130
val checker = UnmappedInputFieldChecker(wiredInputObjects, options.unmappedInputFields)
@@ -136,18 +137,17 @@ class SchemaParser internal constructor(
136137
}
137138

138139
/**
139-
* Runs the directive wirings on the assembled schema, where type references are resolved, and takes the wired types from it.
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.
140141
*/
141-
private fun wire(schemaObjects: SchemaObjects): SchemaObjects {
142-
val schema = directiveWiringHelper.wireSchema(schemaObjects.toSchema())
143-
val directives = schema.directives.associateBy { it.name }
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
144146
return schemaObjects.copy(
145-
query = schema.queryType,
146-
mutation = schema.mutationType,
147-
subscription = schema.subscriptionType,
148-
dictionary = schemaObjects.dictionary.mapTo(linkedSetOf()) { schema.typeMap.getValue((it as GraphQLNamedType).name) },
149-
directives = schemaObjects.directives.mapTo(linkedSetOf()) { directives.getValue(it.name) },
150-
schemaAppliedDirectives = schema.schemaAppliedDirectives
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 }
151151
)
152152
}
153153

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

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ import graphql.introspection.Introspection
44
import graphql.introspection.Introspection.DirectiveLocation.*
55
import graphql.kickstart.tools.directive.SchemaDirectiveWiringEnvironmentImpl.Parameters
66
import graphql.language.NamedNode
7-
import graphql.language.Node
87
import graphql.language.NodeParentTree
98
import graphql.schema.*
109
import graphql.schema.idl.RuntimeWiring
1110
import graphql.schema.idl.SchemaDirectiveWiring
12-
import graphql.util.TraversalControl
13-
import graphql.util.TraverserContext
1411
import java.util.*
1512

1613
internal class DirectiveWiringHelper(
@@ -21,46 +18,40 @@ internal class DirectiveWiringHelper(
2118

2219
val isWiringRequired = runtimeWiring.registeredDirectiveWiring.isNotEmpty() || runtimeWiring.directiveWiring.isNotEmpty()
2320

24-
fun wireSchema(schema: GraphQLSchema): GraphQLSchema = SchemaTransformer.transformSchema(schema, object : GraphQLTypeVisitorStub() {
25-
private val references = TypeReferences(schema)
26-
27-
override fun visitGraphQLObjectType(node: GraphQLObjectType, context: TraverserContext<GraphQLSchemaElement>) =
28-
wire(node, node.definition, context, ::wireObject)
29-
30-
override fun visitGraphQLInterfaceType(node: GraphQLInterfaceType, context: TraverserContext<GraphQLSchemaElement>) =
31-
wire(node, node.definition, context, ::wireInterFace)
32-
33-
override fun visitGraphQLInputObjectType(node: GraphQLInputObjectType, context: TraverserContext<GraphQLSchemaElement>) =
34-
wire(node, node.definition, context, ::wireInputObject)
35-
36-
override fun visitGraphQLEnumType(node: GraphQLEnumType, context: TraverserContext<GraphQLSchemaElement>) =
37-
wire(node, node.definition, context, ::wireEnum)
38-
39-
override fun visitGraphQLUnionType(node: GraphQLUnionType, context: TraverserContext<GraphQLSchemaElement>) =
40-
wire(node, node.definition, context, ::wireUnion)
41-
42-
// types without a definition, like the introspection types or types a wiring builds in code, aren't wired
43-
private fun <T : GraphQLNamedType> wire(node: T, definition: Node<*>?, context: TraverserContext<GraphQLSchemaElement>, wireType: (T) -> T): TraversalControl {
44-
if (definition != null) {
45-
val wired = wireType(node)
46-
if (wired !== node) changeNode(context, references.inFields(wired))
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
4735
}
48-
return TraversalControl.CONTINUE
36+
if (wired !== type) changed[type.name] = wired
4937
}
50-
})
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+
}
5142

5243
/**
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.
44+
* Swaps the schema's own types that a type refers to for type references, like they were before the schema was built.
5545
*/
5646
private class TypeReferences(private val schema: GraphQLSchema) {
5747
// the types wirings build in code, rebuilt once each
5848
private val built = IdentityHashMap<GraphQLType, GraphQLType>()
5949

6050
@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)) }
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)) }
6455
is GraphQLInputObjectType -> type.transform { it.clearFields().fields(type.fields.map(::inInputField)) }
6556
else -> type
6657
} as T
@@ -76,11 +67,13 @@ internal class DirectiveWiringHelper(
7667
private fun of(type: GraphQLType): GraphQLType = when (type) {
7768
is GraphQLNonNull -> GraphQLNonNull.nonNull(of(type.wrappedType))
7869
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 GraphQLUnionType, is GraphQLInputObjectType ->
81-
if (schema.typeMap[type.name] === type) GraphQLTypeReference.typeRef(type.name) else built.getOrPut(type) { inFields(type) }
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) }
8273
else -> type
8374
}
75+
76+
private fun of(types: List<GraphQLNamedOutputType>) = types.map { of(it) as GraphQLNamedOutputType }
8477
}
8578

8679
private fun wireObject(objectType: GraphQLObjectType): GraphQLObjectType {

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -256,24 +256,35 @@ class DirectiveTest {
256256
}
257257

258258
@Test
259-
fun `should let directive wirings change the types of self-referencing fields`() {
259+
fun `should let directive wirings change types that refer to each other`() {
260260
val schema = SchemaParser.newParser()
261261
.schemaString(
262262
"""
263263
directive @required on FIELD_DEFINITION
264264
directive @many on INPUT_FIELD_DEFINITION
265265
directive @wrap on FIELD_DEFINITION
266+
directive @described on INTERFACE | ENUM
266267
267268
type Query {
268269
person(filter: PersonFilter): Person
269270
}
270271
271-
type Person {
272+
interface Named @described {
273+
name: String
274+
}
275+
276+
type Person implements Named {
272277
name: String
273278
age: Int
274279
friend: Person @required
275280
best: Person @wrap
276281
someone: Someone
282+
state: AllowedState @required
283+
}
284+
285+
enum AllowedState @described {
286+
ALLOWED
287+
DISALLOWED
277288
}
278289
279290
union Someone = Person
@@ -303,6 +314,13 @@ class DirectiveTest {
303314
return environment.element.transform { it.type(wrapper) }
304315
}
305316
})
317+
.directive("described", object : SchemaDirectiveWiring {
318+
override fun onInterface(environment: SchemaDirectiveWiringEnvironment<GraphQLInterfaceType>): GraphQLInterfaceType =
319+
environment.element.transform { it.description("described") }
320+
321+
override fun onEnum(environment: SchemaDirectiveWiringEnvironment<GraphQLEnumType>): GraphQLEnumType =
322+
environment.element.transform { it.description("described") }
323+
})
306324
.build()
307325
.makeExecutableSchema()
308326

@@ -314,6 +332,9 @@ class DirectiveTest {
314332
assertEquals(GraphQLTypeUtil.simplePrint(filter.getField("parent").type), "[PersonFilter]")
315333
assert(GraphQLTypeUtil.unwrapAll(person.getField("friend").type) === person)
316334
assert(wrapper.getField("value").type === person)
335+
assertEquals(person.interfaces.map { it.description }, listOf("described"))
336+
assertEquals(GraphQLTypeUtil.simplePrint(person.getField("state").type), "AllowedState!")
337+
assertEquals((GraphQLTypeUtil.unwrapAll(person.getField("state").type) as GraphQLEnumType).description, "described")
317338
}
318339

319340
@Test
@@ -830,7 +851,7 @@ class DirectiveTest {
830851
EXECUTION
831852
}
832853

833-
private enum class AllowedState {
854+
enum class AllowedState {
834855
ALLOWED,
835856
DISALLOWED
836857
}
@@ -884,6 +905,7 @@ class DirectiveTest {
884905
val friend: Person get() = this
885906
val best: Person get() = this
886907
val someone: Person get() = this
908+
val state: AllowedState get() = AllowedState.ALLOWED
887909
}
888910

889911
data class PersonFilter(

0 commit comments

Comments
 (0)