Skip to content

Commit 9304507

Browse files
authored
Support creating the new context map (ExpediaGroup#1292)
* Support creating the new context map * Fix subscription hooks * Update docs and deprecation notice * Use inline builder * Update unit test docs * Use default context map in subscriptions test * Undo public arg name changes
1 parent e2e12f5 commit 9304507

File tree

17 files changed

+211
-34
lines changed

17 files changed

+211
-34
lines changed

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/GraphQLContext.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ package com.expediagroup.graphql.generator.execution
2020
* Marker interface to indicate that the implementing class should be considered
2121
* as the GraphQL context. This means the implementing class will not appear in the schema.
2222
*/
23+
@Deprecated("The generic context object is deprecated in favor of the context map")
2324
interface GraphQLContext

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLContextFactory.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,12 @@ interface GraphQLContextFactory<out Context : GraphQLContext, Request> {
2727
* Generate GraphQL context based on the incoming request and the corresponding response.
2828
* If no context should be generated and used in the request, return null.
2929
*/
30+
@Deprecated("The generic context object is deprecated in favor of the context map")
3031
suspend fun generateContext(request: Request): Context?
32+
33+
/**
34+
* GraphQL Java 17 has a new context map instead of a generic object. Implementing this method
35+
* will set the context map in the execution input.
36+
*/
37+
suspend fun generateContextMap(request: Request): Map<*, Any>? = null
3138
}

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandler.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@ open class GraphQLRequestHandler(
3636
* This should only be used for queries and mutations.
3737
* Subscriptions require more specific server logic and will need to be handled separately.
3838
*/
39-
open suspend fun executeRequest(request: GraphQLRequest, context: GraphQLContext? = null): GraphQLResponse<*> {
39+
open suspend fun executeRequest(request: GraphQLRequest, context: GraphQLContext? = null, graphQLContext: Map<*, Any>? = null): GraphQLResponse<*> {
4040
// We should generate a new registry for every request
4141
val dataLoaderRegistry = dataLoaderRegistryFactory?.generate()
42-
val executionInput = request.toExecutionInput(context, dataLoaderRegistry)
42+
val executionInput = request.toExecutionInput(context, dataLoaderRegistry, graphQLContext)
4343

4444
return try {
4545
graphQL.executeAsync(executionInput).await().toGraphQLResponse()

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLServer.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ open class GraphQLServer<Request>(
4444

4545
return if (graphQLRequest != null) {
4646
val context = contextFactory.generateContext(request)
47+
val graphQLContext = contextFactory.generateContextMap(request)
48+
4749
when (graphQLRequest) {
48-
is GraphQLRequest -> requestHandler.executeRequest(graphQLRequest, context)
50+
is GraphQLRequest -> requestHandler.executeRequest(graphQLRequest, context, graphQLContext)
4951
is GraphQLBatchRequest -> GraphQLBatchResponse(
5052
graphQLRequest.requests.map {
51-
requestHandler.executeRequest(it, context)
53+
requestHandler.executeRequest(it, context, graphQLContext)
5254
}
5355
)
5456
}

servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/requestExtensions.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,14 @@ import org.dataloader.DataLoaderRegistry
2323
/**
2424
* Convert the common [GraphQLRequest] to the execution input used by graphql-java
2525
*/
26-
fun GraphQLRequest.toExecutionInput(graphQLContext: Any? = null, dataLoaderRegistry: DataLoaderRegistry? = null): ExecutionInput =
26+
fun GraphQLRequest.toExecutionInput(graphQLContext: Any? = null, dataLoaderRegistry: DataLoaderRegistry? = null, graphQLContextMap: Map<*, Any>? = null): ExecutionInput =
2727
ExecutionInput.newExecutionInput()
2828
.query(this.query)
2929
.operationName(this.operationName)
3030
.variables(this.variables ?: emptyMap())
31-
.context(graphQLContext)
31+
.also { builder ->
32+
graphQLContext?.let { builder.context(it) }
33+
graphQLContextMap?.let { builder.graphQLContext(it) }
34+
}
3235
.dataLoaderRegistry(dataLoaderRegistry ?: DataLoaderRegistry())
3336
.build()

servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandlerTest.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.expediagroup.graphql.server.types.GraphQLRequest
2424
import graphql.ExecutionInput
2525
import graphql.GraphQL
2626
import graphql.execution.AbortExecutionException
27+
import graphql.schema.DataFetchingEnvironment
2728
import graphql.schema.GraphQLSchema
2829
import io.mockk.every
2930
import io.mockk.mockk
@@ -122,6 +123,22 @@ class GraphQLRequestHandlerTest {
122123
assertNull(response.extensions)
123124
}
124125

126+
@Test
127+
@ExperimentalCoroutinesApi
128+
fun `execute graphQL query with graphql context`() = runBlockingTest {
129+
val context = mapOf("foo" to "JUNIT context value")
130+
val request = GraphQLRequest(query = "query { graphQLContextualValue }")
131+
132+
val response = graphQLRequestHandler.executeRequest(request, context = null, graphQLContext = context)
133+
assertNotNull(response.data as? Map<*, *>) { data ->
134+
assertNotNull(data["graphQLContextualValue"] as? String) { msg ->
135+
assertEquals("JUNIT context value", msg)
136+
}
137+
}
138+
assertNull(response.errors)
139+
assertNull(response.extensions)
140+
}
141+
125142
@Test
126143
@ExperimentalCoroutinesApi
127144
fun `execute graphQL query throwing uncaught exception`() = runBlockingTest {
@@ -164,6 +181,8 @@ class GraphQLRequestHandlerTest {
164181
fun alwaysThrows(): String = throw Exception("JUNIT Failure")
165182

166183
fun contextualValue(context: MyContext): String = context.value ?: "default"
184+
185+
fun graphQLContextualValue(dataFetchingEnvironment: DataFetchingEnvironment): String = dataFetchingEnvironment.graphQlContext.get("foo") ?: "default"
167186
}
168187

169188
data class MyContext(val value: String? = null) : GraphQLContext

servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLServerTest.kt

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,10 @@ class GraphQLServerTest {
4040
}
4141
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
4242
coEvery { generateContext(any()) } returns MockContext()
43+
coEvery { generateContextMap(any()) } returns mapOf("foo" to 1)
4344
}
4445
val mockHandler = mockk<GraphQLRequestHandler> {
45-
coEvery { executeRequest(any(), any()) } returns mockk()
46+
coEvery { executeRequest(any(), any(), any()) } returns mockk()
4647
}
4748

4849
val server = GraphQLServer(mockParser, mockContextFactory, mockHandler)
@@ -52,7 +53,7 @@ class GraphQLServerTest {
5253
coVerify(exactly = 1) {
5354
mockParser.parseRequest(any())
5455
mockContextFactory.generateContext(any())
55-
mockHandler.executeRequest(any(), any())
56+
mockHandler.executeRequest(any(), any(), any())
5657
}
5758
}
5859

@@ -63,9 +64,34 @@ class GraphQLServerTest {
6364
}
6465
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
6566
coEvery { generateContext(any()) } returns null
67+
coEvery { generateContextMap(any()) } returns mapOf(1 to "foo")
68+
}
69+
val mockHandler = mockk<GraphQLRequestHandler> {
70+
coEvery { executeRequest(any(), null, any()) } returns mockk()
71+
}
72+
73+
val server = GraphQLServer(mockParser, mockContextFactory, mockHandler)
74+
75+
runBlockingTest { server.execute(mockk()) }
76+
77+
coVerify(exactly = 1) {
78+
mockParser.parseRequest(any())
79+
mockContextFactory.generateContext(any())
80+
mockHandler.executeRequest(any(), null, any())
81+
}
82+
}
83+
84+
@Test
85+
fun `null graphQL context is used and passed to the request handler`() {
86+
val mockParser = mockk<GraphQLRequestParser<MockHttpRequest>> {
87+
coEvery { parseRequest(any()) } returns mockk<GraphQLRequest>()
88+
}
89+
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
90+
coEvery { generateContext(any()) } returns null
91+
coEvery { generateContextMap(any()) } returns null
6692
}
6793
val mockHandler = mockk<GraphQLRequestHandler> {
68-
coEvery { executeRequest(any(), null) } returns mockk()
94+
coEvery { executeRequest(any(), null, any()) } returns mockk()
6995
}
7096

7197
val server = GraphQLServer(mockParser, mockContextFactory, mockHandler)
@@ -75,7 +101,7 @@ class GraphQLServerTest {
75101
coVerify(exactly = 1) {
76102
mockParser.parseRequest(any())
77103
mockContextFactory.generateContext(any())
78-
mockHandler.executeRequest(any(), null)
104+
mockHandler.executeRequest(any(), any(), null)
79105
}
80106
}
81107

@@ -86,6 +112,7 @@ class GraphQLServerTest {
86112
}
87113
val mockContextFactory = mockk<GraphQLContextFactory<MockContext, MockHttpRequest>> {
88114
coEvery { generateContext(any()) } returns MockContext()
115+
coEvery { generateContextMap(any()) } returns null
89116
}
90117
val mockHandler = mockk<GraphQLRequestHandler> {
91118
coEvery { executeRequest(any(), any()) } returns mockk()
@@ -100,7 +127,7 @@ class GraphQLServerTest {
100127
}
101128
coVerify(exactly = 0) {
102129
mockContextFactory.generateContext(any())
103-
mockHandler.executeRequest(any(), any())
130+
mockHandler.executeRequest(any(), any(), any())
104131
}
105132
}
106133
}

servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/extensions/RequestExtensionsKtTest.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import org.dataloader.DataLoaderRegistry
2222
import org.junit.jupiter.api.Test
2323
import kotlin.test.assertEquals
2424
import kotlin.test.assertNotNull
25-
import kotlin.test.assertNull
2625

2726
class RequestExtensionsKtTest {
2827

@@ -31,7 +30,7 @@ class RequestExtensionsKtTest {
3130
val request = GraphQLRequest(query = "query { whatever }")
3231
val executionInput = request.toExecutionInput()
3332
assertEquals(request.query, executionInput.query)
34-
assertNull(executionInput.context)
33+
assertNotNull(executionInput.variables)
3534
assertNotNull(executionInput.dataLoaderRegistry)
3635
}
3736

@@ -67,4 +66,13 @@ class RequestExtensionsKtTest {
6766
assertEquals(request.query, executionInput.query)
6867
assertEquals(dataLoaderRegistry, executionInput.dataLoaderRegistry)
6968
}
69+
70+
@Test
71+
fun `verify can convert request with context map to execution input`() {
72+
val request = GraphQLRequest(query = "query { whatever }")
73+
val context = mapOf("foo" to 1)
74+
75+
val executionInput = request.toExecutionInput(graphQLContextMap = context)
76+
assertEquals(1, executionInput.graphQLContext.get("foo"))
77+
}
7078
}

servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/subscriptions/ApolloSubscriptionHooks.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,45 @@ interface ApolloSubscriptionHooks {
2929
* You can reject the connection by throwing an exception.
3030
* If you need to forward state to execution, update and return the [GraphQLContext].
3131
*/
32+
@Deprecated("The generic context object is deprecated in favor of the context map")
3233
fun onConnect(
3334
connectionParams: Map<String, String>,
3435
session: WebSocketSession,
3536
graphQLContext: GraphQLContext?
3637
): GraphQLContext? = graphQLContext
3738

39+
/**
40+
* Allows validation of connectionParams prior to starting the connection.
41+
* You can reject the connection by throwing an exception.
42+
* If you need to forward state to execution, update and return the context map.
43+
*/
44+
fun onConnectWithContext(
45+
connectionParams: Map<String, String>,
46+
session: WebSocketSession,
47+
graphQLContext: Map<*, Any>?
48+
): Map<*, Any>? = graphQLContext
49+
3850
/**
3951
* Called when the client executes a GraphQL operation.
4052
* The context can not be updated here, it is read only.
4153
*/
54+
@Deprecated("The generic context object is deprecated in favor of the context map")
4255
fun onOperation(
4356
operationMessage: SubscriptionOperationMessage,
4457
session: WebSocketSession,
4558
graphQLContext: GraphQLContext?
4659
): Unit = Unit
4760

61+
/**
62+
* Called when the client executes a GraphQL operation.
63+
* The context can not be updated here, it is read only.
64+
*/
65+
fun onOperationWithContext(
66+
operationMessage: SubscriptionOperationMessage,
67+
session: WebSocketSession,
68+
graphQLContext: Map<*, Any>?
69+
): Unit = Unit
70+
4871
/**
4972
* Called when client's unsubscribes
5073
*/

servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/subscriptions/ApolloSubscriptionProtocolHandler.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,10 @@ class ApolloSubscriptionProtocolHandler(
107107
session: WebSocketSession
108108
): Flux<SubscriptionOperationMessage> {
109109
val context = sessionState.getContext(session)
110+
val graphQLContext = sessionState.getGraphQLContext(session)
110111

111112
subscriptionHooks.onOperation(operationMessage, session, context)
113+
subscriptionHooks.onOperationWithContext(operationMessage, session, graphQLContext)
112114

113115
if (operationMessage.id == null) {
114116
logger.error("GraphQL subscription operation id is required")
@@ -130,7 +132,7 @@ class ApolloSubscriptionProtocolHandler(
130132

131133
try {
132134
val request = objectMapper.convertValue<GraphQLRequest>(payload)
133-
return subscriptionHandler.executeSubscription(request, context)
135+
return subscriptionHandler.executeSubscription(request, context, graphQLContext)
134136
.asFlux()
135137
.map {
136138
if (it.errors?.isNotEmpty() == true) {
@@ -164,8 +166,11 @@ class ApolloSubscriptionProtocolHandler(
164166
runBlocking {
165167
val connectionParams = castToMapOfStringString(operationMessage.payload)
166168
val context = contextFactory.generateContext(session)
167-
val onConnect = subscriptionHooks.onConnect(connectionParams, session, context)
168-
sessionState.saveContext(session, onConnect)
169+
val graphQLContext = contextFactory.generateContextMap(session)
170+
val onConnectContext = subscriptionHooks.onConnect(connectionParams, session, context)
171+
val onConnectGraphQLContext = subscriptionHooks.onConnectWithContext(connectionParams, session, graphQLContext)
172+
sessionState.saveContext(session, onConnectContext)
173+
sessionState.saveContextMap(session, onConnectGraphQLContext)
169174
}
170175
}
171176

0 commit comments

Comments
 (0)