Introducción

1

¿Ya terminaste el Curso de NestJS: Programación Modular?

2

Platzi Store: presentación del proyecto e instalación

Database

3

Cómo instalar Docker para este proyecto

4

Configuración de PostgresSQL en Docker

5

Explorando postgres con interfaces gráficas y terminal

6

Integración de node-postgres con NestJS

7

Conexión como inyectable y ejecutando un SELECT

8

Usando variables de ambiente

TypeORM

9

¿Qué es un ORM? Instalando y configurando TypeORM Module

10

Creando tu primera entidad

11

TypeORM: active record vs. repositories

12

Crear, actualizar y eliminar

13

Cambiar a Mysql demo (opcional)

Migraciones

14

Sync Mode vs. Migraciones en TypeORM

15

Configurando migraciones y npm scripts

16

Corriendo migraciones

17

Modificando una entidad

Relaciones

18

Relaciones uno a uno

19

Resolviendo la relación uno a uno en el controlador

20

Relaciones uno a muchos

21

Resolviendo la relación uno a muchos en el controlador

22

Relaciones muchos a muchos

23

Resolviendo la relación muchos a muchos en el controlador

24

Manipulación de arreglos en relaciones muchos a muchos

25

Relaciones muchos a muchos personalizadas

26

Resolviendo la relación muchos a muchos personalizada en el controlador

Consultas

27

Paginación

28

Filtrando precios con operadores

29

Agregando indexadores

30

Modificando el naming

31

Serializar

Migración a NestJS 9 y TypeORM 0.3

32

Actualizando Dependencias para NestJS 9

33

Cambios en TypeORM 0.3

34

Migraciones en TypeORM 0.3

Próximos pasos

35

Cómo solucionar una referencia circular entre módulos

36

Continúa con el Curso de NestJS: Autenticación con Passport y JWT

No tienes acceso a esta clase

¡Continúa aprendiendo! Únete y comienza a potenciar tu carrera

Manipulación de arreglos en relaciones muchos a muchos

24/36
Recursos

Aportes 9

Preguntas 3

Ordenar por:

¿Quieres ver más aportes, preguntas y respuestas de la comunidad?

Al método de añadir categorías yo le añadí algunas validaciones como comprobar si existe el product o la category y comprobar si ya contiene esta categoría para que no se repita.

async addCategoryToProduct(productId: number, categoryId: number) {
    const product = await this.productRepo.findOne(productId, {
      relations: ['categories', 'brand'],
    });
    if (!product) {
      throw new NotFoundException(`Product #${productId} not found`);
    }
    const category = await this.categoryRepo.findOne(categoryId);
    if (!category) {
      throw new NotFoundException(`Category #${categoryId} not found`);
    }
    if (!product.categories.find((item) => item.id == categoryId)) {
      product.categories.push(category);
    }
    return this.productRepo.save(product);
  }

Reutilizar endpoint de update y aceptar multiples ids

Se me ocurrió que puede llegar a ser mas util agregar la lógica de eliminar y añadir categorías al producto dentro del endpoint de update, en vez de tener que recibir mas de una petición si el usuario quiere, por ejemplo, cambiar el titulo del producto y añadir una categoría.

Para mantener un código limpio hice dos métodos privados dentro del service de producto, como hizo el profesor, uno para eliminar categorías y otro para añadir.

\

Añadir

  • Se obtienen todas las categorías usando el array de ids que mande el usuario (typeorm va a ignorar las categorías que no encuentre)
  • Con el spread operator unimos las categorías ya existentes con las nuevas
  private async addCategoriesToProduct(
    product: Product,
    categoriesIds: Category['id'][],
  ) {
    const newCategories = await this.categoryRepo.findBy({
      id: In(categoriesIds),
    });
    return [...product.categories, ...newCategories];
  }

Eliminar

  • Con el método filter eliminamos aquellas categorías que su id este incluido en el array de categorías a eliminar
  private deleteCategoriesFromProduct(
    product: Product,
    categoriesIdsToDelete: Category['id'][],
  ) {
    /*
      Filter method is faster than a
      traditional for loop with splice method and break sentence, in small arrays
    */
    return product.categories.filter(
      (category) => !categoriesIdsToDelete.includes(category.id),
    );
  }

Metodo Update

  • Si el usuario mando categorías para añadir, llamamos al método privado y guardamos el nuevo array de categorías en el producto actual
  • Para eliminar categorías se usa la misma lógica
 async update(id: number, changes: UpdateProductDto) {
    const product = await this.findOne(id);
    if (changes.brandId) {
      product.brand = await this.brandRepository.findOne({
        where: { id: changes.brandId },
      });
    }

    if (changes.categoriesIdsToDelete) {
      product.categories = this.deleteCategoriesFromProduct(
        product,
        changes.categoriesIdsToDelete,
      );
    }

    if (changes.categoriesIds) {
      product.categories = await this.addCategoriesToProduct(
        product,
        changes.categoriesIds,
      );
    }
    this.productRepo.merge(product, changes);
    return this.productRepo.save(product);
  }

DTO

export class UpdateProductDto extends PartialType(CreateProductDto) {
  @IsArray()
  @IsOptional()
  @ApiProperty()
  readonly categoriesIdsToDelete?: Category['id'][];
}

Peticion de ejemplo con Insomnia

Una peticion de ejemplo para cambia el nombre del producto, añadir la categoría con id 2 y borrar la categoría con id 1

{
	"categoriesIds": [2],
	"name": "Nuevo titulo",
	"categoriesIdsToDelete": [1]
}

Apuntes

Remover una categoría de un producto

// src\products\services\products.service.ts
async removeCategoryByProduct(productId: number, categoryId: number){
  const product = await this.productRepo.findOne(productId, {
    relations: ['categories']
  });
  product.categories = product.categories.filter((item) => {
    return item.id !== categoryId
  })
}
// src\products\controllers\products.controller.ts
@Delete(':id/category/:categoryId')
deleteCategory(
  @Param('id', ParseIntPipe) id: number,
  @Param('categoryId', ParseIntPipe) categoryId: number
){
  return this.productsService.removeCategoryByProduct(id, categoryId);
}

Agregar categoría a un producto

// src\products\services\products.service.ts
async addCategoryToProduct(productId: number, categoryId: number){
  const product = await this.productRepo.findOne(productId, {
    relations: ['categories']
  });
  const category = await this.categoryRepo.findOne(categoryId);
  product.categories.push(category);
  return this.productRepo.save(product);
}
// src\products\controllers\products.controller.ts
@Put(':id/category/:categoryId')
addCategoryToProduct(
  @Param('id', ParseIntPipe) idProduct: number,
  @Param('categoryId', ParseIntPipe) idCategory: number,
) {
  return this.productsService.addCategoryToProduct(idProduct, idCategory);
}

Manipulación de arreglos en relaciones muchos a muchos

Ya logramos resolver nuestras relaciones muchos a muchos, pero que pasa si quiero ligar un producto a una categoría más, o que ya no quiero que haga parte de una categoría x.

Para resolver este tipo de problemas, TypeORM nos da una serie de funciones manejadas con base a arreglos que son bastante cómodas de usar para solucionar estos problemas.

Para esto, vamos al código y veamos como resolver esto:

  • **src/products/services/product.service.ts**:
@Injectable()
export class ProductsService {
  // ...
  async update(id: number, changes: UpdateProductDto) {
    const product = await this.productRepo.findOneBy({ id });

		// ...
    // para actualizar las categorías, puede ser similar a como las insertamo al crearlas
    if (changes.categoriesIds) {
      const categories = await this.categoryRepo.findBy({
        id: In(changes.categoriesIds),
      });
      product.categories = categories;
    }

    this.productRepo.merge(product, changes);
    return this.productRepo.save(product);
  }
}

El problema de resolver el actualizar las categorías de esta forma puede ser algo complejo a la larga, debido a que para borrar una sola categoría, tenemos que enviar todas las categorías menos la que no queremos, y para agregar una categoría tendríamos que enviar todas las categorías que enviamos y la nueva, y esto puede ser confuso y peligroso a medida que nuestra aplicación crece.

Aquí es cuando entran las formas de manipular arreglos específicas con TypeORM, para implementarlas, vamos a tener que crear servicios adicionales que nos permitan manejar esta manipulación de manera más comoda, veamos como hacerlo:

  • **src/products/services/product.service.ts**:
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';

import { Product } from './../entities/product.entity';
import { Category } from '../entities/category.entity';
import { Brand } from '../entities/brand.entity';

import { CreateProductDto, UpdateProductDto } from '../dtos/product.dto';

@Injectable()
export class ProductsService {
  // ...

  // eliminar la categoría relacionada a un producto
  async removeCategoryByProduct(productId: number, categoryId: number) {
    // buscamos nuestro producto en base al id
    const product = await this.productRepo.findOne({
      where: { id: productId },
      // es importante traer la relación de las categorías para poder manipularla
      relations: ['categories'],
    });

    // ahora, para eliminar esta categoría, debemos utilizar manipulación de arrays como lo haríamos normalmente
    product.categories = product.categories.filter(
      (item) => item.id !== categoryId,
    );

    // ahora guardamos el product
    return this.productRepo.save(product);
  }

