> Manuales > Manual de NestJS

Práctica de Nest y TypeORM: Cómo recibir endpoints de API una entidad y sus entidades relacionadas, para realizar inserciones y updates, realizando los correspondientes procesos de validación con el DTO y actualización de los datos relacionados.

Añadir y validar elementos a una relación n a m de TypeORM

En el artículo anterior aprendimos a definir relaciones de muchos a muchos en TypeORM y Nest. Ahora que tenemos definidas las entidades y hemos comprobado que se han creado las tablas, ya podemos empezar a crear elementos de ambas entidades y relacionarlos.

Recuerda que seguremos con la aplicación que hemos ido desarrollando a lo largo del Manual de Nest.

Actualización del DTO al dar de alta productos

Al crear productos vamos a usar el archivo de validaciones product.dto.ts, en el que debemos introducir ahora la posibilidad de que nos envíen un array de tallas (sizes) cuando se carga un producto.

Para ello debemos crear un nuevo campo dentro del DTO, en el que recibiremos las tallas como un array de cadenas de nombres. Cada producto podrá tener 0 sizes, 1 o cualquier otra cantidad. Todas esas tallas llegarán en el array, en cadenas sueltas.

Para definir que una vamos a recibir un nuevo campo con un array de cadenas lo declaramos así en el DTO:

sizes: string[];

Pero además de informar del tipo de lo que estoy recibiendo, queremos montar las validaciones en el DTO, por lo que necesitamos comprobar:

Para validar si es un array usamos el decorador @IsArray() proporcionado por "class-validator", tal como aprendimos a trabajar en el artículo de validaciones con ValidationPipe.

Para validar además que los elementos del array sean cadenas, lo podemos hacer con el decorador @IsString de "class-validator", indicando que realmente lo que son cadenas son los elementos del array. Esto lo indicamos con un objeto de configuración en el que ponemos "each: true", tal como ves en este código.

@IsString({ each: true })
@IsArray()
sizes: string[];

Dado el ProductDto que acabamos de modificar, los datos que va a requerir el endpoint de alta de un producto serán algo parecido a esto:

{
    "name": "Camisa Giga",
    "description": "Una camisa gigante",
    "stock": 100,
    "sizes": ["L", "XL", "XXL"]
}

Actualización del servicio

Ahora necesitamos actualizar el código del servicio, para realizar el alta de las tallas según creamos o actualizamos los productos. Vamos a necesitar añadir todo el código necesario para gestionar las tallas, que incluye este proceso:

Al recibir una lista de tallas en un array de cadenas:

El método insert() del servicio que implementa este proceso es el siguiente:

async insert(body: ProductDto) {
    const sizes = await Promise.all(body.sizes.map(size => this.selectOrCreateSize(size)));
    const product = this.productsRepository.create(
      {
        ...body,
        sizes,
      }
    );
    await this.productsRepository.save(product);
    return product;
}

En la primera línea de este método hacemos el array de objetos Size. El código puede parecer un poco complejo, pero vamos a explicarlo.

Promise.all() se encarga de recibir un array de promesas y espera a resolverlas todas. Cuando todas esas promesas se hayan resuelto, devuelve un array con todos los valores que hayan surgido al resolverse las promesas.

En este artículo puedes ver más información sobre promise.all().

A promise.all() le pasamos una serie de promesas, que surgen de la ejecución del método selectOrCreateSize(). Gracias al recorrido con map() sobre body.sizes, este método se ejecuta para cada elemento del array de tallas. Por tanto, para cada talla, invocamos al método selectOrCreateSize(size), enviando la talla actual. Ese método nos devuelve una promesa, que cuando se resuelva nos entregará un elemento de la tabla de tallas, ya sea porque lo ha conseguido encontrar o porque lo ha creado en la tabla.

Luego se sustuye el array de tallas sobre el objeto body de la solicitud que teníamos antes. Por último se guarda el producto, igual que hacíamos antes, devolviendo el producto creado finalmente al controlador.

Ahora podemos ver el método selectOrCreateSize(), que se encarga de buscar una talla en la tabla y seleccionarla. En caso que no la encuentre, simplemente la crea.

private async selectOrCreateSize(size: string) {
    const sizeEntity = await this.sizesRepository.findOne({ size });
    if (sizeEntity) {
      return sizeEntity;
    }
    const newSize = this.sizesRepository.create({ size });
    return this.sizesRepository.save(newSize);
}

El método busca una talla con findOne(). Para buscar la talla le pasa un objeto donde hay una propiedad "size" (que es el nombre del campo sobre el que queremos buscar) con el valor que viene por parámetro al método.

Si encuentra esta size, entonces la devuelve. Si no la encuentra, simplemente crea un elemento con esa cadena y lo guarda, devolviendo el elemento guardado.

Ahora vamos a ver cómo transformar el método update del servicio para conseguir más o menos un comportamiento similar.

El problema aquí es que sizes no siempre va a estar en el body, por lo tanto no siempre se tienen que cambiar los tamaños de los productos. Por este asunto, lo que hacemos es solamente colocar un valor de sizes definido en el caso que el body contuviera ese array.

async update(id: number, body: ProductDto | ProductPatchDto) {
  const productCount = await this.productsRepository.count({ id });
  if(productCount === 0) {
    throw new NotFoundException(`No se encuentra el producto ${id}`);
  }
  const sizes = body.sizes && (await Promise.all(body.sizes.map(size => this.selectOrCreateSize(size))));
  const product = await this.productsRepository.preload({
    id,
    ...body,
    sizes,
  });
  return this.productsRepository.save(product);
}

Además hemos hecho una consulta antes que nada para saber si realmente existe un producto con el id recibido por parámetro, de modo que no se realiza ninguna actualización de la tabla de sizes si no existe un producto que actualizar.

Opción "cascade" en las relaciones con TypeORM

Para facilitar las inserciones de tallas (entidad relacionada Size) cuando damos de alta un producto vamos a realizar una configuración extra. Este paso adicional es meramente opcional y solo lo abordamos a modo de aprendizaje de una de las configuraciones típicas de las relaciones.

TypeORM permite una configuración especial cuando definimos una relación que permite crear de manera automática ítems relacionados.

Por ejemplo, dada la relación de productos y tallas, al crear un producto puedo indicar un listado de tallas. El propio framework puede encargarse de salvar las tallas automáticamente, cuando damos de alta un producto.

Esta configuración especial se llama "cascade" y para definirla usamos un tercer parámetro en el decorador, en el que indicamos un objeto de propiedades de configuración.

La configuración "cascade" la introducimos en la entidad principal de la relación, aquella que tiene el @JoinTable. En nuestro caso en la entidad de Products.

@ManyToMany(() => Size, size => size.products, { cascade: true })
@JoinTable()
sizes: Size[];

Habiendo configurado la relación con las tallas de modo "cascade", nuestro ejemplo de alta de la entidad de tallas puede cambiar un poco, ya que no necesitamos ahora salvar las tallas. Así que el método "selectOrCreateSize" no necesitaría hacer el save() de los objetos Size, quedando de esta manera.

private async selectOrCreateSize(size: string): Promise<Size> {
  let sizeEntity = await this.sizeRepository.findOne({ size });
  if(sizeEntity) {
    return sizeEntity;
  }
  return this.sizeRepository.create({ size });
}

Recuerda que las tallas las indicamos en un array de elementos de la entidad de Size. Si le pasamos ese array, la opción "cascade" se asegurará que las tallas se salven en la base de datos, esto implicará que las tallas nuevas que podamos tener en el array se guarden, aunque en este método no se haya realizado el save().

Esto también repercute en el método para hacer el update de un producto, ya que no sería necesario buscar si existe el producto que se pretende actualizar. Simplemente, se hace todo el proceso para poder actualizarlo y, si después de hacer el preload del producto para su edición, se detecta que no existe tal producto, no se llegarán a guardar las tallas, porque no se han llegado a salvar.

async update(id: number, body: ProductDto | ProductPatchDto): Promise<Product> {
    const sizes = body.sizes && await Promise.all(body.sizes.map(size => this.selectOrCreateSize(size)));
    let inputProduct = {
      id,
      ...body,
      sizes,
    }
    const product = await this.productRepository.preload(inputProduct);
    if(product) {
      return this.productRepository.save(product);
    }
    throw new NotFoundException(`No he encontrado el producto con id ${id}`);
}

Como has comprobado, la opción cascade resulta muy cómoda. Sin embargo queda mencionar un detalle que advierten en la documentación de TypeORM. La opción "cascade" puede ser peligrosa en determinadas situaciones, ya que si se introduce un bug o un problema de seguridad aumentan las posibilidades de que al modificar una entidad los errores se propaguen con las entidades que están relacionadas, aumentando posibles daños colaterales.

Ahora sí hemos cubierto todo el tratamiento de entidades y los datos relacionados de n a m con otras entidades de la base de datos. Esperamos que estos artículos te hayan aclarado bastante este tipo de relación y que puedas implementarla sin mayores complicaciones en tus aplicaciones.

Miguel Angel Alvarez

Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...

Manual