Skip to content

Commit 1ac514f

Browse files
committed
spring server updates
- update `GraphQLContextFactory` to use coroutines for building the context - extract `SubscriptionHandler` logic from default `WebSocketHandler`, default subscription `WebSocketHandler` will now call available `SubscriberHandler` bean - update `SimpleSubscriptionHandler` to use `GraphQLContext` - fixes #360 - add error handling to `SimpleSubscriptionHandler`
1 parent 2d319b4 commit 1ac514f

File tree

14 files changed

+370
-63
lines changed

14 files changed

+370
-63
lines changed

examples/spring/src/main/kotlin/com/expediagroup/graphql/sample/context/MyGraphQLContextFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import org.springframework.stereotype.Component
1111
@Component
1212
class MyGraphQLContextFactory: GraphQLContextFactory<MyGraphQLContext> {
1313

14-
override fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): MyGraphQLContext = MyGraphQLContext(
14+
override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): MyGraphQLContext = MyGraphQLContext(
1515
myCustomValue = request.headers.getFirst("MyHeader") ?: "defaultContext",
1616
request = request,
1717
response = response)

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/SubscriptionAutoConfiguration.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.expediagroup.graphql.spring
1818

1919
import com.expediagroup.graphql.spring.execution.SimpleSubscriptionHandler
2020
import com.expediagroup.graphql.spring.execution.SubscriptionHandler
21+
import com.expediagroup.graphql.spring.execution.SubscriptionWebSocketHandler
2122
import com.expediagroup.graphql.spring.operations.Subscription
2223
import com.fasterxml.jackson.databind.ObjectMapper
2324
import graphql.GraphQL
@@ -39,13 +40,16 @@ class SubscriptionAutoConfiguration {
3940

4041
@Bean
4142
@ConditionalOnMissingBean
42-
fun subscriptionHandler(graphQL: GraphQL, objectMapper: ObjectMapper): SubscriptionHandler = SimpleSubscriptionHandler(graphQL, objectMapper)
43+
fun subscriptionHandler(graphQL: GraphQL): SubscriptionHandler = SimpleSubscriptionHandler(graphQL)
4344

4445
@Bean
4546
@ConditionalOnMissingBean
4647
fun websocketHandlerAdapter(): WebSocketHandlerAdapter = WebSocketHandlerAdapter()
4748

4849
@Bean
49-
fun subscriptionHandlerMapping(config: GraphQLConfigurationProperties, subscriptionHandler: SubscriptionHandler): HandlerMapping =
50-
SimpleUrlHandlerMapping(mapOf(config.subscriptions.endpoint to subscriptionHandler), Ordered.HIGHEST_PRECEDENCE)
50+
fun subscriptionWebSocketHandler(handler: SubscriptionHandler, objectMapper: ObjectMapper) = SubscriptionWebSocketHandler(handler, objectMapper)
51+
52+
@Bean
53+
fun subscriptionHandlerMapping(config: GraphQLConfigurationProperties, subscriptionWebSocketHandler: SubscriptionWebSocketHandler): HandlerMapping =
54+
SimpleUrlHandlerMapping(mapOf(config.subscriptions.endpoint to subscriptionWebSocketHandler), Ordered.HIGHEST_PRECEDENCE)
5155
}

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilter.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,29 @@
1616

1717
package com.expediagroup.graphql.spring.execution
1818

19+
import kotlinx.coroutines.reactor.mono
1920
import org.springframework.core.Ordered
20-
import org.springframework.core.annotation.Order
2121
import org.springframework.web.server.ServerWebExchange
2222
import org.springframework.web.server.WebFilter
2323
import org.springframework.web.server.WebFilterChain
2424
import reactor.core.publisher.Mono
2525

2626
/**
2727
* [org.springframework.core.Ordered] value used for the [ContextWebFilter] order in which it will be applied to the incoming requests.
28+
* Smaller value take higher precedence.
2829
*/
2930
const val GRAPHQL_CONTEXT_FILTER_ODER = 0
3031

3132
/**
3233
* Default web filter that populates GraphQL context in the reactor subscriber context.
3334
*/
34-
@Order(GRAPHQL_CONTEXT_FILTER_ODER)
3535
class ContextWebFilter(private val contextFactory: GraphQLContextFactory<Any>) : WebFilter, Ordered {
3636

3737
@Suppress("ForbiddenVoid")
38-
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
39-
val context = contextFactory.generateContext(exchange.request, exchange.response)
40-
return chain.filter(exchange).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, context) }
38+
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> = mono {
39+
contextFactory.generateContext(exchange.request, exchange.response)
40+
}.flatMap { graphQLContext ->
41+
chain.filter(exchange).subscriberContext { it.put(GRAPHQL_CONTEXT_KEY, graphQLContext) }
4142
}
4243

4344
override fun getOrder(): Int = GRAPHQL_CONTEXT_FILTER_ODER

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ interface GraphQLContextFactory<out T : Any> {
3333
/**
3434
* Generate GraphQL context based on the incoming request and the corresponding response.
3535
*/
36-
fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): T
36+
suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): T
3737
}
3838