  // añadir la categoría relacionada a un producto
  async addCategoryByProduct(productId: number, categoryId: number) {
    // buscamos nuestro producto en base al id
    const product = await this.productRepo.findOne({
      where: { id: productId },
      // es importante traer la relación de las categorías para poder manipularla
      relations: ['categories'],
    });

    // buscamos la categoría especifica en el repositorio
    const category = await this.categoryRepo.findOneBy({ id: categoryId });
    // en lo que encuentre la categoría, la añadimos al arreglo de categorías
    product.categories.push(category);

    // ahora guardamos el product
    return this.productRepo.save(product);
  }
}

Ahora, como estos son métodos nuevos, debemos añadirlos en el controlador para tener los endpoints donde los podamos usar, vamos a hacer esto:

  • **src/products/controlers/product.controler.ts**:
@ApiTags('products')
@Controller('products')
export class ProductsController {
  // ...

  // añadimos el nuevo endpoint DELETE para borrar una categoría a un producto
  @Delete(':id/category/:categoryId')
  deleteCategory(
    @Param('id', ParseIntPipe) id: number,
    // recibimos el parámetro extra de la siguiente forma:
    @Param('categoryId', ParseIntPipe) categoryId: number,
  ) {
    // usamos el id del producto y el id de la categoría
    return this.productsService.removeCategoryByProduct(id, categoryId);
  }

  // añadimos el nuevo endpoint PUT para anadir una categoría a un producto
  @Put(':id/category/:categoryId')
  addCategory(
    @Param('id', ParseIntPipe) id: number,
    // recibimos el parámetro extra de la siguiente forma:
    @Param('categoryId', ParseIntPipe) categoryId: number,
  ) {
    // usamos el id del producto y el id de la categoría
    return this.productsService.addCategoryByProduct(id, categoryId);
  }
}

Y listo, ya hemos logrado agregar esta lógica usando la manipulación de arrays que nos da TypeORM que se usa como se usa JavaScript normal, para administrar nuestras relaciones muchos a muchos.

Modifique el código para que no se pueda agregar una categoría existente


  async addCategoryByProduct(productId: number, categoryId: number) {
    const product = await this.productRepo.findOne(productId, {
      relations: ['categories'],
    });
    const category = await this.categoryRepo.findOne(categoryId);
    if (!category) {
      throw new NotFoundException(`Category #${categoryId} not found`);
    }
    const exists = product.categories.some(
      (element) => element.id === categoryId,
    );
    if (!exists) {
      product.categories.push(category);
    }
    return this.productRepo.save(product);
  }
Me ha parecido excelente cómo TypeORM nos abstrae de todo el proceso de modificación de la tabla pivote entre productos y categorías. Ahora, con solo manipular un array usando métodos como `filter` o `push`, se realizan todos los cambios necesarios de forma automática en segundo plano.
Enfocado a una lista para agregar varias categorias o eliminar: ```js async removeCategories(productId: number, ids: number[]) { const product = await this.productRepository.findOne({ where: { id: productId }, relations: { categories: true }, }); product.categories = product.categories.filter( (category) => !ids.includes(category.id), ); return this.productRepository.save(product); } async addCategories(productId: number, ids: number[]) { const product = await this.productRepository.findOne({ where: { id: productId }, relations: { categories: true }, }); const categories = await this.categoryRepository.findBy({ id: In(ids), }); product.categories.concat(categories); return this.productRepository.save(product); } ```en el controlador: ```js @Put(':id/categories') addCategories( @Param('id', ParseIntPipe) id: number, @Body() payload: AddCategoriesProductDto, ) { return this.productsService.addCategories(id, payload.categoriesIds); } @Delete(':id/categories') deleteCategories( @Param('id', ParseIntPipe) id: number, @Body() payload: DeleteCategoriesProductDto, ) { return this.productsService.removeCategories(id, payload.categoriesIds); } ```DTO para agregar o eliminar varias categories: ```js export class DeleteCategoriesProductDto { @IsArray() @IsNotEmpty() @ApiProperty({ description: `Categorias a eliminar/agregar del producto`, type: [Number], }) readonly categoriesIds: number[]; } export class AddCategoriesProductDto extends PartialType( DeleteCategoriesProductDto, ) {} ```

Si inyectamos el repositorio de Brand el metodo findOne ha cambiado ahora se debe llamar con el findOneBy y asi ya no te genera el error

Se me ocurre que se podria usar el metodo includes para dejar solo los ids que hayan en el request, algo asi:

<code> 
product = product.categories.filter(item => categoriesIds.includes(item));

si habria que enviar un array de categorias con los que ya hubiera tenido el producto pero seria un solo endpoint y en el front al final seria modificar el array y algún botón de confirmar que envía la petición al api.