Skip to content

Commit fc97f67

Browse files
authored
[server] spring aware data fetcher (#813)
* [server] spring aware data fetcher Create new `SpringDataFetcher` that automatically resolves Spring `@Autowired` beans that are specified as function arguments. ```kotlin class SpringQuery : Query { fun getWidget(@GraphQLIgnore @Autowired repository: WidgetRepository, id: Int): Widget = repository.findWidget(id) } ``` NOTE: `@Autowired` arguments should be explicitly excluded from the GraphQL schema by also specifying `@GraphQLIgnore`. * examples * documentation * update spring bean docs * update junit * fix docs
1 parent cb44b69 commit fc97f67

File tree

18 files changed

+379
-109
lines changed

18 files changed

+379
-109
lines changed

docs/schema-generator/writing-schemas/arguments.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Query {
2121

2222
This behavior is true for all arguments except for the special classes for the [GraphQLContext](../execution/contextual-data) and the [DataFetchingEnvironment](../execution/data-fetching-environment)
2323

24-
### Input Types
24+
## Input Types
2525

2626
Query and mutation function arguments are automatically converted to corresponding GraphQL input fields. GraphQL makes a
2727
distinction between input and output types and requires unique names for all the types. Since we can use the same
@@ -69,7 +69,7 @@ If you know a type will only be used for input types you can call your class som
6969
append `Input` if the class name already ends with `Input` but that means you can not use this type as output because
7070
the schema would have two types with the same name and that would be invalid.
7171

72-
### Optional input fields
72+
## Optional input fields
7373

7474
Kotlin requires variables/values to be initialized upon their declaration either from the user input OR by providing
7575
defaults (even if they are marked as nullable). Therefore in order for a GraphQL input field to be optional it needs to be
@@ -82,12 +82,16 @@ fun doSomethingWithOptionalInput(requiredValue: Int, optionalValue: Int?) = "req
8282
NOTE: Non nullable input fields will always require users to specify the value regardless of whether a default Kotlin value
8383
is provided or not.
8484

85-
NOTE: Even though you could specify a default value in Kotlin `optionalValue: Int? = null`, this will not be used. This is because
86-
if no value is provided to the schema, `graphql-java` passes null as the value. The Kotlin default value will never be
87-
used. For example, with argument `optionalList: List<Int>? = emptyList()`, the value will be null if not passed a value by
88-
the client.
85+
NOTE: Even though you could specify a default values for arguments in Kotlin `optionalValue: Int? = null`, this will not
86+
be used. If query does not explicitly specify root argument values, our function data fetcher will default to use null as
87+
the value. This is because Kotlin properties always have to be initialized, and we cannot determine whether underlying
88+
argument has default value or not. As a result, Kotlin default value will never be used. For example, with argument
89+
`optionalList: List<Int>? = emptyList()`, the value will be null if not passed a value by the client.
8990

90-
### Default values
91+
See [optional undefined arguments](../execution/optional-undefined-arguments) for details how to determine whether argument
92+
was specified or not.
9193

92-
Default argument values are currently not supported. See issue
93-
[#53](https://github.com/ExpediaGroup/graphql-kotlin/issues/53) for more details.
94+
## Default values
95+
96+
Default argument values are currently not supported. See issue [#53](https://github.com/ExpediaGroup/graphql-kotlin/issues/53)
97+
for more details.
Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
---
22
id: nested-arguments
3-
title: Nested Arguments
3+
title: Nested Resolvers and Shared Arguments
44
---
55

6-
There are a few ways in which you can access data in a query from different levels of arguments. Say we have the following schema:
6+
There are a few ways in which shared arguments can be accessed from different resolver levels. Say we have the following schema:
77

88
```graphql
99
type Query {
@@ -19,13 +19,14 @@ type Photo {
1919
}
2020
```
2121

22-
In Kotlin code, when we are in the `photos` function, if we want access to the parent field `findUser` and its
23-
arguments there are a couple ways we can access it:
22+
In Kotlin code, when we are resolving `photos`, if we want access to the parent field `findUser` and its arguments there
23+
are a couple ways we can access it:
2424

2525

2626
## DataFetchingEnvironment
27-
You can add the `DataFetchingEnvironment` as an argument. This class will be ignored by the schema generator and will allow you to view the entire query sent to the
28-
server. See more in the [DataFetchingEnvironment documentation](../execution/data-fetching-environment)
27+
28+
You can add the `DataFetchingEnvironment` as an argument. This class will be ignored by the schema generator and will allow
29+
you to view the entire query sent to the server. See more in the [DataFetchingEnvironment documentation](../execution/data-fetching-environment)
2930

3031
```kotlin
3132
class User {
@@ -37,8 +38,9 @@ class User {
3738
```
3839

3940
## GraphQL Context
40-
You can add the `GraphQLContext` as an argument. This class will be ignored by the schema generator and will allow you to view the context object you set up in the
41-
data fetchers. See more in the [GraphQLContext documentation](../execution/contextual-data)
41+
42+
You can add the `GraphQLContext` as an argument. This class will be ignored by the schema generator and will allow you to
43+
view the context object you set up in the data fetchers. See more in the [GraphQLContext documentation](../execution/contextual-data)
4244

4345
```kotlin
4446
class User {
@@ -49,7 +51,7 @@ class User {
4951
}
5052
```
5153

52-
## Excluding from the Schema
54+
## Excluding Arguments from the Schema
5355
You can construct the child objects by passing down arguments as non-public fields or annotate the argument with [@GraphQLIgnore](../customizing-schemas/excluding-fields)
5456

5557
```kotlin
@@ -65,35 +67,6 @@ class User(private val userId: String) {
6567
}
6668
```
6769

68-
## Spring BeanFactoryAware
69-
You can use Spring beans to wire different objects together at runtime.
70-
There is an example of how to set this up in the example app in [TopLevelBeanFactoryQuery.kt](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/TopLevelBeanFactoryQuery.kt)
71-
72-
```kotlin
73-
@Component
74-
class UsersQuery : Query, BeanFactoryAware {
75-
private lateinit var beanFactory: BeanFactory
76-
77-
@GraphQLIgnore
78-
override fun setBeanFactory(beanFactory: BeanFactory) {
79-
this.beanFactory = beanFactory
80-
}
81-
82-
fun findUser(id: String): SubQuery = beanFactory.getBean(User::class.java, id)
83-
}
84-
85-
@Component
86-
@Scope("prototype")
87-
class User @Autowired(required = false) constructor(private val userId: String) {
88-
89-
@Autowired
90-
private lateinit var service: PhotoService
91-
92-
fun photos(numberOfPhotos: Int): List<Photo> = service.findPhotos(userId, numberOfPhotos)
93-
}
94-
```
95-
96-
------
70+
## Spring Integration
9771

98-
We have examples of these techniques implemented in Spring boot in the [example
99-
app](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/NestedQueries.kt).
72+
See [Writing Schemas with Spring](../../spring-server/spring-schema.md) for integration details.

docs/spring-server/spring-beans.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ can be customized by providing custom beans in your application context. See sec
1414
| FederatedSchemaGeneratorConfig | Federated schema generator configuration information. You can customize the configuration by providing `TopLevelNames`, `FederatedSchemaGeneratorHooks` and `KotlinDataFetcherFactoryProvider` beans.<br><br>_Created instead of default `SchemaGeneratorConfig` if federation is enabled_. |
1515
| FederatedTypeRegistry | Default type registry without any resolvers. See [Federated Type Resolution](../federated/type-resolution.md) for more details.<br><br>_Created only if federation is enabled. You should register your custom type registry bean whenever implementing federated GraphQL schema with extended types_. |
1616
| GraphQLSchema | GraphQL schema generated based on the schema generator configuration and `Query`, `Mutation` and `Subscription` objects available in the application context. |
17-
| KotlinDataFetcherFactoryProvider | Factory used during schema construction to obtain `DataFetcherFactory` that should be used for target function and property resolution.|
17+
| KotlinDataFetcherFactoryProvider | Factory used during schema construction to obtain `DataFetcherFactory` that should be used for target function (using Spring aware `SpringDataFetcher`) and property resolution. |
1818
| SchemaGeneratorConfig | Schema generator configuration information, see [Schema Generator Configuration](../schema-generator/customizing-schemas/generator-config.md) for details. Can be customized by providing `TopLevelNames`, [SchemaGeneratorHooks](../schema-generator/customizing-schemas/generator-config.md) and `KotlinDataFetcherFactoryProvider` beans. |
1919

2020
## Execution

docs/spring-server/spring-overview.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ type Query {
8888
}
8989
9090
type Mutation {
91-
myAwesomeMutation(widget: Widget!): Widget!
91+
myAwesomeMutation(widget: WidgetInput!): Widget!
9292
}
9393
9494
type Subscription {
@@ -99,6 +99,11 @@ type Widget {
9999
id: Int!
100100
value: String!
101101
}
102+
103+
input WidgetInput {
104+
id: Int!
105+
value: String!
106+
}
102107
```
103108

104109
## Default Routes

docs/spring-server/spring-schema.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
id: spring-schema
3+
title: Writing Schemas with Spring
4+
---
5+
6+
In order to expose your queries, mutations and/or subscriptions in the GraphQL schema you simply need to create beans that
7+
implement corresponding marker interface and they will be automatically picked up by `graphql-kotlin-spring-server`
8+
auto-configuration library.
9+
10+
```kotlin
11+
@Component
12+
class MyAwesomeQuery : Query {
13+
fun myAwesomeQuery(): Widget { ... }
14+
}
15+
16+
@Component
17+
class MyAwesomeMutation : Mutation {
18+
fun myAwesomeMutation(widget: Widget): Widget { ... }
19+
}
20+
21+
@Component
22+
class MyAwesomeSubscription : Subscription {
23+
fun myAwesomeSubscription(): Publisher<Widget> { ... }
24+
}
25+
26+
data class Widget(val id: Int, val value: String)
27+
```
28+
29+
will result in a Spring Boot reactive GraphQL web application with following schema.
30+
31+
```graphql
32+
schema {
33+
query: Query
34+
mutation: Mutation
35+
subscription: Subscription
36+
}
37+
38+
type Query {
39+
myAwesomeQuery: Widget!
40+
}
41+
42+
type Mutation {
43+
myAwesomeMutation(widget: WidgetInput!): Widget!
44+
}
45+
46+
type Subscription {
47+
myAwesomeSubscription: Widget!
48+
}
49+
50+
type Widget {
51+
id: Int!
52+
value: String!
53+
}
54+
55+
input WidgetInput {
56+
id: Int!
57+
value: String!
58+
}
59+
```
60+
61+
## Spring Query Beans
62+
63+
Spring will automatically autowire dependent beans to our Spring query beans. Refer to [Spring Documentation](https://docs.spring.io/spring/docs/current/spring-framework-reference/) for details.
64+
65+
```kotlin
66+
@Component
67+
class WidgetQuery(private val repository: WidgetRepository) : Query {
68+
fun getWidget(id: Int): Widget = repository.findWidget(id)
69+
}
70+
```
71+
72+
## Spring Data Fetcher
73+
74+
`graphql-kotlin-spring-server` provides Spring aware data fetcher that automatically autowires Spring beans when they are
75+
specified as function arguments. `@Autowired` arguments should be explicitly excluded from the GraphQL schema by also
76+
specifying `@GraphQLIgnore`.
77+
78+
```kotlin
79+
@Component
80+
class SpringQuery : Query {
81+
fun getWidget(@GraphQLIgnore @Autowired repository: WidgetRepository, id: Int): Widget = repository.findWidget(id)
82+
}
83+
```
84+
85+
> NOTE: if you are using custom data fetcher make sure that you extend `SpringDataFetcher` instead of a base `FunctionDataFetcher`.
86+
87+
## Spring BeanFactoryAware
88+
89+
You can use Spring beans to wire different objects together at runtime. Instead of autowiring specific beans as properties,
90+
you can also dynamically resolve beans by using bean factories. There is an example of how to set this up in the example
91+
app in the [TopLevelBeanFactoryQuery.kt](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/TopLevelBeanFactoryQuery.kt).
92+
93+
```kotlin
94+
@Component
95+
class UsersQuery : Query, BeanFactoryAware {
96+
private lateinit var beanFactory: BeanFactory
97+
98+
@GraphQLIgnore
99+
override fun setBeanFactory(beanFactory: BeanFactory) {
100+
this.beanFactory = beanFactory
101+
}
102+
103+
fun findUser(id: String): SubQuery = beanFactory.getBean(User::class.java, id)
104+
}
105+
106+
@Component
107+
@Scope("prototype")
108+
class User @Autowired(required = false) constructor(private val userId: String) {
109+
110+
@Autowired
111+
private lateinit var service: PhotoService
112+
113+
fun photos(numberOfPhotos: Int): List<Photo> = service.findPhotos(userId, numberOfPhotos)
114+
}
115+
```
116+
117+
------
118+
119+
We have examples of these techniques implemented in Spring boot in the [example
120+
app](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/NestedQueries.kt).

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/Application.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
2828
import graphql.execution.DataFetcherExceptionHandler
2929
import org.springframework.boot.autoconfigure.SpringBootApplication
3030
import org.springframework.boot.runApplication
31+
import org.springframework.context.ApplicationContext
3132
import org.springframework.context.annotation.Bean
3233

3334
@SpringBootApplication
@@ -40,8 +41,11 @@ class Application {
4041
fun hooks(wiringFactory: KotlinDirectiveWiringFactory) = CustomSchemaGeneratorHooks(wiringFactory)
4142

4243
@Bean
43-
fun dataFetcherFactoryProvider(springDataFetcherFactory: SpringDataFetcherFactory, objectMapper: ObjectMapper) =
44-
CustomDataFetcherFactoryProvider(springDataFetcherFactory, objectMapper)
44+
fun dataFetcherFactoryProvider(
45+
springDataFetcherFactory: SpringDataFetcherFactory,
46+
objectMapper: ObjectMapper,
47+
applicationContext: ApplicationContext
48+
) = CustomDataFetcherFactoryProvider(springDataFetcherFactory, objectMapper, applicationContext)
4549

4650
@Bean
4751
fun dataFetcherExceptionHandler(): DataFetcherExceptionHandler = CustomDataFetcherExceptionHandler()

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/dataloaders/DataLoaderConfiguration.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package com.expediagroup.graphql.examples.dataloaders
22

3-
import com.expediagroup.graphql.examples.model.Company
4-
import com.expediagroup.graphql.examples.query.CompanyService
3+
import com.expediagroup.graphql.examples.query.Company
54
import com.expediagroup.graphql.spring.execution.DataLoaderRegistryFactory
65
import org.dataloader.DataLoader
76
import org.dataloader.DataLoaderRegistry
87
import org.springframework.context.annotation.Bean
98
import org.springframework.context.annotation.Configuration
9+
import org.springframework.stereotype.Component
1010
import java.util.concurrent.CompletableFuture
1111

1212
@Configuration
1313
class DataLoaderConfiguration(private val companyService: CompanyService) {
14+
1415
@Bean
1516
fun dataLoaderRegistryFactory(): DataLoaderRegistryFactory {
1617
return object : DataLoaderRegistryFactory {
@@ -25,3 +26,13 @@ class DataLoaderConfiguration(private val companyService: CompanyService) {
2526
}
2627
}
2728
}
29+
30+
@Component
31+
class CompanyService {
32+
private val companies = listOf(
33+
Company(id = 1, name = "FirstCompany"),
34+
Company(id = 2, name = "SecondCompany")
35+
)
36+
37+
fun getCompanies(ids: List<Int>): List<Company> = companies
38+
}

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/execution/CustomDataFetcherFactoryProvider.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.expediagroup.graphql.examples.execution
1919
import com.expediagroup.graphql.execution.SimpleKotlinDataFetcherFactoryProvider
2020
import com.fasterxml.jackson.databind.ObjectMapper
2121
import graphql.schema.DataFetcherFactory
22+
import org.springframework.context.ApplicationContext
2223
import kotlin.reflect.KClass
2324
import kotlin.reflect.KFunction
2425
import kotlin.reflect.KProperty
@@ -28,14 +29,16 @@ import kotlin.reflect.KProperty
2829
*/
2930
class CustomDataFetcherFactoryProvider(
3031
private val springDataFetcherFactory: SpringDataFetcherFactory,
31-
private val objectMapper: ObjectMapper
32+
private val objectMapper: ObjectMapper,
33+
private val applicationContext: ApplicationContext
3234
) : SimpleKotlinDataFetcherFactoryProvider(objectMapper) {
3335

3436
override fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>) = DataFetcherFactory {
3537
CustomFunctionDataFetcher(
3638
target = target,
3739
fn = kFunction,
38-
objectMapper = objectMapper
40+
objectMapper = objectMapper,
41+
appContext = applicationContext
3942
)
4043
}
4144

examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/execution/CustomFunctionDataFetcher.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@
1616

1717
package com.expediagroup.graphql.examples.execution
1818

19-
import com.expediagroup.graphql.execution.FunctionDataFetcher
19+
import com.expediagroup.graphql.spring.execution.SpringDataFetcher
2020
import com.fasterxml.jackson.databind.ObjectMapper
2121
import graphql.schema.DataFetchingEnvironment
22+
import org.springframework.context.ApplicationContext
2223
import reactor.core.publisher.Mono
2324
import kotlin.reflect.KFunction
2425

2526
/**
2627
* Custom function data fetcher that adds support for Reactor Mono.
2728
*/
28-
class CustomFunctionDataFetcher(target: Any?, fn: KFunction<*>, objectMapper: ObjectMapper) : FunctionDataFetcher(target, fn, objectMapper) {
29+
class CustomFunctionDataFetcher(
30+
target: Any?,
31+
fn: KFunction<*>,
32+
objectMapper: ObjectMapper,
33+
appContext: ApplicationContext
34+
) : SpringDataFetcher(target, fn, objectMapper, appContext) {
2935

3036
override fun get(environment: DataFetchingEnvironment): Any? = when (val result = super.get(environment)) {
3137
is Mono<*> -> result.toFuture()

0 commit comments

Comments
 (0)