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
+}
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
diff --git a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt
index d8dfaada..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
@@ -18,7 +20,6 @@ import graphql.language.UnionTypeDefinition
import graphql.schema.GraphQLScalarType
import graphql.schema.idl.ScalarInfo
import org.slf4j.LoggerFactory
-import java.lang.reflect.Field
import java.lang.reflect.Method
/**
@@ -134,7 +135,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 +217,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
@@ -230,33 +231,51 @@ 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)) {
+ if (item.clazz.equals(Object::class.java)) {
getResolverInfoFromTypeDictionary(item.type.name)
} 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 -> {
@@ -287,7 +306,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..99b5cc6c 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,40 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
.definition(definition)
.description(getDocumentation(definition))
+ builder.withDirectives(*buildDirectives(definition.directives, setOf(), 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)
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 {
@@ -145,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)
@@ -152,10 +185,11 @@ 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))
}
}
- return builder.build()
+ return directiveGenerator.onInputObject(builder.build(), DirectiveBehavior.Params(runtimeWiring))
}
private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
@@ -168,6 +202,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!")
@@ -179,7 +215,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 {
@@ -190,11 +226,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 {
@@ -205,8 +243,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 {
@@ -241,8 +281,11 @@ 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))
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..d5905032 100644
--- a/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt
+++ b/src/main/kotlin/com/coxautodev/graphql/tools/SchemaParserBuilder.kt
@@ -1,13 +1,22 @@
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
+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 +40,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 +88,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.
*/
@@ -142,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)
@@ -151,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(definitions)) }
+ return definitions.toList()
+ }
+
private fun parseDocuments(): List {
val parser = Parser()
val documents = mutableListOf()
@@ -181,7 +201,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) {
@@ -260,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
@@ -280,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
@@ -333,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) {
@@ -358,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..d8fb49de
--- /dev/null
+++ b/src/main/kotlin/com/coxautodev/graphql/tools/TypeDefinitionFactory.java
@@ -0,0 +1,18 @@
+package com.coxautodev.graphql.tools;
+
+import graphql.language.Definition;
+
+import java.util.List;
+
+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);
+
+}
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..97dc9f9c
--- /dev/null
+++ b/src/main/kotlin/com/coxautodev/graphql/tools/relay/RelayConnectionFactory.kt
@@ -0,0 +1,92 @@
+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
+
+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.directivesWithField() }
+ .filter { it.name == "connection" }
+ }
+
+ private fun createDefinitions(directive: DirectiveWithField): List {
+ val definitions = mutableListOf()
+ 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)
+ .fieldDefinition(FieldDefinition("edges", ListType(TypeName(type + "Edge"))))
+ .fieldDefinition(FieldDefinition("pageInfo", TypeName("PageInfo")))
+ .build()
+
+ private fun createEdgeDefinition(connectionType: String, nodeType: String): ObjectTypeDefinition =
+ ObjectTypeDefinition.newObjectTypeDefinition()
+ .name(connectionType + "Edge")
+ .fieldDefinition(FieldDefinition("cursor", TypeName("String")))
+ .fieldDefinition(FieldDefinition("node", TypeName(nodeType)))
+ .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
+ }
+
+ 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/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt
new file mode 100644
index 00000000..ed7aac52
--- /dev/null
+++ b/src/main/kotlin/graphql/schema/idl/DirectiveBehavior.kt
@@ -0,0 +1,54 @@
+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 {
+
+ 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())
+ }
+
+ 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)
+ }
+
+}
diff --git a/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy b/src/test/groovy/com/coxautodev/graphql/tools/RelayConnectionSpec.groovy
index f3456415..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 {
@@ -13,6 +18,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 +34,10 @@ class RelayConnectionSpec extends Specification {
node: User!
}
+
type User {
id: ID!
- name: String
+ name: String @uppercase
}
type PageInfo {
@@ -48,6 +56,7 @@ class RelayConnectionSpec extends Specification {
}
''')
.resolvers(new QueryResolver())
+ .directive("uppercase", new UppercaseDirective())
.build()
.makeExecutableSchema()
GraphQL gql = GraphQL.newGraphQL(schema)
@@ -79,7 +88,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"
}
@@ -112,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..914a53fe
--- /dev/null
+++ b/src/test/java/com/coxautodev/graphql/tools/ReactiveTest.java
@@ -0,0 +1,63 @@
+package com.coxautodev.graphql.tools;
+
+import graphql.GraphQL;
+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();
+
+ 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 {
+// Single> organization(int organizationid) {
+// return Single.just(Optional.empty()); //CompletableFuture.completedFuture(null);
+// }
+
+ Future> organization(int organizationid) {
+ return CompletableFuture.completedFuture(Optional.of(new Organization()));
+ }
+ }
+
+ 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 4e9aab77..e196623a 100644
--- a/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java
+++ b/src/test/java/com/coxautodev/graphql/tools/RelayConnectionTest.java
@@ -1,32 +1,71 @@
package com.coxautodev.graphql.tools;
+import graphql.GraphQL;
+import graphql.execution.AsyncExecutionStrategy;
import graphql.relay.Connection;
import graphql.relay.SimpleListConnection;
import graphql.schema.DataFetchingEnvironment;
+import graphql.schema.GraphQLSchema;
+import groovy.lang.Closure;
import org.junit.Test;
-import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
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)
.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" +
+ " cursor\n" +
+ " node {\n" +
+ " id\n" +
+ " name\n" +
+ " }\n" +
+ " },\n" +
+ " pageInfo {\n" +
+ " hasPreviousPage,\n" +
+ " hasNextPage\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;
+ }
}
+
+
}
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 857c07ec..e832c296 100644
--- a/src/test/resources/RelayConnection.graphqls
+++ b/src/test/resources/RelayConnection.graphqls
@@ -1,22 +1,8 @@
type Query {
- users(first: Int, after: String): UserConnection
-}
-
-type UserConnection {
- edges: [UserEdge!]!
- pageInfo: PageInfo!
-}
-
-type UserEdge {
- cursor: String!
- node: User!
+ users(first: Int, after: String): UserRelayConnection @connection(for: "User")
}
type User {
id: ID!
name: String
}
-
-type PageInfo {
-
-}