Skip to content

Commit b65d61a

Browse files
smyrickShane Myrick
and
Shane Myrick
authored
Generate context with suspend and nullable in subscriptions (ExpediaGroup#1053)
* Generate context with suspend and nullable in subscriptions * Update tests * Do not cache null context for subscriptions * Rename internal methods * WIP * Update subsctiption javadocs * Use expression body * Update docs for context Co-authored-by: Shane Myrick <[email protected]>
1 parent 46807b8 commit b65d61a

File tree

28 files changed

+210
-200
lines changed

28 files changed

+210
-200
lines changed

examples/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ subprojects {
4040

4141
val kotlinVersion: String by project
4242
val junitVersion: String by project
43+
val kotlinCoroutinesVersion: String by project
4344

4445
val detektVersion: String by project
4546
val ktlintVersion: String by project
@@ -50,6 +51,8 @@ subprojects {
5051

5152
dependencies {
5253
implementation(kotlin("stdlib", kotlinVersion))
54+
implementation(kotlin("reflect", kotlinVersion))
55+
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$kotlinCoroutinesVersion")
5356
testImplementation(kotlin("test-junit5", kotlinVersion))
5457
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitVersion")
5558
testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitVersion")

examples/server/ktor-server/src/main/kotlin/com/expediagroup/graphql/examples/server/ktor/KtorGraphQLContextFactory.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import io.ktor.request.ApplicationRequest
2525
*/
2626
class KtorGraphQLContextFactory : GraphQLContextFactory<AuthorizedContext, ApplicationRequest> {
2727

28-
override fun generateContext(request: ApplicationRequest): AuthorizedContext {
28+
override suspend fun generateContext(request: ApplicationRequest): AuthorizedContext {
2929
val loggedInUser = User(
3030
email = "[email protected]",
3131
firstName = "Someone",

examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/context/MyGraphQLContext.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@ import org.springframework.web.reactive.socket.WebSocketSession
2424
* Simple [GraphQLContext] that holds extra value and the [ServerRequest]
2525
*/
2626
class MyGraphQLContext(
27-
val myCustomValue: String,
28-
val request: ServerRequest
27+
val request: ServerRequest,
28+
val myCustomValue: String
2929
) : GraphQLContext
3030

3131
/**
3232
* Simple [GraphQLContext] that holds extra value and the [WebSocketSession]
3333
*/
3434
class MySubscriptionGraphQLContext(
3535
val request: WebSocketSession,
36-
var subscriptionValue: String? = null
36+
var auth: String? = null
3737
) : GraphQLContext

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

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,20 @@ import org.springframework.web.reactive.socket.WebSocketSession
2929
@Component
3030
class MyGraphQLContextFactory : SpringGraphQLContextFactory<MyGraphQLContext>() {
3131

32-
override fun generateContext(request: ServerRequest): MyGraphQLContext = MyGraphQLContext(
33-
myCustomValue = request.headers().firstHeader("MyHeader") ?: "defaultContext",
34-
request = request
32+
override suspend fun generateContext(request: ServerRequest): MyGraphQLContext = MyGraphQLContext(
33+
request = request,
34+
myCustomValue = request.headers().firstHeader("MyHeader") ?: "defaultContext"
3535
)
3636
}
3737

38+
/**
39+
* [GraphQLContextFactory] that generates [MySubscriptionGraphQLContext] that will be available when processing subscription operations.
40+
*/
3841
@Component
3942
class MySubscriptionGraphQLContextFactory : SpringSubscriptionGraphQLContextFactory<MySubscriptionGraphQLContext>() {
4043

41-
override fun generateContext(request: WebSocketSession): MySubscriptionGraphQLContext = MySubscriptionGraphQLContext(
44+
override suspend fun generateContext(request: WebSocketSession): MySubscriptionGraphQLContext = MySubscriptionGraphQLContext(
4245
request = request,
43-
subscriptionValue = null
46+
auth = null
4447
)
4548
}

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

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ package com.expediagroup.graphql.examples.server.spring.execution
1919
import com.expediagroup.graphql.examples.server.spring.context.MySubscriptionGraphQLContext
2020
import com.expediagroup.graphql.generator.execution.GraphQLContext
2121
import com.expediagroup.graphql.server.spring.subscriptions.ApolloSubscriptionHooks
22-
import kotlinx.coroutines.reactor.mono
2322
import org.springframework.web.reactive.socket.WebSocketSession
24-
import reactor.core.publisher.Mono
2523

2624
/**
2725
* A simple implementation of Apollo Subscription Lifecycle Events.
@@ -31,12 +29,11 @@ class MySubscriptionHooks : ApolloSubscriptionHooks {
3129
override fun onConnect(
3230
connectionParams: Map<String, String>,
3331
session: WebSocketSession,
34-
graphQLContext: GraphQLContext
35-
): Mono<GraphQLContext> = mono {
36-
if (graphQLContext is MySubscriptionGraphQLContext) {
37-
val bearer = connectionParams["Authorization"] ?: "none"
38-
graphQLContext.subscriptionValue = bearer
32+
graphQLContext: GraphQLContext?
33+
): GraphQLContext? {
34+
if (graphQLContext != null && graphQLContext is MySubscriptionGraphQLContext) {
35+
graphQLContext.auth = connectionParams["Authorization"]
3936
}
40-
graphQLContext
37+
return graphQLContext
4138
}
4239
}

examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/subscriptions/SimpleSubscription.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,5 @@ class SimpleSubscription : Subscription {
8282

8383
@GraphQLDescription("Returns a value from the subscription context")
8484
fun subscriptionContext(myGraphQLContext: MySubscriptionGraphQLContext): Flux<String> =
85-
Flux.just(myGraphQLContext.subscriptionValue ?: "", "value 2", "value3")
85+
Flux.just(myGraphQLContext.auth ?: "no-auth")
8686
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ open class FunctionDataFetcher(
5959
* Invoke a suspend function or blocking function, passing in the [target] if not null or default to using the source from the environment.
6060
*/
6161
override fun get(environment: DataFetchingEnvironment): Any? {
62-
val instance = target ?: environment.getSource<Any?>()
62+
val instance: Any? = target ?: environment.getSource<Any?>()
6363
val instanceParameter = fn.instanceParameter
6464

6565
return if (instance != null && instanceParameter != null) {

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

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,8 @@
1616

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

19-
import java.util.concurrent.ConcurrentHashMap
20-
import java.util.concurrent.ConcurrentMap
21-
2219
/**
2320
* Marker interface to indicate that the implementing class should be considered
2421
* as the GraphQL context. This means the implementing class will not appear in the schema.
2522
*/
2623
interface GraphQLContext
27-
28-
/**
29-
* Default [GraphQLContext] that can be used if there is none provided. Exposes generic concurrent hash map
30-
* that can be populated with custom data.
31-
*/
32-
class DefaultGraphQLContext : GraphQLContext {
33-
val contents: ConcurrentMap<Any, Any> = ConcurrentHashMap()
34-
}

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

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

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

19-
import com.expediagroup.graphql.generator.execution.DefaultGraphQLContext
2019
import com.expediagroup.graphql.generator.execution.GraphQLContext
2120

2221
/**
@@ -28,12 +27,5 @@ interface GraphQLContextFactory<out Context : GraphQLContext, Request> {
2827
* Generate GraphQL context based on the incoming request and the corresponding response.
2928
* If no context should be generated and used in the request, return null.
3029
*/
31-
fun generateContext(request: Request): Context?
32-
}
33-
34-
/**
35-
* Default context factory that generates GraphQL context with empty concurrent map that can store any elements.
36-
*/
37-
class DefaultGraphQLContextFactory<T> : GraphQLContextFactory<DefaultGraphQLContext, T> {
38-
override fun generateContext(request: T) = DefaultGraphQLContext()
30+
suspend fun generateContext(request: Request): Context?
3931
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package com.expediagroup.graphql.server.extensions
1818

19-
import com.expediagroup.graphql.generator.execution.DefaultGraphQLContext
2019
import com.expediagroup.graphql.types.GraphQLRequest
2120
import graphql.ExecutionInput
2221
import org.dataloader.DataLoaderRegistry
@@ -29,6 +28,6 @@ fun GraphQLRequest.toExecutionInput(graphQLContext: Any? = null, dataLoaderRegis
2928
.query(this.query)
3029
.operationName(this.operationName)
3130
.variables(this.variables ?: emptyMap())
32-
.context(graphQLContext ?: DefaultGraphQLContext())
31+
.context(graphQLContext)
3332
.dataLoaderRegistry(dataLoaderRegistry ?: DataLoaderRegistry())
3433
.build()

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@
1616

1717
package com.expediagroup.graphql.server.extensions
1818

19-
import com.expediagroup.graphql.generator.execution.GraphQLContext
2019
import com.expediagroup.graphql.types.GraphQLRequest
2120
import io.mockk.mockk
2221
import org.dataloader.DataLoaderRegistry
2322
import org.junit.jupiter.api.Test
2423
import kotlin.test.assertEquals
2524
import kotlin.test.assertNotNull
26-
import kotlin.test.assertTrue
25+
import kotlin.test.assertNull
2726

2827
class RequestExtensionsKtTest {
2928

@@ -32,7 +31,7 @@ class RequestExtensionsKtTest {
3231
val request = GraphQLRequest(query = "query { whatever }")
3332
val executionInput = request.toExecutionInput()
3433
assertEquals(request.query, executionInput.query)
35-
assertTrue(executionInput.context is GraphQLContext)
34+
assertNull(executionInput.context)
3635
assertNotNull(executionInput.dataLoaderRegistry)
3736
}
3837

servers/graphql-kotlin-spring-server/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ tasks {
3535
limit {
3636
counter = "BRANCH"
3737
value = "COVEREDRATIO"
38-
minimum = "0.74".toBigDecimal()
38+
minimum = "0.70".toBigDecimal()
3939
}
4040
}
4141
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class SubscriptionAutoConfiguration {
6767

6868
@Bean
6969
@ConditionalOnMissingBean
70-
fun springSubscriptionGraphQLContextFactory(): SpringSubscriptionGraphQLContextFactory<*> = DefaultSpringSubscriptionGraphQLContextFactory
70+
fun springSubscriptionGraphQLContextFactory(): SpringSubscriptionGraphQLContextFactory<*> = DefaultSpringSubscriptionGraphQLContextFactory()
7171

7272
@Bean
7373
fun apolloSubscriptionProtocolHandler(

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

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

19-
import com.expediagroup.graphql.generator.execution.DefaultGraphQLContext
2019
import com.expediagroup.graphql.generator.execution.GraphQLContext
2120
import com.expediagroup.graphql.server.execution.GraphQLContextFactory
2221
import org.springframework.web.reactive.function.server.ServerRequest
@@ -27,8 +26,8 @@ import org.springframework.web.reactive.function.server.ServerRequest
2726
abstract class SpringGraphQLContextFactory<out T : GraphQLContext> : GraphQLContextFactory<T, ServerRequest>
2827

2928
/**
30-
* Basic implementation of [SpringGraphQLContextFactory] that just returns a [DefaultGraphQLContext]
29+
* Basic implementation of [SpringGraphQLContextFactory] that just returns null
3130
*/
32-
class DefaultSpringGraphQLContextFactory : SpringGraphQLContextFactory<DefaultGraphQLContext>() {
33-
override fun generateContext(request: ServerRequest) = DefaultGraphQLContext()
31+
class DefaultSpringGraphQLContextFactory : SpringGraphQLContextFactory<GraphQLContext>() {
32+
override suspend fun generateContext(request: ServerRequest): GraphQLContext? = null
3433
}

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package com.expediagroup.graphql.server.spring.subscriptions
1717

1818
import com.expediagroup.graphql.generator.execution.GraphQLContext
1919
import org.springframework.web.reactive.socket.WebSocketSession
20-
import reactor.core.publisher.Mono
2120

2221
/**
2322
* Implementation of Apollo Subscription Server Lifecycle Events
@@ -33,8 +32,8 @@ interface ApolloSubscriptionHooks {
3332
fun onConnect(
3433
connectionParams: Map<String, String>,
3534
session: WebSocketSession,
36-
graphQLContext: GraphQLContext
37-
): Mono<GraphQLContext> = Mono.just(graphQLContext)
35+
graphQLContext: GraphQLContext?
36+
): GraphQLContext? = graphQLContext
3837

3938
/**
4039
* Called when the client executes a GraphQL operation.
@@ -43,7 +42,7 @@ interface ApolloSubscriptionHooks {
4342
fun onOperation(
4443
operationMessage: SubscriptionOperationMessage,
4544
session: WebSocketSession,
46-
graphQLContext: GraphQLContext
45+
graphQLContext: GraphQLContext?
4746
): Unit = Unit
4847

4948
/**

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

Lines changed: 22 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616

1717
package com.expediagroup.graphql.server.spring.subscriptions
1818

19-
import com.expediagroup.graphql.generator.execution.DefaultGraphQLContext
20-
import com.expediagroup.graphql.generator.execution.GraphQLContext
2119
import com.expediagroup.graphql.server.spring.GraphQLConfigurationProperties
2220
import com.expediagroup.graphql.server.spring.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_INIT
2321
import com.expediagroup.graphql.server.spring.subscriptions.SubscriptionOperationMessage.ClientMessages.GQL_CONNECTION_TERMINATE
@@ -33,6 +31,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
3331
import com.fasterxml.jackson.module.kotlin.convertValue
3432
import com.fasterxml.jackson.module.kotlin.readValue
3533
import kotlinx.coroutines.ExperimentalCoroutinesApi
34+
import kotlinx.coroutines.runBlocking
3635
import org.slf4j.LoggerFactory
3736
import org.springframework.web.reactive.socket.WebSocketSession
3837
import reactor.core.publisher.Flux
@@ -66,7 +65,7 @@ class ApolloSubscriptionProtocolHandler(
6665
return try {
6766
when (operationMessage.type) {
6867
GQL_CONNECTION_INIT.type -> onInit(operationMessage, session)
69-
GQL_START.type -> onStart(operationMessage, session)
68+
GQL_START.type -> startSubscription(operationMessage, session)
7069
GQL_STOP.type -> onStop(operationMessage, session)
7170
GQL_CONNECTION_TERMINATE.type -> onDisconnect(session)
7271
else -> onUnknownOperation(operationMessage, session)
@@ -104,17 +103,18 @@ class ApolloSubscriptionProtocolHandler(
104103
@Suppress("Detekt.TooGenericExceptionCaught")
105104
private fun startSubscription(
106105
operationMessage: SubscriptionOperationMessage,
107-
session: WebSocketSession,
108-
context: GraphQLContext
106+
session: WebSocketSession
109107
): Flux<SubscriptionOperationMessage> {
108+
val context = sessionState.getContext(session)
109+
110110
subscriptionHooks.onOperation(operationMessage, session, context)
111111

112112
if (operationMessage.id == null) {
113113
logger.error("GraphQL subscription operation id is required")
114114
return Flux.just(basicConnectionErrorMessage)
115115
}
116116

117-
if (sessionState.operationExists(session, operationMessage)) {
117+
if (sessionState.doesOperationExist(session, operationMessage)) {
118118
logger.info("Already subscribed to operation ${operationMessage.id} for session ${session.id}")
119119
return Flux.empty()
120120
}
@@ -149,13 +149,23 @@ class ApolloSubscriptionProtocolHandler(
149149
}
150150

151151
private fun onInit(operationMessage: SubscriptionOperationMessage, session: WebSocketSession): Flux<SubscriptionOperationMessage> {
152-
val connectionParams = getConnectionParams(operationMessage.payload)
153-
val graphQLContext = contextFactory.generateContext(session) ?: DefaultGraphQLContext()
154-
val onConnect = subscriptionHooks.onConnect(connectionParams, session, graphQLContext)
155-
sessionState.saveContext(session, onConnect)
156-
val acknowledgeMessage = Flux.just(acknowledgeMessage)
152+
saveContext(operationMessage, session)
153+
val acknowledgeMessage = Mono.just(acknowledgeMessage)
157154
val keepAliveFlux = getKeepAliveFlux(session)
158155
return acknowledgeMessage.concatWith(keepAliveFlux)
156+
.onErrorReturn(getConnectionErrorMessage(operationMessage))
157+
}
158+
159+
/**
160+
* Generate the context and save it for all future messages.
161+
*/
162+
private fun saveContext(operationMessage: SubscriptionOperationMessage, session: WebSocketSession) {
163+
runBlocking {
164+
val connectionParams = getConnectionParams(operationMessage.payload)
165+
val context = contextFactory.generateContext(session)
166+
val onConnect = subscriptionHooks.onConnect(connectionParams, session, context)
167+
sessionState.saveContext(session, onConnect)
168+
}
159169
}
160170

161171
/**
@@ -172,25 +182,6 @@ class ApolloSubscriptionProtocolHandler(
172182
return emptyMap()
173183
}
174184

175-
/**
176-
* Called when the client sends the start message.
177-
* It triggers the specific hooks first, runs the operation, and appends it with a complete message.
178-
*/
179-
private fun onStart(
180-
operationMessage: SubscriptionOperationMessage,
181-
session: WebSocketSession
182-
): Flux<SubscriptionOperationMessage> {
183-
val context = sessionState.getContext(session)
184-
185-
// If we do not have a context, that means the init message was never sent
186-
return if (context != null) {
187-
context.flatMapMany { startSubscription(operationMessage, session, it) }
188-
} else {
189-
val message = getConnectionErrorMessage(operationMessage)
190-
Flux.just(message)
191-
}
192-
}
193-
194185
/**
195186
* Called with the publisher has completed on its own.
196187
*/
@@ -216,7 +207,7 @@ class ApolloSubscriptionProtocolHandler(
216207
private fun onDisconnect(session: WebSocketSession): Flux<SubscriptionOperationMessage> {
217208
subscriptionHooks.onDisconnect(session)
218209
sessionState.terminateSession(session)
219-
return Flux.empty<SubscriptionOperationMessage>()
210+
return Flux.empty()
220211
}
221212

222213
private fun onUnknownOperation(operationMessage: SubscriptionOperationMessage, session: WebSocketSession): Flux<SubscriptionOperationMessage> {

0 commit comments

Comments
 (0)