From bf5481565cd3f787862c1a9fcb789cf3a8feb7d8 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sun, 4 Nov 2018 11:59:02 +0100 Subject: [PATCH 01/11] Added example mutation showing partial updates --- example/pom.xml | 14 +++++------ .../tools/example/resolvers/Mutation.java | 25 +++++++++++++++++++ example/src/main/resources/swapi.graphqls | 14 ++++++++++- 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 example/src/main/java/com/coxautodev/graphql/tools/example/resolvers/Mutation.java diff --git a/example/pom.xml b/example/pom.xml index 27ca8381..12e417b8 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.0.4.RELEASE + 2.0.5.RELEASE @@ -22,7 +22,7 @@ UTF-8 UTF-8 1.8 - 5.0.2 + 5.0.6 @@ -46,24 +46,24 @@ - com.graphql-java + com.graphql-java-kickstart graphql-spring-boot-starter ${graphql-spring-boot-starter.version} - com.graphql-java + com.graphql-java-kickstart graphiql-spring-boot-starter ${graphql-spring-boot-starter.version} - com.graphql-java + com.graphql-java-kickstart voyager-spring-boot-starter ${graphql-spring-boot-starter.version} - com.graphql-java + com.graphql-java-kickstart graphql-java-tools - 5.2.3 + 5.3.5 diff --git a/example/src/main/java/com/coxautodev/graphql/tools/example/resolvers/Mutation.java b/example/src/main/java/com/coxautodev/graphql/tools/example/resolvers/Mutation.java new file mode 100644 index 00000000..dee37d07 --- /dev/null +++ b/example/src/main/java/com/coxautodev/graphql/tools/example/resolvers/Mutation.java @@ -0,0 +1,25 @@ +package com.coxautodev.graphql.tools.example.resolvers; + +import com.coxautodev.graphql.tools.GraphQLMutationResolver; +import com.coxautodev.graphql.tools.example.types.Human; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.UUID; + +@Component +public class Mutation implements GraphQLMutationResolver { + + public Human createHuman(Map createHumanInput) { + String name = null; + if (createHumanInput.containsKey("name")) { + name = createHumanInput.get("name"); + } + String homePlanet = "Jakku"; + if (createHumanInput.containsKey("homePlanet")) { + homePlanet = createHumanInput.get("homePlanet"); + } + return new Human(UUID.randomUUID().toString(), name, null, homePlanet); + } + +} diff --git a/example/src/main/resources/swapi.graphqls b/example/src/main/resources/swapi.graphqls index 2d16587e..9dcc311d 100644 --- a/example/src/main/resources/swapi.graphqls +++ b/example/src/main/resources/swapi.graphqls @@ -57,4 +57,16 @@ type Droid implements Character { appearsIn: [Episode] # The primary function of the droid primaryFunction: String -} \ No newline at end of file +} + +type Mutation { + # Creates a new human character + createHuman(input: CreateHumanInput!): Human +} + +input CreateHumanInput { + # The name of the human + name: String + # The home planet of the human, or null if unknown + homePlanet: String +} From e1f54fc176f4bb6dbd45ac8658fe93f1c8060111 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Tue, 13 Nov 2018 08:45:12 +0100 Subject: [PATCH 02/11] Added example mutation showing partial updates --- .../graphql/tools/SchemaClassScanner.kt | 14 +++---- .../coxautodev/graphql/tools/SchemaParser.kt | 38 ++++++++++++++++++- .../graphql/tools/SchemaParserBuilder.kt | 9 ++++- .../graphql/schema/idl/DirectiveBehavior.kt | 22 +++++++++++ .../graphql/tools/RelayConnectionSpec.groovy | 8 +++- .../graphql/tools/RelayConnectionTest.java | 38 ++++++++++++++++++- src/test/resources/RelayConnection.graphqls | 7 +++- 7 files changed, 121 insertions(+), 15 deletions(-) create mode 100644 src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt index d8dfaada..45232a91 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt @@ -17,8 +17,8 @@ import graphql.language.TypeName import graphql.language.UnionTypeDefinition import graphql.schema.GraphQLScalarType import graphql.schema.idl.ScalarInfo +import graphql.schema.idl.SchemaDirectiveWiring import org.slf4j.LoggerFactory -import java.lang.reflect.Field import java.lang.reflect.Method /** @@ -134,7 +134,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap>, al val dictionary = try { Maps.unmodifiableBiMap(HashBiMap.create, JavaType>().also { dictionary.filter { - it.value.javaType != null + it.value.javaType != null && it.value.typeClass() != java.lang.Object::class.java && !java.util.Map::class.java.isAssignableFrom(it.value.typeClass()) && it.key !is InputObjectTypeDefinition @@ -216,9 +216,9 @@ internal class SchemaClassScanner(initialDictionary: BiMap>, al } } - private fun getResolverInfoFromTypeDictionary(typeName: String) : ResolverInfo? { + private fun getResolverInfoFromTypeDictionary(typeName: String): ResolverInfo? { val dictionaryType = initialDictionary[typeName]?.get() - return if(dictionaryType != null) { + return if (dictionaryType != null) { resolverInfosByDataClass[dictionaryType] ?: DataClassResolverInfo(dictionaryType); } else { null @@ -233,13 +233,13 @@ internal class SchemaClassScanner(initialDictionary: BiMap>, al val resolverInfo: ResolverInfo? = if (resolverInfoList.size > 1) { MultiResolverInfo(resolverInfoList) } else { - if(item.clazz.equals(Object::class.java)) { + if (item.clazz.equals(Object::class.java)) { getResolverInfoFromTypeDictionary(item.type.name) } else { resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz) } } - if(resolverInfo == null) { + if (resolverInfo == null) { 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.") } @@ -287,7 +287,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap>, al if (options.preferGraphQLResolver && realEntry.hasResolverRef()) { log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference") } else { - if(java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) { + if (java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) { throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}") } throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.unwrap()}:\n| ${reference.getDescription()}") diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt index f41ec32d..62645fe3 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt @@ -1,5 +1,6 @@ package com.coxautodev.graphql.tools +import graphql.introspection.Introspection import graphql.language.AbstractNode import graphql.language.ArrayValue import graphql.language.BooleanValue @@ -22,6 +23,7 @@ import graphql.language.TypeDefinition import graphql.language.TypeName import graphql.language.UnionTypeDefinition import graphql.language.Value +import graphql.schema.GraphQLDirective import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLInputObjectType @@ -36,7 +38,12 @@ import graphql.schema.GraphQLType import graphql.schema.GraphQLTypeReference import graphql.schema.GraphQLUnionType import graphql.schema.TypeResolverProxy +import graphql.schema.idl.DirectiveBehavior +import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.ScalarInfo +import graphql.schema.idl.SchemaGeneratorHelper +import java.util.ArrayList +import java.util.HashSet import kotlin.reflect.KClass /** @@ -44,7 +51,7 @@ import kotlin.reflect.KClass * * @author Andrew Potter */ -class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, private val options: SchemaParserOptions) { +class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, private val options: SchemaParserOptions, private val runtimeWiring: RuntimeWiring) { companion object { const val DEFAULT_DEPRECATION_MESSAGE = "No longer supported" @@ -79,6 +86,9 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat private val permittedTypesForInputObject: Set = (inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet() + private val schemaGeneratorHelper = SchemaGeneratorHelper() + private val directiveGenerator = DirectiveBehavior() + /** * Parses the given schema with respect to the given dictionary and returns GraphQL objects. */ @@ -124,19 +134,42 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat .definition(definition) .description(getDocumentation(definition)) + val directiveDefinitions = setOf() + builder.withDirectives(*buildDirectives(definition.directives, directiveDefinitions, Introspection.DirectiveLocation.OBJECT)) + definition.implements.forEach { implementsDefinition -> val interfaceName = (implementsDefinition as TypeName).name builder.withInterface(interfaces.find { it.name == interfaceName } ?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!")) } definition.getExtendedFieldDefinitions(extensionDefinitions).forEach { fieldDefinition -> + fieldDefinition.description builder.field { field -> createField(field, fieldDefinition) + // todo: apply directives to the fieldDefinition to use to find dataFetcher we're really after 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")) + + val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring)) + GraphQLFieldDefinition.Builder(wiredField) } } - return builder.build() + val objectType = builder.build() + + return directiveGenerator.onObject(objectType, DirectiveBehavior.Params(runtimeWiring)) + } + + private fun buildDirectives(directives: List, directiveDefinitions: Set, directiveLocation: Introspection.DirectiveLocation): Array { + val names = HashSet() + + val output = ArrayList() + for (directive in directives) { + if (!names.contains(directive.name)) { + names.add(directive.name) + output.add(schemaGeneratorHelper.buildDirective(directive, directiveDefinitions, directiveLocation)) + } + } + return output.toTypedArray() } private fun createInputObject(definition: InputObjectTypeDefinition): GraphQLInputObjectType { @@ -243,6 +276,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat argument.type(determineInputType(argumentDefinition.type)) } } + field.withDirectives(*buildDirectives(fieldDefinition.directives, setOf(), Introspection.DirectiveLocation.FIELD_DEFINITION)) return field } diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt index 8922b669..51d978dd 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt @@ -8,6 +8,8 @@ import graphql.language.Document import graphql.parser.Parser import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLScalarType +import graphql.schema.idl.RuntimeWiring +import graphql.schema.idl.SchemaDirectiveWiring import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.channels.ReceiveChannel @@ -31,6 +33,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio private val files = mutableListOf() private val resolvers = mutableListOf>() private val scalars = mutableListOf() + private val runtimeWiringBuilder = RuntimeWiring.newRuntimeWiring() private var options = SchemaParserOptions.defaultOptions() /** @@ -78,6 +81,10 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio this.scalars.addAll(scalars) } + fun directive(name: String, directive: SchemaDirectiveWiring) = this.apply { + this.runtimeWiringBuilder.directive(name, directive) + } + /** * Add arbitrary classes to the parser's dictionary, overriding the generated type name. */ @@ -181,7 +188,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio /** * Build the parser with the supplied schema and dictionary. */ - fun build() = SchemaParser(scan(), options) + fun build() = SchemaParser(scan(), options, runtimeWiringBuilder.build()) } class InvalidSchemaError(pce: ParseCancellationException, private val recognitionException: RecognitionException) : RuntimeException(pce) { diff --git a/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt new file mode 100644 index 00000000..a86e7c20 --- /dev/null +++ b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt @@ -0,0 +1,22 @@ +package graphql.schema.idl + +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType + +class DirectiveBehavior { + + private val directiveHelper = SchemaGeneratorDirectiveHelper() + + fun onObject(element: GraphQLObjectType, params: Params): GraphQLObjectType { + return directiveHelper.onObject(element, params.toParameters()) + } + + fun onField(element: GraphQLFieldDefinition, params: Params): GraphQLFieldDefinition { + return directiveHelper.onField(element, params.toParameters()) + } + + + data class Params(val runtimeWiring: RuntimeWiring) { + internal fun toParameters() = SchemaGeneratorDirectiveHelper.Parameters(null, runtimeWiring, null, null) + } +} diff --git a/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy b/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy index f3456415..d0076fa4 100644 --- a/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy +++ b/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy @@ -13,6 +13,8 @@ class RelayConnectionSpec extends Specification { def "relay connection types are compatible"() { when: GraphQLSchema schema = SchemaParser.newParser().schemaString('''\ + directive @uppercase on FIELD_DEFINITION + type Query { users(first: Int, after: String): UserConnection otherTypes: AnotherTypeConnection @@ -27,9 +29,10 @@ class RelayConnectionSpec extends Specification { node: User! } + type User { id: ID! - name: String + name: String @uppercase } type PageInfo { @@ -48,6 +51,7 @@ class RelayConnectionSpec extends Specification { } ''') .resolvers(new QueryResolver()) + .directive("uppercase", new RelayConnectionTest.UppercaseDirective()) .build() .makeExecutableSchema() GraphQL gql = GraphQL.newGraphQL(schema) @@ -79,7 +83,7 @@ class RelayConnectionSpec extends Specification { noExceptionThrown() data.users.edges.size == 1 data.users.edges[0].node.id == "1" - data.users.edges[0].node.name == "name" + data.users.edges[0].node.name == "NAME" data.otherTypes.edges.size == 1 data.otherTypes.edges[0].node.echo == "echo" } diff --git a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java index 4e9aab77..da8b9438 100644 --- a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java +++ b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java @@ -2,18 +2,27 @@ import graphql.relay.Connection; import graphql.relay.SimpleListConnection; -import graphql.schema.DataFetchingEnvironment; +import graphql.schema.*; +import graphql.schema.idl.SchemaDirectiveWiring; +import graphql.schema.idl.SchemaDirectiveWiringEnvironment; +import graphql.schema.idl.SchemaDirectiveWiringEnvironmentImpl; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; public class RelayConnectionTest { + private static final Logger log = LoggerFactory.getLogger(RelayConnectionTest.class); + @Test public void compiles() { SchemaParser.newParser().file("RelayConnection.graphqls") .resolvers(new QueryResolver()) .dictionary(User.class) + .directive("connection", new RelayConnection()) + .directive("uppercase", new UppercaseDirective()) .build() .makeExecutableSchema(); } @@ -29,4 +38,31 @@ static class User { Long id; String name; } + + static class RelayConnection implements SchemaDirectiveWiring { + + @Override + public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment environment) { + GraphQLFieldDefinition field = environment.getElement(); + log.info("Transforming field"); + return field; + } + + } + + static class UppercaseDirective implements SchemaDirectiveWiring { + + @Override + public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { + GraphQLFieldDefinition field = env.getElement(); + DataFetcher dataFetcher = DataFetcherFactories.wrapDataFetcher(field.getDataFetcher(), ((dataFetchingEnvironment, value) -> { + if (value == null) { + return null; + } + String uppercase = ((String) value).toUpperCase(); + return uppercase; + })); + return field.transform(builder -> builder.dataFetcher(dataFetcher)); + } + } } diff --git a/src/test/resources/RelayConnection.graphqls b/src/test/resources/RelayConnection.graphqls index 857c07ec..6d06ae5b 100644 --- a/src/test/resources/RelayConnection.graphqls +++ b/src/test/resources/RelayConnection.graphqls @@ -1,5 +1,8 @@ +directive @connection on FIELD_DEFINITION +directive @uppercase on FIELD_DEFINITION + type Query { - users(first: Int, after: String): UserConnection + users(first: Int, after: String): UserConnection @connection } type UserConnection { @@ -14,7 +17,7 @@ type UserEdge { type User { id: ID! - name: String + name: String @uppercase } type PageInfo { From cc5a857e987ec170b9080cbe897aa73070e03fed Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Tue, 13 Nov 2018 09:42:23 +0100 Subject: [PATCH 03/11] Moved UppercaseDirective class into groovy test case --- .../graphql/tools/RelayConnectionSpec.groovy | 23 +++++++++- .../graphql/tools/ReactiveTest.java | 46 +++++++++++++++++++ .../graphql/tools/RelayConnectionTest.java | 15 ------ src/test/resources/Reactive.graphqls | 12 +++++ src/test/resources/RelayConnection.graphqls | 3 +- 5 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java create mode 100644 src/test/resources/Reactive.graphqls diff --git a/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy b/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy index d0076fa4..998df420 100644 --- a/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy +++ b/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy @@ -4,8 +4,13 @@ import graphql.GraphQL import graphql.execution.AsyncExecutionStrategy import graphql.relay.Connection import graphql.relay.SimpleListConnection +import graphql.schema.DataFetcher +import graphql.schema.DataFetcherFactories import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLSchema +import graphql.schema.idl.SchemaDirectiveWiring +import graphql.schema.idl.SchemaDirectiveWiringEnvironment import spock.lang.Specification class RelayConnectionSpec extends Specification { @@ -51,7 +56,7 @@ class RelayConnectionSpec extends Specification { } ''') .resolvers(new QueryResolver()) - .directive("uppercase", new RelayConnectionTest.UppercaseDirective()) + .directive("uppercase", new UppercaseDirective()) .build() .makeExecutableSchema() GraphQL gql = GraphQL.newGraphQL(schema) @@ -116,5 +121,21 @@ class RelayConnectionSpec extends Specification { } } + static class UppercaseDirective implements SchemaDirectiveWiring { + + @Override + GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { + GraphQLFieldDefinition field = env.getElement(); + DataFetcher dataFetcher = DataFetcherFactories.wrapDataFetcher(field.getDataFetcher(), { + dataFetchingEnvironment, value -> + if (value == null) { + return null + } + return ((String) value).toUpperCase() + }) + return field.transform({ builder -> builder.dataFetcher(dataFetcher) }); + } + } + } diff --git a/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java b/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java new file mode 100644 index 00000000..8ea9a402 --- /dev/null +++ b/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java @@ -0,0 +1,46 @@ +package com.coxautodev.graphql.tools; + +import graphql.GraphQL; +import graphql.execution.AsyncExecutionStrategy; +import graphql.schema.GraphQLSchema; +import groovy.lang.Closure; +import org.junit.Test; + +import java.util.HashMap; +import java.util.concurrent.Future; + +public class ReactiveTest { + + @Test + public void futureSucceeds() { + GraphQLSchema schema = SchemaParser.newParser().file("Reactive.graphqls") + .resolvers(new Query()) + .build() + .makeExecutableSchema(); + + GraphQL gql = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(new AsyncExecutionStrategy()) + .build(); + Utils.assertNoGraphQlErrors(gql, new HashMap<>(), new Object(), new Closure(null) { + @Override + public String call() { + return "query { organization(organizationId: 1) { user { id } } }"; + } + }); + } + + static class Query implements GraphQLQueryResolver { + Future organization(int organizationid) { + return null; + } + } + + static class Organization { + private User user; + } + + static class User { + private Long id; + private String name; + } +} diff --git a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java index da8b9438..adc48249 100644 --- a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java +++ b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java @@ -22,7 +22,6 @@ public void compiles() { .resolvers(new QueryResolver()) .dictionary(User.class) .directive("connection", new RelayConnection()) - .directive("uppercase", new UppercaseDirective()) .build() .makeExecutableSchema(); } @@ -50,19 +49,5 @@ public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment env) { - GraphQLFieldDefinition field = env.getElement(); - DataFetcher dataFetcher = DataFetcherFactories.wrapDataFetcher(field.getDataFetcher(), ((dataFetchingEnvironment, value) -> { - if (value == null) { - return null; - } - String uppercase = ((String) value).toUpperCase(); - return uppercase; - })); - return field.transform(builder -> builder.dataFetcher(dataFetcher)); - } - } } diff --git a/src/test/resources/Reactive.graphqls b/src/test/resources/Reactive.graphqls new file mode 100644 index 00000000..acd34b62 --- /dev/null +++ b/src/test/resources/Reactive.graphqls @@ -0,0 +1,12 @@ +type Query { + organization(organizationId: ID): Organization +} + +type Organization { + user: User +} + +type User { + id: ID + name: String +} diff --git a/src/test/resources/RelayConnection.graphqls b/src/test/resources/RelayConnection.graphqls index 6d06ae5b..863dbac7 100644 --- a/src/test/resources/RelayConnection.graphqls +++ b/src/test/resources/RelayConnection.graphqls @@ -1,5 +1,4 @@ directive @connection on FIELD_DEFINITION -directive @uppercase on FIELD_DEFINITION type Query { users(first: Int, after: String): UserConnection @connection @@ -17,7 +16,7 @@ type UserEdge { type User { id: ID! - name: String @uppercase + name: String } type PageInfo { From 025a2bcaab131dbae2096960af33f5c3be35b99d Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Tue, 13 Nov 2018 11:13:48 +0100 Subject: [PATCH 04/11] Updated unit test --- .../graphql/tools/RelayConnectionTest.java | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java index adc48249..da593d01 100644 --- a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java +++ b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java @@ -1,16 +1,18 @@ package com.coxautodev.graphql.tools; +import graphql.GraphQL; +import graphql.execution.AsyncExecutionStrategy; import graphql.relay.Connection; import graphql.relay.SimpleListConnection; import graphql.schema.*; import graphql.schema.idl.SchemaDirectiveWiring; import graphql.schema.idl.SchemaDirectiveWiringEnvironment; -import graphql.schema.idl.SchemaDirectiveWiringEnvironmentImpl; +import groovy.lang.Closure; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; +import java.util.*; public class RelayConnectionTest { @@ -18,27 +20,54 @@ public class RelayConnectionTest { @Test public void compiles() { - SchemaParser.newParser().file("RelayConnection.graphqls") + GraphQLSchema schema = SchemaParser.newParser().file("RelayConnection.graphqls") .resolvers(new QueryResolver()) .dictionary(User.class) - .directive("connection", new RelayConnection()) + .directive("connection", new ConnectionDirective()) .build() .makeExecutableSchema(); + + GraphQL gql = GraphQL.newGraphQL(schema) + .queryExecutionStrategy(new AsyncExecutionStrategy()) + .build(); + + Map variables = new HashMap<>(); + variables.put("limit", 10); + Utils.assertNoGraphQlErrors(gql, variables, new Closure(null) { + @Override + public String call() { + return "query {\n" + + " users {\n" + + " edges {\n" + + " node {\n" + + " id\n" + + " name\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + } + }); } static class QueryResolver implements GraphQLQueryResolver { // fixme #114: desired return type to use: Connection public Connection users(int first, String after, DataFetchingEnvironment env) { - return new SimpleListConnection(new ArrayList<>()).get(env); + return new SimpleListConnection<>(Collections.singletonList(new User(1L, "Luke"))).get(env); } } static class User { Long id; String name; + + public User(Long id, String name) { + this.id = id; + this.name = name; + } } - static class RelayConnection implements SchemaDirectiveWiring { + static class ConnectionDirective implements SchemaDirectiveWiring { @Override public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment environment) { From eb1e9ed8f384eb1385730b6b52e42c74911283b7 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 12:40:26 +0100 Subject: [PATCH 05/11] TypeDefinitionFactory for dynamically adding type definitions --- .../graphql/tools/SchemaClassScanner.kt | 33 ++++++-- .../coxautodev/graphql/tools/SchemaParser.kt | 1 - .../graphql/tools/SchemaParserBuilder.kt | 25 +++++- .../graphql/tools/TypeDefinitionFactory.java | 11 +++ .../tools/relay/RelayConnectionFactory.kt | 77 +++++++++++++++++++ .../graphql/schema/idl/DirectiveBehavior.kt | 2 +- .../graphql/tools/ReactiveTest.java | 21 ++++- .../graphql/tools/RelayConnectionTest.java | 9 +++ src/test/resources/RelayConnection.graphqls | 18 +---- 9 files changed, 166 insertions(+), 31 deletions(-) create mode 100644 src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java create mode 100644 src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt index 45232a91..8e08cca8 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt @@ -8,6 +8,8 @@ import graphql.language.FieldDefinition import graphql.language.InputObjectTypeDefinition import graphql.language.InputValueDefinition import graphql.language.InterfaceTypeDefinition +import graphql.language.ListType +import graphql.language.NonNullType import graphql.language.ObjectTypeDefinition import graphql.language.ObjectTypeExtensionDefinition import graphql.language.ScalarTypeDefinition @@ -17,7 +19,6 @@ import graphql.language.TypeName import graphql.language.UnionTypeDefinition import graphql.schema.GraphQLScalarType import graphql.schema.idl.ScalarInfo -import graphql.schema.idl.SchemaDirectiveWiring import org.slf4j.LoggerFactory import java.lang.reflect.Method @@ -230,7 +231,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap>, al */ private fun scanQueueItemForPotentialMatches(item: QueueItem) { val resolverInfoList = this.resolverInfos.filter { it.dataClassType == item.clazz } - val resolverInfo: ResolverInfo? = if (resolverInfoList.size > 1) { + val resolverInfo: ResolverInfo = (if (resolverInfoList.size > 1) { MultiResolverInfo(resolverInfoList) } else { if (item.clazz.equals(Object::class.java)) { @@ -238,25 +239,43 @@ internal class SchemaClassScanner(initialDictionary: BiMap>, al } else { resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz) } - } - if (resolverInfo == null) { - 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.") - } + }) ?: 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.") scanResolverInfoForPotentialMatches(item.type, resolverInfo) } private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) { type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field -> +// val searchField = applyDirective(field) val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo) fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver + fieldResolver.scanForMatches().forEach { potentialMatch -> - handleFoundType(typeClassMatcher.match(potentialMatch)) +// if (potentialMatch.graphQLType is TypeName && !definitionsByName.containsKey((potentialMatch.graphQLType.name))) { +// val typeDefinition = ObjectTypeDefinition.newObjectTypeDefinition() +// .name(potentialMatch.graphQLType.name) +// .build() +// handleFoundType(TypeClassMatcher.ValidMatch(typeDefinition, typeClassMatcher.toRealType(potentialMatch), potentialMatch.reference)) +// } else { + handleFoundType(typeClassMatcher.match(potentialMatch)) +// } } } } +// private fun applyDirective(field: FieldDefinition): FieldDefinition { +// val connectionDirectives = field.directives.filter { it.name == "connection" } +// if (connectionDirectives.isNotEmpty()) { +// val directive = connectionDirectives.first() +// val originalType:TypeName = field.type as TypeName +// val wrappedField = field.deepCopy() +// wrappedField.type = TypeName(originalType.name + "Connection") +// return wrappedField +// } +// return field +// } + private fun handleFoundType(match: TypeClassMatcher.Match) { when (match) { is TypeClassMatcher.ScalarMatch -> { diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt index 62645fe3..774e5ee3 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt @@ -146,7 +146,6 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat fieldDefinition.description builder.field { field -> createField(field, fieldDefinition) - // todo: apply directives to the fieldDefinition to use to find dataFetcher we're really after 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")) val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring)) diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt index 51d978dd..991c10ba 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt @@ -1,10 +1,17 @@ package com.coxautodev.graphql.tools +import com.coxautodev.graphql.tools.relay.RelayConnectionFactory import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.collect.BiMap import com.google.common.collect.HashBiMap import com.google.common.collect.Maps +import graphql.language.Definition import graphql.language.Document +import graphql.language.FieldDefinition +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.ObjectTypeDefinition +import graphql.language.TypeName import graphql.parser.Parser import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLScalarType @@ -149,7 +156,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio * Scan for classes with the supplied schema and dictionary. Used for testing. */ private fun scan(): ScannedSchemaObjects { - val definitions = parseDefinitions() + val definitions = appendDynamicDefinitions(parseDefinitions()) val customScalars = scalars.associateBy { it.name } return SchemaClassScanner(dictionary.getDictionary(), definitions, resolvers, customScalars, options) @@ -158,6 +165,12 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio private fun parseDefinitions() = parseDocuments().flatMap { it.definitions } + private fun appendDynamicDefinitions(baseDefinitions: List>): List> { + val definitions = baseDefinitions.toMutableList() + options.typeDefinitionFactories.forEach { definitions.addAll(it.create(baseDefinitions)) } + return definitions.toList() + } + private fun parseDocuments(): List { val parser = Parser() val documents = mutableListOf() @@ -267,7 +280,8 @@ data class SchemaParserOptions internal constructor( val proxyHandlers: List, val preferGraphQLResolver: Boolean, val introspectionEnabled: Boolean, - val coroutineContext: CoroutineContext + val coroutineContext: CoroutineContext, + val typeDefinitionFactories: List ) { companion object { @JvmStatic @@ -287,6 +301,7 @@ data class SchemaParserOptions internal constructor( private var preferGraphQLResolver = false private var introspectionEnabled = true private var coroutineContext: CoroutineContext? = null + private var typeDefinitionFactories: MutableList = mutableListOf(RelayConnectionFactory()) fun contextClass(contextClass: Class<*>) = this.apply { this.contextClass = contextClass @@ -340,6 +355,10 @@ data class SchemaParserOptions internal constructor( this.coroutineContext = context } + fun typeDefinitionFactory(factory: TypeDefinitionFactory) = this.apply { + this.typeDefinitionFactories.add(factory) + } + fun build(): SchemaParserOptions { val coroutineContext = coroutineContext ?: Dispatchers.Default val wrappers = if (useDefaultGenericWrappers) { @@ -365,7 +384,7 @@ data class SchemaParserOptions internal constructor( } return SchemaParserOptions(contextClass, wrappers, allowUnimplementedResolvers, objectMapperProvider, - proxyHandlers, preferGraphQLResolver, introspectionEnabled, coroutineContext) + proxyHandlers, preferGraphQLResolver, introspectionEnabled, coroutineContext, typeDefinitionFactories) } } diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java b/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java new file mode 100644 index 00000000..4d3f68c6 --- /dev/null +++ b/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java @@ -0,0 +1,11 @@ +package com.coxautodev.graphql.tools; + +import graphql.language.Definition; + +import java.util.List; + +public interface TypeDefinitionFactory { + + List> create(final List> existing); + +} diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt b/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt new file mode 100644 index 00000000..087b6155 --- /dev/null +++ b/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt @@ -0,0 +1,77 @@ +package com.coxautodev.graphql.tools.relay + +import com.coxautodev.graphql.tools.TypeDefinitionFactory +import graphql.language.Definition +import graphql.language.Directive +import graphql.language.FieldDefinition +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.ObjectTypeDefinition +import graphql.language.StringValue +import graphql.language.TypeDefinition +import graphql.language.TypeName + +class RelayConnectionFactory : TypeDefinitionFactory { + + override fun create(existing: List>): List> { + val definitions = mutableListOf>() + val definitionsByName = existing.filterIsInstance>() + .associateBy { it.name } + .toMutableMap() + + findConnectionDirectives(existing) + .flatMap { createDefinitions(it) } + .forEach { + if (!definitionsByName.containsKey(it.name)) { + definitionsByName[it.name] = it + definitions.add(it) + } + } + + if (!definitionsByName.containsKey("PageInfo")) { + definitions.add(createPageInfo()) + } + + return definitions + } + + private fun findConnectionDirectives(definitions: List>): List { + return definitions.filterIsInstance() + .flatMap { it.fieldDefinitions } + .flatMap { it.directives } + .filter { it.name == "connection" } + } + + private fun createDefinitions(directive: Directive): List { + val definitions = mutableListOf() + definitions.add(createEdgeDefinition(directive.forTypeName())) + definitions.add(createConnectionDefinition(directive.forTypeName())) + return definitions.toList() + } + + private fun createConnectionDefinition(type: String): ObjectTypeDefinition = + ObjectTypeDefinition.newObjectTypeDefinition() + .name(type + "Connection") + .fieldDefinition(FieldDefinition("edges", ListType(TypeName(type + "Edge")))) + .fieldDefinition(FieldDefinition("pageInfo", TypeName("PageInfo"))) + .build() + + private fun createEdgeDefinition(type: String): ObjectTypeDefinition = + ObjectTypeDefinition.newObjectTypeDefinition() + .name(type + "Edge") + .fieldDefinition(FieldDefinition("cursor", TypeName("String"))) + .fieldDefinition(FieldDefinition("node", TypeName(type))) + .build() + + private fun createPageInfo(): ObjectTypeDefinition = + ObjectTypeDefinition.newObjectTypeDefinition() + .name("PageInfo") + .fieldDefinition(FieldDefinition("hasPreviousPage", NonNullType(TypeName("Boolean")))) + .fieldDefinition(FieldDefinition("hasNextPage", NonNullType(TypeName("Boolean")))) + .build() + + private fun Directive.forTypeName(): String { + return (this.getArgument("for").value as StringValue).value + } + +} diff --git a/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt index a86e7c20..16f8aa9c 100644 --- a/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt +++ b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt @@ -15,8 +15,8 @@ class DirectiveBehavior { return directiveHelper.onField(element, params.toParameters()) } - data class Params(val runtimeWiring: RuntimeWiring) { internal fun toParameters() = SchemaGeneratorDirectiveHelper.Parameters(null, runtimeWiring, null, null) } + } diff --git a/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java b/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java index 8ea9a402..914a53fe 100644 --- a/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java +++ b/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java @@ -4,17 +4,30 @@ import graphql.execution.AsyncExecutionStrategy; import graphql.schema.GraphQLSchema; import groovy.lang.Closure; +import io.reactivex.Single; +import io.reactivex.internal.operators.single.SingleJust; import org.junit.Test; import java.util.HashMap; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; +import static io.reactivex.Maybe.just; + public class ReactiveTest { @Test public void futureSucceeds() { + SchemaParserOptions options = SchemaParserOptions.newOptions() + .genericWrappers( + new SchemaParserOptions.GenericWrapper(Single.class, 0), + new SchemaParserOptions.GenericWrapper(SingleJust.class, 0) + ) + .build(); GraphQLSchema schema = SchemaParser.newParser().file("Reactive.graphqls") .resolvers(new Query()) + .options(options) .build() .makeExecutableSchema(); @@ -30,8 +43,12 @@ public String call() { } static class Query implements GraphQLQueryResolver { - Future organization(int organizationid) { - return null; +// Single> organization(int organizationid) { +// return Single.just(Optional.empty()); //CompletableFuture.completedFuture(null); +// } + + Future> organization(int organizationid) { + return CompletableFuture.completedFuture(Optional.of(new Organization())); } } diff --git a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java index da593d01..ee9d4441 100644 --- a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java +++ b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java @@ -20,10 +20,14 @@ public class RelayConnectionTest { @Test public void compiles() { + SchemaParserOptions options = SchemaParserOptions.newOptions() + + .build(); GraphQLSchema schema = SchemaParser.newParser().file("RelayConnection.graphqls") .resolvers(new QueryResolver()) .dictionary(User.class) .directive("connection", new ConnectionDirective()) + .options(options) .build() .makeExecutableSchema(); @@ -39,10 +43,15 @@ public String call() { return "query {\n" + " users {\n" + " edges {\n" + + " cursor\n" + " node {\n" + " id\n" + " name\n" + " }\n" + + " },\n" + + " pageInfo {\n" + + " hasPreviousPage,\n" + + " hasNextPage\n" + " }\n" + " }\n" + "}"; diff --git a/src/test/resources/RelayConnection.graphqls b/src/test/resources/RelayConnection.graphqls index 863dbac7..073abca6 100644 --- a/src/test/resources/RelayConnection.graphqls +++ b/src/test/resources/RelayConnection.graphqls @@ -1,24 +1,8 @@ -directive @connection on FIELD_DEFINITION - type Query { - users(first: Int, after: String): UserConnection @connection -} - -type UserConnection { - edges: [UserEdge!]! - pageInfo: PageInfo! -} - -type UserEdge { - cursor: String! - node: User! + users(first: Int, after: String): UserConnection @connection(for: "User") } type User { id: ID! name: String } - -type PageInfo { - -} From 44c9de44576f31b8723615ea19fb7fca5f55cfb3 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 13:35:28 +0100 Subject: [PATCH 06/11] Use the type name for the connection as defined in the schema --- .../tools/relay/RelayConnectionFactory.kt | 33 ++++++++++++++----- src/test/resources/RelayConnection.graphqls | 2 +- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt b/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt index 087b6155..97dc9f9c 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt @@ -1,12 +1,15 @@ package com.coxautodev.graphql.tools.relay import com.coxautodev.graphql.tools.TypeDefinitionFactory +import graphql.language.Argument +import graphql.language.Comment import graphql.language.Definition import graphql.language.Directive import graphql.language.FieldDefinition import graphql.language.ListType import graphql.language.NonNullType import graphql.language.ObjectTypeDefinition +import graphql.language.SourceLocation import graphql.language.StringValue import graphql.language.TypeDefinition import graphql.language.TypeName @@ -35,32 +38,32 @@ class RelayConnectionFactory : TypeDefinitionFactory { return definitions } - private fun findConnectionDirectives(definitions: List>): List { + private fun findConnectionDirectives(definitions: List>): List { return definitions.filterIsInstance() .flatMap { it.fieldDefinitions } - .flatMap { it.directives } + .flatMap { it.directivesWithField() } .filter { it.name == "connection" } } - private fun createDefinitions(directive: Directive): List { + private fun createDefinitions(directive: DirectiveWithField): List { val definitions = mutableListOf() - definitions.add(createEdgeDefinition(directive.forTypeName())) - definitions.add(createConnectionDefinition(directive.forTypeName())) + definitions.add(createConnectionDefinition(directive.getTypeName())) + definitions.add(createEdgeDefinition(directive.getTypeName(), directive.forTypeName())) return definitions.toList() } private fun createConnectionDefinition(type: String): ObjectTypeDefinition = ObjectTypeDefinition.newObjectTypeDefinition() - .name(type + "Connection") + .name(type) .fieldDefinition(FieldDefinition("edges", ListType(TypeName(type + "Edge")))) .fieldDefinition(FieldDefinition("pageInfo", TypeName("PageInfo"))) .build() - private fun createEdgeDefinition(type: String): ObjectTypeDefinition = + private fun createEdgeDefinition(connectionType: String, nodeType: String): ObjectTypeDefinition = ObjectTypeDefinition.newObjectTypeDefinition() - .name(type + "Edge") + .name(connectionType + "Edge") .fieldDefinition(FieldDefinition("cursor", TypeName("String"))) - .fieldDefinition(FieldDefinition("node", TypeName(type))) + .fieldDefinition(FieldDefinition("node", TypeName(nodeType))) .build() private fun createPageInfo(): ObjectTypeDefinition = @@ -74,4 +77,16 @@ class RelayConnectionFactory : TypeDefinitionFactory { return (this.getArgument("for").value as StringValue).value } + private fun Directive.withField(field: FieldDefinition): DirectiveWithField { + return DirectiveWithField(field, this.name, this.arguments, this.sourceLocation, this.comments) + } + + private fun FieldDefinition.directivesWithField(): List { + return this.directives.map { it.withField(this) } + } + + class DirectiveWithField(val field: FieldDefinition, name: String, arguments: List, sourceLocation: SourceLocation, comments: List) : Directive(name, arguments, sourceLocation, comments) { + fun getTypeName() = (field.type as TypeName).name + } + } diff --git a/src/test/resources/RelayConnection.graphqls b/src/test/resources/RelayConnection.graphqls index 073abca6..e832c296 100644 --- a/src/test/resources/RelayConnection.graphqls +++ b/src/test/resources/RelayConnection.graphqls @@ -1,5 +1,5 @@ type Query { - users(first: Int, after: String): UserConnection @connection(for: "User") + users(first: Int, after: String): UserRelayConnection @connection(for: "User") } type User { From b87092004c7b3499a0bbd043c3fd5c6bf48c4caf Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 14:00:05 +0100 Subject: [PATCH 07/11] Use the type name for the connection as defined in the schema --- .../com/coxautodev/graphql/tools/SchemaParserBuilder.kt | 2 +- .../coxautodev/graphql/tools/TypeDefinitionFactory.java | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt index 991c10ba..d5905032 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt @@ -167,7 +167,7 @@ class SchemaParserBuilder constructor(private val dictionary: SchemaParserDictio private fun appendDynamicDefinitions(baseDefinitions: List>): List> { val definitions = baseDefinitions.toMutableList() - options.typeDefinitionFactories.forEach { definitions.addAll(it.create(baseDefinitions)) } + options.typeDefinitionFactories.forEach { definitions.addAll(it.create(definitions)) } return definitions.toList() } diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java b/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java index 4d3f68c6..d8fb49de 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java +++ b/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java @@ -6,6 +6,13 @@ public interface TypeDefinitionFactory { + /** + * Called after parsing the SDL for creating any additional type definitions. + * All existing definitions are passed in. Return only the newly created definitions. + * + * @param existing all existing definitions + * @return any new definitions that should be added + */ List> create(final List> existing); } From 04e2733ae823813a1cd73afc84760e1c4e989335 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 14:03:25 +0100 Subject: [PATCH 08/11] Cleaned up unit test --- .../graphql/tools/RelayConnectionTest.java | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java index ee9d4441..e196623a 100644 --- a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java +++ b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java @@ -4,30 +4,22 @@ import graphql.execution.AsyncExecutionStrategy; import graphql.relay.Connection; import graphql.relay.SimpleListConnection; -import graphql.schema.*; -import graphql.schema.idl.SchemaDirectiveWiring; -import graphql.schema.idl.SchemaDirectiveWiringEnvironment; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLSchema; import groovy.lang.Closure; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; public class RelayConnectionTest { - private static final Logger log = LoggerFactory.getLogger(RelayConnectionTest.class); - @Test public void compiles() { - SchemaParserOptions options = SchemaParserOptions.newOptions() - - .build(); GraphQLSchema schema = SchemaParser.newParser().file("RelayConnection.graphqls") .resolvers(new QueryResolver()) .dictionary(User.class) - .directive("connection", new ConnectionDirective()) - .options(options) .build() .makeExecutableSchema(); @@ -35,12 +27,12 @@ public void compiles() { .queryExecutionStrategy(new AsyncExecutionStrategy()) .build(); - Map variables = new HashMap<>(); + Map variables = new HashMap<>(); variables.put("limit", 10); Utils.assertNoGraphQlErrors(gql, variables, new Closure(null) { @Override public String call() { - return "query {\n" + + return "query {\n" + " users {\n" + " edges {\n" + " cursor\n" + @@ -60,7 +52,6 @@ public String call() { } static class QueryResolver implements GraphQLQueryResolver { - // fixme #114: desired return type to use: Connection public Connection users(int first, String after, DataFetchingEnvironment env) { return new SimpleListConnection<>(Collections.singletonList(new User(1L, "Luke"))).get(env); } @@ -76,16 +67,5 @@ public User(Long id, String name) { } } - static class ConnectionDirective implements SchemaDirectiveWiring { - - @Override - public GraphQLFieldDefinition onField(SchemaDirectiveWiringEnvironment environment) { - GraphQLFieldDefinition field = environment.getElement(); - log.info("Transforming field"); - return field; - } - - } - } From 2bec6dae1b5b7d41c39bf4e697b6ba0f7d740464 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 14:14:49 +0100 Subject: [PATCH 09/11] Extended supported types for directives --- .../coxautodev/graphql/tools/SchemaParser.kt | 19 +++++++---- .../graphql/schema/idl/DirectiveBehavior.kt | 32 +++++++++++++++++++ 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt index 774e5ee3..172de0f8 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt @@ -134,8 +134,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat .definition(definition) .description(getDocumentation(definition)) - val directiveDefinitions = setOf() - builder.withDirectives(*buildDirectives(definition.directives, directiveDefinitions, Introspection.DirectiveLocation.OBJECT)) + builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.OBJECT)) definition.implements.forEach { implementsDefinition -> val interfaceName = (implementsDefinition as TypeName).name @@ -177,6 +176,8 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat .definition(definition) .description(getDocumentation(definition)) + builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INPUT_OBJECT)) + definition.inputValueDefinitions.forEach { inputDefinition -> builder.field { field -> field.name(inputDefinition.name) @@ -187,7 +188,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat } } - return builder.build() + return directiveGenerator.onInputObject(builder.build(), DirectiveBehavior.Params(runtimeWiring)) } private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType { @@ -200,6 +201,8 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat .definition(definition) .description(getDocumentation(definition)) + builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.ENUM)) + definition.enumValueDefinitions.forEach { enumDefinition -> val enumName = enumDefinition.name val enumValue = type.unwrap().enumConstants.find { (it as Enum<*>).name == enumName } ?: throw SchemaError("Expected value for name '$enumName' in enum '${type.unwrap().simpleName}' but found none!") @@ -211,7 +214,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat } } - return builder.build() + return directiveGenerator.onEnum(builder.build(), DirectiveBehavior.Params(runtimeWiring)) } private fun createInterfaceObject(definition: InterfaceTypeDefinition): GraphQLInterfaceType { @@ -222,11 +225,13 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat .description(getDocumentation(definition)) .typeResolver(TypeResolverProxy()) + builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INTERFACE)) + definition.fieldDefinitions.forEach { fieldDefinition -> builder.field { field -> createField(field, fieldDefinition) } } - return builder.build() + return directiveGenerator.onInterface(builder.build(), DirectiveBehavior.Params(runtimeWiring)) } private fun createUnionObject(definition: UnionTypeDefinition, types: List): GraphQLUnionType { @@ -237,8 +242,10 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat .description(getDocumentation(definition)) .typeResolver(TypeResolverProxy()) + builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.UNION)) + getLeafUnionObjects(definition, types).forEach { builder.possibleType(it) } - return builder.build() + return directiveGenerator.onUnion(builder.build(), DirectiveBehavior.Params(runtimeWiring)) } private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List): List { diff --git a/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt index 16f8aa9c..ed7aac52 100644 --- a/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt +++ b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt @@ -1,7 +1,15 @@ package graphql.schema.idl +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLEnumType +import graphql.schema.GraphQLEnumValueDefinition import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLInputObjectField +import graphql.schema.GraphQLInputObjectType +import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLUnionType class DirectiveBehavior { @@ -15,6 +23,30 @@ class DirectiveBehavior { return directiveHelper.onField(element, params.toParameters()) } + fun onInterface(element: GraphQLInterfaceType, params: Params): GraphQLInterfaceType = + directiveHelper.onInterface(element, params.toParameters()) + + fun onUnion(element: GraphQLUnionType, params: Params): GraphQLUnionType = + directiveHelper.onUnion(element, params.toParameters()) + + fun onScalar(element: GraphQLScalarType, params: Params): GraphQLScalarType = + directiveHelper.onScalar(element, params.toParameters()) + + fun onEnum(element: GraphQLEnumType, params: Params): GraphQLEnumType = + directiveHelper.onEnum(element, params.toParameters()) + + fun onEnumValue(element: GraphQLEnumValueDefinition, params: Params): GraphQLEnumValueDefinition = + directiveHelper.onEnumValue(element, params.toParameters()) + + fun onArgument(element: GraphQLArgument, params: Params): GraphQLArgument = + directiveHelper.onArgument(element, params.toParameters()) + + fun onInputObject(element: GraphQLInputObjectType, params: Params): GraphQLInputObjectType = + directiveHelper.onInputObjectType(element, params.toParameters()) + + fun onInputObjectField(element: GraphQLInputObjectField, params: Params): GraphQLInputObjectField = + directiveHelper.onInputObjectField(element, params.toParameters()) + data class Params(val runtimeWiring: RuntimeWiring) { internal fun toParameters() = SchemaGeneratorDirectiveHelper.Parameters(null, runtimeWiring, null, null) } From 192f0827fddafbba484d631967fe2b1beb8c2c54 Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 14:31:55 +0100 Subject: [PATCH 10/11] Extended supported types for directives --- src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt index 172de0f8..99b5cc6c 100644 --- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt +++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt @@ -185,6 +185,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat field.description(getDocumentation(inputDefinition)) field.defaultValue(inputDefinition.defaultValue) field.type(determineInputType(inputDefinition.type)) + field.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION)) } } @@ -280,6 +281,8 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat argument.description(getDocumentation(argumentDefinition)) argument.defaultValue(buildDefaultValue(argumentDefinition.defaultValue)) argument.type(determineInputType(argumentDefinition.type)) + argument.withDirectives(*buildDirectives(argumentDefinition.directives, setOf(), Introspection.DirectiveLocation.ARGUMENT_DEFINITION)) + } } field.withDirectives(*buildDirectives(fieldDefinition.directives, setOf(), Introspection.DirectiveLocation.FIELD_DEFINITION)) From 041c36ffa1bb44fe352f98fbd169458c7b06d32e Mon Sep 17 00:00:00 2001 From: Michiel Oliemans Date: Sat, 17 Nov 2018 14:33:21 +0100 Subject: [PATCH 11/11] Updated minor version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 95a469bf..07cecb17 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.graphql-java-kickstart graphql-java-tools - 5.3.6-SNAPSHOT + 5.4.0-SNAPSHOT jar GraphQL Java Tools