19 Commits

Author SHA1 Message Date
banjjoknim
1eed470c7e feat : @GraphQLName 어노테이션 예제 코드 추가 2022-08-04 12:17:33 +09:00
banjjoknim
eb505572a1 refactor : Long, LocalDateTime 타입 프로퍼티 추가 2022-08-04 12:14:07 +09:00
banjjoknim
19de2ec725 feat : GraphQL 에서 기본적으로 지원하지 않는 Long, java.time 관련 타입 Scalars 생성 훅 설정 추가 2022-08-04 12:13:35 +09:00
banjjoknim
cf95180522 build : graphql-java-extended-scalars 의존성 추가 2022-08-03 18:13:22 +09:00
banjjoknim
7d8bcdfc82 resolve conflict 2022-08-02 17:03:27 +09:00
banjjoknim
c3a6cbbdf9 test : Person Query 테스트 추가 2022-08-02 17:02:16 +09:00
banjjoknim
5a17add9d1 test : Person Query 테스트 추가 2022-08-02 17:01:22 +09:00
banjjoknim
0165c1eacb refactor : Query 함수에 GraphQL 어노테이션 추가 2022-08-02 16:01:40 +09:00
banjjoknim
738246a31f feat : GraphQL Context 구현체 추가 2022-08-02 15:06:51 +09:00
banjjoknim
1721962444 docs : README.md 항목 수정 2022-08-02 15:05:00 +09:00
banjjoknim
f46c2178c0 feat : GraphQL Context 추가 2022-08-02 14:47:31 +09:00
banjjoknim
5bb45cc2e6 feat : Schema, Query, Mutation, Subscription 추가 2022-08-02 14:12:35 +09:00
banjjoknim
4516309e8f docs : GraphQL Kotlin 공식 문서 링크 및 제목 수정 2022-08-02 13:52:30 +09:00
banjjoknim
33224bb7f2 docs : graphql package 설정 설명 주석 추가 2022-08-02 13:50:11 +09:00
banjjoknim
288369de97 add graphql package configuration 2022-08-02 13:33:00 +09:00
banjjoknim
0543c82018 build : GraphQL Kotlin Spring Server 의존성 추가 2022-08-02 11:08:57 +09:00
banjjoknim
35865696ed build : Spring Web MVC(servlet) stack -> Spring WebFlux(reactive) stack 으로 의존성 변경 2022-08-02 11:06:56 +09:00
banjjoknim
164cdd73d0 docs : README.md 및 공식 문서 링크 추가 2022-08-02 11:02:05 +09:00
banjjoknim
e8f1d57f43 graphql-kotlin initial commit 2022-08-02 10:47:00 +09:00
5 changed files with 220 additions and 0 deletions

View File

