Definir nuevos tipos de datos escalares en GraphQL

Escrito por picodotdev el .
blog-stack java planeta-codigo programacion
Comentarios

GraphQL

GraphQL es una alternativa a una interfaz REST con las ventajas de permitir al consumidor obtener únicamente los datos que requiere y realizar varias consultas en una misma petición.

GraphQL por defecto soporta un conjunto de tipos escalares en los datos entre los que están varios numéricos, cadenas, booleanos, enumerados además de los tipos o estructuras de datos definidos en la interfaz del servicio. Sin embargo, si es necesario es posible definir nuevos tipos de datos escalares como podría ser el caso de un tipo de dato para representar una fecha fecha y otro de importe monetario.

El objeto en Java que representa una fecha con Java 8 sería LocalDate y la clase para el importe monetario podría ser un BigDecimal o alguna de la librería JavaMoney.

Para que GraphQL soporte un nuevo tipo de dato escalar es necesario implementar una clase que realice la conversión. Esta clase se encarga de realizar la conversión entre el escalar añadido a una representación a devolver en las respuestas de las peticiones y la conversión entre la representación en consultas al tipo de dato hay que proporcionar al servicio. La clase debe implementar la interfaz Coercing y construyendo un objeto GraphQLScalarType proporcionárselo a GraphQL en la definición del servicio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package io.github.picodotdev.blogbitix.graphql;

import graphql.schema.Coercing;
import graphql.schema.CoercingParseValueException;
import graphql.schema.CoercingSerializeException;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class LocalDateCoercing implements Coercing<LocalDate, String> {

    private DateTimeFormatter formatter;

    public LocalDateCoercing() {
        this(DateTimeFormatter.ISO_DATE);
    }

    public LocalDateCoercing(DateTimeFormatter formatter) {
        this.formatter = formatter;
    }

    @Override
    public String serialize(Object dataFetcherResult) {
        try {
            LocalDate date = (LocalDate) dataFetcherResult;
            return date.format(formatter);
        } catch (Exception e) {
            throw new CoercingSerializeException(e);
        }
    }

    @Override
    public LocalDate parseValue(Object input) {
        return parse(input);
    }

    @Override
    public LocalDate parseLiteral(Object input) {
        return parse(input);
    }

    private LocalDate parse(Object input) {
        try {
            String string = (String) input;
            return LocalDate.parse(string, formatter);
        } catch (Exception e) {
            throw new CoercingParseValueException(e);
        }
    }
}

Al definir el esquema se proporciona con el método scalars una lista con los tipos de datos escalares adicionales, en este caso una instancia de GraphQLScalarType con una instancia de LocalDateCoercing. Además en el descriptor del esquema hay que declarar el nuevo escalar con la palabra clave scalar.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.github.picodotdev.blogbitix.graphql;

...

@SpringBootApplication
@ServletComponentScan
public class Main {

    ...

    @Bean
    public LibraryRepository buildLibraryRepository() {
        return new LibraryRepository();
    }

    @Bean
    public ServletRegistrationBean graphQLServletRegistrationBean(LibraryRepository libraryRepository) throws Exception {
        GraphQLSchema schema = SchemaParser.newParser()
                .schemaString(IOUtils.resourceToString("/library.graphqls", Charset.forName("UTF-8")))
                .resolvers(new Query(libraryRepository), new Mutation(libraryRepository), new BookResolver(libraryRepository))
                .scalars(new GraphQLScalarType("LocalDate", "LocalDate scalar", new LocalDateCoercing()))
                .build()
                .makeExecutableSchema();
                
        ...
   }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
scalar LocalDate

type Book {
    id: Long
    title: String
    author: Author
    isbn: String
    date: LocalDate
    comments(after: String, limit: Long): CommentsConnection
    batchedComments(after: String, limit: Long): CommentsConnection
}

...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package io.github.picodotdev.blogbitix.graphql;

...

public class LibraryRepository {

    private long sequence;
    private List<Book> books;
    private List<Comment> comments;
    private List<Author> authors;

    public LibraryRepository() {
        this.sequence = 0l;
        this.books = new ArrayList<>();
        this.comments = new ArrayList<>();
        this.authors = new ArrayList<>();

        Author a1 = new Author(nextId(), "Philip K. Dick");
        Author a2 = new Author(nextId(), "George R. R. Martin");
        Author a3 = new Author(nextId(), "Umberto Eco");
        Author a4 = new Author(nextId(), "Andreas Eschbach");
        Author a5 = new Author(nextId(), "Ernest Cline");
        Author a6 = new Author(nextId(), "Anónimo");

        this.authors.addAll(List.of(a1, a2, a3, a4, a5, a6));

        LongStream.range(1, 10).forEach(i -> this.comments.add(new Comment(i,"Comment " + i)));

        this.books.addAll(
            List.of(
                new Book(nextId(), "Ojo en el cielo", a1, LocalDate.of(1957, 1, 1), this.comments),
                new Book(nextId(), "Muerte de la luz", a2, LocalDate.of(1977, 1, 1), this.comments),
                new Book(nextId(), "El nombre de la rosa", a3, LocalDate.of(1980, 1, 1), this.comments),
                new Book(nextId(), "Los tejedores de cabellos", a4, LocalDate.of(1995, 1, 1), this.comments),
                new Book(nextId(), "Ready Player One", a5, LocalDate.of(2011, 1, 1), this.comments)
            )
        );
    }
  
    ...
      
}

Añadiendo al tipo Book una fecha de publicación usando este nuevo tipo escalar al realizar una consulta y devolver el dato se realiza la conversión.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ curl -XPOST -H "Content-Type: application/json" -d '{"query": "query Book{books{title date}}"}' http://localhost:8080/library
{
  "data": {
    "books": [
      {
        "title": "Ojo en el cielo",
        "date": "1957-01-01"
      },
      {
        "title": "Muerte de la luz",
        "date": "1977-01-01"
      },
      {
        "title": "El nombre de la rosa",
        "date": "1980-01-01"
      },
      {
        "title": "Los tejedores de cabellos",
        "date": "1995-01-01"
      },
      {
        "title": "Ready Player One",
        "date": "2011-01-01"
      }
    ]
  }
}

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando el comando ./gradlew run. Requiere Java 9+ o Docker.