Skip to content

Commit 542f6db

Browse files
gscheibeldariuszkuc
authored andcommitted
Adding optional datafetcher predicate to run test on function parameters (#45)
* Adding optional datafetcher predicate to run test on function parameters * Add more flexibility to the predicate * Pass the data fetching environment for more flexibility * Implement the data fetcher predicate to leverage Bean Validation via Spring. * Moving ConstraintError instantiation to an extension fucntion * Update javadoc and minor readability improvement * Rephrase incorrect javadoc * Remove unecessary nullability on the DataFetcherExecutionValidator's validator
1 parent 5050232 commit 542f6db

File tree

13 files changed

+297
-10
lines changed

13 files changed

+297
-10
lines changed

detekt_baseline.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
<ID>UnsafeCast:SchemaGeneratorAsyncTests.kt$SchemaGeneratorAsyncTests$schema.getObjectType("TopLevelQuery").getFieldDefinition("asynchronouslyDo").type as GraphQLNonNull</ID>
1414
<ID>UnsafeCast:SchemaGeneratorAsyncTests.kt$SchemaGeneratorAsyncTests$schema.getObjectType("TopLevelQuery").getFieldDefinition("asynchronouslyDoSingle").type as GraphQLNonNull</ID>
1515
<ID>UnsafeCast:SchemaGeneratorAsyncTests.kt$SchemaGeneratorAsyncTests$schema.getObjectType("TopLevelQuery").getFieldDefinition("maybe").type as GraphQLNonNull</ID>
16+
<ID>ComplexInterface:SchemaGeneratorHooks.kt$SchemaGeneratorHooks</ID>
1617
</Whitelist>
1718
</SmellBaseline>

example/src/main/kotlin/com.expedia.graphql.sample/Application.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@ package com.expedia.graphql.sample
33
import com.expedia.graphql.TopLevelObjectDef
44
import com.expedia.graphql.sample.context.MyGraphQLContextBuilder
55
import com.expedia.graphql.sample.dataFetchers.SpringDataFetcherFactory
6+
import com.expedia.graphql.sample.exceptions.CustomDataFetcherExceptionHandler
67
import com.expedia.graphql.sample.extension.CustomSchemaGeneratorHooks
78
import com.expedia.graphql.sample.mutation.Mutation
89
import com.expedia.graphql.sample.query.Query
910
import com.expedia.graphql.schema.SchemaGeneratorConfig
1011
import com.expedia.graphql.toSchema
1112
import com.fasterxml.jackson.module.kotlin.KotlinModule
13+
import graphql.execution.AsyncExecutionStrategy
1214
import graphql.schema.GraphQLSchema
1315
import graphql.schema.idl.SchemaPrinter
16+
import graphql.servlet.DefaultExecutionStrategyProvider
17+
import graphql.servlet.GraphQLErrorHandler
1418
import graphql.servlet.GraphQLInvocationInputFactory
1519
import graphql.servlet.GraphQLObjectMapper
1620
import graphql.servlet.GraphQLQueryInvoker
@@ -23,6 +27,7 @@ import org.springframework.boot.web.servlet.ServletRegistrationBean
2327
import org.springframework.context.annotation.Bean
2428
import org.springframework.context.annotation.ComponentScan
2529
import javax.servlet.http.HttpServlet
30+
import javax.validation.Validator
2631

2732
@SpringBootApplication
2833
@ComponentScan("com.expedia.graphql")
@@ -31,9 +36,9 @@ class Application {
3136
private val logger = LoggerFactory.getLogger(Application::class.java)
3237

3338
@Bean
34-
fun schemaConfig(dataFetcherFactory: SpringDataFetcherFactory): SchemaGeneratorConfig = SchemaGeneratorConfig(
35-
supportedPackages = "com.expedia",
36-
hooks = CustomSchemaGeneratorHooks(),
39+
fun schemaConfig(dataFetcherFactory: SpringDataFetcherFactory, validator: Validator): SchemaGeneratorConfig = SchemaGeneratorConfig(
40+
supportedPackages = listOf("com.expedia"),
41+
hooks = CustomSchemaGeneratorHooks(validator),
3742
dataFetcherFactory = dataFetcherFactory
3843
)
3944

@@ -68,12 +73,19 @@ class Application {
6873
.build()
6974

7075
@Bean
71-
fun graphQLQueryInvoker(): GraphQLQueryInvoker = GraphQLQueryInvoker.newBuilder()
72-
.build()
76+
fun graphQLQueryInvoker(): GraphQLQueryInvoker {
77+
val exceptionHandler = CustomDataFetcherExceptionHandler()
78+
val executionStrategyProvider = DefaultExecutionStrategyProvider(AsyncExecutionStrategy(exceptionHandler))
79+
80+
return GraphQLQueryInvoker.newBuilder()
81+
.withExecutionStrategyProvider(executionStrategyProvider)
82+
.build()
83+
}
7384

7485
@Bean
7586
fun graphQLObjectMapper(): GraphQLObjectMapper = GraphQLObjectMapper.newBuilder()
7687
.withObjectMapperConfigurer(ObjectMapperConfigurer { it.registerModule(KotlinModule()) })
88+
.withGraphQLErrorHandler( GraphQLErrorHandler { it })
7789
.build()
7890

7991
@Bean
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.expedia.graphql.sample.exceptions
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
4+
import graphql.ErrorType
5+
import graphql.ErrorType.ValidationError
6+
import graphql.ExceptionWhileDataFetching
7+
import graphql.GraphQLError
8+
import graphql.execution.DataFetcherExceptionHandler
9+
import graphql.execution.DataFetcherExceptionHandlerParameters
10+
import graphql.execution.ExecutionPath
11+
import graphql.language.SourceLocation
12+
import org.slf4j.LoggerFactory
13+
14+
class CustomDataFetcherExceptionHandler : DataFetcherExceptionHandler {
15+
private val log = LoggerFactory.getLogger(CustomDataFetcherExceptionHandler::class.java)
16+
17+
override fun accept(handlerParameters: DataFetcherExceptionHandlerParameters) {
18+
val exception = handlerParameters.exception
19+
val sourceLocation = handlerParameters.field.sourceLocation
20+
val path = handlerParameters.path
21+
22+
val error: GraphQLError = when(exception) {
23+
is ValidationException -> ValidationDataFetchingGraphQLError(exception.constraintErrors, path, exception, sourceLocation)
24+
else -> ExceptionWhileDataFetching(path, exception, sourceLocation)
25+
}
26+
27+
handlerParameters.executionContext.addError(error, path)
28+
log.warn(error.message, exception)
29+
}
30+
}
31+
32+
@JsonIgnoreProperties("exception")
33+
class ValidationDataFetchingGraphQLError(
34+
val constraintErrors: List<ConstraintError>,
35+
path: ExecutionPath,
36+
exception: Throwable,
37+
sourceLocation: SourceLocation
38+
) : ExceptionWhileDataFetching(
39+
path,
40+
exception,
41+
sourceLocation
42+
) {
43+
override fun getErrorType(): ErrorType = ValidationError
44+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.expedia.graphql.sample.exceptions
2+
3+
import javax.validation.ConstraintViolation
4+
5+
class ValidationException(val constraintErrors: List<ConstraintError>) : RuntimeException("Validation error")
6+
7+
data class ConstraintError(val path: String, val message: String, val type: String)
8+
9+
fun ConstraintViolation<*>.asConstraintError() = ConstraintError(
10+
path = this.propertyPath.toString(),
11+
message = this.message,
12+
type = this.leafBean.toString()
13+
)

example/src/main/kotlin/com.expedia.graphql.sample/extension/CustomSchemaGeneratorHooks.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
package com.expedia.graphql.sample.extension
22

3+
import com.expedia.graphql.sample.validation.DataFetcherExecutionValidator
4+
import com.expedia.graphql.schema.hooks.DataFetcherExecutionPredicate
35
import com.expedia.graphql.schema.hooks.NoopSchemaGeneratorHooks
46
import graphql.language.StringValue
57
import graphql.schema.Coercing
68
import graphql.schema.GraphQLScalarType
79
import graphql.schema.GraphQLType
810
import java.util.UUID
11+
import javax.validation.Validator
912
import kotlin.reflect.KClass
1013
import kotlin.reflect.KType
1114

1215
/**
1316
* Schema generator hook that adds additional scalar types.
1417
*/
15-
class CustomSchemaGeneratorHooks: NoopSchemaGeneratorHooks() {
18+
class CustomSchemaGeneratorHooks(validator: Validator) : NoopSchemaGeneratorHooks() {
1619

1720
/**
1821
* Register additional GraphQL scalar types.
@@ -22,6 +25,7 @@ class CustomSchemaGeneratorHooks: NoopSchemaGeneratorHooks() {
2225
else -> null
2326
}
2427

28+
override val dataFetcherExecutionPredicate: DataFetcherExecutionPredicate? = DataFetcherExecutionValidator(validator)
2529
}
2630

2731
internal val graphqlUUIDType = GraphQLScalarType("UUID",
@@ -42,4 +46,4 @@ private object UUIDCoercing : Coercing<UUID, String> {
4246
}
4347

4448
override fun serialize(dataFetcherResult: Any?): String = dataFetcherResult.toString()
45-
}
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.expedia.graphql.sample.query
2+
3+
import org.springframework.stereotype.Component
4+
import javax.validation.Valid
5+
import javax.validation.constraints.Pattern
6+
7+
@Component
8+
class ValidatedQuery : Query {
9+
fun argumentWithValidation(@Valid arg: TypeWithPattern): String = arg.lowerCaseOnly
10+
}
11+
12+
data class TypeWithPattern(
13+
@field:Pattern(regexp = "[a-z]*")
14+
val lowerCaseOnly: String
15+
)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package com.expedia.graphql.sample.validation
2+
3+
import com.expedia.graphql.sample.exceptions.ValidationException
4+
import com.expedia.graphql.sample.exceptions.asConstraintError
5+
import com.expedia.graphql.schema.Parameter
6+
import com.expedia.graphql.schema.hooks.DataFetcherExecutionPredicate
7+
import graphql.schema.DataFetchingEnvironment
8+
import javax.validation.ConstraintViolation
9+
import javax.validation.Valid
10+
import javax.validation.Validator
11+
12+
class DataFetcherExecutionValidator(private val validator: Validator) : DataFetcherExecutionPredicate() {
13+
14+
override fun <T> evaluate(value: T, parameter: Parameter, argumentName: String, environment: DataFetchingEnvironment): Any {
15+
val parameterAnnotated = parameter.annotations.any { it.annotationClass == Valid::class }
16+
return if (parameterAnnotated) {
17+
validator.validate(value)
18+
} else {
19+
emptySet()
20+
}
21+
}
22+
23+
override fun onFailure(evaluationResult: Any, parameter: Parameter, argumentName: String, environment: DataFetchingEnvironment): Nothing {
24+
val violations = evaluationResult as Set<ConstraintViolation<*>>
25+
throw ValidationException(violations.map { it.asConstraintError() })
26+
}
27+
28+
override fun test(evaluationResult: Any): Boolean = evaluationResult is Set<*> && evaluationResult.isEmpty()
29+
}

src/main/kotlin/com/expedia/graphql/schema/KotlinDataFetcher.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.expedia.graphql.schema
22

33
import com.expedia.graphql.annotations.GraphQLContext
4+
import com.expedia.graphql.schema.hooks.DataFetcherExecutionPredicate
45
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
56
import graphql.schema.DataFetcher
67
import graphql.schema.DataFetchingEnvironment
@@ -23,7 +24,8 @@ data class Parameter(val klazz: Class<*>, val annotations: List<Annotation>)
2324
class KotlinDataFetcher(
2425
private val target: Any?,
2526
private val fn: KFunction<*>,
26-
private val args: Map<String, Parameter>
27+
private val args: Map<String, Parameter>,
28+
private val executionPredicate: DataFetcherExecutionPredicate?
2729
) : DataFetcher<Any> {
2830

2931
override fun get(environment: DataFetchingEnvironment): Any? {
@@ -38,7 +40,9 @@ class KotlinDataFetcher(
3840
if (annotations.any { it.annotationClass == GraphQLContext::class }) {
3941
environment.getContext()
4042
} else {
41-
mapper.convertValue(environment.arguments[name], klazz)
43+
val value = mapper.convertValue(environment.arguments[name], klazz)
44+
45+
executionPredicate?.execute(value = value, parameter = it.value, argumentName = name, environment = environment) ?: value
4246
}
4347
}.toTypedArray())
4448
} else {

src/main/kotlin/com/expedia/graphql/schema/generator/SchemaGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ internal class SchemaGenerator(
143143
}
144144

145145
if (!abstract) {
146-
val dataFetcher: DataFetcher<*> = KotlinDataFetcher(target, fn, args)
146+
val dataFetcher: DataFetcher<*> = KotlinDataFetcher(target, fn, args, config.hooks.dataFetcherExecutionPredicate)
147147
val hookDataFetcher = config.hooks.didGenerateDataFetcher(fn, dataFetcher)
148148
builder.dataFetcher(hookDataFetcher)
149149
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.expedia.graphql.schema.hooks
2+
3+
import com.expedia.graphql.schema.Parameter
4+
import graphql.schema.DataFetchingEnvironment
5+
6+
/**
7+
* Perform runtime evaluations of each parameter passed to any KotlinDataFetcher.
8+
*
9+
* The DataFetcherExecutionPredicate is declared globally for all the datafetchers instances and all the parameters.
10+
* However a more precise logic (at the field level) is possible depending on the implement of `evaluate` and `test`
11+
*
12+
* The predicate logic is split into two parts (evaluate and test) so the result of the evaluation like a list of errors
13+
* can be passed to the onFailure method and added to an exception
14+
*
15+
* Because the DataFetcherExecutionPredicate is global, it's not possible to have methods where the type is inferred.
16+
*
17+
* It's recommended to check the type of the different arguments.
18+
*/
19+
abstract class DataFetcherExecutionPredicate {
20+
21+
/**
22+
* Perform the predicate logic by evaluating the argument and its value
23+
* Then depending on the result either returning the value itself to continue the datafetcher invocation
24+
* or break the data fetching execution.
25+
*
26+
* @param parameter the function argument reference containing the KClass and the argument annotations
27+
* @param argumentName the name of the argument as declared in the query / kotlin function
28+
* @param environment the DataFetchingEnvironment in which the data fetcher is executed (gives access to field info, execution context etc)
29+
* @param value the value to execute the predicate against.
30+
*/
31+
fun <T> execute(value: T, parameter: Parameter, argumentName: String, environment: DataFetchingEnvironment): T {
32+
val evaluationResult = evaluate(value, parameter, argumentName, environment)
33+
34+
return if (test(evaluationResult)) {
35+
value
36+
} else {
37+
onFailure(evaluationResult, parameter, argumentName, environment)
38+
}
39+
}
40+
41+
/**
42+
* Evaluate if the value passed respects some constraints.
43+
*
44+
* @param parameter the function argument reference containing the KClass and the argument annotations
45+
* @param argumentName the name of the argument as declared in the query / kotlin function
46+
* @param environment the DataFetchingEnvironment in which the data fetcher is executed (gives access to field info, execution context etc)
47+
* @param value the value to execute the predicate against.
48+
*
49+
* @return the result of the evaluation eg: List of errors
50+
*/
51+
abstract fun <T> evaluate(value: T, parameter: Parameter, argumentName: String, environment: DataFetchingEnvironment): Any
52+
53+
/**
54+
* Assert that the result of the {@link #evaluate(T, Parameter, String, DataFetchingEnvironment)} method is as expected eg: the list of errors is empty
55+
*
56+
* @param evaluationResult the result of the evaluation
57+
*
58+
* @return whether the parameter passes the predicate
59+
*/
60+
abstract fun test(evaluationResult: Any): Boolean
61+
62+
/**
63+
* If the test is unsuccessful, this function will be invoked.
64+
*
65+
* An exception can then be thrown to block the data fetcher execution
66+
*
67+
* @param evaluationResult the object return by the `evaluate` function
68+
* @param parameter the function argument reference containing the KClass and the argument annotations
69+
* @param argumentName the name of the argument as declared in the query / kotlin function
70+
* @param environment the DataFetchingEnvironment in which the data fetcher is executed (gives access to field info, execution context etc)
71+
*/
72+
abstract fun onFailure(evaluationResult: Any, parameter: Parameter, argumentName: String, environment: DataFetchingEnvironment): Nothing
73+
}

0 commit comments

Comments
 (0)