3939
/**
4040
* Default context factory that generates empty GraphQL context.
4141
*/
4242
internal object EmptyContextFactory : GraphQLContextFactory<GraphQLContext> {
4343

44-
override fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): GraphQLContext = GraphQLContext.newContext().build()
44+
override suspend fun generateContext(request: ServerHttpRequest, response: ServerHttpResponse): GraphQLContext = GraphQLContext.newContext().build()
4545
}

graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/spring/execution/SubscriptionHandler.kt

Lines changed: 16 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -16,65 +16,41 @@
1616

1717
package com.expediagroup.graphql.spring.execution
1818

19+
import com.expediagroup.graphql.spring.exception.SimpleKotlinGraphQLError
1920
import com.expediagroup.graphql.spring.model.GraphQLRequest
2021
import com.expediagroup.graphql.spring.model.GraphQLResponse
2122
import com.expediagroup.graphql.spring.model.toExecutionInput
2223
import com.expediagroup.graphql.spring.model.toGraphQLResponse
23-
import com.fasterxml.jackson.databind.ObjectMapper
24-
import com.fasterxml.jackson.module.kotlin.readValue
2524
import graphql.ExecutionResult
2625
import graphql.GraphQL
2726
import org.reactivestreams.Publisher
28-
import org.slf4j.LoggerFactory
29-
import org.springframework.web.reactive.socket.WebSocketHandler
30-
import org.springframework.web.reactive.socket.WebSocketSession
3127
import reactor.core.publisher.Flux
3228
import reactor.core.publisher.Mono
3329
import reactor.core.publisher.toFlux
3430

