Skip to content

Commit

Permalink
[generator] Replace GraphQLContext annotation with interface (#610)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Mar 18, 2020
1 parent 9a54d5d commit 6311df8
Show file tree
Hide file tree
Showing 23 changed files with 278 additions and 282 deletions.
35 changes: 21 additions & 14 deletions docs/execution/contextual-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ title: Contextual Data
---

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

The contents of the GraphQL context vary across applications and it is up to the GraphQL server developers to decide
what it should contain. For Spring based applications, `graphql-kotlin-spring-server` provides a simple mechanism to
Expand All @@ -18,17 +18,23 @@ Once context factory bean is available in the Spring application context it will
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)
for additional details

Once your application is configured to build your custom `MyGraphQLContext`, simply add `@GraphQLContext` annotation to
any function argument and the corresponding GraphQL context from the environment will be automatically injected during
execution.
## GraphQLContext Interface

The easiest way to specify a context class is to use the `GraphQLContext` marker interface. This interface does not require any implementations,
it is just used to inform the schema generator that this is the class that should be used as the context for every request.

```kotlin
class ContextualQuery {
class MyGraphQLContext(val customValue: String) : GraphQLContext
```

Then you can just use the class as an argument and it will be automatically injected during execution time.

```kotlin
class ContextualQuery {
fun contextualQuery(
value: Int,
@GraphQLContext context: MyGraphQLContext
): ContextualResponse = ContextualResponse(value, context.myCustomValue)
context: MyGraphQLContext,
value: Int
): String = "The custom value was ${context.customValue} and the value was $value"
}
```

Expand All @@ -40,10 +46,11 @@ schema {
}

type Query {
contextualQuery(
value: Int!
): ContextualResponse!
contextualQuery(value: Int!): String!
}
```

Note that the `@GraphQLContext` annotated argument is not reflected in the GraphQL schema.
Note that the argument that implements `GraphQLContext` is not reflected in the GraphQL schema.

### Customization
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)
42 changes: 9 additions & 33 deletions docs/spring-server/spring-graphql-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,33 @@ id: spring-graphql-context
title: Generating GraphQL Context
---

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

For example if we define our custom context as follows:

```kotlin
data class MyGraphQLContext(val myCustomValue: String)
class MyGraphQLContext(val myCustomValue: String) : GraphQLContext
```

We can generate corresponding `GraphQLContextFactory` bean:
We can generate the corresponding `GraphQLContextFactory` bean:

```kotlin
@Component
class MyGraphQLContextFactory: GraphQLContextFactory<MyGraphQLContext> {
override suspend fun generateContext(
request: ServerHttpRequest,
request: ServerHttpRequest,
response: ServerHttpResponse
): MyGraphQLContext = MyGraphQLContext(
myCustomValue = request.headers.getFirst("MyHeader") ?: "defaultContext"
myCustomValue = request.headers.getFirst("MyHeader") ?: "defaultValue"
)
}
```

Once your application is configured to build your custom `MyGraphQLContext`, we can then specify it as function argument by annotating it with `@GraphQLContext`.
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.
While executing the query, the corresponding GraphQL context will be read from the environment and automatically injected to the function input arguments.

```kotlin
@Component
class ContextualQuery: Query {
fun contextualQuery(
value: Int,
@GraphQLContext context: MyGraphQLContext
): ContextualResponse = ContextualResponse(value, context.myCustomValue)
}
```

The above query would produce the following GraphQL schema:

```graphql
schema {
query: Query
}

type Query {
contextualQuery(
value: Int!
): ContextualResponse!
}
```

Notice that the `@GraphQLContext` annotated argument is not reflected in the generated GraphQL schema.
For more details see the [Contextual Data documentation](../execution/contextual-data).
249 changes: 125 additions & 124 deletions docs/writing-schemas/arguments.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,125 @@
---
id: arguments
title: Arguments
---

Method arguments are automatically exposed as part of the arguments to the corresponding GraphQL fields.

```kotlin
class SimpleQuery{

@GraphQLDescription("performs some operation")
fun doSomething(@GraphQLDescription("super important value") value: Int): Boolean = true
}
```

The above Kotlin code will generate following GraphQL schema:

```graphql
type Query {
"""performs some operation"""
doSomething(
"""super important value"""
value: Int!
): Boolean!
}
```

This behavior is true for all arguments except for the GraphQL context objects. See section below for detailed
information about `@GraphQLContext`.

### Input Types

Query and mutation function arguments are automatically converted to corresponding GraphQL input fields. GraphQL makes a
distinction between input and output types and requires unique names for all the types. Since we can use the same
objects for input and output in our Kotlin functions, `graphql-kotlin-schema-generator` will automatically append
`Input` suffix to the query input objects.

```kotlin
class WidgetMutation {

@GraphQLDescription("modifies passed in widget so it doesn't have null value")
fun processWidget(@GraphQLDescription("widget to be modified") widget: Widget): Widget {
if (null == widget.value) {
widget.value = 42
}
return widget
}
}

@GraphQLDescription("A useful widget")
data class Widget(
@GraphQLDescription("The widget's value that can be null")
var value: Int? = nul
) {
@GraphQLDescription("returns original value multiplied by target OR null if original value was null")
fun multiplyValueBy(multiplier: Int) = value?.times(multiplier)
}
```

Will generate

```graphql
type Mutation {
"""modifies passed in widget so it doesn't have null value"""
processWidget(
"""widget to be modified"""
widget: WidgetInput!
): Widget!
}

