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 { - -}