diff --git a/docs/execution/contextual-data.md b/docs/execution/contextual-data.md index 3ca0af1296..53a53dc724 100644 --- a/docs/execution/contextual-data.md +++ b/docs/execution/contextual-data.md @@ -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 @@ -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" } ``` @@ -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) diff --git a/docs/spring-server/spring-graphql-context.md b/docs/spring-server/spring-graphql-context.md index 3cfa6617ec..ab99d6956a 100644 --- a/docs/spring-server/spring-graphql-context.md +++ b/docs/spring-server/spring-graphql-context.md @@ -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 { 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). diff --git a/docs/writing-schemas/arguments.md b/docs/writing-schemas/arguments.md index 834e91919e..acb19b56a6 100644 --- a/docs/writing-schemas/arguments.md +++ b/docs/writing-schemas/arguments.md @@ -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? = 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? = 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. diff --git a/docs/writing-schemas/nested-queries.md b/docs/writing-schemas/nested-queries.md index 76c617df5f..10656d3381 100644 --- a/docs/writing-schemas/nested-queries.md +++ b/docs/writing-schemas/nested-queries.md @@ -1,49 +1,49 @@ ---- -id: nested-queries -title: Nested Queries ---- - -There are a few ways in which you can access data in a nested query. Say we have the following schema - -```graphql -type Query { - findUsers(name: String!): User -} - -type User { - id: ID! - photos(numberOfPhotos: Int!): [Photo!]! -} - -type Photo { - url: String! -} -``` - -In Kotlin code, when we are in the `photos` function, if we want access to the parent field `findUsers` and it's -arguments there are a couple ways we can access it. - -* You can add the `DataFetchingEnvironment` as an argument which will allow you to view the entire query sent to the - server - -```kotlin -fun photos(environment: DataFetchingEnvironment, numberOfPhotos: Int): List { - val nameInput = environment.executionStepInfo.parent.arguments["name"] - return getPhotosFromDataSource() -} -``` - -* You can add the `@GraphQLContext` as an argument which will allow you to view the context object you set up in the - data fetchers - -```kotlin -fun photos(@GraphQLContext context: MyContextObject, numberOfPhotos: Int): List { - val nameInput = context.getDataFromMyCustomFunction() - return getPhotosFromDataSource() -} -``` - ------- - -As an example we have some ways implemented in Spring boot in the [example -app](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/NestedQueries.kt). +--- +id: nested-queries +title: Nested Queries +--- + +There are a few ways in which you can access data in a nested query. Say we have the following schema: + +```graphql +type Query { + findUsers(name: String!): User +} + +type User { + id: ID! + photos(numberOfPhotos: Int!): [Photo!]! +} + +type Photo { + url: String! +} +``` + +In Kotlin code, when we are in the `photos` function, if we want access to the parent field `findUsers` and its +arguments there are a couple ways we can access it: + +* You can add the `DataFetchingEnvironment` as an argument which will allow you to view the entire query sent to the + server. See more in the [DataFetchingEnvironment documentation](../execution/data-fetching-environment) + +```kotlin +fun photos(environment: DataFetchingEnvironment, numberOfPhotos: Int): List { + val nameInput = environment.executionStepInfo.parent.arguments["name"] + return getPhotosFromDataSource() +} +``` + +* You can add the `GraphQLContext` as an argument which will allow you to view the context object you set up in the + data fetchers. See more in the [GraphQLContext documentation](../execution/contextual-data) + +```kotlin +fun photos(context: MyContextObject, numberOfPhotos: Int): List { + val nameInput = context.getDataFromMyCustomFunction() + return getPhotosFromDataSource() +} +``` + +------ + +We have examples of these techniques implemented in Spring boot in the [example +app](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/NestedQueries.kt). diff --git a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/context/MyGraphQLContext.kt b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/context/MyGraphQLContext.kt index 0f4c7fc277..92e6a296b1 100644 --- a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/context/MyGraphQLContext.kt +++ b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/context/MyGraphQLContext.kt @@ -16,11 +16,11 @@ package com.expediagroup.graphql.examples.context -import com.expediagroup.graphql.annotations.GraphQLContext +import com.expediagroup.graphql.execution.GraphQLContext import org.springframework.http.server.reactive.ServerHttpRequest import org.springframework.http.server.reactive.ServerHttpResponse /** * Simple [GraphQLContext] that holds extra value. */ -class MyGraphQLContext(val myCustomValue: String, val request: ServerHttpRequest, val response: ServerHttpResponse, var subscriptionValue: String? = null) +class MyGraphQLContext(val myCustomValue: String, val request: ServerHttpRequest, val response: ServerHttpResponse, var subscriptionValue: String? = null) : GraphQLContext diff --git a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ContextualQuery.kt b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ContextualQuery.kt index fa9671ace8..05fffd826a 100644 --- a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ContextualQuery.kt +++ b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/query/ContextualQuery.kt @@ -16,7 +16,6 @@ package com.expediagroup.graphql.examples.query -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.annotations.GraphQLDescription import com.expediagroup.graphql.examples.context.MyGraphQLContext import com.expediagroup.graphql.examples.model.ContextualResponse @@ -37,6 +36,6 @@ class ContextualQuery : Query { fun contextualQuery( @GraphQLDescription("some value that will be returned to the user") value: Int, - @GraphQLContext context: MyGraphQLContext + context: MyGraphQLContext ): ContextualResponse = ContextualResponse(value, context.myCustomValue) } diff --git a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/subscriptions/SimpleSubscription.kt b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/subscriptions/SimpleSubscription.kt index 4c1699db4d..44a9c2b6f8 100644 --- a/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/subscriptions/SimpleSubscription.kt +++ b/examples/spring/src/main/kotlin/com/expediagroup/graphql/examples/subscriptions/SimpleSubscription.kt @@ -16,7 +16,6 @@ package com.expediagroup.graphql.examples.subscriptions -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.annotations.GraphQLDescription import com.expediagroup.graphql.examples.context.MyGraphQLContext import com.expediagroup.graphql.spring.operations.Subscription @@ -62,6 +61,6 @@ class SimpleSubscription : Subscription { fun flow(): Publisher = flowOf(1, 2, 4).asPublisher() @GraphQLDescription("Returns a value from the subscription context") - fun subscriptionContext(@GraphQLContext myGraphQLContext: MyGraphQLContext): Publisher = + fun subscriptionContext(myGraphQLContext: MyGraphQLContext): Publisher = flowOf(myGraphQLContext.subscriptionValue ?: "", "value 2", "value3").asPublisher() } diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt index 7b46974c60..bee5a5e19f 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcher.kt @@ -82,7 +82,7 @@ open class FunctionDataFetcher( * If the parameter is of a special type then we do not read the input and instead just pass on that value. * * The special values include: - * - If the parameter is annotated with [com.expediagroup.graphql.annotations.GraphQLContext], + * - If the parameter is marked as a [com.expediagroup.graphql.execution.GraphQLContext], * then return the environment context * * - The entire environment is returned if the parameter is of type [DataFetchingEnvironment] diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/annotations/GraphQLContext.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/GraphQLContext.kt similarity index 59% rename from graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/annotations/GraphQLContext.kt rename to graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/GraphQLContext.kt index f2756d6b34..e5bc5df1e4 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/annotations/GraphQLContext.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/execution/GraphQLContext.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Expedia, Inc + * Copyright 2020 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,15 @@ * limitations under the License. */ -package com.expediagroup.graphql.annotations +package com.expediagroup.graphql.execution /** - * Mark something for the GraphQL context. + * Marker interface to indicate that the implementing class should be considered + * as the GraphQL context. This means the implementing class will not appear in the schema. */ -annotation class GraphQLContext +interface GraphQLContext + +/** + * Can be used as a default [GraphQLContext] if there is none provided. + */ +class EmptyGraphQLContext : GraphQLContext diff --git a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt index 609fa5cd2c..c45bd73f12 100644 --- a/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt +++ b/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/extensions/kParameterExtensions.kt @@ -16,18 +16,17 @@ package com.expediagroup.graphql.generator.extensions -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.exceptions.CouldNotGetNameOfKParameterException +import com.expediagroup.graphql.execution.GraphQLContext import graphql.schema.DataFetchingEnvironment import kotlin.reflect.KParameter -import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.isSubclassOf internal fun KParameter.isInterface() = this.type.getKClass().isInterface() internal fun KParameter.isList() = this.type.getKClass().isSubclassOf(List::class) -internal fun KParameter.isGraphQLContext() = this.findAnnotation() != null +internal fun KParameter.isGraphQLContext() = this.type.getKClass().isSubclassOf(GraphQLContext::class) internal fun KParameter.isDataFetchingEnvironment() = this.type.classifier == DataFetchingEnvironment::class diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcherTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcherTest.kt index 57b56d1b4a..37b8cb59f4 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcherTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/execution/FunctionDataFetcherTest.kt @@ -16,7 +16,6 @@ package com.expediagroup.graphql.execution -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.annotations.GraphQLName import com.fasterxml.jackson.annotation.JsonProperty import graphql.GraphQLException @@ -34,6 +33,8 @@ import kotlin.test.assertTrue internal class FunctionDataFetcherTest { + internal class MyContext(val value: String) : GraphQLContext + internal class MyClass { fun print(string: String) = string @@ -41,7 +42,7 @@ internal class FunctionDataFetcherTest { fun printList(items: List) = items.joinToString(separator = ":") - fun context(@GraphQLContext string: String) = string + fun contextClass(myContext: MyContext) = myContext.value fun dataFetchingEnvironment(environment: DataFetchingEnvironment): String = environment.field.name @@ -92,10 +93,10 @@ internal class FunctionDataFetcherTest { } @Test - fun `valid target with context`() { - val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::context) + fun `valid target with context class`() { + val dataFetcher = FunctionDataFetcher(target = MyClass(), fn = MyClass::contextClass) val mockEnvironmet: DataFetchingEnvironment = mockk() - every { mockEnvironmet.getContext() } returns "foo" + every { mockEnvironmet.getContext() } returns MyContext("foo") assertEquals(expected = "foo", actual = dataFetcher.get(mockEnvironmet)) } diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KFunctionExtensionsKtTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KFunctionExtensionsKtTest.kt index 0612617299..fc7819caa7 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KFunctionExtensionsKtTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/extensions/KFunctionExtensionsKtTest.kt @@ -16,8 +16,8 @@ package com.expediagroup.graphql.generator.extensions -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.annotations.GraphQLIgnore +import com.expediagroup.graphql.execution.GraphQLContext import graphql.schema.DataFetchingEnvironment import org.junit.jupiter.api.Test import kotlin.test.assertEquals @@ -39,7 +39,7 @@ internal class KFunctionExtensionsKtTest { } @Test - fun `getValidArguments should ignore @GraphQLContext`() { + fun `getValidArguments should ignore GraphQLContext classes`() { val args = TestingClass::context.getValidArguments() assertEquals(expected = 1, actual = args.size) assertEquals(expected = "notContext", actual = args.first().getName()) @@ -57,8 +57,10 @@ internal class KFunctionExtensionsKtTest { fun ignored(@GraphQLIgnore ignoredArg: String, notIgnored: String) = "$ignoredArg and $notIgnored" - fun context(@GraphQLContext contextArg: String, notContext: String) = "$contextArg and $notContext" + fun context(contextClass: TestContext, notContext: String) = "Context was $contextClass and value was $notContext" fun dataFetchingEnvironment(environment: DataFetchingEnvironment, notEnvironment: String): String = "${environment.field.name} and $notEnvironment" } + + private class TestContext : GraphQLContext } diff --git a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/GenerateFunctionTest.kt b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/GenerateFunctionTest.kt index 2dab807470..fb1e9c16a1 100644 --- a/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/GenerateFunctionTest.kt +++ b/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/types/GenerateFunctionTest.kt @@ -16,15 +16,14 @@ package com.expediagroup.graphql.generator.types -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.annotations.GraphQLDescription import com.expediagroup.graphql.annotations.GraphQLDirective import com.expediagroup.graphql.annotations.GraphQLIgnore import com.expediagroup.graphql.annotations.GraphQLName import com.expediagroup.graphql.exceptions.TypeNotSupportedException import com.expediagroup.graphql.execution.FunctionDataFetcher +import com.expediagroup.graphql.execution.GraphQLContext import graphql.ExceptionWhileDataFetching -import graphql.Scalars import graphql.Scalars.GraphQLInt import graphql.Scalars.GraphQLString import graphql.execution.DataFetcherResult @@ -58,6 +57,8 @@ internal class GenerateFunctionTest : TypeTestHelper() { override fun nestedReturnType(): MyImplementation = MyImplementation() } + internal class MyContext(val value: String) : GraphQLContext + @GraphQLDirective(locations = [Introspection.DirectiveLocation.FIELD_DEFINITION]) internal annotation class FunctionDirective(val arg: String) @@ -73,7 +74,7 @@ internal class GenerateFunctionTest : TypeTestHelper() { @GraphQLName("renamedFunction") fun originalName(input: String) = input - fun context(@GraphQLContext context: String, string: String) = "$context and $string" + fun context(context: MyContext, string: String) = "${context.value} and $string" fun ignoredParameter(color: String, @GraphQLIgnore ignoreMe: String) = "$color and $ignoreMe" @@ -141,7 +142,7 @@ internal class GenerateFunctionTest : TypeTestHelper() { assertEquals("functionDirective", directive.name) assertEquals("happy", directive.arguments[0].value) assertEquals("arg", directive.arguments[0].name) - assertEquals(GraphQLNonNull(Scalars.GraphQLString), directive.arguments[0].type) + assertEquals(GraphQLNonNull(GraphQLString), directive.arguments[0].type) assertEquals( directive.validLocations()?.toSet(), setOf(Introspection.DirectiveLocation.FIELD_DEFINITION) diff --git a/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/GraphQLAutoConfiguration.kt b/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/GraphQLAutoConfiguration.kt index 617458ea26..77e295057e 100644 --- a/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/GraphQLAutoConfiguration.kt +++ b/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/GraphQLAutoConfiguration.kt @@ -130,5 +130,5 @@ class GraphQLAutoConfiguration { fun contextWebFilter( config: GraphQLConfigurationProperties, graphQLContextFactory: GraphQLContextFactory<*> - ): ContextWebFilter = ContextWebFilter(config, graphQLContextFactory) + ): ContextWebFilter<*> = ContextWebFilter(config, graphQLContextFactory) } diff --git a/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt b/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt index 3326949ce7..19252dbae8 100644 --- a/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt +++ b/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt @@ -16,6 +16,7 @@ package com.expediagroup.graphql.spring.execution +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.spring.GraphQLConfigurationProperties import kotlinx.coroutines.reactor.mono import org.springframework.core.Ordered @@ -33,7 +34,7 @@ const val GRAPHQL_CONTEXT_FILTER_ODER = 0 /** * Default web filter that populates GraphQL context in the reactor subscriber context. */ -open class ContextWebFilter(config: GraphQLConfigurationProperties, private val contextFactory: GraphQLContextFactory) : WebFilter, Ordered { +open class ContextWebFilter(config: GraphQLConfigurationProperties, private val contextFactory: GraphQLContextFactory) : WebFilter, Ordered { private val graphQLRoute = enforceAbsolutePath(config.endpoint) private val subscriptionsRoute = enforceAbsolutePath(config.subscriptions.endpoint) diff --git a/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/GraphQLContextFactory.kt b/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/GraphQLContextFactory.kt index ce827c4726..789572fafe 100644 --- a/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/GraphQLContextFactory.kt +++ b/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/GraphQLContextFactory.kt @@ -16,7 +16,8 @@ package com.expediagroup.graphql.spring.execution -import graphql.GraphQLContext +import com.expediagroup.graphql.execution.EmptyGraphQLContext +import com.expediagroup.graphql.execution.GraphQLContext import org.springframework.http.server.reactive.ServerHttpRequest import org.springframework.http.server.reactive.ServerHttpResponse @@ -28,7 +29,7 @@ const val GRAPHQL_CONTEXT_KEY = "graphQLContext" /** * Factory that generates GraphQL context. */ -interface GraphQLContextFactory { +interface GraphQLContextFactory { /** * Generate GraphQL context based on the incoming request and the corresponding response. @@ -39,7 +40,7 @@ interface GraphQLContextFactory { /** * Default context factory that generates empty GraphQL context. */ -internal object EmptyContextFactory : GraphQLContextFactory { +internal object EmptyContextFactory : GraphQLContextFactory { - override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): GraphQLContext = GraphQLContext.newContext().build() + override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse) = EmptyGraphQLContext() } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SchemaConfigurationTest.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SchemaConfigurationTest.kt index f9e42bb909..8a3ba647af 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SchemaConfigurationTest.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SchemaConfigurationTest.kt @@ -18,6 +18,7 @@ package com.expediagroup.graphql.spring import com.expediagroup.graphql.SchemaGeneratorConfig import com.expediagroup.graphql.TopLevelObject +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.execution.KotlinDataFetcherFactoryProvider import com.expediagroup.graphql.execution.SimpleKotlinDataFetcherFactoryProvider import com.expediagroup.graphql.spring.execution.ContextWebFilter @@ -166,7 +167,7 @@ class SchemaConfigurationTest { .build() @Bean - fun myCustomContextFactory(): GraphQLContextFactory> = mockk() + fun myCustomContextFactory(): GraphQLContextFactory<*> = mockk() @Bean fun myDataLoaderRegistryFactory(): DataLoaderRegistryFactory = mockk() @@ -175,11 +176,13 @@ class SchemaConfigurationTest { fun myCustomContextWebFilter( config: GraphQLConfigurationProperties, graphQLContextFactory: GraphQLContextFactory<*> - ): ContextWebFilter = object : ContextWebFilter(config, graphQLContextFactory) { - private val regex = config.endpoint.toRegex() + ) = CustomWebFilter(config, graphQLContextFactory) + } + + class CustomWebFilter(config: GraphQLConfigurationProperties, graphQLContextFactory: GraphQLContextFactory) : ContextWebFilter(config, graphQLContextFactory) { + private val regex = config.endpoint.toRegex() - override fun isApplicable(path: String): Boolean = regex.matches(path) - } + override fun isApplicable(path: String): Boolean = regex.matches(path) } class BasicQuery : Query { diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/ContextWebFilterTest.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/ContextWebFilterTest.kt index 689450257a..5fa9b33e8c 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/ContextWebFilterTest.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/ContextWebFilterTest.kt @@ -16,11 +16,11 @@ package com.expediagroup.graphql.spring.context +import com.expediagroup.graphql.execution.EmptyGraphQLContext import com.expediagroup.graphql.spring.GraphQLConfigurationProperties import com.expediagroup.graphql.spring.execution.ContextWebFilter import com.expediagroup.graphql.spring.execution.GRAPHQL_CONTEXT_KEY import com.expediagroup.graphql.spring.execution.GraphQLContextFactory -import graphql.GraphQLContext import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -54,8 +54,8 @@ class ContextWebFilterTest { } } - val simpleFactory: GraphQLContextFactory = mockk { - coEvery { generateContext(any(), any()) } returns GraphQLContext.newContext().build() + val simpleFactory: GraphQLContextFactory<*> = mockk { + coEvery { generateContext(any(), any()) } returns EmptyGraphQLContext() } val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), simpleFactory) @@ -63,13 +63,13 @@ class ContextWebFilterTest { .verifyComplete() assertNotNull(generatedContext) - val graphQLContext = generatedContext?.getOrDefault(GRAPHQL_CONTEXT_KEY, null) + val graphQLContext = generatedContext?.getOrDefault(GRAPHQL_CONTEXT_KEY, null) assertNotNull(graphQLContext) } @Test fun `verify web filter order`() { - val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), mockk()) + val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), mockk>()) assertEquals(expected = 0, actual = contextFilter.order) } @@ -90,8 +90,8 @@ class ContextWebFilterTest { } } - val simpleFactory: GraphQLContextFactory = mockk { - coEvery { generateContext(any(), any()) } returns GraphQLContext.newContext().build() + val simpleFactory: GraphQLContextFactory<*> = mockk { + coEvery { generateContext(any(), any()) } returns EmptyGraphQLContext() } val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), simpleFactory) @@ -104,7 +104,7 @@ class ContextWebFilterTest { @Test fun `verify context web filter is applicable on default graphql routes`() { - val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), mockk()) + val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), mockk>()) for (path in listOf("/graphql", "/subscriptions")) { assertTrue(contextFilter.isApplicable(path)) } @@ -119,7 +119,7 @@ class ContextWebFilterTest { packages = listOf("com.expediagroup.graphql"), subscriptions = GraphQLConfigurationProperties.SubscriptionConfigurationProperties(endpoint = subscriptionRoute)) - val contextFilter = ContextWebFilter(props, mockk()) + val contextFilter = ContextWebFilter(props, mockk>()) for (path in listOf("/${graphQLRoute.toLowerCase()}", "/${subscriptionRoute.toLowerCase()}")) { assertTrue(contextFilter.isApplicable(path)) } @@ -127,7 +127,7 @@ class ContextWebFilterTest { @Test fun `verify context web filter is not applicable on non graphql routes`() { - val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), mockk()) + val contextFilter = ContextWebFilter(GraphQLConfigurationProperties(packages = listOf("com.expediagroup.graphql")), mockk>()) assertFalse(contextFilter.isApplicable("/whatever")) } } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/GraphQLContextFactoryIT.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/GraphQLContextFactoryIT.kt index cc12e44490..1714aa5c9e 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/GraphQLContextFactoryIT.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/context/GraphQLContextFactoryIT.kt @@ -16,7 +16,7 @@ package com.expediagroup.graphql.spring.context -import com.expediagroup.graphql.annotations.GraphQLContext +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.spring.execution.GRAPHQL_CONTEXT_FILTER_ODER import com.expediagroup.graphql.spring.execution.GraphQLContextFactory import com.expediagroup.graphql.spring.model.GraphQLRequest @@ -86,8 +86,8 @@ class GraphQLContextFactoryIT(@Autowired private val testClient: WebTestClient) } class ContextualQuery : Query { - fun context(@GraphQLContext ctx: CustomContext): CustomContext = ctx + fun context(ctx: CustomContext): CustomContext = ctx } - data class CustomContext(val first: String?, val second: String?) + data class CustomContext(val first: String?, val second: String?) : GraphQLContext } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/QueryHandlerTest.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/QueryHandlerTest.kt index 990f40f86a..8e47d24042 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/QueryHandlerTest.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/QueryHandlerTest.kt @@ -18,8 +18,8 @@ package com.expediagroup.graphql.spring.execution import com.expediagroup.graphql.SchemaGeneratorConfig import com.expediagroup.graphql.TopLevelObject -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.exceptions.GraphQLKotlinException +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.spring.exception.SimpleKotlinGraphQLError import com.expediagroup.graphql.spring.model.GraphQLRequest import com.expediagroup.graphql.toSchema @@ -153,8 +153,8 @@ class QueryHandlerTest { fun alwaysThrows(): String = throw GraphQLKotlinException("JUNIT Failure") - fun contextualValue(@GraphQLContext context: MyContext): String = context.value ?: "default" + fun contextualValue(context: MyContext): String = context.value ?: "default" } - data class MyContext(val value: String? = null) + data class MyContext(val value: String? = null) : GraphQLContext } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt index e2d2055d9a..d13f4156a1 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandlerTest.kt @@ -18,8 +18,8 @@ package com.expediagroup.graphql.spring.execution import com.expediagroup.graphql.SchemaGeneratorConfig import com.expediagroup.graphql.TopLevelObject -import com.expediagroup.graphql.annotations.GraphQLContext import com.expediagroup.graphql.exceptions.GraphQLKotlinException +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.spring.model.GraphQLRequest import com.expediagroup.graphql.toSchema import graphql.ErrorType @@ -119,10 +119,10 @@ class SubscriptionHandlerTest { fun alwaysThrows(): Flux = Flux.error(GraphQLKotlinException("JUNIT subscription failure")) - fun contextualTicker(@GraphQLContext context: SubscriptionContext): Flux = Flux.range(1, 5) + fun contextualTicker(context: SubscriptionContext): Flux = Flux.range(1, 5) .delayElements(Duration.ofMillis(100)) .map { "${context.value}:${Random.nextInt(100)}" } } - data class SubscriptionContext(val value: String) + data class SubscriptionContext(val value: String) : GraphQLContext } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt index 1c5b6c9c42..7c149514da 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionWebSocketHandlerIT.kt @@ -16,7 +16,7 @@ package com.expediagroup.graphql.spring.execution -import com.expediagroup.graphql.annotations.GraphQLContext +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.spring.model.GraphQLRequest import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage import com.expediagroup.graphql.spring.model.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT @@ -196,10 +196,10 @@ class SubscriptionWebSocketHandlerIT(@LocalServerPort private var port: Int) { .delayElements(Duration.ofMillis(100)) .map { Random.nextInt() } - fun ticker(@GraphQLContext ctx: SubscriptionContext): Flux = Flux.just("${ctx.value}:${Random.nextInt()}") + fun ticker(ctx: SubscriptionContext): Flux = Flux.just("${ctx.value}:${Random.nextInt()}") } - data class SubscriptionContext(val value: String) + data class SubscriptionContext(val value: String) : GraphQLContext private fun SubscriptionOperationMessage.toJson() = objectMapper.writeValueAsString(this) } diff --git a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/routes/RouteConfigurationIT.kt b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/routes/RouteConfigurationIT.kt index ff53768bae..39914c27a8 100644 --- a/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/routes/RouteConfigurationIT.kt +++ b/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/routes/RouteConfigurationIT.kt @@ -16,7 +16,7 @@ package com.expediagroup.graphql.spring.routes -import com.expediagroup.graphql.annotations.GraphQLContext +import com.expediagroup.graphql.execution.GraphQLContext import com.expediagroup.graphql.spring.REQUEST_PARAM_OPERATION_NAME import com.expediagroup.graphql.spring.REQUEST_PARAM_QUERY import com.expediagroup.graphql.spring.REQUEST_PARAM_VARIABLES @@ -60,10 +60,10 @@ class RouteConfigurationIT(@Autowired private val testClient: WebTestClient) { class SimpleQuery : Query { fun hello(name: String) = "Hello $name!" - fun context(@GraphQLContext ctx: CustomContext) = ctx.value + fun context(ctx: CustomContext) = ctx.value } - data class CustomContext(val value: String) + data class CustomContext(val value: String) : GraphQLContext @Test fun `verify SDL route`() {