Compare commits
19 Commits
spring-clo
...
graphql-ko
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eed470c7e | ||
|
|
eb505572a1 | ||
|
|
19de2ec725 | ||
|
|
cf95180522 | ||
|
|
7d8bcdfc82 | ||
|
|
c3a6cbbdf9 | ||
|
|
5a17add9d1 | ||
|
|
0165c1eacb | ||
|
|
738246a31f | ||
|
|
1721962444 | ||
|
|
f46c2178c0 | ||
|
|
5bb45cc2e6 | ||
|
|
4516309e8f | ||
|
|
33224bb7f2 | ||
|
|
288369de97 | ||
|
|
0543c82018 | ||
|
|
35865696ed | ||
|
|
164cdd73d0 | ||
|
|
e8f1d57f43 |
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user