Skip to content

Commit

Permalink
Enable support for specifying custom DataFetchers (#152)
Browse files Browse the repository at this point in the history
* Enable coroutine support in default KotlinDataFetcher

* Enable support for specifying custom DataFetchers

* fix compilation errors

* simplify KotlinDataFetcherFactoryProvider

* add accidentally removed data fetcher tests

* renaming variable name
  • Loading branch information
dariuszkuc authored and smyrick committed Jan 24, 2019
1 parent 4d230fc commit 92c58be
Show file tree
Hide file tree
Showing 19 changed files with 144 additions and 148 deletions.
17 changes: 14 additions & 3 deletions example/src/main/kotlin/com/expedia/graphql/sample/Application.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package com.expedia.graphql.sample
import com.expedia.graphql.DirectiveWiringHelper
import com.expedia.graphql.SchemaGeneratorConfig
import com.expedia.graphql.TopLevelObject
import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
import com.expedia.graphql.hooks.SchemaGeneratorHooks
import com.expedia.graphql.sample.context.MyGraphQLContextBuilder
import com.expedia.graphql.sample.dataFetchers.CustomDataFetcherFactoryProvider
import com.expedia.graphql.sample.dataFetchers.SpringDataFetcherFactory
import com.expedia.graphql.sample.directives.DirectiveWiringFactory
import com.expedia.graphql.sample.directives.LowercaseDirectiveWiring
Expand Down Expand Up @@ -42,10 +45,18 @@ class Application {
fun wiringFactory() = DirectiveWiringFactory()

@Bean
fun schemaConfig(dataFetcherFactory: SpringDataFetcherFactory, validator: Validator, wiringFactory: DirectiveWiringFactory): SchemaGeneratorConfig = SchemaGeneratorConfig(
fun hooks(validator: Validator, wiringFactory: DirectiveWiringFactory) =
CustomSchemaGeneratorHooks(validator, DirectiveWiringHelper(wiringFactory, mapOf("lowercase" to LowercaseDirectiveWiring())))

@Bean
fun dataFetcherFactoryProvider(springDataFetcherFactory: SpringDataFetcherFactory, hooks: SchemaGeneratorHooks) =
CustomDataFetcherFactoryProvider(springDataFetcherFactory, hooks)

@Bean
fun schemaConfig(hooks: SchemaGeneratorHooks, dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider): SchemaGeneratorConfig = SchemaGeneratorConfig(
supportedPackages = listOf("com.expedia"),
hooks = CustomSchemaGeneratorHooks(validator, DirectiveWiringHelper(wiringFactory, mapOf("lowercase" to LowercaseDirectiveWiring()))),
dataFetcherFactory = dataFetcherFactory
hooks = hooks,
dataFetcherFactoryProvider = dataFetcherFactoryProvider
)

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.expedia.graphql.sample.dataFetchers

import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
import com.expedia.graphql.hooks.SchemaGeneratorHooks
import graphql.schema.DataFetcherFactory
import kotlin.reflect.KClass
import kotlin.reflect.KProperty

/**
* Custom DataFetcherFactory provider that returns custom Spring based DataFetcherFactory for resolving lateinit properties.
*/
class CustomDataFetcherFactoryProvider(
private val springDataFetcherFactory: SpringDataFetcherFactory,
hooks: SchemaGeneratorHooks
) : KotlinDataFetcherFactoryProvider(hooks) {

override fun propertyDataFetcherFactory(kClazz: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any> =
if (kProperty.isLateinit) {
springDataFetcherFactory
} else {
super.propertyDataFetcherFactory(kClazz, kProperty)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.expedia.graphql.sample.extension

import com.expedia.graphql.DirectiveWiringHelper
import com.expedia.graphql.hooks.DataFetcherExecutionPredicate
import com.expedia.graphql.execution.DataFetcherExecutionPredicate
import com.expedia.graphql.hooks.SchemaGeneratorHooks
import com.expedia.graphql.sample.validation.DataFetcherExecutionValidator
import graphql.language.StringValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.springframework.stereotype.Component
* schema and will be automatically autowired at runtime using value from the environment.
*
* @see com.expedia.graphql.sample.context.MyGraphQLContextBuilder
* @see com.expedia.graphql.KotlinDataFetcher
* @see com.expedia.graphql.FunctionDataFetcher
*/
@Component
class ContextualQuery: Query {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.expedia.graphql.sample.validation

import com.expedia.graphql.hooks.DataFetcherExecutionPredicate
import com.expedia.graphql.execution.DataFetcherExecutionPredicate
import com.expedia.graphql.sample.exceptions.ValidationException
import com.expedia.graphql.sample.exceptions.asConstraintError
import graphql.schema.DataFetchingEnvironment
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/expedia/graphql/SchemaGeneratorConfig.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.expedia.graphql

import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
import com.expedia.graphql.hooks.NoopSchemaGeneratorHooks
import com.expedia.graphql.hooks.SchemaGeneratorHooks
import graphql.schema.DataFetcherFactory

/**
* Settings for generating the schema.
Expand All @@ -12,5 +12,5 @@ data class SchemaGeneratorConfig(
val topLevelQueryName: String = "TopLevelQuery",
val topLevelMutationName: String = "TopLevelMutation",
val hooks: SchemaGeneratorHooks = NoopSchemaGeneratorHooks(),
val dataFetcherFactory: DataFetcherFactory<*>? = null
val dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider = KotlinDataFetcherFactoryProvider(hooks)
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.expedia.graphql.hooks
package com.expedia.graphql.execution

import graphql.schema.DataFetchingEnvironment
import kotlin.reflect.KParameter

/**
* Perform runtime evaluations of each parameter passed to any KotlinDataFetcher.
* Perform runtime evaluations of each parameter passed to any FunctionDataFetcher.
*
* The DataFetcherExecutionPredicate is declared globally for all the datafetchers instances and all the parameters.
* However a more precise logic (at the field level) is possible depending on the implement of `evaluate`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.expedia.graphql
package com.expedia.graphql.execution

import com.expedia.graphql.generator.extensions.getName
import com.expedia.graphql.generator.extensions.isGraphQLContext
import com.expedia.graphql.generator.extensions.javaTypeClass
import com.expedia.graphql.hooks.DataFetcherExecutionPredicate
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
Expand All @@ -16,16 +16,19 @@ import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.valueParameters

/**
* Simple DataFetcher that invokes function on the target object.
* Simple DataFetcher that invokes target function on the given object.
*
* @param target The target object that performs the data fetching
* @param target The target object that performs the data fetching, if not specified then this data fetcher will attempt
* to use source object from the environment
* @param fn The Kotlin function being invoked
* @param objectMapper Jackson ObjectMapper that will be used to deserialize environment arguments to the expected function arguments
* @param executionPredicate Predicate to run to map the value to a new result
*/
class KotlinDataFetcher(
class FunctionDataFetcher(
private val target: Any?,
private val fn: KFunction<*>,
private val executionPredicate: DataFetcherExecutionPredicate?
private val objectMapper: ObjectMapper = jacksonObjectMapper(),
private val executionPredicate: DataFetcherExecutionPredicate? = null
) : DataFetcher<Any> {

@Suppress("Detekt.SpreadOperator")
Expand All @@ -51,13 +54,9 @@ class KotlinDataFetcher(
} else {
val name = param.getName()
val klazz = param.type.javaTypeClass
val value = mapper.convertValue(environment.arguments[name], klazz)
val value = objectMapper.convertValue(environment.arguments[name], klazz)
val predicateResult = executionPredicate?.evaluate(value = value, parameter = param, environment = environment)

predicateResult ?: value
}

private companion object {
private val mapper = jacksonObjectMapper()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.expedia.graphql.execution

import com.expedia.graphql.hooks.SchemaGeneratorHooks
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import graphql.schema.DataFetcherFactories
import graphql.schema.DataFetcherFactory
import graphql.schema.PropertyDataFetcher
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KProperty

/**
* DataFetcherFactoryProvider is used during schema construction to obtain [DataFetcherFactory] that should be used
* for target function and property resolution.
*/
open class KotlinDataFetcherFactoryProvider(private val hooks: SchemaGeneratorHooks) {

private val defaultObjectMapper = jacksonObjectMapper()

/**
* Retrieve instance of [DataFetcherFactory] that will be used to resolve target function.
*
* @param target target object that performs the data fetching or NULL if target object should be dynamically
* retrieved during data fetcher execution from [graphql.schema.DataFetchingEnvironment]
* @param kFunction Kotlin function being invoked
*/
open fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any> =
DataFetcherFactories.useDataFetcher(
FunctionDataFetcher(
target = target,
fn = kFunction,
objectMapper = defaultObjectMapper,
executionPredicate = hooks.dataFetcherExecutionPredicate))

/**
* Retrieve instance of [DataFetcherFactory] that will be used to resolve target property.
*
* @param kClass parent class that contains this property
* @param kProperty Kotlin property that should be resolved
*/
open fun propertyDataFetcherFactory(kClass: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any> =
DataFetcherFactories.useDataFetcher(PropertyDataFetcher(kProperty.name))
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.expedia.graphql.generator.types

import com.expedia.graphql.KotlinDataFetcher
import com.expedia.graphql.exceptions.InvalidInputFieldTypeException
import com.expedia.graphql.generator.SchemaGenerator
import com.expedia.graphql.generator.TypeBuilder
Expand Down Expand Up @@ -41,9 +40,7 @@ internal class FunctionTypeBuilder(generator: SchemaGenerator) : TypeBuilder(gen
}

if (!abstract) {
val dataFetcher = KotlinDataFetcher(target, fn, config.hooks.dataFetcherExecutionPredicate)
val hookDataFetcher = config.hooks.didGenerateDataFetcher(fn, dataFetcher)
builder.dataFetcher(hookDataFetcher)
builder.dataFetcherFactory(config.dataFetcherFactoryProvider.functionDataFetcherFactory(target = target, kFunction = fn))
}

val monadType = config.hooks.willResolveMonad(fn.returnType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import com.expedia.graphql.generator.TypeBuilder
import com.expedia.graphql.generator.extensions.getPropertyDeprecationReason
import com.expedia.graphql.generator.extensions.getPropertyDescription
import com.expedia.graphql.generator.extensions.isPropertyGraphQLID
import graphql.schema.DataFetcherFactory
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLNonNull
import graphql.schema.GraphQLOutputType
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
Expand All @@ -21,29 +19,14 @@ internal class PropertyTypeBuilder(generator: SchemaGenerator) : TypeBuilder(gen
.description(prop.getPropertyDescription(parentClass))
.name(prop.name)
.type(propertyType)
.dataFetcherFactory(config.dataFetcherFactoryProvider.propertyDataFetcherFactory(kClass = parentClass, kProperty = prop))
.deprecate(prop.getPropertyDeprecationReason(parentClass))

generator.directives(prop).forEach {
fieldBuilder.withDirective(it)
}

val field = if (config.dataFetcherFactory != null && prop.isLateinit) {
updatePropertyFieldBuilder(propertyType, fieldBuilder, config.dataFetcherFactory)
} else {
fieldBuilder
}.build()

val field = fieldBuilder.build()
return config.hooks.onRewireGraphQLType(prop.returnType, field) as GraphQLFieldDefinition
}

private fun updatePropertyFieldBuilder(propertyType: GraphQLOutputType, fieldBuilder: GraphQLFieldDefinition.Builder, dataFetcherFactory: DataFetcherFactory<*>?): GraphQLFieldDefinition.Builder {
val updatedFieldBuilder = if (propertyType is GraphQLNonNull) {
val graphQLOutputType = propertyType.wrappedType as? GraphQLOutputType
if (graphQLOutputType != null) fieldBuilder.type(graphQLOutputType) else fieldBuilder
} else {
fieldBuilder
}

return updatedFieldBuilder.dataFetcherFactory(dataFetcherFactory)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.expedia.graphql.hooks

import com.expedia.graphql.execution.DataFetcherExecutionPredicate
import com.expedia.graphql.generator.extensions.getTypeOfFirstArgument
import graphql.schema.DataFetcher
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLSchema
import graphql.schema.GraphQLType
Expand Down Expand Up @@ -73,12 +73,6 @@ interface SchemaGeneratorHooks {
*/
fun didGenerateGraphQLType(type: KType, generatedType: GraphQLType) = Unit

/**
* Called after converting the function to a data fetcher allowing wrapping the fetcher to modify data or instrument it.
* This is more useful than the graphql.execution.instrumentation.Instrumentation as you have the function type here
*/
fun didGenerateDataFetcher(function: KFunction<*>, dataFetcher: DataFetcher<*>): DataFetcher<*> = dataFetcher

/**
* Called after converting the function to a field definition but before adding to the schema to allow customization
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package com.expedia.graphql.dataFetchers
package com.expedia.graphql.execution

import com.expedia.graphql.TopLevelObject
import com.expedia.graphql.SchemaGeneratorConfig
import com.expedia.graphql.TopLevelObject
import com.expedia.graphql.extensions.deepName
import com.expedia.graphql.hooks.NoopSchemaGeneratorHooks
import com.expedia.graphql.toSchema
import graphql.GraphQL
import graphql.schema.DataFetcher
import graphql.schema.DataFetcherFactories
import graphql.schema.DataFetcherFactory
import graphql.schema.DataFetcherFactoryEnvironment
import graphql.schema.DataFetchingEnvironment
import org.junit.jupiter.api.Test
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
import kotlin.test.assertEquals

class CustomDataFetcherTests {
@Test
fun `Custom DataFetcher can be used on functions`() {
val config = SchemaGeneratorConfig(supportedPackages = listOf("com.expedia"), dataFetcherFactory = PetDataFetcherFactory())
val config = SchemaGeneratorConfig(supportedPackages = listOf("com.expedia"), dataFetcherFactoryProvider = CustomDataFetcherFactoryProvider())
val schema = toSchema(listOf(TopLevelObject(AnimalQuery())), config = config)

val animalType = schema.getObjectType("Animal")
assertEquals("AnimalDetails", animalType.getFieldDefinition("details").type.deepName)
assertEquals("AnimalDetails!", animalType.getFieldDefinition("details").type.deepName)

val graphQL = GraphQL.newGraphQL(schema).build()
val execute = graphQL.execute("{ findAnimal { id type details { specialId } } }")
Expand All @@ -46,8 +49,14 @@ data class Animal(

data class AnimalDetails(val specialId: Int)

class PetDataFetcherFactory : DataFetcherFactory<Any> {
override fun get(environment: DataFetcherFactoryEnvironment?): DataFetcher<Any> = AnimalDetailsDataFetcher()
class CustomDataFetcherFactoryProvider : KotlinDataFetcherFactoryProvider(NoopSchemaGeneratorHooks()) {

override fun propertyDataFetcherFactory(kClazz: KClass<*>, kProperty: KProperty<*>): DataFetcherFactory<Any> =
if (kProperty.isLateinit) {
DataFetcherFactories.useDataFetcher(AnimalDetailsDataFetcher())
} else {
super.propertyDataFetcherFactory(kClazz, kProperty)
}
}

class AnimalDetailsDataFetcher : DataFetcher<Any> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.expedia.graphql.dataFetchers
package com.expedia.graphql.execution

import com.expedia.graphql.TopLevelObject
import com.expedia.graphql.exceptions.GraphQLKotlinException
import com.expedia.graphql.getTestSchemaConfigWithHooks
import com.expedia.graphql.hooks.DataFetcherExecutionPredicate
import com.expedia.graphql.hooks.SchemaGeneratorHooks
import com.expedia.graphql.toSchema
import graphql.ExceptionWhileDataFetching
Expand Down
Loading

0 comments on commit 92c58be

Please sign in to comment.