3531
/**
36-
* WebSocket handler for handling GraphQL subscriptions.
32+
* GraphQL subscription handler.
3733
*/
38-
interface SubscriptionHandler : WebSocketHandler {
34+
interface SubscriptionHandler {
3935

4036
/**
41-
* Execute GraphQL subscription request and return a Publisher that emits 0 to N [GraphQLResponse]s.
37+
* Execute GraphQL subscription request and return a Reactor Flux Publisher that emits 0 to N [GraphQLResponse]s.
4238
*/
4339
fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse>
4440
}
4541

4642
/**
47-
* Default WebSocket handler for handling GraphQL subscriptions.
43+
* Default implementation of GraphQL subscription handler.
4844
*/
49-
open class SimpleSubscriptionHandler(
50-
private val graphQL: GraphQL,
51-
private val objectMapper: ObjectMapper
52-
) : SubscriptionHandler {
53-
54-
private val logger = LoggerFactory.getLogger(SubscriptionHandler::class.java)
55-
56-
@Suppress("ForbiddenVoid")
57-
override fun handle(session: WebSocketSession): Mono<Void> {
58-
val response = session.receive()
59-
.concatMap {
60-
val graphQLRequest = objectMapper.readValue<GraphQLRequest>(it.payloadAsText)
61-
executeSubscription(graphQLRequest)
62-
.doOnSubscribe { logger.trace("WebSocket GraphQL subscription subscribe, ID=${session.id}") }
63-
.doOnCancel { logger.trace("WebSocket GraphQL subscription cancel, ID=${session.id}") }
64-
.doOnComplete { logger.trace("WebSocket GraphQL subscription complete, ID=${session.id}") }
65-
.doFinally { session.close() }
66-
}
67-
.map { objectMapper.writeValueAsString(it) }
68-
.map { session.textMessage(it) }
69-
70-
return session.send(response)
71-
}
72-
73-
override fun getSubProtocols(): List<String> = listOf("graphql-ws")
74-
75-
override fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse> =
76-
graphQL.execute(graphQLRequest.toExecutionInput())
77-
.getData<Publisher<ExecutionResult>>()
78-
.toFlux()
79-
.map { it.toGraphQLResponse() }
45+
open class SimpleSubscriptionHandler(private val graphQL: GraphQL) : SubscriptionHandler {
46+
47+
override fun executeSubscription(graphQLRequest: GraphQLRequest): Flux<GraphQLResponse> = Mono.subscriberContext().
48+
flatMapMany { reactorContext ->
49+
val graphQLContext = reactorContext.getOrDefault<Any>(GRAPHQL_CONTEXT_KEY, null)
50+
graphQL.execute(graphQLRequest.toExecutionInput(graphQLContext))
51+
.getData<Publisher<ExecutionResult>>()
52+
.toFlux()
53+
.map { result -> result.toGraphQLResponse() }
54+
.onErrorResume { error -> Flux.just(GraphQLResponse(errors = listOf(SimpleKotlinGraphQLError(error)))) }
55+
}
8056
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.spring.execution
18+
19+
import com.expediagroup.graphql.spring.model.GraphQLRequest
20+
import com.fasterxml.jackson.databind.ObjectMapper
21+
import com.fasterxml.jackson.module.kotlin.readValue
22+
import org.slf4j.LoggerFactory
23+
import org.springframework.web.reactive.socket.WebSocketHandler
24+
import org.springframework.web.reactive.socket.WebSocketSession
25+
import reactor.core.publisher.Mono
26+
27+
/**
28+
* Default WebSocket handler for handling GraphQL subscriptions.
29+
*/
30+
class SubscriptionWebSocketHandler(
31+
private val subscriptionHandler: SubscriptionHandler,
32+
private val objectMapper: ObjectMapper
33+
) : WebSocketHandler {
34+
35+
private val logger = LoggerFactory.getLogger(SubscriptionWebSocketHandler::class.java)
36+
37+
@Suppress("ForbiddenVoid")
38+
override fun handle(session: WebSocketSession): Mono<Void> {
39+
val response = session.receive()
40+
.concatMap {
41+
val graphQLRequest = objectMapper.readValue<GraphQLRequest>(it.payloadAsText)
42+
subscriptionHandler.executeSubscription(graphQLRequest)
43+
.doOnSubscribe { logger.trace("WebSocket GraphQL subscription subscribe, ID=${session.id}") }
44+
.doOnCancel { logger.trace("WebSocket GraphQL subscription cancel, ID=${session.id}") }
45+
.doOnComplete { logger.trace("WebSocket GraphQL subscription complete, ID=${session.id}") }
46+
.doFinally { session.close() }
47+
}
48+
.map { objectMapper.writeValueAsString(it) }
49+
.map { session.textMessage(it) }
50+
51+
return session.send(response)
52+
}
53+
54+
override fun getSubProtocols(): List<String> = listOf("graphql-ws")
55+
}

graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/FederationConfigurationTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.expediagroup.graphql.spring
218

319
import com.expediagroup.graphql.SchemaGeneratorConfig

graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SchemaConfigurationTest.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.expediagroup.graphql.spring
218

319
import com.expediagroup.graphql.SchemaGeneratorConfig

graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/SubscriptionConfigurationTest.kt

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.expediagroup.graphql.spring
218

319
import com.expediagroup.graphql.SchemaGeneratorConfig
@@ -8,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
824
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
925
import graphql.GraphQL
1026
import graphql.schema.GraphQLSchema
27+
import io.mockk.every
1128
import io.mockk.mockk
1229
import org.assertj.core.api.Assertions.assertThat
1330
import org.junit.jupiter.api.Test
@@ -99,7 +116,9 @@ class SubscriptionConfigurationTest {
99116
fun subscription(): Subscription = SimpleSubscription()
100117

101118
@Bean
102-
fun subscriptionHandler(): SubscriptionHandler = mockk()
119+
fun subscriptionHandler(): SubscriptionHandler = mockk {
120+
every { executeSubscription(any()) } returns Flux.empty()
121+
}
103122

104123
@Bean
105124
fun webSocketHandlerAdapter(): WebSocketHandlerAdapter = mockk()

graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/ContextWebFilterTest.kt

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,25 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.expediagroup.graphql.spring.execution
218

319
import graphql.GraphQLContext
420
import io.mockk.every
521
import io.mockk.mockk
622
import org.junit.jupiter.api.Test
7-
import org.springframework.http.server.reactive.ServerHttpRequest
823
import org.springframework.web.server.ServerWebExchange
924
import org.springframework.web.server.WebFilterChain
1025
import reactor.core.publisher.Mono
@@ -15,13 +30,12 @@ class ContextWebFilterTest {
1530

1631
@Test
1732
fun `verify web filter populates context in the subscriber context`() {
18-
val mockRequest = mockk<ServerHttpRequest>()
19-
val exchange = mockk<ServerWebExchange> {
20-
every { request } returns mockRequest
33+
val exchange: ServerWebExchange = mockk {
34+
every { request } returns mockk()
2135
every { response } returns mockk()
2236
}
23-
val chain = mockk<WebFilterChain> {
24-
every { filter(exchange) } returns Mono.empty()
37+
val chain: WebFilterChain = mockk {
38+
every { filter(any()) } returns Mono.empty()
2539
}
2640

2741
val contextFilter = ContextWebFilter(EmptyContextFactory)

graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/spring/execution/QueryHandlerTest.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
/*
2+
* Copyright 2019 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
117
package com.expediagroup.graphql.spring.execution
218

319
import com.expediagroup.graphql.SchemaGeneratorConfig
@@ -99,7 +115,7 @@ class QueryHandlerTest {
99115
@Test
100116
@ExperimentalCoroutinesApi
101117
fun `execute graphQL query with context`() = runBlockingTest(Context.of(GRAPHQL_CONTEXT_KEY, MyContext("JUNIT context value")).asCoroutineContext()) {
102-
val request = GraphQLRequest(query = "query { context }")
118+
val request = GraphQLRequest(query = "query { contextualValue }")
103119

104120
val response = queryHandler.executeQuery(request)
105121
assertNotNull(response.data as? Map<*, *>) { data ->
@@ -137,7 +153,7 @@ class QueryHandlerTest {
137153

138154
fun alwaysThrows(): String = throw GraphQLKotlinException("JUNIT Failure")
139155

140-
fun context(@GraphQLContext context: MyContext): String = context.value ?: "default"
156+
fun contextualValue(@GraphQLContext context: MyContext): String = context.value ?: "default"
141157
}
142158

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

0 commit comments

Comments
 (0)