"""A useful widget"""
type Widget {

"""The widget's value that can be null"""
value: Int

"""
returns original value multiplied by target OR null if original value was null
"""
multiplyValueBy(multiplier: Int!): Int
}

"""A useful widget"""
input WidgetInput {

"""The widget's value that can be null"""
value: Int
}

```

Please note that only fields are exposed in the input objects. Functions will only be available on the GraphQL output
types.

If you know a type will only be used for input types you can call your class `CustomTypeInput`. The library will not
append `Input` if the class name already ends with `Input` but that means you can not use this type as output because
the schema would have two types with the same name and will be invalid.

### Optional input fields

Kotlin requires variables/values to be initialized upon their declaration either from the user input OR by providing
defaults (even if they are marked as nullable). Therefore in order for GraphQL input field to be optional it needs to be
nullable and also specify default Kotlin value.

```kotlin
@GraphQLDescription("query with optional input")
fun doSomethingWithOptionalInput(
@GraphQLDescription("this field is required") requiredValue: Int,
@GraphQLDescription("this field is optional") optionalValue: Int?)
= "required value=$requiredValue, optional value=$optionalValue"
```

NOTE: Non nullable input fields will always require users to specify the value regardless whether default Kotlin value
is provided or not.

NOTE: Even though you could specify a default value in Kotlin `optionalValue: Int? = null`, this will not be used since
if no value is provided to the schema `graphql-java` passes null as the value so the Kotlin default value will never be
used, like in this argument `optionalList: List<Int>? = emptyList()`, the value will be null if not passed a value by
the client.

### Default values

Default argument values are currently not supported. See issue
[#53](https://github.com/ExpediaGroup/graphql-kotlin/issues/53) for more details.
---
id: arguments
title: Arguments
---

Method arguments are automatically exposed as part of the arguments to the corresponding GraphQL fields.

```kotlin
class SimpleQuery{

@GraphQLDescription("performs some operation")
fun doSomething(@GraphQLDescription("super important value") value: Int): Boolean = true
}
```

The above Kotlin code will generate following GraphQL schema:

```graphql
type Query {
"""performs some operation"""
doSomething(
"""super important value"""
value: Int!
): Boolean!
}
```

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)

### Input Types

Query and mutation function arguments are automatically converted to corresponding GraphQL input fields. GraphQL makes a
distinction between input and output types and requires unique names for all the types. Since we can use the same
objects for input and output in our Kotlin functions, `graphql-kotlin-schema-generator` will automatically append
an `Input` suffix to the query input objects.

For example, the following code:

```kotlin
class WidgetMutation {

@GraphQLDescription("modifies passed in widget so it doesn't have null value")
fun processWidget(@GraphQLDescription("widget to be modified") widget: Widget): Widget {
if (null == widget.value) {
widget.value = 42
}
return widget
}
}

@GraphQLDescription("A useful widget")
data class Widget(
@GraphQLDescription("The widget's value that can be null")
var value: Int? = nul
) {
@GraphQLDescription("returns original value multiplied by target OR null if original value was null")
fun multiplyValueBy(multiplier: Int) = value?.times(multiplier)
}
```

Will generate the following schema:

```graphql
type Mutation {
"""modifies passed in widget so it doesn't have null value"""
processWidget(
"""widget to be modified"""
widget: WidgetInput!
): Widget!
}

"""A useful widget"""
type Widget {

"""The widget's value that can be null"""
value: Int

"""
returns original value multiplied by target OR null if original value was null
"""
multiplyValueBy(multiplier: Int!): Int
}

"""A useful widget"""
input WidgetInput {

"""The widget's value that can be null"""
value: Int
}

```

Please note that only fields are exposed in the input objects. Functions will only be available on the GraphQL output
types.

If you know a type will only be used for input types you can call your class something like `CustomTypeInput`. The library will not
append `Input` if the class name already ends with `Input` but that means you can not use this type as output because
the schema would have two types with the same name and that would be invalid.

### Optional input fields

Kotlin requires variables/values to be initialized upon their declaration either from the user input OR by providing
defaults (even if they are marked as nullable). Therefore in order for a GraphQL input field to be optional it needs to be
nullable and also specify a default Kotlin value.

```kotlin
@GraphQLDescription("query with optional input")
fun doSomethingWithOptionalInput(
@GraphQLDescription("this field is required") requiredValue: Int,
@GraphQLDescription("this field is optional") optionalValue: Int?)
= "required value=$requiredValue, optional value=$optionalValue"
```

NOTE: Non nullable input fields will always require users to specify the value regardless of whether a default Kotlin value
is provided or not.

NOTE: Even though you could specify a default value in Kotlin `optionalValue: Int? = null`, this will not be used. This is because
if no value is provided to the schema, `graphql-java` passes null as the value. The Kotlin default value will never be
used. For example, with argument `optionalList: List<Int>? = emptyList()`, the value will be null if not passed a value by
the client.

### Default values

Default argument values are currently not supported. See issue
[#53](https://github.com/ExpediaGroup/graphql-kotlin/issues/53) for more details.
Loading

0 comments on commit 6311df8

Please sign in to comment.