Skip to content

Commit 627054e

Browse files
smyrickShane MyrickRobert Del Favero
authored
[generator] Replace GraphQLContext annotation with interface (ExpediaGroup#610)
* BREAKING CHANGE: Change GraphQLContext from annotation to interface Instead of using an annotation and having to remember to include it in function arguments, we can create a marker interface for the GraphQLContext which library users can implement on a class to indicate that this class is the GraphQLContext. This moves the declaration from the arguments to the class itself which I think makes it more clear. This is obviously a large breaking change to fundamental part of our library. It requires heavy documentation changes and review to take place before the 2.0 release. Please review and provided feedback on the interface package path as well * Change GraphQLContextFactory to return the interface * Combine interface and default implementation into single file * Revert deletion of annotation class Undo the deletion of the old annotation and instead make these changes a minor feature addition * Update context docs * Update docs wording * typos * typo * typos, minor wording changes * typos, minor wording changes * Remove context annotation * Update the docs * GraphQLContextFactory must accept a GraphQLContext type * Change generic type of ContextWebFilter Instead of having a generic of a GraphQLContextFactory, we can make the generic for the type required of the context factory * Simplify kFunction unit tests Co-authored-by: Shane Myrick <[email protected]> Co-authored-by: Robert Del Favero <[email protected]>
1 parent 5988890 commit 627054e

File tree

23 files changed

+278
-282
lines changed

23 files changed

+278
-282
lines changed

docs/execution/contextual-data.md

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ title: Contextual Data
44
---
55

66
All GraphQL servers have a concept of a "context". A GraphQL context contains metadata that is useful to the GraphQL
7-
server, but shouldn't necessarily be part of the GraphQL query's API. A prime example of something that is appropriate
7+
server, but shouldn't necessarily be part of the GraphQL schema. A prime example of something that is appropriate
88
for the GraphQL context would be trace headers for an OpenTracing system such as
9-
[Haystack](https://expediadotcom.github.io/haystack). The GraphQL query itself does not need the information to perform
10-
its function, but the server itself needs the information to ensure observability.
9+
[Haystack](https://expediadotcom.github.io/haystack). The GraphQL query does not need the information to perform
10+
its function, but the server needs the information to ensure observability.
1111

1212
The contents of the GraphQL context vary across applications and it is up to the GraphQL server developers to decide
1313
what it should contain. For Spring based applications, `graphql-kotlin-spring-server` provides a simple mechanism to
@@ -18,17 +18,23 @@ Once context factory bean is available in the Spring application context it will
1818
to populate GraphQL context based on the incoming request and make it available during query execution. See [graphql-kotlin-spring-server documentation](../spring-server/spring-graphql-context)
1919
for additional details
2020

21-
Once your application is configured to build your custom `MyGraphQLContext`, simply add `@GraphQLContext` annotation to
22-
any function argument and the corresponding GraphQL context from the environment will be automatically injected during
23-
execution.
21+
## GraphQLContext Interface
22+
23+
The easiest way to specify a context class is to use the `GraphQLContext` marker interface. This interface does not require any implementations,
24+
it is just used to inform the schema generator that this is the class that should be used as the context for every request.
2425

2526
```kotlin
26-
class ContextualQuery {
27+
class MyGraphQLContext(val customValue: String) : GraphQLContext
28+
```
29+
30+
Then you can just use the class as an argument and it will be automatically injected during execution time.
2731

32+
```kotlin
33+
class ContextualQuery {
2834
fun contextualQuery(
29-
value: Int,
30-
@GraphQLContext context: MyGraphQLContext
31-
): ContextualResponse = ContextualResponse(value, context.myCustomValue)
35+
context: MyGraphQLContext,
36+
value: Int
37+
): String = "The custom value was ${context.customValue} and the value was $value"
3238
}
3339
```
3440

@@ -40,10 +46,11 @@ schema {
4046
}
4147

4248
type Query {
43-
contextualQuery(
44-
value: Int!
45-
): ContextualResponse!
49+
contextualQuery(value: Int!): String!
4650
}
4751
```
4852

49-
Note that the `@GraphQLContext` annotated argument is not reflected in the GraphQL schema.
53+
Note that the argument that implements `GraphQLContext` is not reflected in the GraphQL schema.
54+
55+
### Customization
56+
The context is injected into the execution through the `FunctionDataFetcher` class. If you want to customize the logic on how the context is determined, that is possible to override. See more details on the [Fetching Data documentation](./fetching-data)

docs/spring-server/spring-graphql-context.md

Lines changed: 9 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,57 +3,33 @@ id: spring-graphql-context
33
title: Generating GraphQL Context
44
---
55

6-
`graphql-kotlin-spring-server` provides a simple mechanism to build [GraphQL context](../execution/contextual-data) per query execution through
6+
`graphql-kotlin-spring-server` provides a simple mechanism to build a [GraphQL context](../execution/contextual-data) per query execution through
77
[GraphQLContextFactory](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/GraphQLContextFactory.kt).
8-
Once context factory bean is available in the Spring application context it will then be used in a corresponding
8+
Once a context factory bean is available, it will then be used in
99
[ContextWebFilter](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt)
10-
to populate GraphQL context based on the incoming request and make it available during query execution.
10+
to populate the GraphQL context based on the incoming request and make it available during query execution.
1111

1212
For example if we define our custom context as follows:
1313

1414
```kotlin
15-
data class MyGraphQLContext(val myCustomValue: String)
15+
class MyGraphQLContext(val myCustomValue: String) : GraphQLContext
1616
```
1717

18-
We can generate corresponding `GraphQLContextFactory` bean:
18+
We can generate the corresponding `GraphQLContextFactory` bean:
1919

2020
```kotlin
2121
@Component
2222
class MyGraphQLContextFactory: GraphQLContextFactory<MyGraphQLContext> {
2323
override suspend fun generateContext(
24-
request: ServerHttpRequest,
24+
request: ServerHttpRequest,
2525
response: ServerHttpResponse
2626
): MyGraphQLContext = MyGraphQLContext(
27-
myCustomValue = request.headers.getFirst("MyHeader") ?: "defaultContext"
27+
myCustomValue = request.headers.getFirst("MyHeader") ?: "defaultValue"
2828
)
2929
}
3030
```
3131

32-
Once your application is configured to build your custom `MyGraphQLContext`, we can then specify it as function argument by annotating it with `@GraphQLContext`.
32+
Once your application is configured to build your custom `MyGraphQLContext`, we can then specify it as function argument but it will not be included in the schema.
3333
While executing the query, the corresponding GraphQL context will be read from the environment and automatically injected to the function input arguments.
3434

35-
```kotlin
36-
@Component
37-
class ContextualQuery: Query {
38-
fun contextualQuery(
39-
value: Int,
40-
@GraphQLContext context: MyGraphQLContext
41-
): ContextualResponse = ContextualResponse(value, context.myCustomValue)
42-
}
43-
```
44-
45-
The above query would produce the following GraphQL schema:
46-
47-
```graphql
48-
schema {
49-
query: Query
50-
}
51-
52-
type Query {
53-
contextualQuery(
54-
value: Int!
55-
): ContextualResponse!
56-
}
57-
```
58-
59-
Notice that the `@GraphQLContext` annotated argument is not reflected in the generated GraphQL schema.
35+
For more details see the [Contextual Data documentation](../execution/contextual-data).

docs/writing-schemas/arguments.md

Lines changed: 125 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -1,124 +1,125 @@
1-
---
2-
id: arguments
3-
title: Arguments
4-
---
5-
6-
Method arguments are automatically exposed as part of the arguments to the corresponding GraphQL fields.
7-
8-
```kotlin
9-
class SimpleQuery{
10-
11-
@GraphQLDescription("performs some operation")
12-
fun doSomething(@GraphQLDescription("super important value") value: Int): Boolean = true
13-
}
14-
```
15-
16-
The above Kotlin code will generate following GraphQL schema:
17-
18-
```graphql
19-
type Query {
20-
"""performs some operation"""
21-
doSomething(
22-
"""super important value"""
23-
value: Int!
24-
): Boolean!
25-
}
26-
```
27-
28-
This behavior is true for all arguments except for the GraphQL context objects. See section below for detailed
29-
information about `@GraphQLContext`.
30-
31-
### Input Types
32-
33-
Query and mutation function arguments are automatically converted to corresponding GraphQL input fields. GraphQL makes a
34-
distinction between input and output types and requires unique names for all the types. Since we can use the same
35-
objects for input and output in our Kotlin functions, `graphql-kotlin-schema-generator` will automatically append
36-
`Input` suffix to the query input objects.
37-
38-
```kotlin
39-
class WidgetMutation {
40-
41-
@GraphQLDescription("modifies passed in widget so it doesn't have null value")
42-
fun processWidget(@GraphQLDescription("widget to be modified") widget: Widget): Widget {
43-
if (null == widget.value) {
44-
widget.value = 42
45-
}
46-
return widget
47-
}
48-
}
49-
50-
@GraphQLDescription("A useful widget")
51-
data class Widget(
52-
@GraphQLDescription("The widget's value that can be null")
53-
var value: Int? = nul
54-
) {
55-
@GraphQLDescription("returns original value multiplied by target OR null if original value was null")
56-
fun multiplyValueBy(multiplier: Int) = value?.times(multiplier)
57-
}
58-
```
59-
60-
Will generate
61-
62-
```graphql
63-
type Mutation {
64-
"""modifies passed in widget so it doesn't have null value"""
65-
processWidget(
66-
"""widget to be modified"""
67-
widget: WidgetInput!
68-
): Widget!
69-
}
70-
71-
"""A useful widget"""
72-
type Widget {
73-
74-
"""The widget's value that can be null"""
75-
value: Int
76-
77-
"""
78-
returns original value multiplied by target OR null if original value was null
79-
"""
80-
multiplyValueBy(multiplier: Int!): Int
81-
}
82-
83-
"""A useful widget"""
84-
input WidgetInput {
85-
86-
"""The widget's value that can be null"""
87-
value: Int
88-
}
89-
90-
```
91-
92-
Please note that only fields are exposed in the input objects. Functions will only be available on the GraphQL output
93-
types.
94-
95-
If you know a type will only be used for input types you can call your class `CustomTypeInput`. The library will not
96-
append `Input` if the class name already ends with `Input` but that means you can not use this type as output because
97-
the schema would have two types with the same name and will be invalid.
98-
99-
### Optional input fields
100-
101-
Kotlin requires variables/values to be initialized upon their declaration either from the user input OR by providing
102-
defaults (even if they are marked as nullable). Therefore in order for GraphQL input field to be optional it needs to be
103-
nullable and also specify default Kotlin value.
104-
105-
```kotlin
106-
@GraphQLDescription("query with optional input")
107-
fun doSomethingWithOptionalInput(
108-
@GraphQLDescription("this field is required") requiredValue: Int,
109-
@GraphQLDescription("this field is optional") optionalValue: Int?)
110-
= "required value=$requiredValue, optional value=$optionalValue"
111-
```
112-
113-
NOTE: Non nullable input fields will always require users to specify the value regardless whether default Kotlin value
114-
is provided or not.
115-
116-
NOTE: Even though you could specify a default value in Kotlin `optionalValue: Int? = null`, this will not be used since
117-
if no value is provided to the schema `graphql-java` passes null as the value so the Kotlin default value will never be
118-
used, like in this argument `optionalList: List<Int>? = emptyList()`, the value will be null if not passed a value by
119-
the client.
120-
121-
### Default values
122-
123-
Default argument values are currently not supported. See issue
124-
[#53](https://github.com/ExpediaGroup/graphql-kotlin/issues/53) for more details.
1+
---
2+
id: arguments
3+
title: Arguments
4+
---
5+
6+
Method arguments are automatically exposed as part of the arguments to the corresponding GraphQL fields.
7+
8+
```kotlin
9+
class SimpleQuery{
10+
11+
@GraphQLDescription("performs some operation")
12+
fun doSomething(@GraphQLDescription("super important value") value: Int): Boolean = true
13+
}
14+
```
15+
16+
The above Kotlin code will generate following GraphQL schema:
17+
18+
```graphql
19+
type Query {
20+
"""performs some operation"""
21+
doSomething(
22+
"""super important value"""
23+
value: Int!
24+
): Boolean!
25+
}
26+
```
27+
28+
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)
29+
30+
### Input Types
31+
32+
Query and mutation function arguments are automatically converted to corresponding GraphQL input fields. GraphQL makes a
33+
distinction between input and output types and requires unique names for all the types. Since we can use the same
34+
objects for input and output in our Kotlin functions, `graphql-kotlin-schema-generator` will automatically append
35+
an `Input` suffix to the query input objects.
36+
37+
For example, the following code:
38+
39+
```kotlin
40+
class WidgetMutation {
41+
42+
@GraphQLDescription("modifies passed in widget so it doesn't have null value")
43+
fun processWidget(@GraphQLDescription("widget to be modified") widget: Widget): Widget {
44+
if (null == widget.value) {
45+
widget.value = 42
46+
}
47+
return widget
48+
}
49+
}
50+
51+
@GraphQLDescription("A useful widget")
52+
data class Widget(
53+
@GraphQLDescription("The widget's value that can be null")
54+
var value: Int? = nul
55+
) {
56+
@GraphQLDescription("returns original value multiplied by target OR null if original value was null")
57+
fun multiplyValueBy(multiplier: Int) = value?.times(multiplier)
58+
}
59+
```
60+
61+
Will generate the following schema:
62+
63+
```graphql
64+
type Mutation {
65+
"""modifies passed in widget so it doesn't have null value"""
66+
processWidget(
67+
"""widget to be modified"""
68+
widget: WidgetInput!
69+
): Widget!
70+
}
71+
72+
"""A useful widget"""
73+
type Widget {
74+
75+
"""The widget's value that can be null"""
76+
value: Int
77+
78+
"""
79+
returns original value multiplied by target OR null if original value was null
80+
"""
81+
multiplyValueBy(multiplier: Int!): Int
82+
}
83+
84+
"""A useful widget"""
85+
input WidgetInput {
86+
87+
"""The widget's value that can be null"""
88+
value: Int
89+
}
90+
91+
```
92+
93+
Please note that only fields are exposed in the input objects. Functions will only be available on the GraphQL output
94+
types.
95+
96+
If you know a type will only be used for input types you can call your class something like `CustomTypeInput`. The library will not
97+
append `Input` if the class name already ends with `Input` but that means you can not use this type as output because
98+
the schema would have two types with the same name and that would be invalid.
99+
100+
### Optional input fields
101+
102+
Kotlin requires variables/values to be initialized upon their declaration either from the user input OR by providing
103+
defaults (even if they are marked as nullable). Therefore in order for a GraphQL input field to be optional it needs to be
104+
nullable and also specify a default Kotlin value.
105+
106+
```kotlin
107+
@GraphQLDescription("query with optional input")
108+
fun doSomethingWithOptionalInput(
109+
@GraphQLDescription("this field is required") requiredValue: Int,
110+
@GraphQLDescription("this field is optional") optionalValue: Int?)
111+
= "required value=$requiredValue, optional value=$optionalValue"
112+
```
113+
114+
NOTE: Non nullable input fields will always require users to specify the value regardless of whether a default Kotlin value
115+
is provided or not.
116+
117+
NOTE: Even though you could specify a default value in Kotlin `optionalValue: Int? = null`, this will not be used. This is because
118+
if no value is provided to the schema, `graphql-java` passes null as the value. The Kotlin default value will never be
119+
used. For example, with argument `optionalList: List<Int>? = emptyList()`, the value will be null if not passed a value by
120+
the client.
121+
122+
### Default values
123+
124+
Default argument values are currently not supported. See issue
125+
[#53](https://github.com/ExpediaGroup/graphql-kotlin/issues/53) for more details.

0 commit comments

Comments
 (0)