¿Qué aprenderá?

Al finalizar este tutorial el estudiante estará en capacidad de incorporar GraphQL a una API desarrollada en Nest.js.

¿Qué necesita?

Para realizar este taller Ud. debe:

  1. Tener implementada la persistencia, la lógica, las pruebas unitarias de la lógica, los controladores y las pruebas del API.

El código que acompaña a este tutorial puede ser consultado en este repositorio:

https://github.com/MISW4403-Diseno-y-construccion-de-APIs/MISW4403_202214_GraphQL 

Para el uso de GraphQL y todos los elementos que se necesitan a lo largo de este tutorial se deben instalar las siguientes dependencias:

$ npm install --save @nestjs/graphql @nestjs/apollo graphql apollo-server-express

Los paquetes anteriores son las dependencias de GraphQL y Apollo (un servidor de GraphQL para Nest.js).

Luego de que estos paquetes se hayan instalado en el proyecto, se debe importar el módulo GraphQLModule en el módulo principal de la aplicación. El método forRoot del módulo recibe dos parámetros:

La importación en el módulo AppModule se vería de la siguiente forma

import { GraphQLModule } from '@nestjs/graphql';
import { join } from 'path';
import { ApolloDriver } from '@nestjs/apollo';
  imports: [
    GraphQLModule.forRoot({
      autoSchemaFile: join(process.cwd(), 'src/schema.gql'),
      driver: ApolloDriver
    }),
]
/* archivo: src/app.module.ts */

Al agregar la referencia del módulo GraphQLModule en el módulo principal aparecerá el siguiente error en el proyecto:

GraphQLError: Query root type must be provided.

Esto se soluciona una vez se haya agregado al menos una query, paso que se explica más adelante en este tutorial.

Como primer paso se debe definir el esquema sobre el cuál se trabajará en el entorno GraphQL de la aplicación. Dado que dentro de lo desarrollado en el curso ya se cuenta con un modelado de entidades, utilizaremos esas entidades para construir el modelo.

Cada entidad la anotaremos con @ObjectType y los atributos con @Field.

Por ejemplo, para agregar la entidad MuseumEntity al esquema de GraphQL, se deberían agregar las anotaciones que se ven a continuación:

import { ArtworkEntity } from '../artwork/artwork.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ExhibitionEntity } from '../exhibition/exhibition.entity';
import { Field, ObjectType } from '@nestjs/graphql';

@ObjectType()
@Entity()
export class MuseumEntity {
   @Field()
   @PrimaryGeneratedColumn('uuid')
   id: string;

   @Field()
   @Column()
   name: string;

   @Field()
   @Column()
   description: string;

   @Field()
   @Column()
   address: string;

   @Field()
   @Column()
   city: string;

   @Field()
   @Column()
   image: string;

   @OneToMany(() => ExhibitionEntity, exhibition => exhibition.museum)
   exhibitions: ExhibitionEntity[];

   @Field(type => [ArtworkEntity])
   @OneToMany(() => ArtworkEntity, artwork => artwork.museum)
   artworks: ArtworkEntity[];
}
/* archivo: src/museum/museum.entity.ts */

Note que por simplicidad no se ha incluido en el esquema la relación exhibitions dado que solo agregaremos el objeto ArtworkEntity.

Para que la relación artworks tenga sentido, en el esquema GraphQL también se debe anotar la entidad ArtworkEntity de la siguiente forma:

import { ExhibitionEntity } from "../exhibition/exhibition.entity";
import { MuseumEntity } from "../museum/museum.entity";
import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from "typeorm";
import { ImageEntity } from "../image/image.entity";
import { ArtistEntity } from "../artist/artist.entity";
import { Field, ObjectType } from "@nestjs/graphql";

@ObjectType()
@Entity()
export class ArtworkEntity {

   @Field()
   @PrimaryGeneratedColumn("uuid")
   id: string;
   
   @Field()
   @Column()
   name: string;
   
   @Field()
   @Column()
   year: number;
   
   @Field()
   @Column()
   description: string;
   
   @Field()
   @Column()
   type: string;
   
   @Field()
   @Column()
   mainImage: string;

   @Field(type => MuseumEntity)
   @ManyToOne(() => MuseumEntity, museum => museum.artworks)
   museum: MuseumEntity;

   @ManyToOne(() => ExhibitionEntity, exhibition => exhibition.artworks)
   exhibition: ExhibitionEntity;

   @OneToMany(() => ImageEntity, image => image.artwork)
   images: ImageEntity[];

   @ManyToOne(() => ArtistEntity, artist => artist.artworks)
   artist: ArtistEntity;
}
/* archivo: src/artwork/artwork.entity.ts */

Esto modificará el esquema automáticamente una vez que se ejecute el programa.

La clase resolver será la encargada retornar ciertos datos según las queries definidas sobre cierto objeto en GraphQL. Para definir una nueva clase resolver, por ejemplo para el recurso museum, se debe ejecutar en la terminal el siguiente código:

nest generate resolver museum --no-spec

Esto creará en la carpeta "museum" una nueva clase denominada MuseumResolver.

Dentro de esta clase, se recibe como parámetro inyectado en el constructor, el servicio del recurso. Adicionalmente, se deben definir los tipos de peticiones que se permitirán para ese recurso en particular.

En este caso se definirán dos peticiones: obtener la lista de museos y obtener un museo por su ID. Para definir un método como una nueva petición para el recurso que estamos manejando, en el resolver el método debe ir anotado con @Query indicando el tipo de retorno.

Los parámetros, de forma similar, se deben marcar con la anotación @Args. Esta sería la implementación del resolver con estas dos peticiones:

import { Args, Query, Resolver } from '@nestjs/graphql';
import { MuseumService } from './museum.service';
import { Museum } from './museum.entity';

@Resolver()
export class MuseumResolver {
    constructor(private museumService: MuseumService) {}

    @Query(() => [Museum])
    museums(): Promise<Museum[]> {
        return this.museumService.findAll();
    }

    @Query(() => Museum)
    museum(@Args('id') id: string): Promise<Museum> {
        return this.museumService.findOne(id);
    }
   
}
/* src/museum/museum.resolver.ts */

Al incluir lo anterior el programa deberá ejecutarse sin errores.

En este ejemplo se definirán tres mutaciones: crear un museo, actualizar un museo y eliminar un museo. Para definir una nueva mutación, se utiliza la anotación @Mutation y se especifica el tipo de retorno del método, de igual forma, se definen los argumentos del método mediante la anotación @Args.

Dado que para la creación de museos utilizamos como referencia la clase MuseumDto, le agregaremos a esa clase la anotación @InputType. Esta anotación indica que se recibirán objetos de ese tipo como argumentos de las mutaciones

import { Field, InputType } from '@nestjs/graphql';
import {IsNotEmpty, IsString, IsUrl} from 'class-validator';

@InputType()
export class MuseumDto {
 @Field()
 @IsString()
 @IsNotEmpty()
 readonly name: string;

 @Field()
 @IsString()
 @IsNotEmpty()
 readonly description: string;

 @Field()
 @IsString()
 @IsNotEmpty()
 readonly address: string;

 @Field()
 @IsString()
 @IsNotEmpty()
 readonly city: string;
 
 @Field()
 @IsUrl()
 @IsNotEmpty()
 readonly image: string;
}
/* archivo: src/museum/museum.dto.ts */

Finalmente, definimos las mutaciones createMuseum, updateMuseum y deleteMuseum así:

...
   @Mutation(() => MuseumEntity)
   createMuseum(@Args('museum') museumDto: MuseumDto): Promise<MuseumEntity> {
       const museum = plainToInstance(MuseumEntity, museumDto);
       return this.museumService.create(museum);
   }

   @Mutation(() => MuseumEntity)
   updateMuseum(@Args('id') id: string, @Args('museum') museumDto: MuseumDto): Promise<MuseumEntity> {
       const museum = plainToInstance(MuseumEntity, museumDto);
       return this.museumService.update(id, museum);
   }

   @Mutation(() => String)
   deleteMuseum(@Args('id') id: string) {
       this.museumService.delete(id);
       return id;
   }
...
/* archivo: src/museum/museum.resolver.ts */

Con esto ya quedaría definida correctamente la incorporación de GraphQL para el recurso Museum.

Para visualizar el entorno de GraphQL para el API, accedemos mediante un navegador a la URL http://localhost:3000/graphql. Esto despliega un playground donde podemos ver el esquema de GraphQL y ejecutar las consultas y mutaciones implementadas.

Para obtener, por ejemplo, el id y el nombre de todos los museos podemos ejecutar la siguiente consulta:

{
  museums {
    id
    name
  }
}

También podríamos obtener, por ejemplo, el nombre y la ciudad de un museo en específico, y sus obras de arte, y de cada obra de arte solo obtener el nombre.

Esta sería entonces la consulta:

{
  museum (id: "3730f124-f6a0-496e-8cc0-c61088a12a2e") {
    name
    city
    artworks {
      name
    }
  }
}

A diferencia de una petición común al API, no se están trayendo todos los atributos de los museos ni del museo específico sino que únicamente se traen los atributos que nos son de interés.

Ahora ejecutemos una mutación. Un ejemplo del código para ejecutar la mutación createMuseum es el siguiente:

mutation {
  createMuseum(museum: {
    name: "Museo del oro"
    description: "Museo insignia de Colombia"
    address: "Cra. 5 No. 15-25"
    city: "Bogotá"
    image: "https://www.banrepcultural.org/bogota/museo-del-oro"
  }) {
    id
    name
  }
}

Esta instrucción creará un nuevo museo y retornará el id asignado por la base de datos y el nombre.