Skip to content

Commit e1f54fc

Browse files
committed
Added example mutation showing partial updates
1 parent bf54815 commit e1f54fc

File tree

7 files changed

+121
-15
lines changed

7 files changed

+121
-15
lines changed

src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import graphql.language.TypeName
1717
import graphql.language.UnionTypeDefinition
1818
import graphql.schema.GraphQLScalarType
1919
import graphql.schema.idl.ScalarInfo
20+
import graphql.schema.idl.SchemaDirectiveWiring
2021
import org.slf4j.LoggerFactory
21-
import java.lang.reflect.Field
2222
import java.lang.reflect.Method
2323

2424
/**
@@ -134,7 +134,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
134134
val dictionary = try {
135135
Maps.unmodifiableBiMap(HashBiMap.create<TypeDefinition<*>, JavaType>().also {
136136
dictionary.filter {
137-
it.value.javaType != null
137+
it.value.javaType != null
138138
&& it.value.typeClass() != java.lang.Object::class.java
139139
&& !java.util.Map::class.java.isAssignableFrom(it.value.typeClass())
140140
&& it.key !is InputObjectTypeDefinition
@@ -216,9 +216,9 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
216216
}
217217
}
218218

219-
private fun getResolverInfoFromTypeDictionary(typeName: String) : ResolverInfo? {
219+
private fun getResolverInfoFromTypeDictionary(typeName: String): ResolverInfo? {
220220
val dictionaryType = initialDictionary[typeName]?.get()
221-
return if(dictionaryType != null) {
221+
return if (dictionaryType != null) {
222222
resolverInfosByDataClass[dictionaryType] ?: DataClassResolverInfo(dictionaryType);
223223
} else {
224224
null
@@ -233,13 +233,13 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
233233
val resolverInfo: ResolverInfo? = if (resolverInfoList.size > 1) {
234234
MultiResolverInfo(resolverInfoList)
235235
} else {
236-
if(item.clazz.equals(Object::class.java)) {
236+
if (item.clazz.equals(Object::class.java)) {
237237
getResolverInfoFromTypeDictionary(item.type.name)
238238
} else {
239239
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
240240
}
241241
}
242-
if(resolverInfo == null) {
242+
if (resolverInfo == null) {
243243
throw throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
244244
}
245245

@@ -287,7 +287,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
287287
if (options.preferGraphQLResolver && realEntry.hasResolverRef()) {
288288
log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference")
289289
} else {
290-
if(java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) {
290+
if (java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) {
291291
throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}")
292292
}
293293
throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.unwrap()}:\n| ${reference.getDescription()}")

src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.coxautodev.graphql.tools
22

3+
import graphql.introspection.Introspection
34
import graphql.language.AbstractNode
45
import graphql.language.ArrayValue
56
import graphql.language.BooleanValue
@@ -22,6 +23,7 @@ import graphql.language.TypeDefinition
2223
import graphql.language.TypeName
2324
import graphql.language.UnionTypeDefinition
2425
import graphql.language.Value
26+
import graphql.schema.GraphQLDirective
2527
import graphql.schema.GraphQLEnumType
2628
import graphql.schema.GraphQLFieldDefinition
2729
import graphql.schema.GraphQLInputObjectType
@@ -36,15 +38,20 @@ import graphql.schema.GraphQLType
3638
import graphql.schema.GraphQLTypeReference
3739
import graphql.schema.GraphQLUnionType
3840
import graphql.schema.TypeResolverProxy
41+
import graphql.schema.idl.DirectiveBehavior
42+
import graphql.schema.idl.RuntimeWiring
3943
import graphql.schema.idl.ScalarInfo
44+
import graphql.schema.idl.SchemaGeneratorHelper
45+
import java.util.ArrayList
46+
import java.util.HashSet
4047
import kotlin.reflect.KClass
4148

4249
/**
4350
* Parses a GraphQL Schema and maps object fields to provided class methods.
4451
*
4552
* @author Andrew Potter
4653
*/
47-
class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, private val options: SchemaParserOptions) {
54+
class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, private val options: SchemaParserOptions, private val runtimeWiring: RuntimeWiring) {
4855

4956
companion object {
5057
const val DEFAULT_DEPRECATION_MESSAGE = "No longer supported"
@@ -79,6 +86,9 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
7986
private val permittedTypesForInputObject: Set<String> =
8087
(inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet()
8188

89+
private val schemaGeneratorHelper = SchemaGeneratorHelper()
90+
private val directiveGenerator = DirectiveBehavior()
91+
8292
/**
8393
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
8494
*/
@@ -124,19 +134,42 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
124134
.definition(definition)
125135
.description(getDocumentation(definition))
126136

137+
val directiveDefinitions = setOf<GraphQLDirective>()
138+
builder.withDirectives(*buildDirectives(definition.directives, directiveDefinitions, Introspection.DirectiveLocation.OBJECT))
139+
127140
definition.implements.forEach { implementsDefinition ->
128141
val interfaceName = (implementsDefinition as TypeName).name
129142
builder.withInterface(interfaces.find { it.name == interfaceName } ?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!"))
130143
}
131144

132145
definition.getExtendedFieldDefinitions(extensionDefinitions).forEach { fieldDefinition ->
146+
fieldDefinition.description
133147
builder.field { field ->
134148
createField(field, fieldDefinition)
149+
// todo: apply directives to the fieldDefinition to use to find dataFetcher we're really after
135150
field.dataFetcher(fieldResolversByType[definition]?.get(fieldDefinition)?.createDataFetcher() ?: throw SchemaError("No resolver method found for object type '${definition.name}' and field '${fieldDefinition.name}', this is most likely a bug with graphql-java-tools"))
151+
152+
val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring))
153+
GraphQLFieldDefinition.Builder(wiredField)
136154
}
137155
}
138156

139-
return builder.build()
157+
val objectType = builder.build()
158+
159+
return directiveGenerator.onObject(objectType, DirectiveBehavior.Params(runtimeWiring))
160+
}
161+
162+
private fun buildDirectives(directives: List<Directive>, directiveDefinitions: Set<GraphQLDirective>, directiveLocation: Introspection.DirectiveLocation): Array<GraphQLDirective> {
163+
val names = HashSet<String>()
164+
165+
val output = ArrayList<GraphQLDirective>()
166+
for (directive in directives) {
167+
if (!names.contains(directive.name)) {
168+
names.add(directive.name)
169+
output.add(schemaGeneratorHelper.buildDirective(directive, directiveDefinitions, directiveLocation))
170+
}
171+
}
172+
return output.toTypedArray()
140173
}
141174

142175
private fun createInputObject(definition: InputObjectTypeDefinition): GraphQLInputObjectType {
@@ -243,6 +276,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
243276
argument.type(determineInputType(argumentDefinition.type))
244277
}
245278
}
279+
field.withDirectives(*buildDirectives(fieldDefinition.directives, setOf(), Introspection.DirectiveLocation.FIELD_DEFINITION))
246280
return field
247281
}
248282

