Skip to content

Commit 976052a

Browse files
Shane Myrickdariuszkuc
authored andcommitted
feat: subscriptions (#215)
* feat: subscriptions * rename extension method
1 parent c51ad9b commit 976052a

File tree

22 files changed

+374
-49
lines changed

22 files changed

+374
-49
lines changed

example/pom.xml

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,40 @@
6767
<graphql-kotlin.version>0.2.14-SNAPSHOT</graphql-kotlin.version>
6868
</properties>
6969

70+
<dependencies>
71+
<dependency>
72+
<groupId>org.jetbrains.kotlin</groupId>
73+
<artifactId>kotlin-stdlib</artifactId>
74+
<version>${kotlin.version}</version>
75+
</dependency>
76+
<dependency>
77+
<groupId>com.expedia</groupId>
78+
<artifactId>graphql-kotlin</artifactId>
79+
<version>${graphql-kotlin.version}</version>
80+
</dependency>
81+
<dependency>
82+
<groupId>org.springframework.boot</groupId>
83+
<artifactId>spring-boot-starter-webflux</artifactId>
84+
<version>2.1.2.RELEASE</version>
85+
</dependency>
86+
<dependency>
87+
<groupId>io.projectreactor.netty</groupId>
88+
<artifactId>reactor-netty</artifactId>
89+
<version>0.8.4.RELEASE</version>
90+
</dependency>
91+
<dependency>
92+
<groupId>org.jetbrains.kotlin</groupId>
93+
<artifactId>kotlin-test</artifactId>
94+
<version>${kotlin.version}</version>
95+
<scope>test</scope>
96+
</dependency>
97+
<dependency>
98+
<groupId>com.fasterxml.jackson.module</groupId>
99+
<artifactId>jackson-module-kotlin</artifactId>
100+
<version>2.8.8</version>
101+
</dependency>
102+
</dependencies>
103+
70104
<build>
71105
<sourceDirectory>src/main/kotlin</sourceDirectory>
72106
<plugins>
@@ -107,26 +141,4 @@
107141
</plugins>
108142
</build>
109143

110-
<dependencies>
111-
<dependency>
112-
<groupId>org.jetbrains.kotlin</groupId>
113-
<artifactId>kotlin-stdlib</artifactId>
114-
<version>${kotlin.version}</version>
115-
</dependency>
116-
<dependency>
117-
<groupId>com.expedia</groupId>
118-
<artifactId>graphql-kotlin</artifactId>
119-
<version>${graphql-kotlin.version}</version>
120-
</dependency>
121-
<dependency>
122-
<groupId>org.springframework.boot</groupId>
123-
<artifactId>spring-boot-starter-webflux</artifactId>
124-
</dependency>
125-
<dependency>
126-
<groupId>org.jetbrains.kotlin</groupId>
127-
<artifactId>kotlin-test</artifactId>
128-
<version>${kotlin.version}</version>
129-
<scope>test</scope>
130-
</dependency>
131-
</dependencies>
132144
</project>

example/src/main/kotlin/com/expedia/graphql/sample/Application.kt

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,20 @@ import com.expedia.graphql.sample.exceptions.CustomDataFetcherExceptionHandler
1313
import com.expedia.graphql.sample.extension.CustomSchemaGeneratorHooks
1414
import com.expedia.graphql.sample.mutation.Mutation
1515
import com.expedia.graphql.sample.query.Query
16+
import com.expedia.graphql.sample.subscribtions.Subscription
1617
import com.expedia.graphql.toSchema
1718
import graphql.GraphQL
1819
import graphql.execution.AsyncExecutionStrategy
1920
import graphql.execution.AsyncSerialExecutionStrategy
2021
import graphql.execution.DataFetcherExceptionHandler
22+
import graphql.execution.SubscriptionExecutionStrategy
2123
import graphql.schema.GraphQLSchema
2224
import graphql.schema.idl.SchemaPrinter
2325
import org.slf4j.LoggerFactory
2426
import org.springframework.boot.autoconfigure.SpringBootApplication
2527
import org.springframework.boot.runApplication
2628
import org.springframework.context.annotation.Bean
29+
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter
2730
import javax.validation.Validator
2831

2932
@SpringBootApplication
@@ -62,33 +65,43 @@ class Application {
6265
fun schema(
6366
queries: List<Query>,
6467
mutations: List<Mutation>,
68+
subscriptions: List<Subscription>,
6569
schemaConfig: SchemaGeneratorConfig,
6670
schemaPrinter: SchemaPrinter
6771
): GraphQLSchema {
68-
fun List<Any>.toTopLevelObjectDefs() = this.map {
72+
fun List<Any>.toTopLevelObjects() = this.map {
6973
TopLevelObject(it)
7074
}
7175

7276
val schema = toSchema(
7377
config = schemaConfig,
74-
queries = queries.toTopLevelObjectDefs(),
75-
mutations = mutations.toTopLevelObjectDefs()
78+
queries = queries.toTopLevelObjects(),
79+
mutations = mutations.toTopLevelObjects(),
80+
subsciptions = subscriptions.toTopLevelObjects()
7681
)
7782

7883
logger.info(schemaPrinter.print(schema))
84+
7985
return schema
8086
}
8187

8288
@Bean
8389
fun dataFetcherExceptionHandler(): DataFetcherExceptionHandler = CustomDataFetcherExceptionHandler()
8490

91+
@Bean
92+
fun subscriptionHandler(graphQL: GraphQL) = SubscriptionHandler(graphQL)
93+
94+
@Bean
95+
fun websocketHandlerAdapter() = WebSocketHandlerAdapter()
96+
8597
@Bean
8698
fun graphQL(
8799
schema: GraphQLSchema,
88100
dataFetcherExceptionHandler: DataFetcherExceptionHandler
89101
): GraphQL = GraphQL.newGraphQL(schema)
90102
.queryExecutionStrategy(AsyncExecutionStrategy(dataFetcherExceptionHandler))
91103
.mutationExecutionStrategy(AsyncSerialExecutionStrategy(dataFetcherExceptionHandler))
104+
.subscriptionExecutionStrategy(SubscriptionExecutionStrategy(dataFetcherExceptionHandler))
92105
.build()
93106
}
94107

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
package com.expedia.graphql.sample
22

3+
import com.expedia.graphql.sample.context.MyGraphQLContext
4+
import graphql.ExecutionInput
5+
36
data class GraphQLRequest(
47
val query: String,
58
val operationName: String? = null,
69
val variables: Map<String, Any>? = null
710
)
11+
12+
fun GraphQLRequest.toExecutionInput(graphQLContext: MyGraphQLContext? = null): ExecutionInput =
13+
ExecutionInput.newExecutionInput()
14+
.query(this.query)
15+
.operationName(this.operationName)
16+
.variables(this.variables)
17+
.context(graphQLContext)
18+
.build()
Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.expedia.graphql.sample
22

33
import com.expedia.graphql.sample.context.MyGraphQLContext
4-
import graphql.ExecutionInput
54
import graphql.GraphQL
65
import org.springframework.stereotype.Component
76
import reactor.core.publisher.Mono
@@ -12,14 +11,9 @@ class QueryHandler(private val graphql: GraphQL) {
1211
fun executeQuery(request: GraphQLRequest): Mono<GraphQLResponse> = Mono.subscriberContext()
1312
.flatMap { ctx ->
1413
val graphQLContext: MyGraphQLContext = ctx.get("graphQLContext")
15-
val input = ExecutionInput.newExecutionInput()
16-
.query(request.query)
17-
.operationName(request.operationName)
18-
.variables(request.variables)
19-
.context(graphQLContext)
20-
.build()
14+
val input = request.toExecutionInput(graphQLContext)
2115

2216
Mono.fromFuture(graphql.executeAsync(input))
2317
.map { executionResult -> executionResult.toGraphQLResponse() }
2418
}
25-
}
19+
}

example/src/main/kotlin/com/expedia/graphql/sample/RoutesConfiguration.kt

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.type.MapType
55
import com.fasterxml.jackson.databind.type.TypeFactory
66
import graphql.schema.GraphQLSchema
77
import graphql.schema.idl.SchemaPrinter
8+
import org.slf4j.LoggerFactory
89
import org.springframework.beans.factory.annotation.Value
910
import org.springframework.context.annotation.Bean
1011
import org.springframework.context.annotation.Configuration
@@ -13,6 +14,8 @@ import org.springframework.http.HttpMethod
1314
import org.springframework.http.MediaType
1415
import org.springframework.web.reactive.function.server.ServerRequest
1516
import org.springframework.web.reactive.function.server.bodyToMono
17+
import org.springframework.web.reactive.function.server.html
18+
import org.springframework.web.reactive.function.server.json
1619
import org.springframework.web.reactive.function.server.router
1720
import reactor.core.publisher.Mono
1821

@@ -25,16 +28,21 @@ class RoutesConfiguration(
2528
@Value("classpath:/graphql-playground.html") private val playgroundHtml: Resource
2629
) {
2730

31+
private val logger = LoggerFactory.getLogger(RoutesConfiguration::class.java)
2832
private val mapTypeReference: MapType = TypeFactory.defaultInstance().constructMapType(HashMap::class.java, String::class.java, Any::class.java)
2933

3034
@Bean
3135
fun graphQLRoutes() = router {
3236
(POST("/graphql") or GET("/graphql")).invoke { serverRequest ->
3337
createGraphQLRequest(serverRequest)
3438
.flatMap { graphQLRequest -> queryHandler.executeQuery(graphQLRequest) }
35-
.flatMap { result -> ok().contentType(MediaType.APPLICATION_JSON).syncBody(result) }
39+
.flatMap { result -> ok().json().syncBody(result) }
3640
.switchIfEmpty(badRequest().build())
3741
}
42+
}
43+
44+
@Bean
45+
fun sdlRoute() = router {
3846
GET("/sdl") {
3947
ok().contentType(MediaType.TEXT_PLAIN).syncBody(schemaPrinter.print(schema))
4048
}
@@ -43,16 +51,16 @@ class RoutesConfiguration(
4351
@Bean
4452
fun graphQLToolRoutes() = router {
4553
GET("/playground") {
46-
ok().contentType(MediaType.TEXT_HTML).syncBody(playgroundHtml)
54+
ok().html().syncBody(playgroundHtml)
4755
}
4856
}
4957

5058
private fun createGraphQLRequest(serverRequest: ServerRequest): Mono<GraphQLRequest> = when {
5159
serverRequest.method() == HttpMethod.POST -> serverRequest.bodyToMono()
5260
serverRequest.queryParam("query").isPresent -> {
5361
val query = serverRequest.queryParam("query").get()
54-
val operationName = serverRequest.queryParam("operationName").orElseGet { null }
55-
val variables = serverRequest.queryParam("variables").orElseGet { null }
62+
val operationName: String? = serverRequest.queryParam("operationName").orElseGet { null }
63+
val variables: String? = serverRequest.queryParam("variables").orElseGet { null }
5664
val graphQLVariables: Map<String, Any>? = variables?.let {
5765
objectMapper.readValue(it, mapTypeReference)
5866
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.expedia.graphql.sample
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.fasterxml.jackson.module.kotlin.readValue
5+
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
6+
import graphql.ExecutionResult
7+
import graphql.GraphQL
8+
import org.reactivestreams.Publisher
9+
import org.slf4j.LoggerFactory
10+
import org.springframework.web.reactive.socket.WebSocketHandler
11+
import org.springframework.web.reactive.socket.WebSocketSession
12+
import reactor.core.publisher.Mono
13+
14+
class SubscriptionHandler(private val graphQL: GraphQL) : WebSocketHandler {
15+
16+
private val objectMapper = ObjectMapper().registerKotlinModule()
17+
private val logger = LoggerFactory.getLogger(SubscriptionHandler::class.java)
18+
19+
override fun handle(session: WebSocketSession): Mono<Void> {
20+
21+
// This will not work with Apollo Client. There needs to be special logic to handle the "graphql-ws"
22+
// sub-protocol. That will be up to the server implementation to handle though.
23+
//
24+
// See: https://github.com/ExpediaDotCom/graphql-kotlin/issues/155
25+
return session.send(session.receive()
26+
.doOnSubscribe {
27+
logger.info("Session starting. ID ${session.id}")
28+
}
29+
.doOnCancel {
30+
logger.info("Closing session: ID ${session.id}")
31+
}
32+
.concatMap {
33+
val graphQLRequest = objectMapper.readValue<GraphQLRequest>(it.payloadAsText)
34+
val executionInput = graphQLRequest.toExecutionInput()
35+
val executionResult = graphQL.execute(executionInput)
36+
executionResult.getData<Publisher<ExecutionResult>>()
37+
}
38+
.map { objectMapper.writeValueAsString(it.toGraphQLResponse()) }
39+
.map { session.textMessage(it) }
40+
)
41+
}
42+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.expedia.graphql.sample
2+
3+
import org.springframework.context.annotation.Bean
4+
import org.springframework.context.annotation.Configuration
5+
import org.springframework.core.Ordered.HIGHEST_PRECEDENCE
6+
import org.springframework.web.reactive.HandlerMapping
7+
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping
8+
9+
@Configuration
10+
class WebSocketConfig {
11+
12+
@Bean
13+
fun handlerMapping(subscriptionHandler: SubscriptionHandler) : HandlerMapping {
14+
val handlerMapping = SimpleUrlHandlerMapping()
15+
16+
handlerMapping.urlMap = mapOf("/subscriptions" to subscriptionHandler)
17+
handlerMapping.order = HIGHEST_PRECEDENCE
18+
19+
return handlerMapping
20+
}
21+
}

example/src/main/kotlin/com/expedia/graphql/sample/context/MyGraphQLContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ import org.springframework.http.server.reactive.ServerHttpResponse
77
/**
88
* Simple [GraphQLContext] that holds extra value.
99
*/
10-
class MyGraphQLContext(val myCustomValue: String, val request: ServerHttpRequest, val response: ServerHttpResponse)
10+
class MyGraphQLContext(val myCustomValue: String, val request: ServerHttpRequest, val response: ServerHttpResponse)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.expedia.graphql.sample.subscribtions
2+
3+
import com.expedia.graphql.annotations.GraphQLDescription
4+
import org.springframework.stereotype.Component
5+
import reactor.core.publisher.Flux
6+
import reactor.core.publisher.Mono
7+
import java.time.Duration
8+
import kotlin.random.Random
9+
10+
@Component
11+
class SimpleSubscription : Subscription {
12+
13+
@GraphQLDescription("Returns a single value")
14+
fun singleValueSubscription(): Mono<Int> = Mono.just(1)
15+
16+
@GraphQLDescription("Returns a random number every second")
17+
fun counter(): Flux<Int> = Flux.interval(Duration.ofSeconds(1)).map { Random.nextInt() }
18+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package com.expedia.graphql.sample.subscribtions
2+
3+
interface Subscription

0 commit comments

Comments
 (0)