Ángel David Roque Ayala
Jose Daniel Barría Reyes
muy buen aporte, por cierto en:
async removeCategoryByProduct(productId: number, categoryId: number)
te falto
return this.productRepo.save(product);
Gestionar relaciones muchos a muchos puede volverse incómodo cuando necesitas agregar o eliminar una sola categoría de un producto y el enfoque tradicional te obliga a enviar todo el array de IDs cada vez. TypeORM, combinado con métodos nativos de arrays en JavaScript como filter y push, ofrece una forma más limpia y profesional de resolver este problema.
¿Cómo funciona la actualización del array completo en una relación muchos a muchos?
Cuando creas un producto con varias categorías, simplemente asignas el array de IDs y TypeORM resuelve la relación. Para actualizarlo, podrías recibir un nuevo array de categorías en el método update del servicio, buscar esas categorías y reasignarlas al producto [01:15].
El problema aparece cuando la lista crece. Si un producto pertenece a diez categorías y solo quieres eliminar una, deberías enviar las nueve restantes. Si quieres agregar una, deberías enviar las diez anteriores más la nueva. Enviar siempre el array completo se vuelve propenso a errores.
¿Por qué separar la manipulación del array en métodos independientes?
Una buena práctica es crear métodos específicos para agregar o remover elementos de la relación, en lugar de depender únicamente del método update. Esto permite endpoints dedicados que reciben solo el ID del producto y el ID de la categoría a modificar [03:22].
¿Cómo eliminar una categoría de un producto con filter?
El método removeCategoryByProduct recibe el productId y el categoryId. Primero, se obtiene el producto incluyendo la relación hacia categories, algo fundamental para que el array exista en memoria [05:50].
typescript async removeCategoryByProduct(productId: number, categoryId: number) { const product = await this.productsRepo.findOne(productId, { relations: ['categories'], }); product.categories = product.categories.filter( (item) => item.id !== categoryId, ); return this.productsRepo.save(product); }
filter para generar un nuevo array sin la categoría indicada.filter es un método inmutable: no modifica el array original, sino que devuelve uno nuevo.save.¿Por qué es importante cargar la relación antes de filtrar?
Sin la opción relations: ['categories'], el array categories queda como undefined y el método filter lanza un error: "cannot read property filter of undefined" [05:38]. TypeORM necesita la instrucción explícita para traer los datos relacionados.
¿Qué papel juega ParseIntPipe en los parámetros de URL?
Los parámetros que llegan desde la URL siempre son strings, aunque declares el tipo como number en TypeScript. Esa declaración de tipo es solo una referencia en tiempo de compilación, no una transformación real [07:12].
El pipe ParseIntPipe resuelve esto: convierte el string a número entero y, si el valor no es numérico, rechaza la petición antes de llegar al servicio.
typescript @Delete(':id/category/:categoryId') deleteCategory( @Param('id', ParseIntPipe) id: number, @Param('categoryId', ParseIntPipe) categoryId: number, ) { return this.productsService.removeCategoryByProduct(id, categoryId); }
¿Cómo agregar una categoría a un producto existente con push?
El método addCategoryToProduct también recibe productId y categoryId. Obtiene el producto con su relación y, además, busca la categoría para verificar que exista [09:08].
typescript async addCategoryToProduct(productId: number, categoryId: number) { const product = await this.productsRepo.findOne(productId, { relations: ['categories'], }); const category = await this.categoriesRepo.findOne(categoryId); product.categories.push(category); return this.productsRepo.save(product); }
push es un método mutable: modifica directamente el array original, por lo que no necesitas reasignar.En el controlador, este método se expone como un endpoint PUT que recibe el ID del producto y el ID de la categoría como parámetros de ruta [10:30]. No se necesita body, toda la información viaja en la URL.
La diferencia entre métodos mutables e inmutables de JavaScript es clave aquí: push cambia el array en el que se ejecuta, mientras que filter retorna uno nuevo sin alterar el original. Conocer esta distinción te permite elegir el enfoque correcto según el caso.
Con estos dos endpoints independientes, administrar relaciones muchos a muchos se simplifica considerablemente. Si quieres compartir algún patrón diferente para manejar estas relaciones, deja tu aporte en los comentarios.
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); }
Perfecto Jairo, en el caso de que una persona quisiera agregar una categoría que ya está presente en el producto yo le agregaría el siguiente código.
A ...
if (!product.categories.find((item) => item.id == categoryId)) { product.categories.push(category); }
Le añadimos ...
else { throw new ConflictException( `Category #${categoryId} is already present in this product`, ); }
async addCategoryToProduct(productId: number, categoryId: number) { const product = await this.productRepository.findOne(productId, { relations: ['categories'], }); if (!product) { throw new NotFoundException(`Product #${productId} not found`); } const category = await this.categoriesService.findOne(categoryId); if (!category) { throw new NotFoundException(`Category #${categoryId} not found`); } if (!product.categories.find((item) => item.id == categoryId)) { product.categories.push(category); } else { throw new ConflictException( `Category #${categoryId} is already present in this product`, ); } return this.productRepository.save(product); }
Ángel David Roque Ayala
Jose Daniel Barría Reyes
muy buen aporte, por cierto en:
async removeCategoryByProduct(productId: number, categoryId: number)
te falto
return this.productRepo.save(product);
Juan Collado
Andrés Esteban Rodríguez Jiménez
Daniel Alejandro Barrientos Quispe
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); }
Renato Renzo Luna Herrera
Si los valores por URL siempre entran como string, como hago para validar convertir los valores de un payload usando dtos?
Efrén Quintero Salazar
Excelente curso Mr. Nicolás Por que el producto tiene muchas categorías no debería ser al revés a una categoría le pertenecen muchos productos?
Juan Camilo Noreña López
Es una relación muchos a muchos, las categorías pueden tener muchas productos, así como los productos pueden pertenecer a varias categorías.
Álvaro Rubio Gómez
Hola buenas, Una vez que creamos los métodos de removeCategoryByProduct y addCategoryToProduct creamos dos nuevos endpoints para hacer uso de ellos. ¿Sería mala práctica hacer uso de estos dos métodos dentro del metodo de update? Por ejemplo, si el campo de categoryIds no viene vacio, revisamos el array y llamamos removeCategoryByProduct para los números negativos y a addCategoryToProduct para los numeros positivos.. De esta forma podriamos añadir y eliminar varias categorias en una sola llamada. Gracias de antemano :)
Jairo Campos Ruiz
El update si lo pruebas ya te está funcionado de esta manera sin tener que recurrir a los métodos por que si añades el campo de categories lo sobrescribe. Estos métodos son mas que nada por si tenemos muchas categorías en un producto no tener que añadirlas todas de nuevo para modificar solo una.
Andres Camacho
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.
Vanessa Quiquia
Cristhian Manuel Gonzalez Cano
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
Edward Camilo Martinez Frias
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.
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); }
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
private async addCategoriesToProduct( product: Product, categoriesIds: Category['id'][], ) { const newCategories = await this.categoryRepo.findBy({ id: In(categoriesIds), }); return [...product.categories, ...newCategories]; }
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
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] }
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.
Enfocado a una lista para agregar varias categorias o eliminar:
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, ) {}