@@ -26,6 +26,7 @@ dependencies {
testImplementation("io.projectreactor:reactor-test")
implementation("com.expediagroup", "graphql-kotlin-spring-server", "6.0.0")
implementation("com.graphql-java:graphql-java-extended-scalars:18.1")
}
tasks.withType<KotlinCompile> {

View File

@@ -0,0 +1,107 @@
package com.banjjoknim.graphqlkotlin.configuration
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
import graphql.language.StringValue
import graphql.scalars.ExtendedScalars
import graphql.scalars.util.Kit.typeName
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingParseValueException
import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLScalarType
import graphql.schema.GraphQLType
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import kotlin.reflect.KClass
import kotlin.reflect.KType
/**
* - [Extended Scalars for graphql-java](https://github.com/graphql-java/graphql-java-extended-scalars)
*
* - [Cannot use java.util.Date](https://github.com/ExpediaGroup/graphql-kotlin/discussions/1198)
*
* - [GraphQL Kotlin - Extended Scalars](https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/writing-schemas/scalars/#common-issues)
*
* - [GraphQL Kotlin - Generator Configuration & Hooks](https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/customizing-schemas/generator-config)
*/
@Configuration
class ExtendedScalarsConfiguration {
/**
* 아래와 같이 Bean으로 Hook을 등록해주면 Schema Generator가 Schema를 생성할 때 이 Bean에 정의된 Hook을 이용해서 Schema를 만든다.
*/
@Bean
fun extendedScalarsHooks(): SchemaGeneratorHooks {
return object : SchemaGeneratorHooks {
override fun willGenerateGraphQLType(type: KType): GraphQLType? {
return when (type.classifier as? KClass<*>) {
Long::class -> ExtendedScalars.GraphQLLong
LocalDateTime::class -> localDateTimeScalar()
LocalTime::class -> ExtendedScalars.LocalTime
LocalDate::class -> ExtendedScalars.Date
else -> null
}
}
}
}
/**
* Bean으로 ScalarType을 등록해주지 않으면 어플리케이션 실행시 스키마를 구성하는 단계(스키마에 포함될 타입중에서 LocalDateTime 이 포함되어 있는 경우)에서 아래와 같은 예외가 발생한다.
*
* ```
* graphql.AssertException: All types within a GraphQL schema must have unique names. No two provided types may have the same name.
* No provided type may have a name which conflicts with any built in types (including Scalar and Introspection types). You have redefined the type 'LocalDateTime' from being a 'GraphQLScalarType' to a 'GraphQLScalarType'
* ```
*
* @see graphql.scalars.datetime.DateTimeScalar
*/
@Bean
fun localDateTimeScalar(): GraphQLScalarType? {
val coercing = object : Coercing<LocalDateTime, String> {
override fun serialize(dataFetcherResult: Any): String {
return when (dataFetcherResult) {
is LocalDateTime -> DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(
LocalDateTime.from(dataFetcherResult)
)
is String -> DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(
LocalDateTime.parse(dataFetcherResult)
)
else -> throw CoercingSerializeException(
"Expected something we can convert to 'java.time.LocalDateTime' but was '" +
"${typeName(dataFetcherResult)}'."
)
}
}
override fun parseValue(input: Any): LocalDateTime {
return when (input) {
is LocalDateTime -> input
is String -> LocalDateTime.parse(
input.toString(),
DateTimeFormatter.ISO_LOCAL_DATE_TIME
)
else -> throw CoercingParseValueException(
"Expected a 'String' but was '" + "${typeName(input)}'."
)
}
}
override fun parseLiteral(input: Any): LocalDateTime {
if (input !is StringValue) {
throw CoercingParseLiteralException(
"Expected AST type 'StringValue' but was '${typeName(input)}'."
)
}
return LocalDateTime.parse(input.toString(), DateTimeFormatter.ISO_LOCAL_DATE_TIME)
}
}
return GraphQLScalarType.newScalar()
.name("LocalDateTime")
.description("Custom LocalDateTime Scalar")
.coercing(coercing)
.build()
}
}

View File

@@ -1,5 +1,9 @@
package com.banjjoknim.graphqlkotlin.person
import java.time.LocalDateTime
data class Person(
var name: String,
var age: Long? = 0L,
var birthDate: LocalDateTime = LocalDateTime.now()
)

View File

@@ -1,9 +1,12 @@
package com.banjjoknim.graphqlkotlin.person
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import com.expediagroup.graphql.generator.annotations.GraphQLName
import com.expediagroup.graphql.server.operations.Query
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component
import kotlin.random.Random
@Component
class PersonQuery(
@@ -17,6 +20,7 @@ class PersonQuery(
private val personRepository: PersonRepository
) : Query {
@GraphQLDescription("get Person Instance")
fun getPerson(name: String): Person = Person(name)
/**
@@ -31,7 +35,12 @@ class PersonQuery(
* If you are using custom data fetcher make sure that you extend SpringDataFetcher instead of the base FunctionDataFetcher to keep this functionallity.
* ```
*/
@GraphQLDescription("find Person Instance")
fun findPerson(@GraphQLIgnore @Autowired personRepository: PersonRepository, name: String): Person? {
return personRepository.findPerson(name)
}
@GraphQLDescription("@GraphQLName example")
@GraphQLName("somePerson")
fun randomPerson(name: String): Person = Person(name = name, age = Random.nextLong())
}

View File

@@ -0,0 +1,99 @@
package com.banjjoknim.graphqlkotlin.person
import org.json.JSONObject
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class PersonQueryTest(
@Autowired
private val webTestClient: WebTestClient
) {
@DisplayName("getPerson Query Tests")
@Nested
inner class GetPersonTestCases {
@Test
fun `인자로 넣은 이름을 가진 Person 객체를 얻는다`() {
val query = """
query {
getPerson(name: "colt") {
name
}
}
""".trimIndent()
val json = JSONObject().put("query", query).toString()
webTestClient.post()
.uri("/graphql")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(json)
.exchange()
.expectBody().json("""{"data":{"getPerson":{"name":"colt"}}}""")
.consumeWith {
// assertThat(something...)
println(it.responseHeaders)
}
}
}
@DisplayName("findPerson Query Tests")
@Nested
inner class FindPersonTestCases {
@Test
fun `메모리에 존재하는 Person 객체 중에서 인자와 이름이 일치하는 객체를 얻는다`() {
val query = """
query {
findPerson(name: "banjjoknim") {
name
}
}
""".trimIndent()
val json = JSONObject().put("query", query).toString()
webTestClient.post()
.uri("/graphql")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(json)
.exchange()
.expectBody().json("""{"data":{"findPerson":{"name":"banjjoknim"}}}""")
.consumeWith {
// assertThat(something...)
println(it.responseHeaders)
}
}
@Test
fun `인자와 이름이 일치하는 객체가 메모리에 없으면 null을 얻는다`() {
val query = """
query {
findPerson(name: "invalid") {
name
}
}
""".trimIndent()
val json = JSONObject().put("query", query).toString()
webTestClient.post()
.uri("/graphql")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(json)
.exchange()
.expectBody().json("""{"data":{"findPerson":null}}""")
.consumeWith {
// assertThat(something...)
println(it.responseHeaders)
}
}
}
}