src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import graphql.language.Document
88
import graphql.parser.Parser
99
import graphql.schema.DataFetchingEnvironment
1010
import graphql.schema.GraphQLScalarType
11+
import graphql.schema.idl.RuntimeWiring
12+
import graphql.schema.idl.SchemaDirectiveWiring
1113
import kotlinx.coroutines.Dispatchers
1214
import kotlinx.coroutines.GlobalScope
1315
import kotlinx.coroutines.channels.ReceiveChannel
@@ -31,6 +33,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio
3133
private val files = mutableListOf<String>()
3234
private val resolvers = mutableListOf<GraphQLResolver<*>>()
3335
private val scalars = mutableListOf<GraphQLScalarType>()
36+
private val runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring()
3437
private var options = SchemaParserOptions.defaultOptions()
3538

3639
/**
@@ -78,6 +81,10 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio
7881
this.scalars.addAll(scalars)
7982
}
8083

84+
fun directive(name: String, directive: SchemaDirectiveWiring) = this.apply {
85+
this.runtimeWiringBuilder.directive(name, directive)
86+
}
87+
8188
/**
8289
* Add arbitrary classes to the parser's dictionary, overriding the generated type name.
8390
*/
@@ -181,7 +188,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio
181188
/**
182189
* Build the parser with the supplied schema and dictionary.
183190
*/
184-
fun build() = SchemaParser(scan(), options)
191+
fun build() = SchemaParser(scan(), options, runtimeWiringBuilder.build())
185192
}
186193

187194
class InvalidSchemaError(pce: ParseCancellationException, private val recognitionException: RecognitionException) : RuntimeException(pce) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package graphql.schema.idl
2+
3+
import graphql.schema.GraphQLFieldDefinition
4+
import graphql.schema.GraphQLObjectType
5+
6+
class DirectiveBehavior {
7+
8+
private val directiveHelper = SchemaGeneratorDirectiveHelper()
9+
10+
fun onObject(element: GraphQLObjectType, params: Params): GraphQLObjectType {
11+
return directiveHelper.onObject(element, params.toParameters())
12+
}
13+
14+
fun onField(element: GraphQLFieldDefinition, params: Params): GraphQLFieldDefinition {
15+
return directiveHelper.onField(element, params.toParameters())
16+
}
17+
18+
19+
data class Params(val runtimeWiring: RuntimeWiring) {
20+
internal fun toParameters() = SchemaGeneratorDirectiveHelper.Parameters(null, runtimeWiring, null, null)
21+
}
22+
}

src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class RelayConnectionSpec extends Specification {
1313
def "relay connection types are compatible"() {
1414
when:
1515
GraphQLSchema schema = SchemaParser.newParser().schemaString('''\
16+
directive @uppercase on FIELD_DEFINITION
17+
1618
type Query {
1719
users(first: Int, after: String): UserConnection
1820
otherTypes: AnotherTypeConnection
@@ -27,9 +29,10 @@ class RelayConnectionSpec extends Specification {
2729
node: User!
2830
}
2931
32+
3033
type User {
3134
id: ID!
32-
name: String
35+
name: String @uppercase
3336
}
3437
3538
type PageInfo {
@@ -48,6 +51,7 @@ class RelayConnectionSpec extends Specification {
4851
}
4952
''')
5053
.resolvers(new QueryResolver())
54+
.directive("uppercase", new RelayConnectionTest.UppercaseDirective())
5155
.build()
5256
.makeExecutableSchema()
5357
GraphQL gql = GraphQL.newGraphQL(schema)
@@ -79,7 +83,7 @@ class RelayConnectionSpec extends Specification {
7983
noExceptionThrown()
8084
data.users.edges.size == 1
8185
data.users.edges[0].node.id == "1"
82-
data.users.edges[0].node.name == "name"
86+
data.users.edges[0].node.name == "NAME"
8387
data.otherTypes.edges.size == 1
8488
data.otherTypes.edges[0].node.echo == "echo"
8589
}

src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,27 @@
22

33
import graphql.relay.Connection;
44
import graphql.relay.SimpleListConnection;
5-
import graphql.schema.DataFetchingEnvironment;
5+
import graphql.schema.*;
6+
import graphql.schema.idl.SchemaDirectiveWiring;
7+
import graphql.schema.idl.SchemaDirectiveWiringEnvironment;
8+
import graphql.schema.idl.SchemaDirectiveWiringEnvironmentImpl;
69
import org.junit.Test;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
712

813
import java.util.ArrayList;
914

1015
public class RelayConnectionTest {
1116

17+
private static final Logger log = LoggerFactory.getLogger(RelayConnectionTest.class);
18+
1219
@Test
1320
public void compiles() {
1421
SchemaParser.newParser().file("RelayConnection.graphqls")
1522
.resolvers(new QueryResolver())
1623
.dictionary(User.class)
24+
.directive("connection", new RelayConnection())
25+
.directive("uppercase", new UppercaseDirective())
1726
.build()
1827
.makeExecutableSchema();
1928
}
@@ -29,4 +38,31 @@ static class User {
2938
Long id;
3039
String name;
3140
}
41+
42+
static class RelayConnection implements SchemaDirectiveWiring {
43+
44+
@Override
45+
public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition> environment) {
46+
GraphQLFieldDefinition field = environment.getElement();
47+
log.info("Transforming field");
48+
return field;
49+
}
50+
51+
}
52+
53+
static class UppercaseDirective implements SchemaDirectiveWiring {
54+
55+
@Override
56+
public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment<GraphQLFieldDefinition> env) {
57+
GraphQLFieldDefinition field = env.getElement();
58+
DataFetcher dataFetcher = DataFetcherFactories.wrapDataFetcher(field.getDataFetcher(), ((dataFetchingEnvironment, value) -> {
59+
if (value == null) {
60+
return null;
61+
}
62+
String uppercase = ((String) value).toUpperCase();
63+
return uppercase;
64+
}));
65+
return field.transform(builder -> builder.dataFetcher(dataFetcher));
66+
}
67+
}
3268
}

src/test/resources/RelayConnection.graphqls

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
directive @connection on FIELD_DEFINITION
2+
directive @uppercase on FIELD_DEFINITION
3+
14
type Query {
2-
users(first: Int, after: String): UserConnection
5+
users(first: Int, after: String): UserConnection @connection
36
}
47

58
type UserConnection {
@@ -14,7 +17,7 @@ type UserEdge {
1417

1518
type User {
1619
id: ID!
17-
name: String
20+
name: String @uppercase
1821
}
1922

2023
type PageInfo {

0 commit comments

Comments
 (0)