Skip to content

Commit 4fa5406

Browse files
adamsz-lumeAdam Firen
andauthored
add ktor example (#951)
* add ktor example * add the playground endpoint * implement requested changes for the ktor server example Co-authored-by: Adam Firen <[email protected]>
1 parent 982c311 commit 4fa5406

File tree

20 files changed

+691
-0
lines changed

20 files changed

+691
-0
lines changed

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ Example apps that use `graphql-kotlin` libraries to test and demonstrate usages.
44

55
* [client](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/client) - Example GraphQL Kotlin Client projects using Gradle and Maven together with a simple server to run against.
66
* [federation](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/federation) - Example Spring Boot apps generating federated GraphQL schemas and an Apollo Gateway in NodeJS that exposes the merged federated schema. Please refer to the README files for details on how to run each federated service.
7+
* [ktor-server](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/ktor-server) - Example GraphQL server using Ktor
78
* [spark](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/spark) - Example GraphQL server using Spark framework
89
* [spring](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/spring) - This is a sample Spring Boot app that uses `graphql-kotlin-spring-server` to create a reactive GraphQL web application. Please refer to the README file for details on how to run the application.

examples/ktor-server/README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# GraphQL Kotlin Ktor
2+
3+
[Ktor](http://ktor.io/) is an asynchronous framework for creating microservices, web applications, and more.
4+
5+
### Running locally
6+
Build the application by running the following from examples root directory:
7+
8+
```bash
9+
# build example
10+
./gradlew ktor-example:build
11+
```
12+
13+
> NOTE: in order to ensure you use the right version of Gradle we highly recommend that you use the provided wrapper scripts
14+
15+
Start the server by running `Application.kt` directly from your IDE. (Make sure that your Kotlin Compiler jvm target is 1.8 or greater.)
16+
Alternatively, you can start the server using Gradle.
17+
18+
```bash
19+
cd /path/to/graphql-kotlin/examples
20+
./gradlew ktor-example:run
21+
```
22+
23+
Once the app has started you can explore the example schema by opening Playground endpoint at http://localhost:5000/graphql
24+
25+
#### Example query
26+
27+
You can use the following example query to view several of the related models:
28+
29+
```graphql
30+
query {
31+
searchCourses(params: { ids: [1,2,3] }) {
32+
id
33+
name
34+
books {
35+
title
36+
}
37+
university {
38+
id
39+
name
40+
}
41+
}
42+
43+
searchUniversities(params: { ids: [1]}) {
44+
id
45+
name
46+
}
47+
}
48+
```
49+
50+
You can also query the `longThatNeverComes` field from several of the types. This will throw and exception,
51+
allow you to see how the `ExecutionStrategy` handles throw exceptions in different levels of the query.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
val ktor_version: String by project
2+
val logback_version: String by project
3+
val slf4j_version: String by project
4+
5+
description = "An Example GraphQL service served by Ktor"
6+
7+
plugins {
8+
id("application")
9+
}
10+
11+
application {
12+
mainClassName = "io.ktor.server.netty.EngineMain"
13+
}
14+
15+
val kotlinCoroutinesVersion: String by project
16+
dependencies {
17+
implementation("io.ktor:ktor-server-core:$ktor_version")
18+
implementation("io.ktor:ktor-server-netty:$ktor_version")
19+
implementation("ch.qos.logback:logback-classic:$logback_version")
20+
21+
implementation("com.expediagroup", "graphql-kotlin-schema-generator")
22+
implementation("org.jetbrains.kotlinx", "kotlinx-coroutines-jdk8", kotlinCoroutinesVersion)
23+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ktor_version=1.4.2
2+
logback_version=1.2.1
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright 2020 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+
package com.expediagroup.graphql.examples.ktor
17+
18+
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* Copyright 2020 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+
package com.expediagroup.graphql.examples.ktor
17+
18+
import com.expediagroup.graphql.SchemaGeneratorConfig
19+
import com.expediagroup.graphql.TopLevelObject
20+
import com.expediagroup.graphql.examples.ktor.schema.BookQueryService
21+
import com.expediagroup.graphql.examples.ktor.schema.CourseQueryService
22+
import com.expediagroup.graphql.examples.ktor.schema.HelloQueryService
23+
import com.expediagroup.graphql.examples.ktor.schema.LoginMutationService
24+
import com.expediagroup.graphql.examples.ktor.schema.UniversityQueryService
25+
import com.expediagroup.graphql.examples.ktor.schema.models.BATCH_BOOK_LOADER_NAME
26+
import com.expediagroup.graphql.examples.ktor.schema.models.COURSE_LOADER_NAME
27+
import com.expediagroup.graphql.examples.ktor.schema.models.UNIVERSITY_LOADER_NAME
28+
import com.expediagroup.graphql.examples.ktor.schema.models.User
29+
import com.expediagroup.graphql.examples.ktor.schema.models.batchBookLoader
30+
import com.expediagroup.graphql.examples.ktor.schema.models.batchCourseLoader
31+
import com.expediagroup.graphql.examples.ktor.schema.models.batchUniversityLoader
32+
import com.expediagroup.graphql.toSchema
33+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
34+
import com.fasterxml.jackson.module.kotlin.readValue
35+
import graphql.ExceptionWhileDataFetching
36+
import graphql.ExecutionInput
37+
import graphql.ExecutionResult
38+
import graphql.GraphQL
39+
import io.ktor.application.ApplicationCall
40+
import io.ktor.request.ApplicationRequest
41+
import io.ktor.request.receiveText
42+
import io.ktor.response.respond
43+
import org.dataloader.DataLoaderRegistry
44+
import java.io.IOException
45+
46+
data class AuthorizedContext(val authorizedUser: User? = null, var guestUUID: String? = null)
47+
48+
class GraphQLHandler {
49+
companion object {
50+
private val config = SchemaGeneratorConfig(supportedPackages = listOf("com.expediagroup.graphql.examples"))
51+
private val queries = listOf(
52+
TopLevelObject(HelloQueryService()),
53+
TopLevelObject(BookQueryService()),
54+
TopLevelObject(CourseQueryService()),
55+
TopLevelObject(UniversityQueryService())
56+
)
57+
58+
private val mutations = listOf(
59+
TopLevelObject(LoginMutationService())
60+
)
61+
62+
private val graphQLSchema = toSchema(config, queries, mutations)
63+
val graphQL = GraphQL.newGraphQL(graphQLSchema).build()!!
64+
}
65+
66+
private val mapper = jacksonObjectMapper()
67+
private val dataLoaderRegistry = DataLoaderRegistry()
68+
69+
init {
70+
dataLoaderRegistry.register(UNIVERSITY_LOADER_NAME, batchUniversityLoader)
71+
dataLoaderRegistry.register(COURSE_LOADER_NAME, batchCourseLoader)
72+
dataLoaderRegistry.register(BATCH_BOOK_LOADER_NAME, batchBookLoader)
73+
}
74+
75+
/**
76+
* Get payload from the request.
77+
*/
78+
private suspend fun getPayload(request: ApplicationRequest): Map<String, Any>? {
79+
return try {
80+
mapper.readValue<Map<String, Any>>(request.call.receiveText())
81+
} catch (e: IOException) {
82+
throw IOException("Unable to parse GraphQL payload.")
83+
}
84+
}
85+
86+
/**
87+
* Get the variables passed in the request.
88+
*/
89+
private fun getVariables(payload: Map<String, *>) =
90+
payload.getOrElse("variables") { emptyMap<String, Any>() } as Map<String, Any>
91+
92+
/**
93+
* Find attache user to context (authentication would go here)
94+
*/
95+
private fun getContext(request: ApplicationRequest): AuthorizedContext {
96+
val loggedInUser = User(
97+
email = "[email protected]",
98+
firstName = "Someone",
99+
lastName = "You Don't know",
100+
universityId = 4
101+
)
102+
return AuthorizedContext(loggedInUser)
103+
}
104+
105+
/**
106+
* Get any errors and data from [executionResult].
107+
*/
108+
private fun getResult(executionResult: ExecutionResult): MutableMap<String, Any> {
109+
val result = mutableMapOf<String, Any>()
110+
111+
if (executionResult.errors.isNotEmpty()) {
112+
// if we encounter duplicate errors while data fetching, only include one copy
113+
result["errors"] = executionResult.errors.distinctBy {
114+
if (it is ExceptionWhileDataFetching) {
115+
it.exception
116+
} else {
117+
it
118+
}
119+
}
120+
}
121+
122+
try {
123+
// if data is null, get data will fail exceptionally
124+
result["data"] = executionResult.getData<Any>()
125+
} catch (e: Exception) {}
126+
127+
return result
128+
}
129+
130+
/**
131+
* Execute a query against schema
132+
*/
133+
suspend fun handle(applicationCall: ApplicationCall) {
134+
val payload = getPayload(applicationCall.request)
135+
136+
payload?.let {
137+
// Execute the query against the schema
138+
val executionResult = graphQL.executeAsync(
139+
ExecutionInput.Builder()
140+
.query(payload["query"].toString())
141+
.variables(getVariables(payload))
142+
.dataLoaderRegistry(dataLoaderRegistry)
143+
.context(getContext(applicationCall.request))
144+
).get()
145+
val result = getResult(executionResult)
146+
147+
// write response as json
148+
applicationCall.response.call.respond(mapper.writeValueAsString(result))
149+
}
150+
}
151+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.expediagroup.graphql.examples.ktor
2+
3+
import io.ktor.application.Application
4+
import io.ktor.application.call
5+
import io.ktor.application.install
6+
import io.ktor.http.ContentType
7+
import io.ktor.response.respondText
8+
import io.ktor.routing.Routing
9+
import io.ktor.routing.get
10+
import io.ktor.routing.post
11+
import io.ktor.routing.routing
12+
import java.lang.IllegalStateException
13+
14+
fun Application.graphQLModule() {
15+
install(Routing)
16+
17+
routing {
18+
post("graphql") {
19+
GraphQLHandler().handle(this.call)
20+
}
21+
22+
get("playground") {
23+
this.call.respondText(buildPlaygroundHtml("graphql", "graphql"), ContentType.Text.Html)
24+
}
25+
}
26+
}
27+
28+
private fun buildPlaygroundHtml(graphQLEndpoint: String, subscriptionsEndpoint: String) =
29+
Application::class.java.classLoader.getResource("graphql-playground.html")?.readText()
30+
?.replace("\${graphQLEndpoint}", graphQLEndpoint)
31+
?.replace("\${subscriptionsEndpoint}", subscriptionsEndpoint)
32+
?: throw IllegalStateException("graphql-playground.html cannot be found in the classpath")
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright 2020 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+
package com.expediagroup.graphql.examples.ktor.schema
17+
18+
import com.expediagroup.graphql.annotations.GraphQLDescription
19+
import com.expediagroup.graphql.examples.ktor.schema.models.Book
20+
21+
/**
22+
* Provide Search options for book data
23+
*/
24+
class BookQueryService {
25+
@GraphQLDescription("Return list of books based on BookSearchParameter options")
26+
@Suppress("unused")
27+
suspend fun searchBooks(params: BookSearchParameters) = Book.search(params.ids)
28+
}
29+
30+
data class BookSearchParameters(val ids: List<Long>)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright 2020 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+
package com.expediagroup.graphql.examples.ktor.schema
17+
18+
import com.expediagroup.graphql.examples.ktor.schema.models.Course
19+
20+
class CourseQueryService {
21+
suspend fun searchCourses(params: CourseSearchParameters) = Course.search(params.ids)
22+
}
23+
24+
data class CourseSearchParameters(val ids: List<Long>)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Copyright 2020 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+
package com.expediagroup.graphql.examples.ktor.schema
17+
18+
class HelloQueryService {
19+
fun hello() = "World!"
20+
}

0 commit comments

Comments
 (0)