Skip to content

Commit

Permalink
standardize coroutine context propagation in the execution
Browse files Browse the repository at this point in the history
📝 Description

Attempts to standardize how we can propagate the coroutine/reactor context created by the server when processing incoming requests to the data fetchers that would resolve the underlying fields.

`GraphQLContext` map was introduced in `graphql-java` 17 as means to standardize usage of GraphQL context (previously it could be any object). We can store the original GraphQL request context in the map and then use it from within the data fetchers.

This is a breaking change as we are changing signatures of `GraphQLContextFactory` and `FunctionDataFetcher`.

🔗 Related Issues

* #1336
* #1318
* #1300
* #1257
  • Loading branch information
Dariusz Kuc committed Feb 1, 2022
1 parent 0253109 commit 126e826
Show file tree
Hide file tree
Showing 43 changed files with 381 additions and 335 deletions.

This file was deleted.

@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,25 +17,25 @@
package com.expediagroup.graphql.examples.server.ktor

import com.expediagroup.graphql.examples.server.ktor.schema.models.User
import com.expediagroup.graphql.generator.execution.GraphQLContext
import com.expediagroup.graphql.server.execution.GraphQLContextFactory
import io.ktor.request.ApplicationRequest

/**
* Custom logic for how this example app should create its context given the [ApplicationRequest]
*/
class KtorGraphQLContextFactory : GraphQLContextFactory<AuthorizedContext, ApplicationRequest> {
class KtorGraphQLContextFactory : GraphQLContextFactory<GraphQLContext, ApplicationRequest> {

override suspend fun generateContext(request: ApplicationRequest): AuthorizedContext {
val loggedInUser = User(
override suspend fun generateContextMap(request: ApplicationRequest): Map<Any, Any> = mutableMapOf<Any, Any>(
"user" to User(
email = "fake@site.com",
firstName = "Someone",
lastName = "You Don't know",
universityId = 4
)

// Parse any headers from the Ktor request
val customHeader: String? = request.headers["my-custom-header"]

return AuthorizedContext(loggedInUser, customHeader = customHeader)
).also { map ->
request.headers["my-custom-header"]?.let { customHeader ->
map["customHeader"] = customHeader
}
}
}

This file was deleted.

@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -17,32 +17,16 @@
package com.expediagroup.graphql.examples.server.spring.context

import com.expediagroup.graphql.server.execution.GraphQLContextFactory
import com.expediagroup.graphql.server.spring.execution.SpringGraphQLContextFactory
import com.expediagroup.graphql.server.spring.subscriptions.SpringSubscriptionGraphQLContextFactory
import com.expediagroup.graphql.server.spring.execution.DefaultSpringGraphQLContextFactory
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.socket.WebSocketSession

/**
* [GraphQLContextFactory] that generates [MyGraphQLContext] that will be available when processing GraphQL requests.
* [GraphQLContextFactory] that populates GraphQL context map that will be available when processing GraphQL requests.
*/
@Component
class MyGraphQLContextFactory : SpringGraphQLContextFactory<MyGraphQLContext>() {

override suspend fun generateContext(request: ServerRequest): MyGraphQLContext = MyGraphQLContext(
request = request,
myCustomValue = request.headers().firstHeader("MyHeader") ?: "defaultContext"
)
}

/**
* [GraphQLContextFactory] that generates [MySubscriptionGraphQLContext] that will be available when processing subscription operations.
*/
@Component
class MySubscriptionGraphQLContextFactory : SpringSubscriptionGraphQLContextFactory<MySubscriptionGraphQLContext>() {

override suspend fun generateContext(request: WebSocketSession): MySubscriptionGraphQLContext = MySubscriptionGraphQLContext(
request = request,
auth = null
class MyGraphQLContextFactory : DefaultSpringGraphQLContextFactory() {
override suspend fun generateContextMap(request: ServerRequest): Map<*, Any> = super.generateContextMap(request) + mapOf(
"myCustomValue" to (request.headers().firstHeader("MyHeader") ?: "defaultContext")
)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,8 +16,6 @@

package com.expediagroup.graphql.examples.server.spring.execution

import com.expediagroup.graphql.examples.server.spring.context.MySubscriptionGraphQLContext
import com.expediagroup.graphql.generator.execution.GraphQLContext
import com.expediagroup.graphql.server.spring.subscriptions.ApolloSubscriptionHooks
import org.springframework.web.reactive.socket.WebSocketSession

Expand All @@ -26,14 +24,13 @@ import org.springframework.web.reactive.socket.WebSocketSession
*/
class MySubscriptionHooks : ApolloSubscriptionHooks {

override fun onConnect(
override fun onConnectWithContext(
connectionParams: Map<String, String>,
session: WebSocketSession,
graphQLContext: GraphQLContext?
): GraphQLContext? {
if (graphQLContext != null && graphQLContext is MySubscriptionGraphQLContext) {
graphQLContext.auth = connectionParams["Authorization"]
graphQLContext: Map<*, Any>
): Map<*, Any> = mutableMapOf<Any, Any>().also { contextMap ->
connectionParams["Authorization"]?.let { authValue ->
contextMap["auth"] = authValue
}
return graphQLContext
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,15 +16,15 @@

package com.expediagroup.graphql.examples.server.spring.query

import com.expediagroup.graphql.examples.server.spring.context.MyGraphQLContext
import com.expediagroup.graphql.examples.server.spring.model.ContextualResponse
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.server.operations.Query
import graphql.schema.DataFetchingEnvironment
import org.springframework.stereotype.Component

/**
* Example usage of GraphQLContext. Since the argument [ContextualQuery.contextualQuery] implements
* the GraphQLContext interface it will not appear in the schema and be populated at runtime.
* Example usage of GraphQLContext. Since `DataFetchingEnvironment` is passed as the argument
* of [ContextualQuery.contextualQuery], it will not appear in the schema and be populated at runtime.
*/
@Component
class ContextualQuery : Query {
Expand All @@ -33,6 +33,6 @@ class ContextualQuery : Query {
fun contextualQuery(
@GraphQLDescription("some value that will be returned to the user")
value: Int,
context: MyGraphQLContext
): ContextualResponse = ContextualResponse(value, context.myCustomValue)
env: DataFetchingEnvironment
): ContextualResponse = ContextualResponse(value, env.graphQlContext.getOrDefault("myCustomValue", "defaultValue"))
}
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,7 +16,6 @@

package com.expediagroup.graphql.examples.server.spring.subscriptions

import com.expediagroup.graphql.examples.server.spring.context.MySubscriptionGraphQLContext
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.server.operations.Subscription
import graphql.GraphqlErrorException
Expand Down Expand Up @@ -79,8 +78,4 @@ class SimpleSubscription : Subscription {

return flowOf(dfr, dfr).asPublisher()
}

@GraphQLDescription("Returns a value from the subscription context")
fun subscriptionContext(myGraphQLContext: MySubscriptionGraphQLContext): Flux<String> =
Flux.just(myGraphQLContext.auth ?: "no-auth")
}
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,12 +20,13 @@ import graphql.GraphQLError
import graphql.execution.DataFetcherResult
import graphql.schema.DataFetcher
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.future.future
import java.util.concurrent.CompletableFuture
import kotlin.coroutines.EmptyCoroutineContext

private const val TYPENAME_FIELD = "__typename"
private const val REPRESENTATIONS = "representations"
Expand Down Expand Up @@ -54,7 +55,8 @@ open class EntityResolver(resolvers: List<FederatedTypeResolver<*>>) : DataFetch
val representations: List<Map<String, Any>> = env.getArgument(REPRESENTATIONS)
val indexedBatchRequestsByType = representations.withIndex().groupBy { it.value[TYPENAME_FIELD].toString() }

return GlobalScope.future {
val scope = env.graphQlContext.getOrDefault(CoroutineScope::class, CoroutineScope(EmptyCoroutineContext))
return scope.future {
val data = mutableListOf<Any?>()
val errors = mutableListOf<GraphQLError>()

Expand Down
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,4 +24,5 @@ import com.expediagroup.graphql.generator.execution.GraphQLContext
* request came from the Apollo Gateway. That means we need a special interface
* for the federation context.
*/
@Deprecated(message = "The generic context object is deprecated in favor of the context map", ReplaceWith("graphql.GraphQLContext"))
interface FederatedGraphQLContext : GraphQLContext, HTTPRequestHeaders
@@ -1,5 +1,5 @@
/*
* Copyright 2021 Expedia, Inc
* Copyright 2022 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,6 +20,7 @@ import com.expediagroup.graphql.generator.federation.data.BookResolver
import com.expediagroup.graphql.generator.federation.data.UserResolver
import com.expediagroup.graphql.generator.federation.data.queries.federated.Book
import com.expediagroup.graphql.generator.federation.data.queries.federated.User
import graphql.GraphQLContext
import graphql.GraphQLError
import graphql.schema.DataFetchingEnvironment
import io.mockk.coEvery
Expand All @@ -38,6 +39,7 @@ class EntityQueryResolverTest {
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val result = resolver.get(env).get()
Expand All @@ -56,6 +58,7 @@ class EntityQueryResolverTest {
val resolver = EntityResolver(listOf(mockBookResolver, mockUserResolver))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns listOf(emptyMap<String, Any>())
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val result = resolver.get(env).get()
Expand All @@ -69,6 +72,7 @@ class EntityQueryResolverTest {
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val result = resolver.get(env).get()
Expand All @@ -86,6 +90,7 @@ class EntityQueryResolverTest {
val representations = listOf(mapOf<String, Any>("__typename" to "User", "userId" to 123, "name" to "testName"))
val env: DataFetchingEnvironment = mockk {
every { getArgument<Any>(any()) } returns representations
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val result = resolver.get(env).get()
Expand All @@ -103,6 +108,7 @@ class EntityQueryResolverTest {
val representations = listOf(user1.toRepresentation(), book.toRepresentation(), user2.toRepresentation())
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val spyUserResolver = spyk(UserResolver())
Expand All @@ -128,6 +134,7 @@ class EntityQueryResolverTest {
val representations = listOf(user.toRepresentation(), book.toRepresentation())
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val spyUserResolver: UserResolver = spyk(UserResolver())
Expand All @@ -153,6 +160,7 @@ class EntityQueryResolverTest {
val representations = listOf(user.toRepresentation(), user.toRepresentation())
val env = mockk<DataFetchingEnvironment> {
every { getArgument<Any>(any()) } returns representations
every { graphQlContext } returns GraphQLContext.newContext().build()
}

val mockUserResolver: UserResolver = mockk {
Expand Down

0 comments on commit 126e826

Please sign in to comment.