Definir nuevos tipos de datos escalares en GraphQL

Escrito por el , actualizado el .
java planeta-codigo programacion
Enlace permanente 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 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
package io.github.picodotdev.blogbitix.graphql;

...

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);
        }
    }
}
LocalDateCoercing.java

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
28
29
30
31
package io.github.picodotdev.blogbitix.graphql;

...

@SpringBootApplication
public class Main {

    public static final Logger logger = LoggerFactory.getLogger(Main.class);

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

    @Bean
    public GraphQLSchema graphQLSchema(LibraryRepository libraryRepository) throws IOException {
        return SchemaParser.newParser()
                .schemaString(IOUtils.resourceToString("/library.graphqls", Charset.forName("UTF-8")))
                .resolvers(new Query(libraryRepository), new Mutation(libraryRepository), new BookResolver(libraryRepository), new MagazineResolver(libraryRepository))
                .scalars(GraphQLScalarType.newScalar().name("Long").description("Long scalar").coercing(new LongCoercing()).build(), GraphQLScalarType.newScalar().name("LocalDate").description("LocalDate scalar").coercing(new LocalDateCoercing()).build())
                .dictionary(Magazine.class)
                .build()
                .makeExecutableSchema();
    }

    ...

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
scalar Long
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
}

...
library.graphqls
 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
package io.github.picodotdev.blogbitix.graphql.repository;

...

public class LibraryRepository {

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

    public LibraryRepository() {
        this.sequence = 0l;
        this.books = new ArrayList<>();
        this.comments = new ArrayList<>();
        this.authors = new ArrayList<>();
        this.magazines = 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)
            )
        );

        this.magazines.addAll(
            List.of(
                new Magazine(nextId(), "Muy interesante", 65L),
                new Magazine(nextId(), "PC Actual", 90L)
            )
        );
    }

    ...
}
LibraryRepository.java

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/graphql
{
  "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"
      }
    ]
  }
}
curl.sh
Terminal

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 siguiente comando:
./gradlew run


Comparte el artículo: