Quarkus: exponer una interfaz GraphQL

0
663

Crear un servidor de GraphQL con Quarkus es muy muy fácil. En este artículo veremos como crear una interfaz GraphQL en java, con sus queries, sus mutaciones, su esquema y con una interfaz gráfica que nos ayuda con todo eso.

Índice de contenidos

1. Introducción.

Estamos acostumbrados a usar API REST, pero empieza a ser cada vez más habitual que usemos otras opciones como gRPC o GraphQL.

En este caso veremos como exponer información a través de una interfaz GraphQL.

Este tipo de interfaces tienen algunas ventajas sobre REST:

  • tiene un schema, es tipado
  • puedes conocer facilmente los métodos que expone la API.
  • puedes pedir sólo los datos que necesitas
  • reduce número de peticiones y optimiza el uso de red

2. Antes de empezar

Partimos del ejercicio anterior con Quarkus, donde exponemos un API REST de la tabla periódica, que no deja de ser un CRUD de los de siempre.

Quarkus: como crear microservicio con REST y MongoDB

Como ahora, ya no tengo docker, necesito levantar un contenedor con un servidor de MongoDB.

Y lo voy a hacer con el equivalente OpenSource, podman, de licencia Apache 2.0.

Arrancamos la maquina de podman

podman machine start

y ejecutamos

podman run --name periodic-table-mongo -d -p 27017:27017  -e MONGO_INITDB_ROOT_USERNAME=mongo -e MONGO_INITDB_ROOT_PASSWORD=mongo mongo:latest 

Ya tenemos nuestro mongo arriba.

Si ejecutamos

mvn quarkus:dev

levantamos el proyecto en modo desarrollo

Pero eso ya lo hemos hecho en la versión anterior.

3. Adiós API REST. Hola Interfaz GraphQL

Ahora lo que queremos es quitar los endpoint REST y dotar al microservicio de un API GraphQL

Para ello, quitaremos del POM

<dependency>
	<groupId>io.quarkus</groupId>
	<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>

y añadiremos

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-graphql</artifactId>
</dependency>

En ElementResource anotaremos la clase con @GraphQLApi y expondremos las queries y las mutaciones que deseemos.

Empecemos por una query que nos dé un elemento químico por su símbolo

@Query
@Description("Get a Element by the symbol")
public ElementDto get(@Name("symbol") String symbol) {
	return service.get(symbol);
}

Simplemente, por haber anotado el resource con @GraphQLApi, y por haber anotado ese método con @Query, la implementación del microprofile de GraphQL, que en este caso es el sabor SmallRye, nos ha generado un esquema.

Si vamos a http://localhost:8080/q/graphql-ui nos ofrece una interfaz de usuario con las consultas y mutaciones que podemos hacer, y qué campos podemos pedir.

Interfaz de GraphQL con las consultas y mutaciones que podemos hacer

A la derecha, en «Docs» podemos consultar el schema que se va construyendo a medida que vamos añadiendo consultas.

4. Añadiendo test RestAssured

Antes de añadir consultas adicionales, vamos a familiarizarnos con esto de GraphQL. Y qué mejor manera que añadiendo algún test.

Al final una petición GraphQL no deja de ser una petición POST al endpoint /graphql

@InjectSpy
ElementService service;

@BeforeEach
public void beforeEach() {
	PanacheMock.mock(Element.class);
}

@Test
public void when_getElementBySymbol_then_return_the_element() {
	//given
	GraphQLQuery query = new GraphQLQuery(); 
	query.setQuery("{ element(symbol:\"H\"){ symbol name atomicNumber  } }");
	
	Element hydrogen = spyHydrogen();
	when(Element.findBySymbol("H")).thenReturn(hydrogen);
	
	//when
	Element actual = 
			given()
			.when()
				.header("Content-Type", "application/json")
				.body(query)
				.post("/graphql")
			.then()
				.assertThat()
				.statusCode(200)
				.extract()
				.jsonPath()
				.getObject("data.element", Element.class);
	
	//then
	verify(service, times(1)).get("H");
	
	assertEquals("H", actual.symbol);
	assertEquals("hydrogen", actual.name);
	assertEquals(1, actual.atomicNumber);
}

Lo primero de todo es que inyectamos un spy del servicio, para verificar que llamamos al método get().

Antes de cada test, en el @BeforeEach, mockeamos todas las entidades de mongodb, para no tener que levantarnos un testcontainer.

Pero por lo demás, este test no es un unitario aislado, si no que es colaborativo, ya que no está confinado a una única clase. Atraviesa todas las capas hasta llegar a la de persistencia que es la única mockeada.

Empezamos entrenando la consulta findBySymbol(«H») para que nos devuleva un mock del hidrógeno.

La petición RestAssured, extraemos el body de la respuesta, y proyectamos lo que está en el jsonPath «data.element» sobre un objeto de tipo Element y nos lo guardamos, para hacer algunas aserciones sobre él.

Verificamos, que cuando llamamos al endpoint con esa query, se acaba llamando al método get del servicio con «H», y verificamos que lo que nos devuelve el json, es lo que esperamos.

Ejecutamos los test y vemos que pasa en verde.

5. Mutaciones de creación, actualización y borrado

Venga, vamos a añadir algunas consultas adicionales

@Query
@Description("Get all Elements")
public List<Element> allElements() {
	return service.allElements();
}

@Mutation
@Description("Create an Element")
public Element createElement(Element element) {
	return service.create(element);
}

@Mutation
@Description("Update an Element")
public Element updateElement(Element element) {
	return service.update(element);
}

@Mutation
@Description("Delete an Element")
public Element deleteElement(String symbol) {
	return service.delete(symbol);
}

Pero claro, habrá que implementar los métodos del servicio

public List<Element> allElements() {
	return Element.listAll();
}

public Element create(Element element) {
	Element stored = Element.findBySymbol(element.symbol);
	if (stored == null) {
		element.persist();
		return element;
	} 
	return stored;
}

public Element update(Element element) {
	Element stored = Element.findBySymbol(element.symbol);
	if (stored == null) {
		element.persist();
		return element;
	} 
	stored.name = element.name;
	stored.group = element.group;
	stored.period = element.period;
	stored.atomicNumber = element.atomicNumber;
	stored.atomicMass = element.atomicMass;
	stored.electronConfiguration = element.electronConfiguration;
	stored.update();
	return stored;
}

public Element delete(String symbol) {
	Element stored = Element.findBySymbol(symbol);
	if (stored != null) {
		stored.delete();
	}
	return stored;
}

Como tenemos arrancado Quarkus en modo desarrollo, con mvn quarkus:dev podremos consultar lo que ha cambiado. Refrescamos la pantalla del navegador, y vemos que el texto predictivo del interfaz nos ayuda con las consultas y mutaciones que podemos hacer.

Vamos a hacer una mutación para crear el Hidrógeno.

Interfaz GraphQL con la mutación de creación del hidrógeno

Pero claro también queremos poder consultarlo. Vamos a hacer la consulta por su símbolo

Interfaz GraphQL con la query de consulta del hidrógeno

Ojo, que ha aparecido un ID que no está en nuestro modelo.

Como Element es una entidad que extiende de PanacheMongoEntity resulta, que el id es una propiedad que se refiere al objectID de Mongo.
Es el identificador único de la tupla en Mongo.

Ahora vamos a probar la mutación de actualización, y vamos a cambiar el nombre de Hydrogen por Hidrógeno

mutación de actualización

Y consultamos el elemento para ver que los cambios se han persistido correctamente.

consulta correspondiente al elemento modificado

Las mutaciones, no tienen por qué hacerse de una en una. De hecho, voy a hacer una mutación múltiple para dar de alta todos los elementos de la tabla periódica.

mutation {
	c1:createElement(element:{
		atomicNumber: 1
		symbol: "H"
		name: "Hydrogen"
		atomicMass: 1
		electronConfiguration: "1s1"
	}){ id symbol }
	c2:createElement(element:{
		atomicNumber: 2
		symbol: "He"
		name: "Helium"
		atomicMass: 4.002602
		electronConfiguration: "1s2"
	}){ id symbol }
	c3:createElement(element:{
	  atomicNumber: 3
	  symbol: "Li"
	  name: "Lithium"
	  atomicMass: 6.941
	  electronConfiguration: "[He] 2s1"
	}){ id symbol }
    [...] 
	c118:createElement(element:{
		atomicNumber: 118
		symbol: "Og"
		name: "Oganesson"
		atomicMass: 294
		electronConfiguration: "[Rn] 5f14 6d10 7s2 7p6"
	}){ id symbol } 
}

mutación múltiple de todos los elementos

La única diferencia cuando hacemos más de una mutación en una única request/operación es que debemos dar un alias a cada mutación.
Así que le ponemos el prefijo que veis en el ejemplo anterior a cada una de las mutaciones.

Si ahora consultamos todos los elementos veremos que nos devuelve el JSON que le pedimos

consulta de todos los elementos

En el fondo, cuando nos pidan información no usarán la interfaz gráfica, si no que nos harán una petición parecida a ésta.

curl con la consulta de todos los elementos

6. Conclusiones

Hemos aprendido que hacer una interfaz GraphQL es realmente sencillo con Quarkus. De hecho, podemos usar los mismos servicios para atender una API REST o para atender una API GraphQL.

Por supuesto, no lo he contado pero si en lugar de Element, nuestros servicios devolvieran Uni, ya sólo por eso, serían reactivos y podríamos programarlos como tal. Al venir de serie con Quarkus, se puede compilar a nativo sin problema, con las ventajas que eso aporta.

7. Enlaces y referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad