From 7111f0ef39cee4b94eaf35f5fe8bc1510773d5a2 Mon Sep 17 00:00:00 2001 From: Guillaume Scheibel Date: Tue, 9 Oct 2018 05:56:08 -0500 Subject: [PATCH] Data fetcher factory (#41) * Update Junit dep to Junit5 * Add support for nested queries via custom datafetchers * Add example of custom datafetchers via spring beans * Mark lateinit field as nullable --- example/pom.xml | 17 +++++- .../com.expedia.graphql.sample/Application.kt | 7 ++- .../dataFetchers/SpringDataFetcherFactory.kt | 26 ++++++++ .../query/NestedQueries.kt | 50 ++++++++++++++++ pom.xml | 9 +-- .../graphql/schema/SchemaGeneratorConfig.kt | 4 +- .../schema/generator/SchemaGenerator.kt | 31 ++++++++-- .../dataFetchers/CustomDataFetcherTests.kt | 60 +++++++++++++++++++ .../schema/generator/DirectiveTests.kt | 2 +- .../schema/generator/KClassExtensionsTest.kt | 2 +- .../schema/generator/PolymorphicTests.kt | 15 +++-- .../schema/generator/SchemaGeneratorTest.kt | 19 ++++-- .../schema/generator/TypesCacheTest.kt | 2 +- 13 files changed, 215 insertions(+), 29 deletions(-) create mode 100644 example/src/main/kotlin/com.expedia.graphql.sample/dataFetchers/SpringDataFetcherFactory.kt create mode 100644 example/src/main/kotlin/com.expedia.graphql.sample/query/NestedQueries.kt create mode 100644 src/test/kotlin/com/expedia/graphql/schema/dataFetchers/CustomDataFetcherTests.kt diff --git a/example/pom.xml b/example/pom.xml index 7a669a738c..eadd130b30 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -60,15 +60,15 @@ 1.8 - 1.2.70 + 1.2.71 4.12 5.0.2 - DEVELOPMENT + 0.0.18-SNAPSHOT 6.1.2 - ${project.basedir}/src/main/kotlin + src/main/kotlin kotlin-maven-plugin @@ -140,5 +140,16 @@ + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-test + ${kotlin.version} + test + diff --git a/example/src/main/kotlin/com.expedia.graphql.sample/Application.kt b/example/src/main/kotlin/com.expedia.graphql.sample/Application.kt index 3fa5f94f4d..8429207f9d 100644 --- a/example/src/main/kotlin/com.expedia.graphql.sample/Application.kt +++ b/example/src/main/kotlin/com.expedia.graphql.sample/Application.kt @@ -2,6 +2,7 @@ package com.expedia.graphql.sample import com.expedia.graphql.TopLevelObjectDef import com.expedia.graphql.sample.context.MyGraphQLContextBuilder +import com.expedia.graphql.sample.dataFetchers.SpringDataFetcherFactory import com.expedia.graphql.sample.extension.CustomSchemaGeneratorHooks import com.expedia.graphql.sample.mutation.Mutation import com.expedia.graphql.sample.query.Query @@ -30,7 +31,11 @@ class Application { private val logger = LoggerFactory.getLogger(Application::class.java) @Bean - fun schemaConfig(): SchemaGeneratorConfig = SchemaGeneratorConfig(supportedPackages = "com.expedia", hooks = CustomSchemaGeneratorHooks()) + fun schemaConfig(dataFetcherFactory: SpringDataFetcherFactory): SchemaGeneratorConfig = SchemaGeneratorConfig( + supportedPackages = "com.expedia", + hooks = CustomSchemaGeneratorHooks(), + dataFetcherFactory = dataFetcherFactory + ) @Bean fun schema( diff --git a/example/src/main/kotlin/com.expedia.graphql.sample/dataFetchers/SpringDataFetcherFactory.kt b/example/src/main/kotlin/com.expedia.graphql.sample/dataFetchers/SpringDataFetcherFactory.kt new file mode 100644 index 0000000000..86a1a69f50 --- /dev/null +++ b/example/src/main/kotlin/com.expedia.graphql.sample/dataFetchers/SpringDataFetcherFactory.kt @@ -0,0 +1,26 @@ +package com.expedia.graphql.sample.dataFetchers + +import com.expedia.graphql.schema.extensions.deepName +import graphql.schema.DataFetcher +import graphql.schema.DataFetcherFactory +import graphql.schema.DataFetcherFactoryEnvironment +import org.springframework.beans.factory.BeanFactory +import org.springframework.beans.factory.BeanFactoryAware +import org.springframework.stereotype.Component + +@Component +class SpringDataFetcherFactory: DataFetcherFactory, BeanFactoryAware { + private lateinit var beanFactory: BeanFactory + + override fun setBeanFactory(beanFactory: BeanFactory?) { + this.beanFactory = beanFactory!! + } + + override fun get(environment: DataFetcherFactoryEnvironment?): DataFetcher { + + //Strip out possible `Input` and `!` suffixes added to by the SchemaGenerator + val targetedTypeName = environment?.fieldDefinition?.type?.deepName?.removeSuffix("!")?.removeSuffix("Input") + return beanFactory.getBean("${targetedTypeName}DataFetcher") as DataFetcher + } + +} \ No newline at end of file diff --git a/example/src/main/kotlin/com.expedia.graphql.sample/query/NestedQueries.kt b/example/src/main/kotlin/com.expedia.graphql.sample/query/NestedQueries.kt new file mode 100644 index 0000000000..26b8881a39 --- /dev/null +++ b/example/src/main/kotlin/com.expedia.graphql.sample/query/NestedQueries.kt @@ -0,0 +1,50 @@ +package com.expedia.graphql.sample.query + +import com.fasterxml.jackson.annotation.JsonIgnore +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import org.springframework.beans.factory.BeanFactory +import org.springframework.beans.factory.BeanFactoryAware +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.getBean +import org.springframework.context.annotation.Scope +import org.springframework.stereotype.Component + +@Component +class NestedQueries : Query { + fun findAnimal(context: String): NestedAnimal = NestedAnimal(1, "cat") +} + +data class NestedAnimal( + val id: Int, + val type: String +) { + @JsonIgnore + lateinit var details: NestedAnimalDetails +} + +@Component +@Scope("prototype") +data class NestedAnimalDetails @Autowired(required = false) constructor(private val animalId: Int) { + fun veryDetailledFunction(): String = "Details($animalId)" +} + +@Component("NestedAnimalDetailsDataFetcher") +@Scope("prototype") +class AnimalDetailsDataFetcher : DataFetcher, BeanFactoryAware { + + private lateinit var beanFactory: BeanFactory + + override fun setBeanFactory(beanFactory: BeanFactory) { + this.beanFactory = beanFactory + } + + override fun get(environment: DataFetchingEnvironment?): NestedAnimalDetails { + val id = environment?.getSource()?.id + if (id == null) { + throw Exception("Cannot retrieve animal details, the id is null") + } else { + return beanFactory.getBean(id) + } + } +} diff --git a/pom.xml b/pom.xml index 6cd42b52b6..19c2239768 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,7 @@ 0.29.0 1.0.0.RC8 4.12 + 1.8.9.kotlin13 @@ -209,14 +210,14 @@ org.jetbrains.kotlin - kotlin-test-junit + kotlin-test-junit5 ${kotlin.version} test - junit - junit - ${junit.version} + io.mockk + mockk + ${mockk.version} test diff --git a/src/main/kotlin/com/expedia/graphql/schema/SchemaGeneratorConfig.kt b/src/main/kotlin/com/expedia/graphql/schema/SchemaGeneratorConfig.kt index 1c3ecbb72f..ec2f9bf8ab 100644 --- a/src/main/kotlin/com/expedia/graphql/schema/SchemaGeneratorConfig.kt +++ b/src/main/kotlin/com/expedia/graphql/schema/SchemaGeneratorConfig.kt @@ -3,6 +3,7 @@ package com.expedia.graphql.schema import com.expedia.graphql.schema.generator.completableFutureResolver import com.expedia.graphql.schema.hooks.NoopSchemaGeneratorHooks import com.expedia.graphql.schema.hooks.SchemaGeneratorHooks +import graphql.schema.DataFetcherFactory import kotlin.reflect.KType /** @@ -13,5 +14,6 @@ data class SchemaGeneratorConfig( val topLevelQueryName: String = "TopLevelQuery", val topLevelMutationName: String = "TopLevelMutation", val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks(), - val monadResolver: (KType) -> KType = completableFutureResolver + val monadResolver: (KType) -> KType = completableFutureResolver, + val dataFetcherFactory: DataFetcherFactory<*>? = null ) diff --git a/src/main/kotlin/com/expedia/graphql/schema/generator/SchemaGenerator.kt b/src/main/kotlin/com/expedia/graphql/schema/generator/SchemaGenerator.kt index 97d78aa3cc..ecb03d598d 100644 --- a/src/main/kotlin/com/expedia/graphql/schema/generator/SchemaGenerator.kt +++ b/src/main/kotlin/com/expedia/graphql/schema/generator/SchemaGenerator.kt @@ -22,6 +22,7 @@ import graphql.schema.GraphQLInputObjectType import graphql.schema.GraphQLInputType import graphql.schema.GraphQLInterfaceType import graphql.schema.GraphQLList +import graphql.schema.GraphQLNonNull import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLOutputType import graphql.schema.GraphQLSchema @@ -146,12 +147,30 @@ internal class SchemaGenerator( return builder.build() } - private fun property(prop: KProperty<*>): GraphQLFieldDefinition = GraphQLFieldDefinition.newFieldDefinition() - .description(prop.graphQLDescription()) - .name(prop.name) - .type(graphQLTypeOf(prop.returnType) as GraphQLOutputType) - .deprecate(prop.getDeprecationReason()) - .build() + private fun property(prop: KProperty<*>): GraphQLFieldDefinition { + val propertyType = graphQLTypeOf(prop.returnType) as GraphQLOutputType + + val fieldBuilder = GraphQLFieldDefinition.newFieldDefinition() + .description(prop.graphQLDescription()) + .name(prop.name) + .type(propertyType) + .deprecate(prop.getDeprecationReason()) + + return if (config.dataFetcherFactory != null && prop.isLateinit) { + updatePropertyFieldBuilder(propertyType, fieldBuilder) + } else { + fieldBuilder + }.build() + } + + private fun updatePropertyFieldBuilder(propertyType: GraphQLOutputType, fieldBuilder: GraphQLFieldDefinition.Builder): GraphQLFieldDefinition.Builder { + val updatedFieldBuilder = if (propertyType is GraphQLNonNull) { + fieldBuilder.type(propertyType.wrappedType as GraphQLOutputType) + } else { + fieldBuilder + } + return updatedFieldBuilder.dataFetcherFactory(config.dataFetcherFactory) + } private fun argument(parameter: KParameter): GraphQLArgument { throwIfInterfaceIsNotAuthorized(parameter) diff --git a/src/test/kotlin/com/expedia/graphql/schema/dataFetchers/CustomDataFetcherTests.kt b/src/test/kotlin/com/expedia/graphql/schema/dataFetchers/CustomDataFetcherTests.kt new file mode 100644 index 0000000000..8c34b3f0af --- /dev/null +++ b/src/test/kotlin/com/expedia/graphql/schema/dataFetchers/CustomDataFetcherTests.kt @@ -0,0 +1,60 @@ +package com.expedia.graphql.schema.dataFetchers + +import com.expedia.graphql.TopLevelObjectDef +import com.expedia.graphql.schema.SchemaGeneratorConfig +import com.expedia.graphql.schema.extensions.deepName +import com.expedia.graphql.toSchema +import graphql.GraphQL +import graphql.schema.DataFetcher +import graphql.schema.DataFetcherFactory +import graphql.schema.DataFetcherFactoryEnvironment +import graphql.schema.DataFetchingEnvironment +import org.junit.jupiter.api.Test +import kotlin.test.assertEquals + +class CustomDataFetcherTests { + @Test + fun `Custom DataFetcher can be used on functions`() { + val config = SchemaGeneratorConfig(supportedPackages = "com.expedia", dataFetcherFactory = PetDataFetcherFactory()) + val schema = toSchema(listOf(TopLevelObjectDef(AnimalQuery())), config = config) + + val animalType = schema.getObjectType("Animal") + assertEquals("AnimalDetails", animalType.getFieldDefinition("details").type.deepName) + + val graphQL = GraphQL.newGraphQL(schema).build() + val execute = graphQL.execute("{ findAnimal { id type details { specialId } } }") + + val data = execute.getData>()["findAnimal"] as? Map<*, *> + assertEquals(1, data?.get("id")) + assertEquals("cat", data?.get("type")) + + val details = data?.get("details") as? Map<*, *> + assertEquals(11, details?.get("specialId")) + } +} + +class AnimalQuery { + fun findAnimal(): Animal = Animal(1, "cat") +} + +data class Animal( + val id: Int, + val type: String +) { + lateinit var details: AnimalDetails +} + +data class AnimalDetails(val specialId: Int) + +class PetDataFetcherFactory : DataFetcherFactory { + override fun get(environment: DataFetcherFactoryEnvironment?): DataFetcher = AnimalDetailsDataFetcher() +} + +class AnimalDetailsDataFetcher : DataFetcher { + + override fun get(environment: DataFetchingEnvironment?): AnimalDetails { + val animal = environment?.getSource() + val specialId = animal?.id?.plus(10) ?: 0 + return animal.let { AnimalDetails(specialId) } + } +} diff --git a/src/test/kotlin/com/expedia/graphql/schema/generator/DirectiveTests.kt b/src/test/kotlin/com/expedia/graphql/schema/generator/DirectiveTests.kt index d9483bac20..d061c1ed30 100644 --- a/src/test/kotlin/com/expedia/graphql/schema/generator/DirectiveTests.kt +++ b/src/test/kotlin/com/expedia/graphql/schema/generator/DirectiveTests.kt @@ -8,7 +8,7 @@ import graphql.introspection.Introspection import graphql.schema.GraphQLInputObjectType import graphql.schema.GraphQLNonNull import graphql.schema.GraphQLObjectType -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue diff --git a/src/test/kotlin/com/expedia/graphql/schema/generator/KClassExtensionsTest.kt b/src/test/kotlin/com/expedia/graphql/schema/generator/KClassExtensionsTest.kt index 22c4dcf259..6bdb9f37cf 100644 --- a/src/test/kotlin/com/expedia/graphql/schema/generator/KClassExtensionsTest.kt +++ b/src/test/kotlin/com/expedia/graphql/schema/generator/KClassExtensionsTest.kt @@ -1,7 +1,7 @@ package com.expedia.graphql.schema.generator import com.expedia.graphql.schema.hooks.NoopSchemaGeneratorHooks -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.reflect.KFunction import kotlin.reflect.KProperty import kotlin.test.assertEquals diff --git a/src/test/kotlin/com/expedia/graphql/schema/generator/PolymorphicTests.kt b/src/test/kotlin/com/expedia/graphql/schema/generator/PolymorphicTests.kt index 061ff181bc..9842b4ed08 100644 --- a/src/test/kotlin/com/expedia/graphql/schema/generator/PolymorphicTests.kt +++ b/src/test/kotlin/com/expedia/graphql/schema/generator/PolymorphicTests.kt @@ -5,7 +5,8 @@ import com.expedia.graphql.schema.exceptions.InvalidInputFieldTypeException import com.expedia.graphql.schema.testSchemaConfig import com.expedia.graphql.toSchema import graphql.schema.GraphQLUnionType -import org.junit.Test +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -40,14 +41,18 @@ class PolymorphicTests { assertEquals(implementationType.interfaces.first(), interfaceType) } - @Test(expected = InvalidInputFieldTypeException::class) + @Test fun `Interfaces cannot be used as input field types`() { - toSchema(listOf(TopLevelObjectDef(QueryWithUnAuthorizedInterfaceArgument())), config = testSchemaConfig) + assertThrows(InvalidInputFieldTypeException::class.java) { + toSchema(listOf(TopLevelObjectDef(QueryWithUnAuthorizedInterfaceArgument())), config = testSchemaConfig) + } } - @Test(expected = InvalidInputFieldTypeException::class) + @Test fun `Union cannot be used as input field types`() { - toSchema(listOf(TopLevelObjectDef(QueryWithUnAuthorizedUnionArgument())), config = testSchemaConfig) + assertThrows(InvalidInputFieldTypeException::class.java) { + toSchema(listOf(TopLevelObjectDef(QueryWithUnAuthorizedUnionArgument())), config = testSchemaConfig) + } } } diff --git a/src/test/kotlin/com/expedia/graphql/schema/generator/SchemaGeneratorTest.kt b/src/test/kotlin/com/expedia/graphql/schema/generator/SchemaGeneratorTest.kt index 60048c7193..d4f1c83140 100644 --- a/src/test/kotlin/com/expedia/graphql/schema/generator/SchemaGeneratorTest.kt +++ b/src/test/kotlin/com/expedia/graphql/schema/generator/SchemaGeneratorTest.kt @@ -10,6 +10,7 @@ import com.expedia.graphql.schema.testSchemaConfig import com.expedia.graphql.toSchema import graphql.GraphQL import graphql.schema.GraphQLObjectType +import org.junit.jupiter.api.Assertions.assertThrows import java.net.CookieManager import kotlin.test.Test import kotlin.test.assertEquals @@ -185,19 +186,25 @@ class SchemaGeneratorTest { assertEquals("something", resultWithPrivateParts.fieldDefinitions[0].name) } - @Test(expected = RuntimeException::class) + @Test fun `SchemaGenerator throws when encountering java stdlib`() { - toSchema(listOf(TopLevelObjectDef(QueryWithJavaClass())), config = testSchemaConfig) + assertThrows(RuntimeException::class.java) { + toSchema(listOf(TopLevelObjectDef(QueryWithJavaClass())), config = testSchemaConfig) + } } - @Test(expected = ConflictingTypesException::class) + @Test fun `SchemaGenerator throws when encountering conflicting types`() { - toSchema(queries = listOf(TopLevelObjectDef(QueryWithConflictingTypes())), config = testSchemaConfig) + assertThrows(ConflictingTypesException::class.java) { + toSchema(queries = listOf(TopLevelObjectDef(QueryWithConflictingTypes())), config = testSchemaConfig) + } } - @Test(expected = InvalidSchemaException::class) + @Test fun `SchemaGenerator should throw exception if no queries and no mutations are specified`() { - toSchema(emptyList(), emptyList(), config = testSchemaConfig) + assertThrows(InvalidSchemaException::class.java) { + toSchema(emptyList(), emptyList(), config = testSchemaConfig) + } } class QueryObject { diff --git a/src/test/kotlin/com/expedia/graphql/schema/generator/TypesCacheTest.kt b/src/test/kotlin/com/expedia/graphql/schema/generator/TypesCacheTest.kt index fae75dcaaa..69ccce72ce 100644 --- a/src/test/kotlin/com/expedia/graphql/schema/generator/TypesCacheTest.kt +++ b/src/test/kotlin/com/expedia/graphql/schema/generator/TypesCacheTest.kt @@ -2,7 +2,7 @@ package com.expedia.graphql.schema.generator import com.expedia.graphql.schema.models.KGraphQLType import graphql.schema.GraphQLType -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.reflect.full.starProjectedType import kotlin.test.assertFalse import kotlin.test.assertNotNull