Skip to content

Commit 202e19e

Browse files
author
smyrick
committed
feat: support data and errors with DataFetcherResult
Fixes ExpediaGroup#244 Kotlin functions can now return a instead of just their return type which allows you to modify the errors field with any extra data you need
1 parent 1d00b99 commit 202e19e

File tree

6 files changed

+75
-3
lines changed

6 files changed

+75
-3
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,4 @@ If you have a question about something you can not find in our wiki or javadocs,
100100

101101
One way to run a GraphQL server is with Spring Boot. A sample Spring Boot app that uses [Spring Webflux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html) together with `graphql-kotlin` and [graphql-playground](https://github.com/prisma/graphql-playground) is provided in the [example folder](https://github.com/ExpediaDotCom/graphql-kotlin/tree/master/example). All the examples used in this documentation should be available in the sample app.
102102

103-
In order to run it you can run [Application.kt](https://github.com/ExpediaDotCom/graphql-kotlin/blob/master/example/src/main/kotlin/com.expedia.graphql.sample/Application.kt) directly from your IDE. Alternatively you can also use the Spring Boot maven plugin by running `mvn spring-boot:run` from the command line. Once the app has started you can explore the example schema by opening GraphiQL endpoint at [http://localhost:8080/playground](http://localhost:8080/playground).
103+
In order to run it you can run [Application.kt](https://github.com/ExpediaDotCom/graphql-kotlin/blob/master/example/src/main/kotlin/com.expedia.graphql.sample/Application.kt) directly from your IDE. Alternatively you can also use the Spring Boot maven plugin by running `mvn spring-boot:run` from the command line. Once the app has started you can explore the example schema by opening the Playground endpoint at [http://localhost:8080/playground](http://localhost:8080/playground).

example/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ Start the server:
1616
* Alternatively you can also use the spring boot maven plugin by running `mvn spring-boot:run` from the command line.
1717

1818

19-
Once the app has started you can explore the example schema by opening Playground endpoint at http://localhost:8080/playground.
19+
Once the app has started you can explore the example schema by opening the Playground endpoint at http://localhost:8080/playground.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package com.expedia.graphql.sample.query
2+
3+
import graphql.ExceptionWhileDataFetching
4+
import graphql.execution.DataFetcherResult
5+
import graphql.execution.ExecutionPath
6+
import graphql.language.SourceLocation
7+
import org.springframework.stereotype.Component
8+
9+
@Component
10+
class DataAndErrors : Query {
11+
12+
fun returnDataAndErrors(): DataFetcherResult<String> {
13+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
14+
return DataFetcherResult("Hello from data fetcher", listOf(error))
15+
}
16+
}

src/main/kotlin/com/expedia/graphql/generator/types/FunctionBuilder.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.expedia.graphql.generator.extensions.isGraphQLContext
1313
import com.expedia.graphql.generator.extensions.isGraphQLIgnored
1414
import com.expedia.graphql.generator.extensions.isInterface
1515
import com.expedia.graphql.generator.extensions.safeCast
16+
import graphql.execution.DataFetcherResult
1617
import graphql.schema.FieldCoordinates
1718
import graphql.schema.GraphQLArgument
1819
import graphql.schema.GraphQLFieldDefinition
@@ -51,7 +52,8 @@ internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generat
5152

5253
val typeFromHooks = config.hooks.willResolveMonad(fn.returnType)
5354
val returnType = getWrappedReturnType(typeFromHooks)
54-
builder.type(graphQLTypeOf(returnType).safeCast<GraphQLOutputType>())
55+
val graphQLOutputType = graphQLTypeOf(returnType).safeCast<GraphQLOutputType>()
56+
builder.type(graphQLOutputType)
5557
val graphQLType = builder.build()
5658

5759
val coordinates = FieldCoordinates.coordinates(parentName, fn.name)
@@ -63,10 +65,19 @@ internal class FunctionBuilder(generator: SchemaGenerator) : TypeBuilder(generat
6365
return config.hooks.onRewireGraphQLType(graphQLType, coordinates, codeRegistry).safeCast()
6466
}
6567

68+
/**
69+
* These are the classes that can be returned from data fetchers (ie functions)
70+
* but we only want to expose the wrapped type in the schema.
71+
*
72+
* [Publisher] is used for subscriptions
73+
* [CompletableFuture] is used for asynchronous results
74+
* [DataFetcherResult] is used for returning data and errors in the same response
75+
*/
6676
private fun getWrappedReturnType(returnType: KType): KType =
6777
when {
6878
returnType.getKClass().isSubclassOf(Publisher::class) -> returnType.getTypeOfFirstArgument()
6979
returnType.classifier == CompletableFuture::class -> returnType.getTypeOfFirstArgument()
80+
returnType.classifier == DataFetcherResult::class -> returnType.getTypeOfFirstArgument()
7081
else -> returnType
7182
}
7283

src/test/kotlin/com/expedia/graphql/generator/SchemaGeneratorTest.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ import com.expedia.graphql.exceptions.InvalidIdTypeException
1111
import com.expedia.graphql.extensions.deepName
1212
import com.expedia.graphql.testSchemaConfig
1313
import com.expedia.graphql.toSchema
14+
import graphql.ExceptionWhileDataFetching
1415
import graphql.GraphQL
1516
import graphql.Scalars
17+
import graphql.execution.DataFetcherResult
18+
import graphql.execution.ExecutionPath
19+
import graphql.language.SourceLocation
1620
import graphql.schema.GraphQLNonNull
1721
import graphql.schema.GraphQLObjectType
1822
import org.junit.jupiter.api.Test
@@ -288,6 +292,23 @@ class SchemaGeneratorTest {
288292
assertEquals(Scalars.GraphQLID, serialField?.wrappedType)
289293
}
290294

295+
@Test
296+
fun `SchemaGenerator supports DataFetcherResult as a return type`() {
297+
val schema = toSchema(queries = listOf(TopLevelObject(QueryWithDataFetcherResult())), config = testSchemaConfig)
298+
299+
val graphQL = GraphQL.newGraphQL(schema).build()
300+
val result = graphQL.execute("{ dataAndErrors }")
301+
val data = result.getData<Map<String, String>>()
302+
val errors = result.errors
303+
304+
assertNotNull(data)
305+
val res: String? = data["dataAndErrors"]
306+
assertEquals(actual = res, expected = "Hello")
307+
308+
assertNotNull(errors)
309+
assertEquals(expected = 1, actual = errors.size)
310+
}
311+
291312
class QueryObject {
292313
@GraphQLDescription("A GraphQL query method")
293314
fun query(@GraphQLDescription("A GraphQL value") value: Int): Geography = Geography(value, GeoType.CITY, listOf())
@@ -485,4 +506,11 @@ class SchemaGeneratorTest {
485506
@GraphQLID val serial: UUID,
486507
val type: String
487508
)
509+
510+
class QueryWithDataFetcherResult {
511+
fun dataAndErrors(): DataFetcherResult<String> {
512+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
513+
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
514+
}
515+
}
488516
}

src/test/kotlin/com/expedia/graphql/generator/types/FunctionBuilderTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import com.expedia.graphql.annotations.GraphQLDescription
55
import com.expedia.graphql.annotations.GraphQLDirective
66
import com.expedia.graphql.annotations.GraphQLIgnore
77
import com.expedia.graphql.execution.FunctionDataFetcher
8+
import graphql.ExceptionWhileDataFetching
89
import graphql.Scalars
10+
import graphql.execution.DataFetcherResult
11+
import graphql.execution.ExecutionPath
912
import graphql.introspection.Introspection
13+
import graphql.language.SourceLocation
1014
import graphql.schema.DataFetchingEnvironment
1115
import graphql.schema.FieldCoordinates
1216
import graphql.schema.GraphQLNonNull
@@ -64,6 +68,11 @@ internal class FunctionBuilderTest : TypeTestHelper() {
6468
fun completableFuture(num: Int): CompletableFuture<Int> = CompletableFuture.completedFuture(num)
6569

6670
fun dataFetchingEnvironment(environment: DataFetchingEnvironment): String = environment.field.name
71+
72+
fun dataFetcherResult(): DataFetcherResult<String> {
73+
val error = ExceptionWhileDataFetching(ExecutionPath.rootPath(), RuntimeException(), SourceLocation(1, 1))
74+
return DataFetcherResult.newResult<String>().data("Hello").error(error).build()
75+
}
6776
}
6877

6978
@Test
@@ -208,4 +217,12 @@ internal class FunctionBuilderTest : TypeTestHelper() {
208217
assertEquals(expected = 0, actual = result.arguments.size)
209218
assertEquals("String", (result.type as? GraphQLNonNull)?.wrappedType?.name)
210219
}
220+
221+
@Test
222+
fun `DataFetcherResult return type is valid and unwrapped in the schema`() {
223+
val kFunction = Happy::dataFetcherResult
224+
val result = builder.function(fn = kFunction, parentName = "Query")
225+
226+
assertEquals("String", (result.type as? GraphQLNonNull)?.wrappedType?.name)
227+
}
211228
}

0 commit comments

Comments
 (0)