Skip to content

Commit

Permalink
Data fetcher factory (#41)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gscheibel authored and rickfast committed Oct 9, 2018
1 parent f053b45 commit 7111f0e
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 29 deletions.
17 changes: 14 additions & 3 deletions example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@

<properties>
<java.version>1.8</java.version>
<kotlin.version>1.2.70</kotlin.version>
<kotlin.version>1.2.71</kotlin.version>
<junit.version>4.12</junit.version>
<graphiql.version>5.0.2</graphiql.version>
<graphql-kotlin.version>DEVELOPMENT</graphql-kotlin.version>
<graphql-kotlin.version>0.0.18-SNAPSHOT</graphql-kotlin.version>
<graphql-servlet.version>6.1.2</graphql-servlet.version>
</properties>

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<sourceDirectory>src/main/kotlin</sourceDirectory>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
Expand Down Expand Up @@ -140,5 +140,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Any>, BeanFactoryAware {
private lateinit var beanFactory: BeanFactory

override fun setBeanFactory(beanFactory: BeanFactory?) {
this.beanFactory = beanFactory!!
}

override fun get(environment: DataFetcherFactoryEnvironment?): DataFetcher<Any> {

//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<Any>
}

}
Original file line number Diff line number Diff line change
@@ -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<NestedAnimalDetails>, BeanFactoryAware {

private lateinit var beanFactory: BeanFactory

override fun setBeanFactory(beanFactory: BeanFactory) {
this.beanFactory = beanFactory
}

override fun get(environment: DataFetchingEnvironment?): NestedAnimalDetails {
val id = environment?.getSource<NestedAnimal>()?.id
if (id == null) {
throw Exception("Cannot retrieve animal details, the id is null")
} else {
return beanFactory.getBean(id)
}
}
}
9 changes: 5 additions & 4 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
<kotlin-ktlint.version>0.29.0</kotlin-ktlint.version>
<kotlin-detekt.version>1.0.0.RC8</kotlin-detekt.version>
<junit.version>4.12</junit.version>
<mockk.version>1.8.9.kotlin13</mockk.version>
</properties>

<pluginRepositories>
Expand Down Expand Up @@ -209,14 +210,14 @@
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<artifactId>kotlin-test-junit5</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>${mockk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Map<String, Any>>()["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<Any> {
override fun get(environment: DataFetcherFactoryEnvironment?): DataFetcher<Any> = AnimalDetailsDataFetcher()
}

class AnimalDetailsDataFetcher : DataFetcher<Any> {

override fun get(environment: DataFetchingEnvironment?): AnimalDetails {
val animal = environment?.getSource<Animal>()
val specialId = animal?.id?.plus(10) ?: 0
return animal.let { AnimalDetails(specialId) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 7111f0e

Please sign in to comment.