Skip to content

Commit

Permalink
✨ BFF を追加
Browse files Browse the repository at this point in the history
  • Loading branch information
tatsutakein committed Nov 4, 2023
1 parent 22a7229 commit 5b7db4f
Show file tree
Hide file tree
Showing 30 changed files with 638 additions and 2 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ NITO のユーザーアプリのリポジトリです。
│   アプリケーションのディレクトリ
├── app
│   BFF のディレクトリ
├── bff
│   ビルドロジックを集約したディレクトリ
├── build-logic
Expand Down
12 changes: 12 additions & 0 deletions bff/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# BFF

NITO の BFF のディレクトリです。

```shell
./gradlew :bff:run
```

## アーキテクチャ構成

- [Ktor](https://ktor.io/)
- [GraphQL Kotlin](https://github.com/ExpediaGroup/graphql-kotlin)
40 changes: 40 additions & 0 deletions bff/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import com.expediagroup.graphql.plugin.gradle.config.GraphQLSerializer

plugins {
kotlin("jvm")
id("io.ktor.plugin") version libs.versions.ktor
id("org.jetbrains.kotlin.plugin.serialization") version libs.versions.kotlin
id("com.expediagroup.graphql") version libs.versions.graphqlKotlin
}

group = "club.nito"
version = "0.1.0"

application {
mainClass.set("nito.club.bff.ApplicationKt")

val isDevelopment: Boolean = project.ext.has("development")
applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment")
}

dependencies {
implementation(libs.ktorServerCore)
implementation(libs.ktorServerNetty)
implementation(libs.ktorServerCors)
implementation(libs.ktorServerWebsockets)
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm")
implementation(libs.logback)
implementation(libs.graphqlKotlinKtorServer)
testImplementation("io.ktor:ktor-server-tests-jvm")
testImplementation(kotlin("test"))
// testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}

graphql {
schema {
packages = listOf("nito.club.bff")
}
client {
serializer = GraphQLSerializer.KOTLINX
}
}
3 changes: 3 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/Application.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package nito.club.bff

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
25 changes: 25 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/CustomGraphQLContextFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nito.club.bff

import com.expediagroup.graphql.generator.extensions.plus
import com.expediagroup.graphql.server.ktor.DefaultKtorGraphQLContextFactory
import graphql.GraphQLContext
import io.ktor.server.request.ApplicationRequest
import nito.club.bff.schema.models.User

class CustomGraphQLContextFactory : DefaultKtorGraphQLContextFactory() {
override suspend fun generateContext(request: ApplicationRequest): GraphQLContext =
super.generateContext(request).plus(
mutableMapOf<Any, Any>(
"user" to User(
email = "[email protected]",
firstName = "Someone",
lastName = "You Don't know",
universityId = 4,
),
).also { map ->
request.headers["my-custom-header"]?.let { customHeader ->
map["customHeader"] = customHeader
}
},
)
}
76 changes: 76 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/GraphQLModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package nito.club.bff

import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory
import com.expediagroup.graphql.server.ktor.GraphQL
import com.expediagroup.graphql.server.ktor.graphQLGetRoute
import com.expediagroup.graphql.server.ktor.graphQLPostRoute
import com.expediagroup.graphql.server.ktor.graphQLSDLRoute
import com.expediagroup.graphql.server.ktor.graphQLSubscriptionsRoute
import com.expediagroup.graphql.server.ktor.graphiQLRoute
import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.cors.routing.CORS
import io.ktor.server.routing.Routing
import io.ktor.server.websocket.WebSockets
import io.ktor.server.websocket.pingPeriod
import kotlinx.serialization.json.Json
import nito.club.bff.schema.BookQueryService
import nito.club.bff.schema.CourseQueryService
import nito.club.bff.schema.ExampleSubscriptionService
import nito.club.bff.schema.HelloQueryService
import nito.club.bff.schema.LoginMutationService
import nito.club.bff.schema.UniversityQueryService
import nito.club.bff.schema.dataloaders.BookDataLoader
import nito.club.bff.schema.dataloaders.CourseDataLoader
import nito.club.bff.schema.dataloaders.UniversityDataLoader
import java.time.Duration

@Suppress("unused")
fun Application.graphQLModule() {
val format = Json {
ignoreUnknownKeys = true
}
install(WebSockets) {
pingPeriod = Duration.ofSeconds(1)

contentConverter = KotlinxWebsocketSerializationConverter(format)
}
install(CORS) {
anyHost()
}
install(GraphQL) {
schema {
packages = listOf("nito.club.bff")
queries = listOf(
HelloQueryService(),
BookQueryService(),
CourseQueryService(),
UniversityQueryService(),
)
mutations = listOf(
LoginMutationService(),
)
subscriptions = listOf(
ExampleSubscriptionService(),
)
}
engine {
dataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory(
UniversityDataLoader,
CourseDataLoader,
BookDataLoader,
)
}
server {
contextFactory = CustomGraphQLContextFactory()
}
}
install(Routing) {
graphQLGetRoute()
graphQLPostRoute()
graphQLSubscriptionsRoute()
graphiQLRoute()
graphQLSDLRoute()
}
}
56 changes: 56 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/GraphQLRoutes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package nito.club.bff

import com.expediagroup.graphql.server.ktor.GraphQL
import com.expediagroup.graphql.server.ktor.KtorGraphQLServer
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.serialization.kotlinx.serialization
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.application.plugin
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.application
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import kotlinx.serialization.StringFormat

fun Route.graphQLGetRoute(
endpoint: String = "graphql",
format: StringFormat,
): Route {
val graphQLPlugin = this.application.plugin(GraphQL)
return get(endpoint) {
graphQLPlugin.server.executeRequest(call)
}.apply {
install(ContentNegotiation) {
serialization(ContentType.Application.Json, format)
}
}
}

fun Route.graphQLPostRoute(
endpoint: String = "graphql",
format: StringFormat,
): Route {
val graphQLPlugin = this.application.plugin(GraphQL)
return post(endpoint) {
graphQLPlugin.server.executeRequest(call)
}.apply {
install(ContentNegotiation) {
serialization(ContentType.Application.Json, format)
}
}
}

private suspend inline fun KtorGraphQLServer.executeRequest(call: ApplicationCall) = try {
execute(call.request)?.let {
call.respond(it)
} ?: call.respond(HttpStatusCode.BadRequest)
} catch (e: UnsupportedOperationException) {
call.respond(HttpStatusCode.MethodNotAllowed)
} catch (e: Exception) {
call.respond(HttpStatusCode.BadRequest)
}
7 changes: 7 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/graphql/query/HelloQuery.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package nito.club.bff.graphql.query

import com.expediagroup.graphql.server.operations.Query

object HelloQuery : Query {
fun hello() = "Hello World!"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package nito.club.bff.graphql.server

import com.expediagroup.graphql.server.execution.GraphQLContextFactory
import io.ktor.server.request.ApplicationRequest

class KtorGraphQLContextFactory : GraphQLContextFactory<ApplicationRequest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package nito.club.bff.graphql.server

import com.expediagroup.graphql.server.execution.GraphQLRequestParser
import com.expediagroup.graphql.server.types.GraphQLServerRequest
import com.fasterxml.jackson.databind.ObjectMapper
import io.ktor.server.request.ApplicationRequest
import io.ktor.server.request.receiveText
import io.ktor.utils.io.errors.IOException

class KtorGraphQLRequestParser(
private val mapper: ObjectMapper,
) : GraphQLRequestParser<ApplicationRequest> {
override suspend fun parseRequest(request: ApplicationRequest): GraphQLServerRequest? {
try {
val rawRequest = request.call.receiveText()
return mapper.readValue(rawRequest, GraphQLServerRequest::class.java)
} catch (e: IOException) {
throw IOException("Unable to parse GraphQL payload.")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nito.club.bff.graphql.server

import com.expediagroup.graphql.generator.SchemaGeneratorConfig
import com.expediagroup.graphql.generator.TopLevelObject
import com.expediagroup.graphql.generator.scalars.IDValueUnboxer
import com.expediagroup.graphql.generator.toSchema
import graphql.GraphQL
import nito.club.bff.graphql.query.HelloQuery

object KtorGraphQLSchema {
private val config = SchemaGeneratorConfig(
supportedPackages = listOf("club.nito.graphql"),
)
private val queries = listOf(
TopLevelObject(HelloQuery),
)
private val mutations = listOf<TopLevelObject>()

private val graphQLSchema = toSchema(
config = config,
queries = queries,
mutations = mutations,
)

fun getGraphQLObject(): GraphQL = GraphQL.newGraphQL(graphQLSchema)
.valueUnboxer(IDValueUnboxer())
.build()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package nito.club.bff.graphql.server

import com.expediagroup.graphql.server.execution.GraphQLRequestHandler
import com.expediagroup.graphql.server.execution.GraphQLServer
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.ApplicationRequest
import io.ktor.server.response.respond

object KtorGraphQLServer {
private val mapper = jacksonObjectMapper()
private val ktorGraphQLServer = getGraphQLServer()

suspend fun handle(applicationCall: ApplicationCall) {
val result = ktorGraphQLServer.execute(applicationCall.request)

if (result != null) {
val json = mapper.writeValueAsString(result)
applicationCall.response.call.respond(json)
} else {
applicationCall.response.call.respond(HttpStatusCode.BadRequest, "Invalid request")
}
}

private fun getGraphQLServer(): GraphQLServer<ApplicationRequest> {
val requestParser = KtorGraphQLRequestParser(mapper)
val contextFactory = KtorGraphQLContextFactory()
val graphQL = KtorGraphQLSchema.getGraphQLObject()
val requestHandler = GraphQLRequestHandler(graphQL)

return GraphQLServer(requestParser, contextFactory, requestHandler)
}
}
18 changes: 18 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/schema/BookQueryService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package nito.club.bff.schema

import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.server.operations.Query
import graphql.schema.DataFetchingEnvironment
import nito.club.bff.schema.dataloaders.BookDataLoader
import nito.club.bff.schema.models.Book
import java.util.concurrent.CompletableFuture

class BookQueryService : Query {
@GraphQLDescription("Return list of books based on BookSearchParameter options")
@Suppress("unused")
fun searchBooks(params: BookSearchParameters, dfe: DataFetchingEnvironment): CompletableFuture<List<Book>> =
dfe.getDataLoader<Int, Book>(BookDataLoader.dataLoaderName)
.loadMany(params.ids)
}

data class BookSearchParameters(val ids: List<Int>)
15 changes: 15 additions & 0 deletions bff/src/main/kotlin/nito/club/bff/schema/CourseQueryService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package nito.club.bff.schema

import com.expediagroup.graphql.server.operations.Query
import graphql.schema.DataFetchingEnvironment
import nito.club.bff.schema.dataloaders.CourseDataLoader
import nito.club.bff.schema.models.Course
import java.util.concurrent.CompletableFuture

class CourseQueryService : Query {
fun searchCourses(params: CourseSearchParameters, dfe: DataFetchingEnvironment): CompletableFuture<List<Course>> =
dfe.getDataLoader<Int, Course>(CourseDataLoader.dataLoaderName)
.loadMany(params.ids)
}

data class CourseSearchParameters(val ids: List<Int>)
Loading

0 comments on commit 5b7db4f

Please sign in to comment.