Skip to content

Commit 18d16e0

Browse files
dariuszkucsmyrick
authored andcommitted
Update example app to use Spring Webflux and GraphQL Playground (#190)
* Update example app to use Spring Webflux and GraphQL Playground * example app is no longer using any Servlet based API (i.e. `graphql-java-servlet` and `graphiql-spring-boot-starter`) * example app is now using Spring Webflux and Netty for non-blocking server * introduce GraphQL Playground (https://github.com/prisma/graphql-playground) as a replacement for GraphiQL * enable editorconfig and fix formatting * remove unused properties * add back example recursive query
1 parent 46ed5c2 commit 18d16e0

File tree

15 files changed

+739
-148
lines changed

15 files changed

+739
-148
lines changed

.editorconfig

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# EditorConfig is awesome: http://EditorConfig.org
2+
3+
# top-most EditorConfig file
4+
root = true
5+
6+
# Unix-style newlines with a newline ending every file
7+
[*]
8+
charset = utf-8
9+
end_of_line = lf
10+
indent_style = space
11+
insert_final_newline = true
12+
trim_trailing_whitespace = true
13+
14+
# space indentation for JSON and YML
15+
[*.{json,yml}]
16+
indent_size = 2
17+
18+
# space indentation for Kotlin
19+
[*.{kt,kts}]
20+
indent_size = 4
21+
max_line_length = 200

example/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# graphql-kotlin example
22

3-
One way to run a GraphQL server is with spring boot. This example app uses `graphql-kotlin`, `graphql-java-servlet` and `graphiql`.
3+
One way to run a GraphQL server is with [Spring Boot](https://github.com/spring-projects/spring-boot). This example app uses [Spring Webflux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html) together with `graphql-kotlin` and [graphql-playground](https://github.com/prisma/graphql-playground).
44

55

66
### Running locally
@@ -16,4 +16,4 @@ Start the server:
1616
* Alternatively you can also use the spring boot maven plugin by running `mvn spring-boot:run` from the command line.
1717

1818

19-
Once the app has started you can explore the example schema by opening GraphiQL endpoint at http://localhost:8080/graphiql.
19+
Once the app has started you can explore the example schema by opening Playground endpoint at http://localhost:8080/playground.

example/pom.xml

Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<parent>
66
<groupId>org.springframework.boot</groupId>
77
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>2.1.0.RELEASE</version>
8+
<version>2.1.2.RELEASE</version>
99
<relativePath/>
1010
</parent>
1111

@@ -65,8 +65,6 @@
6565

6666
<!-- Dependency Versions -->
6767
<graphql-kotlin.version>0.2.7-SNAPSHOT</graphql-kotlin.version>
68-
<graphiql.version>5.0.2</graphiql.version>
69-
<graphql-java-servlet.version>6.1.3</graphql-java-servlet.version>
7068
</properties>
7169

7270
<build>
@@ -110,48 +108,25 @@
110108
</build>
111109

112110
<dependencies>
111+
<dependency>
112+
<groupId>org.jetbrains.kotlin</groupId>
113+
<artifactId>kotlin-stdlib</artifactId>
114+
<version>${kotlin.version}</version>
115+
</dependency>
113116
<dependency>
114117
<groupId>com.expedia</groupId>
115118
<artifactId>graphql-kotlin</artifactId>
116119
<version>${graphql-kotlin.version}</version>
117120
</dependency>
118-
119121
<dependency>
120122
<groupId>org.springframework.boot</groupId>
121-
<artifactId>spring-boot-starter-web</artifactId>
122-
<version>2.1.0.RELEASE</version>
123-
</dependency>
124-
125-
<dependency>
126-
<groupId>com.graphql-java</groupId>
127-
<artifactId>graphiql-spring-boot-starter</artifactId>
128-
<version>${graphiql.version}</version>
123+
<artifactId>spring-boot-starter-webflux</artifactId>
129124
</dependency>
130125
<dependency>
131-
<groupId>com.graphql-java</groupId>
132-
<artifactId>graphql-java-servlet</artifactId>
133-
<version>${graphql-java-servlet.version}</version>
134-
<exclusions>
135-
<exclusion>
136-
<groupId>com.fasterxml.jackson.core</groupId>
137-
<artifactId>*</artifactId>
138-
</exclusion>
139-
<exclusion>
140-
<groupId>com.fasterxml.jackson.datatype</groupId>
141-
<artifactId>*</artifactId>
142-
</exclusion>
143-
</exclusions>
126+
<groupId>org.jetbrains.kotlin</groupId>
127+
<artifactId>kotlin-test</artifactId>
128+
<version>${kotlin.version}</version>
129+
<scope>test</scope>
144130
</dependency>
145-
<dependency>
146-
<groupId>org.jetbrains.kotlin</groupId>
147-
<artifactId>kotlin-stdlib</artifactId>
148-
<version>${kotlin.version}</version>
149-
</dependency>
150-
<dependency>
151-
<groupId>org.jetbrains.kotlin</groupId>
152-
<artifactId>kotlin-test</artifactId>
153-
<version>${kotlin.version}</version>
154-
<scope>test</scope>
155-
</dependency>
156131
</dependencies>
157132
</project>

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

Lines changed: 17 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import com.expedia.graphql.SchemaGeneratorConfig
55
import com.expedia.graphql.TopLevelObject
66
import com.expedia.graphql.execution.KotlinDataFetcherFactoryProvider
77
import com.expedia.graphql.hooks.SchemaGeneratorHooks
8-
import com.expedia.graphql.sample.context.MyGraphQLContextBuilder
98
import com.expedia.graphql.sample.datafetchers.CustomDataFetcherFactoryProvider
109
import com.expedia.graphql.sample.datafetchers.SpringDataFetcherFactory
1110
import com.expedia.graphql.sample.directives.DirectiveWiringFactory
@@ -15,28 +14,19 @@ import com.expedia.graphql.sample.extension.CustomSchemaGeneratorHooks
1514
import com.expedia.graphql.sample.mutation.Mutation
1615
import com.expedia.graphql.sample.query.Query
1716
import com.expedia.graphql.toSchema
18-
import com.fasterxml.jackson.module.kotlin.KotlinModule
17+
import graphql.GraphQL
1918
import graphql.execution.AsyncExecutionStrategy
19+
import graphql.execution.AsyncSerialExecutionStrategy
20+
import graphql.execution.DataFetcherExceptionHandler
2021
import graphql.schema.GraphQLSchema
2122
import graphql.schema.idl.SchemaPrinter
22-
import graphql.servlet.DefaultExecutionStrategyProvider
23-
import graphql.servlet.GraphQLErrorHandler
24-
import graphql.servlet.GraphQLInvocationInputFactory
25-
import graphql.servlet.GraphQLObjectMapper
26-
import graphql.servlet.GraphQLQueryInvoker
27-
import graphql.servlet.ObjectMapperConfigurer
28-
import graphql.servlet.SimpleGraphQLHttpServlet
2923
import org.slf4j.LoggerFactory
3024
import org.springframework.boot.autoconfigure.SpringBootApplication
3125
import org.springframework.boot.runApplication
32-
import org.springframework.boot.web.servlet.ServletRegistrationBean
3326
import org.springframework.context.annotation.Bean
34-
import org.springframework.context.annotation.ComponentScan
35-
import javax.servlet.http.HttpServlet
3627
import javax.validation.Validator
3728

3829
@SpringBootApplication
39-
@ComponentScan("com.expedia.graphql")
4030
class Application {
4131

4232
private val logger = LoggerFactory.getLogger(Application::class.java)
@@ -46,11 +36,11 @@ class Application {
4636

4737
@Bean
4838
fun hooks(validator: Validator, wiringFactory: DirectiveWiringFactory) =
49-
CustomSchemaGeneratorHooks(validator, DirectiveWiringHelper(wiringFactory, mapOf("lowercase" to LowercaseDirectiveWiring())))
39+
CustomSchemaGeneratorHooks(validator, DirectiveWiringHelper(wiringFactory, mapOf("lowercase" to LowercaseDirectiveWiring())))
5040

5141
@Bean
5242
fun dataFetcherFactoryProvider(springDataFetcherFactory: SpringDataFetcherFactory, hooks: SchemaGeneratorHooks) =
53-
CustomDataFetcherFactoryProvider(springDataFetcherFactory, hooks)
43+
CustomDataFetcherFactoryProvider(springDataFetcherFactory, hooks)
5444

5545
@Bean
5646
fun schemaConfig(hooks: SchemaGeneratorHooks, dataFetcherFactoryProvider: KotlinDataFetcherFactoryProvider): SchemaGeneratorConfig = SchemaGeneratorConfig(
@@ -70,10 +60,10 @@ class Application {
7060

7161
@Bean
7262
fun schema(
73-
queries: List<Query>,
74-
mutations: List<Mutation>,
75-
schemaConfig: SchemaGeneratorConfig,
76-
schemaPrinter: SchemaPrinter
63+
queries: List<Query>,
64+
mutations: List<Mutation>,
65+
schemaConfig: SchemaGeneratorConfig,
66+
schemaPrinter: SchemaPrinter
7767
): GraphQLSchema {
7868
fun List<Any>.toTopLevelObjectDefs() = this.map {
7969
TopLevelObject(it)
@@ -86,49 +76,20 @@ class Application {
8676
)
8777

8878
logger.info(schemaPrinter.print(schema))
89-
9079
return schema
9180
}
9281

9382
@Bean
94-
fun contextBuilder() = MyGraphQLContextBuilder()
95-
96-
@Bean
97-
fun graphQLInvocationInputFactory(
98-
schema: GraphQLSchema,
99-
contextBuilder: MyGraphQLContextBuilder
100-
): GraphQLInvocationInputFactory = GraphQLInvocationInputFactory.newBuilder(schema)
101-
.withGraphQLContextBuilder(contextBuilder)
102-
.build()
103-
104-
@Bean
105-
fun graphQLQueryInvoker(): GraphQLQueryInvoker {
106-
val exceptionHandler = CustomDataFetcherExceptionHandler()
107-
val executionStrategyProvider = DefaultExecutionStrategyProvider(AsyncExecutionStrategy(exceptionHandler))
108-
109-
return GraphQLQueryInvoker.newBuilder()
110-
.withExecutionStrategyProvider(executionStrategyProvider)
111-
.build()
112-
}
113-
114-
@Bean
115-
fun graphQLObjectMapper(): GraphQLObjectMapper = GraphQLObjectMapper.newBuilder()
116-
.withObjectMapperConfigurer(ObjectMapperConfigurer { it.registerModule(KotlinModule()) })
117-
.withGraphQLErrorHandler(GraphQLErrorHandler { it })
118-
.build()
119-
120-
@Bean
121-
fun graphQLServlet(
122-
invocationInputFactory: GraphQLInvocationInputFactory,
123-
queryInvoker: GraphQLQueryInvoker,
124-
objectMapper: GraphQLObjectMapper
125-
): SimpleGraphQLHttpServlet = SimpleGraphQLHttpServlet.newBuilder(invocationInputFactory)
126-
.withQueryInvoker(queryInvoker)
127-
.withObjectMapper(objectMapper)
128-
.build()
83+
fun dataFetcherExceptionHandler(): DataFetcherExceptionHandler = CustomDataFetcherExceptionHandler()
12984

13085
@Bean
131-
fun graphQLServletRegistration(graphQLServlet: HttpServlet) = ServletRegistrationBean(graphQLServlet, "/graphql")
86+
fun graphQL(
87+
schema: GraphQLSchema,
88+
dataFetcherExceptionHandler: DataFetcherExceptionHandler
89+
): GraphQL = GraphQL.newGraphQL(schema)
90+
.queryExecutionStrategy(AsyncExecutionStrategy(dataFetcherExceptionHandler))
91+
.mutationExecutionStrategy(AsyncSerialExecutionStrategy(dataFetcherExceptionHandler))
92+
.build()
13293
}
13394

13495
fun main(args: Array<String>) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.expedia.graphql.sample
2+
3+
data class GraphQLRequest(
4+
val query: String,
5+
val operationName: String? = null,
6+
val variables: Map<String, Any>? = null
7+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.expedia.graphql.sample
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude
4+
import com.fasterxml.jackson.annotation.JsonInclude.Include
5+
import graphql.ExecutionResult
6+
7+
@JsonInclude(Include.NON_NULL)
8+
data class GraphQLResponse(
9+
val data: Any? = null,
10+
val errors: List<Any>? = null,
11+
val extensions: Map<Any, Any>? = null
12+
)
13+
14+
/**
15+
* Convert ExecutionResult to GraphQLResponse.
16+
*
17+
* NOTE: we need this as graphql-java defaults to only serializing GraphQLError objects so any custom error fields are ignored.
18+
*/
19+
internal fun ExecutionResult.toGraphQLResponse(): GraphQLResponse {
20+
val filteredErrors = if (errors?.isNotEmpty() == true) errors else null
21+
val filteredExtensions = if (extensions?.isNotEmpty() == true) extensions else null
22+
return GraphQLResponse(getData(), filteredErrors, filteredExtensions)
23+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.expedia.graphql.sample
2+
3+
import com.expedia.graphql.sample.context.MyGraphQLContext
4+
import graphql.ExecutionInput
5+
import graphql.GraphQL
6+
import org.springframework.stereotype.Component
7+
import reactor.core.publisher.Mono
8+
9+
@Component
10+
class QueryHandler(private val graphql: GraphQL) {
11+
12+
fun executeQuery(request: GraphQLRequest): Mono<GraphQLResponse> = Mono.subscriberContext()
13+
.flatMap { ctx ->
14+
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()
21+
22+
Mono.fromFuture(graphql.executeAsync(input))
23+
.map { executionResult -> executionResult.toGraphQLResponse() }
24+
}
25+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.expedia.graphql.sample
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import com.fasterxml.jackson.databind.type.MapType
5+
import com.fasterxml.jackson.databind.type.TypeFactory
6+
import graphql.schema.GraphQLSchema
7+
import graphql.schema.idl.SchemaPrinter
8+
import org.springframework.beans.factory.annotation.Value
9+
import org.springframework.context.annotation.Bean
10+
import org.springframework.context.annotation.Configuration
11+
import org.springframework.core.io.Resource
12+
import org.springframework.http.HttpMethod
13+
import org.springframework.http.MediaType
14+
import org.springframework.web.reactive.function.server.ServerRequest
15+
import org.springframework.web.reactive.function.server.bodyToMono
16+
import org.springframework.web.reactive.function.server.router
17+
import reactor.core.publisher.Mono
18+
19+
@Configuration
20+
class RoutesConfiguration(
21+
val schema: GraphQLSchema,
22+
val schemaPrinter: SchemaPrinter,
23+
private val queryHandler: QueryHandler,
24+
private val objectMapper: ObjectMapper,
25+
@Value("classpath:/graphql-playground.html") private val playgroundHtml: Resource
26+
) {
27+
28+
private val mapTypeReference: MapType = TypeFactory.defaultInstance().constructMapType(HashMap::class.java, String::class.java, Any::class.java)
29+
30+
@Bean
31+
fun graphQLRoutes() = router {
32+
(POST("/graphql") or GET("/graphql")).invoke { serverRequest ->
33+
createGraphQLRequest(serverRequest)
34+
.flatMap { graphQLRequest -> queryHandler.executeQuery(graphQLRequest) }
35+
.flatMap { result -> ok().contentType(MediaType.APPLICATION_JSON).syncBody(result) }
36+
.switchIfEmpty(badRequest().build())
37+
}
38+
GET("/sdl") {
39+
ok().contentType(MediaType.TEXT_PLAIN).syncBody(schemaPrinter.print(schema))
40+
}
41+
}
42+
43+
@Bean
44+
fun graphQLToolRoutes() = router {
45+
GET("/playground") {
46+
ok().contentType(MediaType.TEXT_HTML).syncBody(playgroundHtml)
47+
}
48+
}
49+
50+
private fun createGraphQLRequest(serverRequest: ServerRequest): Mono<GraphQLRequest> = when {
51+
serverRequest.method() == HttpMethod.POST -> serverRequest.bodyToMono()
52+
serverRequest.queryParam("query").isPresent -> {
53+
val query = serverRequest.queryParam("query").get()
54+
val operationName = serverRequest.queryParam("operationName").orElseGet { null }
55+
val variables = serverRequest.queryParam("variables").orElseGet { null }
56+
val graphQLVariables: Map<String, Any>? = variables?.let {
57+
objectMapper.readValue(it, mapTypeReference)
58+
}
59+
Mono.just(GraphQLRequest(query = query, operationName = operationName, variables = graphQLVariables))
60+
}
61+
else -> Mono.empty()
62+
}
63+
}
Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
11
package com.expedia.graphql.sample.context
22

3-
import graphql.servlet.GraphQLContext
4-
import javax.security.auth.Subject
5-
import javax.servlet.http.HttpServletRequest
6-
import javax.websocket.server.HandshakeRequest
3+
import com.expedia.graphql.annotations.GraphQLContext
4+
import org.springframework.http.server.reactive.ServerHttpRequest
5+
import org.springframework.http.server.reactive.ServerHttpResponse
76

87
/**
98
* Simple [GraphQLContext] that holds extra value.
109
*/
11-
class MyGraphQLContext(
12-
val myCustomValue: String,
13-
val httpServletRequest: HttpServletRequest? = null,
14-
handshakeRequest: HandshakeRequest? = null,
15-
subject: Subject? = null): GraphQLContext(httpServletRequest, handshakeRequest, subject)
10+
class MyGraphQLContext(val myCustomValue: String, val request: ServerHttpRequest, val response: ServerHttpResponse)

0 commit comments

Comments
 (0)