GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
graphql.org
type Character {
name: String!
appearsIn: [Episode!]!
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
type Query {
hero(episode: Episode): Character
}{
hero {
name
appearsIn
}
}{
"data": {
"hero": {
"name": "R2-D2",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}Requête (GraphQL)
Réponse (JSON)
Schéma
type Review {
stars: Int!
commentary: String
}
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
input ReviewInput {
stars: Int!
commentary: String
}
type Mutation {
createReview(episode: Episode!, review: ReviewInput!): Review
}mutation ($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
comment:commentary
}
}{
"data": {
"createReview": {
"stars": 5,
"comment": "This is a great movie!"
}
}
}Requête (GraphQL)
Réponse (JSON)
Schéma
Variables (JSON)
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:7.0.1")paths:
/articles:
get:
tags: [article]
summary: Get all articles
parameters:
- $ref: '#/components/parameters/offset'
- $ref: '#/components/parameters/limit'
responses:
'200':
description: Articles list response
content:
application/json:
schema:
$ref: '#/components/schemas/Articles'
components:
schemas:
Articles:
title: List of articles
type: object
properties:
pagination:
$ref: '#/components/schemas/PaginatedResource'
articles:
type: array
items:
$ref: '#/components/schemas/Article'
Article:
title: Article
type: object
properties:
id:
title: ID of the article
type: string
format: uuid
readOnly: true
example: e827659f-4489-4f53-90b3-81cf44b1cf97
headline:
title: Headline of the article
type: string
example: Some awesome article!
content:
title: Content of the article
type: string
example: Some interesting article content
createdDate:
title: Creation date of the article
type: string
format: "date-time"
example: "2020-05-26T15:19:06.732068Z"
lastModifiedDate:
title: Date of the last modification of the article
type: string
format: "date-time"
example: "2020-05-26T15:19:06.732068Z"
author:
$ref: '#/components/schemas/Author'
Author:
title: Author
type: object
properties:
id:
title: ID of the author
type: string
format: uuid
readOnly: true
example: e827659f-4489-4f53-90b3-81cf44b1cf97
name:
title: Name of the author
type: string
example: John Doe
PaginatedResource:
title: Pagination properties
type: object
properties:
offset:
title: Pagination offset
type: integer
example: 20
limit:
title: Pagination limit
type: integer
example: 10
total:
title: Total result count for resource
type: integer
example: 250
parameters:
pathId:
name: id
description: ID of the entity
in: path
required: true
schema:
type: string
format: uuid
example: a07abde3-5ddd-4e7c-887e-e8a69eda4d28
offset:
name: offset
description: Pagination offset
in: query
schema:
type: integer
example: 20
limit:
name: limit
description: Pagination limit
in: query
schema:
type: integer
example: 10const val ARTICLES_BASE_PATH = "/api/v1/articles"
@RestController
@RequestMapping(ARTICLES_BASE_PATH)
class ArticleApi(
private val articleService: ArticleService
) {
@GetMapping
@ResponseStatus(HttpStatus.OK)
fun getArticles(
@RequestParam(required = false, defaultValue = "0") offset: Long,
@RequestParam(required = false, defaultValue = "20") limit: Int
) =
articleService.getArticles(PaginationData(offset, limit))
.map { it.toDto() }
.toPageDto()
}type Article {
id: ID!
"""
Headline of the article
Example: `Some awesome article!`
"""
headline: String!
"""
Content of the article
Example: `Some interesting article content`
"""
content: String!
author: Author!
createdDate: String!
lastModifiedDate: String!
}
type Author {
id: ID!
name: String!
}
type Pagination {
offset: Int!
limit: Int!
total: Int!
}
interface PagedResult {
pagination: Pagination!
}
type PagedArticles implements PagedResult {
pagination: Pagination!
articles: [Article!]!
}
type Query {
findArticles(offset: Int = 0, limit: Int = 20): PagedArticles!
}@Component
class ArticleQuery(
private val articleService: ArticleService
) : GraphQLQueryResolver {
fun findArticles(offset: Int, limit: Int) = articleService.getArticles(
PaginationData(
offset = offset.toLong(),
limit = limit
)
)
.map { it.toDto() }
.toPageDto()
}FieldResolverError: No method found as defined in schema <unknown>:2 with any of the following signatures (with or without one of [interface graphql.schema.DataFetchingEnvironment] as the last argument), in priority order:
GraphQLQueryResolver#findArticles(~offset, ~limit)
GraphQLQueryResolver#getFindArticles(~offset, ~limit)Résolution / validation au démarrage
GET /api/v1/articles{
"pagination": {
"offset": 0,
"limit": 20,
"total": 2
},
"articles": [
{
"id": "9a2f1f77-4535-4aae-9c57-1e9b11bbfe63",
"headline": "headline2",
"content": "content2",
"author": {
"id": "d5a9666c-4a63-4aab-a366-0960fbec35aa",
"name": "name2"
},
"created_date": "2020-07-15T12:29:01.066067Z",
"last_modified_date": "2020-07-15T12:29:01.066067Z"
},
{
"id": "3ce1af88-cf58-4504-85f9-7edc7f0a2908",
"headline": "headline1",
"content": "content1",
"author": {
"id": "d5a9666c-4a63-4aab-a366-0960fbec35aa",
"name": "name2"
},
"created_date": "2020-07-15T12:29:00.961553Z",
"last_modified_date": "2020-07-15T12:29:00.961553Z"
}
]
}# POST /graphql
query {
findArticles {
pagination {
total
}
articles {
id
headline
author {
name
}
lastModifiedDate
}
}
}{
"data": {
"findArticles": {
"pagination": {
"total": 2
},
"articles": [
{
"id": "9a2f1f77-4535-4aae-9c57-1e9b11bbfe63",
"headline": "headline2",
"author": {
"name": "name2"
},
"lastModifiedDate": "2020-07-15T12:29:01.066067Z"
},
{
"id": "3ce1af88-cf58-4504-85f9-7edc7f0a2908",
"headline": "headline1",
"author": {
"name": "name2"
},
"lastModifiedDate": "2020-07-15T12:29:00.961553Z"
}
]
}
}
}REST
GraphQL
paths:
/articles:
post:
tags: [article]
summary: Create an article
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleInput'
responses:
'201':
description: Article creation response
content:
application/json:
schema:
$ref: '#/components/schemas/Article'
components:
schemas:
ArticleInput:
title: Article input
type: object
properties:
headline:
title: Headline of the article
type: string
example: Some awesome article!
content:
title: Content of the article
type: string
example: Some interesting article contentconst val ARTICLES_BASE_PATH = "/api/v1/articles"
@RestController
@RequestMapping(ARTICLES_BASE_PATH)
class ArticleApi(
private val articleService: ArticleService
) {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
fun createArticle(
@RequestBody @Valid input: ArticleInputDto,
@AuthenticationPrincipal user: User
) =
articleService.createArticle(input, user).toDto()
}input ArticleInput {
headline: String!
content: String!
}
extend type Mutation {
createArticle(input: ArticleInput!): Article!
}@Component
class ArticleMutation(
private val articleService: ArticleService,
private val validatorService: ValidationService
) : GraphQLMutationResolver {
fun createArticle(input: ArticleInputDto): ArticleDto {
validatorService.validate(input)
return articleService.createArticle(input, authenticatedUser()!!).toDto()
}
}
fun authenticatedUser() = SecurityContextHolder
.getContext()
.authentication
?.principal
as? User/!\ Validation manuelle de l'input à faire
// POST /api/v1/articles
{
"headline": "Some headline...",
"content": "# My awesome Article!!! (WIP)"
}{
"id": "5bd16151-545f-4fea-98fe-af97a0f06926",
"headline": "Some headline...",
"content": "# My awesome Article!!! (WIP)",
"author": {
"id": "690cfd5c-a041-49bf-af5e-df232333cc15",
"name": "name1"
},
"created_date": "2020-07-15T13:22:36.770240Z",
"last_modified_date": "2020-07-15T13:22:36.770240Z"
}# POST /graphql
mutation {
createArticle(input: {
headline: "Some headline...",
content: "# My awesome Article!!! (WIP)"
}) {
id
}
}{
"data": {
"createArticle": {
"id": "fa2aef6b-ef97-442f-a5a3-ffab5d533add"
}
}
}REST
GraphQL
type Article {
id: ID!
"""
Headline of the article
Example: `Some awesome article!`
"""
headline: String!
"""
Content of the article
Example: `Some interesting article content`
"""
content: String!
author: Author!
createdDate: String!
lastModifiedDate: String!
findComments(offset: Int = 0, limit: Int = 20): PagedComments!
}
type Comment {
id: String!
content: String!
author: Author!
createdDate: String!
lastModifiedDate: String!
}
type PagedComments implements PagedResult {
pagination: Pagination!
comments: [Comment!]!
}@Component
class ArticleResolver(
private val commentService: CommentService
) : GraphQLResolver<ArticleDto> {
fun findComments(article: ArticleDto, offset: Int, limit: Int) =
commentService.getArticleComments(
articleId = article.id,
paginationData = PaginationData(
offset = offset.toLong(),
limit = limit
)
)
.map { it.toDto() }
.toPageDto()
}# POST /graphql
query {
findArticles {
articles {
id
headline
findComments(limit: 2) {
pagination {
total
}
comments {
content
author {
name
}
}
}
lastModifiedDate
}
}
}{
"data": {
"findArticles": {
"articles": [
{
"id": "5bd16151-545f-4fea-98fe-af97a0f06926",
"headline": "Some headline...",
"findComments": {
"pagination": {
"total": 0
},
"comments": []
},
"lastModifiedDate": "2020-07-15T13:22:36.770240Z"
},
{
"id": "9a2f1f77-4535-4aae-9c57-1e9b11bbfe63",
"headline": "headline2",
"findComments": {
"pagination": {
"total": 3
},
"comments": [
{
"content": "content4",
"author": {
"name": "name2"
}
},
{
"content": "content5",
"author": {
"name": "name2"
}
}
]
},
"lastModifiedDate": "2020-07-15T12:29:01.066067Z"
}
]
}
}
}class ArticleApiTest : ComponentBase() {
@Test
fun `get articles`() {
// Given
dataDsl {
appUser {
email = "user1@yopmail.com"
name = "user1"
role = Role.USER
}
article()
article()
article()
article()
}
// When / Then
givenUser()
.param("limit", 3)
.get(ARTICLES_BASE_PATH)
.then()
.log().ifValidationFails()
.statusCode(HttpStatus.OK.value())
.body(isValidAgainst("article_list_ok.json"))
}
}{
"pagination": {
"offset": 0,
"limit": 3,
"total": 4
},
"articles": [
{
"id": "{#uuid#}",
"headline": "headline4",
"content": "content4",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"created_date": "{#date_time_format:iso_instant#}",
"last_modified_date": "{#date_time_format:iso_instant#}"
},
{
"id": "{#uuid#}",
"headline": "headline3",
"content": "content3",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"created_date": "{#date_time_format:iso_instant#}",
"last_modified_date": "{#date_time_format:iso_instant#}"
},
{
"id": "{#uuid#}",
"headline": "headline2",
"content": "content2",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"created_date": "{#date_time_format:iso_instant#}",
"last_modified_date": "{#date_time_format:iso_instant#}"
}
]
}
query {
findArticles(limit: 3) {
pagination {
offset
limit
total
}
articles {
id
headline
content
author {
id
name
}
createdDate
lastModifiedDate
}
}
}
Requête (GraphQL)
HTTP Body (JSON)
{
"query": "query {\\n findArticles(limit: 3) {\\n pagination {\\n offset\\n limit\\n total\\n }\\n articles {\\n id\\n headline\\n content\\n author {\\n id\\n name\\n }\\n createdDate\\n lastModifiedDate\\n }\\n }\\n}\\n"
}class ArticleQueryTest : ComponentBase() {
@Test
fun `should find articles paginated`() {
// Given
dataDsl {
appUser {
email = "user1@yopmail.com"
name = "user1"
role = Role.USER
}
article()
article()
article()
article()
}
// When / Then
givenUser()
.contentType(ContentType.JSON)
.body(
graphQLRequestBody(
getResourceContent("findArticles_request.graphql")
)
)
.post(GRAPHQL_PATH)
.then()
.log().ifValidationFails()
.statusCode(HttpStatus.OK.value())
.body(isValidAgainst("findArticles_response.json"))
}
}
{
"data": {
"findArticles": {
"pagination": {
"offset": 0,
"limit": 3,
"total": 4
},
"articles": [
{
"id": "{#uuid#}",
"headline": "headline4",
"content": "content4",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"createdDate": "{#date_time_format:iso_instant#}",
"lastModifiedDate": "{#date_time_format:iso_instant#}"
},
{
"id": "{#uuid#}",
"headline": "headline3",
"content": "content3",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"createdDate": "{#date_time_format:iso_instant#}",
"lastModifiedDate": "{#date_time_format:iso_instant#}"
},
{
"id": "{#uuid#}",
"headline": "headline2",
"content": "content2",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"createdDate": "{#date_time_format:iso_instant#}",
"lastModifiedDate": "{#date_time_format:iso_instant#}"
}
]
}
}
}
query {
findArticles(limit: 3) {
pagination {
offset
limit
total
}
articles {
id
headline
content
author {
id
name
}
createdDate
lastModifiedDate
}
}
}
class ArticleApiTest : ComponentBase() {
@Test
fun `create article`() {
// Given
val user = dataDsl {
appUser {
email = "user1@yopmail.com"
name = "user1"
role = Role.USER
}
}
// When / Then
givenAppUser(user)
.contentType(ContentType.JSON)
.body(
getResourceContent("article_create_input.json")
)
.post(ARTICLES_BASE_PATH)
.then()
.log().ifValidationFails()
.statusCode(HttpStatus.CREATED.value())
.body(isValidAgainst("article_create_ok.json"))
}
}
{
"id": "{#uuid#}",
"headline": "Some awesome article!",
"content": "Some interesting article content",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"created_date": "{#date_time_format:iso_instant#}",
"last_modified_date": "{#date_time_format:iso_instant#}"
}{
"headline": "Some awesome article!",
"content": "Some interesting article content"
}
class ArticleMutationTest : ComponentBase() {
@Test
fun `should create article #1`() {
// Given
val user = dataDsl {
appUser {
email = "user1@yopmail.com"
name = "user1"
role = Role.USER
}
}
// When / Then
givenAppUser(user)
.body(
graphQLRequestBody(
getResourceContent("createArticle_request.graphql")
)
)
.post(GRAPHQL_PATH)
.then()
.log().ifValidationFails()
.statusCode(HttpStatus.OK.value())
.body(isValidAgainst("createArticle_ok_response.json"))
}
}{
"data": {
"createArticle": {
"id": "{#uuid#}",
"headline": "Some awesome article!",
"content": "Some interesting article content",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"createdDate": "{#date_time_format:iso_instant#}",
"lastModifiedDate": "{#date_time_format:iso_instant#}"
}
}
}
mutation {
createArticle(input: {
headline: "Some awesome article!"
content: "Some interesting article content"
}) {
id
headline
content
author {
id
name
}
createdDate
lastModifiedDate
}
}
mutation ($ep: Episode!, $review: ReviewInput!) {
createReview(episode: $ep, review: $review) {
stars
comment:commentary
}
}Requête (GraphQL)
Variables (JSON)
{
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}HTTP Body (JSON)
{
"query": "mutation ($ep: Episode!, $review: ReviewInput!) {\n createReview(episode: $ep, review: $review) {\n stars\n comment:commentary\n }\n}",
"variables": {
"ep": "JEDI",
"review": {
"stars": 5,
"commentary": "This is a great movie!"
}
}
}class ArticleMutationTest : ComponentBase() {
@Test
fun `should create article`() {
// Given
val user = dataDsl {
appUser {
email = "user1@yopmail.com"
name = "user1"
role = Role.USER
}
}
// When / Then
givenAppUser(user)
.body(
graphQLRequestBody(
getResourceContent("createArticle_request_2.graphql"),
"headline" to "Some awesome article!",
"content" to "Some interesting article content"
)
)
.post(GRAPHQL_PATH)
.then()
.log().ifValidationFails()
.statusCode(HttpStatus.OK.value())
.body(isValidAgainst("createArticle_ok_response.json"))
}
}{
"data": {
"createArticle": {
"id": "{#uuid#}",
"headline": "Some awesome article!",
"content": "Some interesting article content",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"createdDate": "{#date_time_format:iso_instant#}",
"lastModifiedDate": "{#date_time_format:iso_instant#}"
}
}
}
mutation ($headline: String!, $content: String!) {
createArticle(input: {
headline: $headline
content: $content
}) {
id
headline
content
author {
id
name
}
createdDate
lastModifiedDate
}
}
class ArticleMutationTest : ComponentBase() {
@Test
fun `should create article`() {
// Given
val user = dataDsl {
appUser {
email = "user1@yopmail.com"
name = "user1"
role = Role.USER
}
}
// When / Then
givenAppUser(user)
.body(
graphQLRequestBody(
getResourceContent("createArticle_request_3.graphql"),
"input" to ArticleInputDto(
headline = "Some awesome article!",
content = "Some interesting article content"
)
)
)
.post(GRAPHQL_PATH)
.then()
.log().ifValidationFails()
.statusCode(HttpStatus.OK.value())
.body(isValidAgainst("createArticle_ok_response.json"))
}
}{
"data": {
"createArticle": {
"id": "{#uuid#}",
"headline": "Some awesome article!",
"content": "Some interesting article content",
"author": {
"id": "{#uuid#}",
"name": "user1"
},
"createdDate": "{#date_time_format:iso_instant#}",
"lastModifiedDate": "{#date_time_format:iso_instant#}"
}
}
}
mutation ($input: ArticleInput!) {
createArticle(input: $input) {
id
headline
content
author {
id
name
}
createdDate
lastModifiedDate
}
}
@Configuration
class GraphQLConfig {
@Bean
fun objectMapperProvider(objectMapper: ObjectMapper): ObjectMapperProvider = ObjectMapperProvider {
configure(objectMapper.copy())
}
@Bean
fun perFieldObjectMapperProvider(objectMapper: ObjectMapper): PerFieldObjectMapperProvider {
return PerFieldObjectMapperProvider {
configure(objectMapper.copy())
}
}
private fun configure(objectMapper: ObjectMapper): ObjectMapper {
return objectMapper
.also {
it.propertyNamingStrategy = PropertyNamingStrategy.LOWER_CAMEL_CASE
}
.findAndRegisterModules()
}
}graphql.tools.use-default-objectmapper: false
graphql.servlet.use-default-objectmapper: falseclass CustomDataFetcherExceptionHandler: DataFetcherExceptionHandler {
override fun onException(handlerParameters: DataFetcherExceptionHandlerParameters): DataFetcherExceptionHandlerResult {
val exception = handlerParameters.exception
val sourceLocation = handlerParameters.sourceLocation
val path = handlerParameters.path
val exceptionWithEnv = DataFetcherNestedException(exception, handlerParameters.dataFetchingEnvironment)
val error = ExceptionWhileDataFetching(path, exceptionWithEnv, sourceLocation)
return DataFetcherExceptionHandlerResult.newResult().error(error).build()
}
}@Component
class CustomGraphQLErrorHandler(
private val errorMessageSource: ResourceBundleMessageSource
) {
@ExceptionHandler(DataFetcherNestedException::class)
fun handle(e: DataFetcherNestedException): GraphQLError = handle(e.originalException, e.dataFetchingEnvironment)
@ExceptionHandler(Throwable::class)
fun handle(e: Throwable): GraphQLError = handle(e, null)
fun handle(e: Throwable, env: DataFetchingEnvironment? = null): GraphQLError {
val envLocations = env?.getSourceLocation()?.toMutableList()
return when (e) {
is BusinessRuleException -> object : GraphQLError {
override fun getMessage(): String = e.getMessage(errorMessageSource, env?.resolveLocale() ?: Locale.ENGLISH)
override fun getErrorType(): ErrorClassification = ErrorType.ValidationError
override fun getLocations(): MutableList<SourceLocation>? = envLocations
override fun getExtensions(): MutableMap<String, Any> = mutableMapOf(
"code" to e.getCode()
)
}
is NotFoundException -> object : GraphQLError {
override fun getMessage(): String = e.message ?: "Not found"
override fun getErrorType(): ErrorClassification = ErrorType.DataFetchingException
override fun getLocations(): MutableList<SourceLocation>? = envLocations
override fun getExtensions(): MutableMap<String, Any> = mutableMapOf()
}
else -> object : GraphQLError {
override fun getMessage(): String = e.message ?: "An error occurred (${e.javaClass.canonicalName})"
override fun getErrorType(): ErrorClassification = ErrorType.DataFetchingException
override fun getLocations(): MutableList<SourceLocation>? = envLocations
override fun getExtensions(): MutableMap<String, Any> = mutableMapOf()
}
}
}
private fun DataFetchingEnvironment.resolveLocale() = getContext<DefaultGraphQLServletContext>()
.httpServletRequest
.locale
private fun DataFetchingEnvironment.getSourceLocation() = listOf(mergedField.singleField.sourceLocation)
}@Configuration
class GraphQLConfig {
@Bean
fun executionStrategy(): ExecutionStrategy = AsyncExecutionStrategy(CustomDataFetcherExceptionHandler())
}graphql.servlet.exception-handlers-enabled: true# @ExceptionHandler enabled
{
"errors": [
{
"message": "User can't be created because there is already a user with this email"
}
]
}# Custom ExceptionHandler + DataFecthingEnvironment
{
"errors": [
{
"message": "L'utilisateur ne peut être créé car il en existe déjà un avec cet email",
"locations": [
{
"line": 2,
"column": 3
}
],
"extensions": {
"code": "error.user.create.conflict",
"classification": "ValidationError"
}
}
]
}# Default configuration
{
"errors": [
{
"message": "Internal Server Error(s) while executing query"
}
]
}{
"data": {
"findArticles": {
"pagination": {
"limit": 20,
"total": 2
},
"articles": [
{
"id": "2d36162a-b5b9-4fee-bda5-01e9fe0d72e7",
"headline": "headline2",
"lastModifiedDate": "2020-07-16T09:10:54.150765Z"
},
{
"id": "0f512968-43fb-4ea9-a309-f0b7f5af8107",
"headline": "headline1",
"lastModifiedDate": "2020-07-16T09:10:54.054128Z"
}
]
}
},
"errors": [
{
"message": "Accès refusé",
"locations": [
{
"line": 11,
"column": 7
}
],
"extensions": {
"classification": "DataFetchingException"
}
},
{
"message": "Accès refusé",
"locations": [
{
"line": 11,
"column": 7
}
],
"extensions": {
"classification": "DataFetchingException"
}
}
]
}query {
findArticles {
pagination {
limit
total
}
articles {
id
headline
lastModifiedDate
votes {
count
}
}
}
}// Header: "Accept-Language: fr"
// env: DataFetchingEnvironment
/**
* @return the current {@link java.util.Locale} instance used for this request
*/
env.locale // == null
env.getContext<DefaultGraphQLServletContext>()
.httpServletRequest
.locale // == Locale.FRENCH"""
A type to use when no result is expected (may result in a null returned value).
"""
scalar Void@Configuration
class GraphQLScalarConfig {
@Bean
fun voidScalarType(): GraphQLScalarType = GraphQLScalarType
.newScalar()
.name("Void")
.coercing(VoidCoercing())
.build()
}class VoidCoercing : Coercing<Unit, String?> {
override fun serialize(dataFetcherResult: Any?): String? = null
override fun parseValue(input: Any?) {}
override fun parseLiteral(input: Any?) {}
}"""
An RFC-3339 compliant UTC DateTime Scalar.
Example:
`1985-04-12T23:20:50.52Z` represents 20 minutes and 50.52 seconds after the 23rd hour of
April 12th, 1985 in UTC.
"""
scalar UtcDateTime@Configuration
class GraphQLScalarConfig {
@Bean
fun utcDateTimeScalarType(): GraphQLScalarType = GraphQLScalarType
.newScalar()
.name("UtcDateTime")
.coercing(UtcDateTimeCoercing())
.build()
}class UtcDateTimeCoercing : Coercing<Instant, String> {
override fun serialize(input: Any?): String {
val instant: Instant = when (input) {
is Instant -> input
is OffsetDateTime -> input.toInstant()
is ZonedDateTime -> input.toInstant()
is String -> parseInstant(input, ::CoercingSerializeException)
else -> throw CoercingSerializeException(
"""Expected something we can convert to 'java.time.Instant' but was '${Kit.typeName(input)}'."""
)
}
try {
return DateTimeFormatter.ISO_INSTANT.format(instant)
} catch (e: DateTimeException) {
throw CoercingSerializeException(
"""Unable to turn TemporalAccessor into Instant because of : '${e.message}'."""
)
}
}
override fun parseValue(input: Any?): Instant = when (input) {
is Instant -> input
is OffsetDateTime -> input.toInstant()
is ZonedDateTime -> input.toInstant()
is String -> parseInstant(input, ::CoercingParseValueException)
else -> throw CoercingParseValueException(
"""Expected a 'String' but was '${Kit.typeName(input)}'."""
)
}
override fun parseLiteral(input: Any?): Instant = (input as StringValue?)
?.let { parseInstant(it.value, ::CoercingParseLiteralException) }
?: throw CoercingParseLiteralException(
"""Expected AST type 'StringValue' but was '${Kit.typeName(input)}'."""
)
private fun parseInstant(input: String, exceptionMaker: (String) -> RuntimeException): Instant = try {
Instant.parse(input)
} catch (e: DateTimeException) {
throw exceptionMaker(
"""Invalid RFC3339 value : '$input'. because of : '${e.message}'"""
)
}
}"""
An RFC-3339 compliant UTC DateTime Scalar.
Example:
`1985-04-12T23:20:50.52Z` represents 20 minutes and 50.52 seconds after the 23rd hour of
April 12th, 1985 in UTC.
"""
scalar UtcDateTime@Configuration
class GraphQLScalarConfig {
@Bean
fun utcDateTimeScalarType(): GraphQLScalarType = GraphQLScalarType
.newScalar(Scalars.GraphQLString)
.name("UtcDateTime")
.build()
}query {
findArticles(first: Int, after: String): ArticleConnection @connection(for: "Article")
}
type ArticleConnection {
edges: [ArticleEdge]
pageInfo: PageInfo
}
type ArticleEdge {
cursor: String
node: Article
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
}Types générés au runtime: