Juan José Jaramillo Cajamarca
Cristian Campo
Me encantó tu código, me hubiera gustado leer una opinión del profesor sobre esto.
Ricardo Tribaldos Hernández
👏👏👏👏
¿Cómo crear, actualizar y eliminar productos en una base de datos utilizando TypeORM?
Al trabajar con bases de datos, uno de los aspectos fundamentales es poder realizar operaciones CRUD (crear, leer, actualizar, eliminar). En este artículo te guiaré a través del proceso de implementación de estas operaciones utilizando TypeORM, un ORM popular para Node.js y TypeScript.
¿Cómo crear un nuevo producto?
La creación de un producto en la base de datos implica generar una nueva instancia de la entidad Product y asignar atributos apropiadamente antes de guardarla. Actualmente, algunos métodos en ProductService necesitan ajustes para trabajar con persistencia de datos en PostgreSQL en lugar de un arreglo en memoria.
Primero, eliminamos el contador innecesario ya que el ID es autoincremental. Usamos el método save del repositorio para guardar la instancia del producto en la base de datos. Aquí un ejemplo del código utilizado:
let newProduct = new Product(); newProduct.image = dto.image; newProduct.name = dto.name; newProduct.description = dto.description; newProduct.price = dto.price; newProduct.stock = dto.stock; // Guardar el nuevo producto en el repositorio await this.productRepository.save(newProduct);
TypeORM ofrece un método más eficiente utilizando create junto con save para manejar múltiples atributos sin necesidad de asignarlos uno por uno:
const newProduct = this.productRepository.create(dto); await this.productRepository.save(newProduct);
¿Cómo actualizar un producto existente?
Para actualizar un producto existente, primero se busca el producto en la base de datos para modificar sus atributos según el DTO recibido. El método merge de TypeORM es útil para fusionar los cambios y luego guardarlos con save:
const productToUpdate = await this.productRepository.findOne(id); if (productToUpdate) { this.productRepository.merge(productToUpdate, dto); await this.productRepository.save(productToUpdate); }
¿Cómo eliminar un producto de la base de datos?
Eliminar un producto es sencillo gracias al método delete proporcionado por el repositorio. Es aconsejable verificar que el producto existe antes de intentar eliminarlo para evitar errores:
const productToDelete = await this.productRepository.findOne(id); if (productToDelete) { await this.productRepository.delete(id); }
¿Cómo manejar errores en las operaciones CRUD?
Al implementar las operaciones CRUD, un aspecto importante es el manejo de errores y la validación de datos. Por ejemplo, al intentar obtener un producto que no existe, podemos lanzar una excepción adecuada:
const product = await this.productRepository.findOne(id); if (!product) { throw new NotFoundException('Product not found'); }
Para resolver promesas de manera eficiente se recomienda siempre usar async y await en las funciones asincrónicas para manejar las respuestas adecuadamente antes de que el controlador procese las solicitudes.
¿Cómo probar las operaciones usando Insomnia?
Una vez implementadas las operaciones CRUD, podemos utilizar herramientas como Insomnia para probarlas. En Insomnia, podemos enviar solicitudes (requests) a endpoints específicos y verificar que las respuestas sean las esperadas. Por ejemplo, al crear un nuevo producto, deberíamos ver reflejados todos sus atributos en la base de datos, y al actualizar el precio de un producto, confirmar que los cambios se han guardado correctamente.
¿Cuáles son los siguientes pasos?
¡El reto ahora es tuyo! Intenta replicar estos procedimientos en las demás entidades de tu proyecto, como marcas, categorías, usuarios y clientes. Este ejercicio te permitirá afianzar tus habilidades y avanzar en el desarrollo de aplicaciones robustas utilizando TypeORM. La práctica y la experimentación constante son el camino hacia el dominio de cualquier herramienta. ¡Adelante!
Juan José Jaramillo Cajamarca
Cristian Campo
Me encantó tu código, me hubiera gustado leer una opinión del profesor sobre esto.
Ricardo Tribaldos Hernández
👏👏👏👏
Hay una actualización de la forma de usar el método findOne del repositorio
const product = await this.productRepo.findOne(id);
si le envías solo el id de tipo number como se encuentra en la línea anterior te retorna el siguiente error:
Type ‘number’ has no properties in common with type ‘FindOneOptions<Product>’.
Pero se puede resolver pasándole un objeto con la propiedad where y el id, de esta forma:
const product = await this.productRepo.findOne({ where: { id } })
También existe otrométodo que es .findOneBy()
A ese método si le puedes pasar el atributo por el que quieres buscar (debes pasarlo de forma clave: valor, pero si la clave es id y el valor que recibe el servicio tiene el mismo nombre "id", entonces sólo escribes id
Queda algo así
nombreFunciónDelServicio(id: number){
const product = await this.productRepo.findOneBy(id)
}
Mauricio De Los santos
El mejor profesor libra por libra
Diego Felipe Ariza Amado
Hola tengo un problema con la creación de las tablas automáticamente. Hasta ahora se crearon products y orders, las demás tablas no se han creado a pesar de que ya tengo configuradas mis entidades y en database module agregue las dos propiedades synchronize: true y autoLoadEntities: true
Alguien podría ayudarme o decirme que puede estar pasando?
Agradezco su ayuda.
Jorge Martinez
Pudiste resolverlo de alguna forma?
Miguel Angel Reyes Moreno
Yo estoy haciendo import de swagger y no tenía el decorador de ApiProperty y sí me dejó hacer update parcial.
Lo que yo creo que sucede es que la terminación del archivo de Nicolas es 'dtos.ts' y debería ser 'dto.ts' en SINGULAR.
Andrés Esteban Rodríguez Jiménez
Héctor Martínez Reséndiz
En versiones recientes se utiliza
findOneBy({ id })
Ya que findOne() fue eliminada de typeorm
Matthew Dominick Rosell Felix
Antonio Aldrin Miranda Peñaranda
Muy interesante, voy a probarlo... gracias por el aporte
Jose Lopez Palencia
Hola, el metodo de eliminar estaria bien de la siguiente forma o se puede optimizar:
async remove(id: number) { const product = await this.findOne(id); if (!product) { throw new NotFoundException(`Product #${id} not found`); } return await this.productRepo.delete(id); }
Matthew Dominick Rosell Felix
La validación del if (!product) en el remove, no es necesario si tu findOne ya lo hace. Solo tendrias que llamas await this.findOne(id) y ya esta
Renato Renzo Luna Herrera
Hola @nicobytes, o alguien mas, podrían explicarme por que todavía tenemos dos conexiones? Si no es así podrían explicarme que pasa aquí exactamente por favor
Juan David Garcia
Hola, tengo 2 módulos productos y compras, mi módulo de compras depende de productos ósea que tiene una llave foránea, a la hora de inserta un dato a mi tabla de compras necesito saber el valor del producto pero solo estoy enviando el id del producto en este caso lo que debería hacer es importar repository o el service del módulo de productos en el service del módulo de compras???
Nicolas Molina
ProfesorHola, genial que inicies a tener estas dudas, tienes dos soluciones puedes compartir el servicio o repository, recomiendo mejor importar el repository. Si tienes errores de dependencia circular igual lo puedes solucionar con el decorador, así:
@Injectable() export class CatsService { constructor( @Inject(forwardRef(() => CommonService)) private commonService: CommonService, ) {} }
También una forma de crear un módulo solo con las entidades y repositorios y luego compartes ese módulo con los demás eso lo puedes ver como ejemplo en la rama 21 del proyecto.
Cayo Legal
Y como se capturan las excepciones?
Elvis Edson Basilio Chambi
De esta forma manejé el error al crear un producto con el mismo nombre.
async create(payload: CreateProductDto) { const newProduct = this.productRepo.create(payload); return await this.productRepo.save(newProduct).catch((error) => { throw new NotFoundException(error.detail); }); }
Res:
{ "statusCode": 404, "message": "Key (name)=(La) already exists.", "error": "Not Found" }
Carlos Rodríguez
Miguel Angel Reyes Moreno
Omar Blanco
Christopher Vilches
Notese que DTO es usado en general para recbir datos desde el body. Pero que pasa cuando tu aplicacion tiene procesos de Cron, job queues, eventos, scripts, y quieres modificar la base de datos desde esos procesos, sin que haya ninguna peticion HTTP de por medio? El patron de DTOs y Pipes en el body se queda algo corto ahi, ya que de todos modos debes ejecutar las validaciones de forma manual cuando quieras hacer un CRUD en la base de datos (crear y modificar al menos). Lo de usar Pipes esta pensado para cuando la data viene unicamente desde un HTTP request. La capa logica o de negocios de una aplicacion no tiene porque estar creada de tal manera que necesariamente funciona con HTTP requests (un cron job que periodicamente modifica data, de todos modos necesita que esa data tenga integridad).
Mi solucion para este problema fue simplemente ejecutar validaciones manuales, seguir usando DTO a pesar de no estar "transfiriendo" nada (porque no necesariamente hay un cliente haciendo peticiones HTTP) y de ejecutar una transformacion + validacion antes de guardar (que es lo mismo que hacen los Pipes, cuando se configuran de esa forma).
Jordan Lancheros
Si tienen una version mas actualizada, donde el findOne les falle aqui esta el codigo que lo soluciona
async findOne(id: number) { const product = await this.productRepo.findOne({ where: { id: id } }); if (!product) { throw new NotFoundException(`Product #${id} not found`); } return product; }
Luis André Vera Chávez
También puede ser asi
this.repo.findOneBy({id})
ZANONI ALFREDO SALAS TOBÓN
ZANONI ALFREDO SALAS TOBÓN
Jose Miguel Nieves
Les dejo el remove con la busqueda antes de intentar eliminar.
async remove(id: number) { //Si no existe, damos error. if (!(await this.findOne(id))) { throw new NotFoundException(); } return this.productRepo.delete(id); }
Arnold Kevin Guzman Campos
Hola, te recomiendo que el findOne lo guardes en una constante o variable, ya que si otro desarrollador lee tu código sabrá rápidamente a qué se refiere esta sentencia, y más si se trata de el servicio, te recomendaría ponerle de nombre algo así como existProduct, saludos :).
Lucas Heim
Implementación de un servicio genérico
Se me ocurrió implementar un servicio genérico que contenga todos los métodos CRUD utilizando los generics de TypeScript. Con esto ganamos que solo debemos implementar una clase genérica y todas los demás servicios de esta clase deberán extender de esta clase genérica, así tendrán todos los métodos CRUD implementados de forma automática y con el mínimo esfuerzo.
Para comenzar implementamos una nueva clase dentro de la carpeta common, la llamaré GenericService.service.ts
Esta clase recibirá 3 Generics Types:
La clase es abstracta para evitar que la implementen por error.
export abstract class GenericService<ENTITY, ID, DTO, PARTIAL_DTO> {}
Además, esta clase recibe en su constructor una instancia de un repositorio genérico (esta será enviada desde la implementación de esta clase)
export abstract class GenericService<ENTITY, ID, DTO, PARTIAL_DTO> { private readonly genericRepository: Repository<ENTITY> constructor(genericRepository: Repository<ENTITY>) { this.genericRepository = genericRepository; } }
Una vez echo esto ya podemos crear los métodos CRUD genéricos:
async create(data: DTO): Promise<ENTITY> { const newItem = this.genericRepository.create(data); await this.genericRepository.save(newItem); return newItem; }
El código completo sería el siguiente:
import { NotFoundException } from '@nestjs/common'; import { Repository } from "typeorm"; export abstract class GenericService<ENTITY, ID, DTO, PARTIAL_DTO> { private readonly genericRepository: Repository<ENTITY> constructor(genericRepository: Repository<ENTITY>) { this.genericRepository = genericRepository; } async create(data: DTO): Promise<ENTITY> { const newItem = this.genericRepository.create(data); await this.genericRepository.save(newItem); return newItem; } async update(id: ID, data: PARTIAL_DTO): Promise<ENTITY> { const product = await this.genericRepository.findOne(id); if (!product) { throw new NotFoundException(`Product with id ${id} not found`); } return this.genericRepository.merge(product, data); } async delete(id: ID): Promise<boolean> { const foundItem = await this.findOne(id); await this.genericRepository.delete(id); return true; } async findAll(): Promise<ENTITY[]> { return this.genericRepository.find(); } async findOne(id: ID): Promise<ENTITY> { const product = await this.genericRepository.findOne(id); if (!product) { throw new NotFoundException(`Product with id ${id} not found`); } return product; } }
Ahora para utilizar esta clase genérica solo debemos extender nuestros servicios normalmente e indicamos los valores de los tipos genéricos, por ejemplo, el servicio de Productos sería solo así:
@Injectable() export class ProductsService extends GenericService<Product, number, CreateProductDto, UpdateProductDto> { constructor(@InjectRepository(Product) private productRepository: Repository<Product>) { super(productRepository); } }
El servicio de usuarios sería igual:
@Injectable() export class UsersService extends GenericService<Client, number, CreateUserDto, UpdateUserDto>{ constructor( @InjectRepository(Client) private clientRepository: Repository<Client>, private productsService: ProductsService, private configService: ConfigService, ) { super(clientRepository); } //all code here }
De esta forma como se puede observar el código se reutiliza muy óiptimamente y no tenemos que esta repitiendo las operaciones CRUD una y otra vez en cada servicio de la aplicación.
Crear, actualizar y eliminar
Ahora vamos a actualizar los servicios de nuestra API para lograr crear una API CRUD completa:
**src/products/services/products.service.ts**:@Injectable() export class ProductsService { constructor( @InjectRepository(Product) private productRepo: Repository<Product>, ) {} // ... // solucionamos error de no manejar la promesa con async/await async findOne(id: number) { const product = await this.productRepo.findOneBy({ id }); if (!product) { throw new NotFoundException(`Product #${id} not found`); } return product; } // Crear producto create(data: CreateProductDto) { // Creamos una nueva instancia del producto desde la clase const newProduct = this.productRepo.create(data); // Guardamos el nuevo producto en la db return this.productRepo.save(newProduct); } // Actualizar producto async update(id: number, changes: UpdateProductDto) { // buscamos el producto const product = await this.productRepo.findOneBy({ id }); // actualizamos el producto this.productRepo.merge(product, changes); // guardamos los datos return this.productRepo.save(product); } // Borrar producto remove(id: number) { return this.productRepo.delete(id); } }
Una vez actualizados estos cambios, vamos a actualizar los controladores de nuestros products:
src/products/controllers/products.controller.ts:@ApiTags('products') @Controller('products') export class ProductsController { constructor(private productsService: ProductsService) {} // ... // nos aseguramos de recibir los tipos de datos correctos @Post() create(@Body() payload: CreateProductDto) { return this.productsService.create(payload); } @Put(':id') update( @Param('id', ParseIntPipe) id: number, @Body() payload: UpdateProductDto, ) { return this.productsService.update(id, payload); } @Delete(':id') delete(@Param('id', ParseIntPipe) id: number) { return this.productsService.remove(id); } }
En caso de que no funcione el endpoint de editar un producto, asegúrate que todos los campos en tu DTO tengan el decorador @ApiProperty() debido a que por si no lo recuerdas, estamos utilizando Swagger para auto documentar nuestro código:
src/products/dtos/products.dto.tsexport class CreateProductDto { @IsString() @IsNotEmpty() @ApiProperty({ description: `product's name` }) // <-- readonly name: string; @IsString() @IsNotEmpty() @ApiProperty() // <-- readonly description: string; @IsNumber() @IsNotEmpty() @IsPositive() @ApiProperty() // <-- readonly price: number; @IsNumber() @IsNotEmpty() // <-- readonly stock: number; @IsUrl() @IsNotEmpty() @ApiProperty() // <-- readonly image: string; } export class UpdateProductDto extends PartialType(CreateProductDto) {}
Y listo, ya hemos creado nuestra aplicación CRUD completamente.
Yo lo hice así:
//src/products/services/products.service.ts async findOne(id: number) { const product = await this.productRepo.findOne(id); if (!product) { throw new NotFoundException(`Product #${id} not found`); } return product; } async remove(id: number) { await this.findOne(id); return this.productRepo.delete(id); }
//src/products/controllers/products.controller.ts @Delete(':id') async delete(@Param('id', ParseIntPipe) id: number) { await this.productsService.remove(id); return { message: `Producto #${id} eliminado`, }; }
Con un error 409 de tipo CONFLICT, sería mejor su definición:
async create(data: CreateProductDto) { // * No es escalable // const newProduct = new Product(); // newProduct.name = data.name; // newProduct.description = data.description; // newProduct.price = data.price; // newProduct.stock = data.stock; // newProduct.image = data.image; const newProduct = this.productRepo.create(data); return await this.productRepo.save(newProduct).catch((error) => { throw new ConflictException(error.detail); }); }
Tuve que hacer estas modificaciones en products.module.ts:
//* Buena práctica: importar en el siguiente orden //* 1. Controlador //* 2. Servicio //* 3. Entidad import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { ProductsController } from './controllers/products.controller'; import { ProductsService } from './services/products.service'; import { Product } from './entities/product.entity'; import { BrandsController } from './controllers/brands.controller'; import { BrandsService } from './services/brands.service'; import { Brand } from './entities/brand.entity'; import { CategoriesController } from './controllers/categories.controller'; import { CategoriesService } from './services/categories.service'; import { Category } from './entities/category.entity'; @Module({ imports: [TypeOrmModule.forFeature([Product, Brand, Category])], controllers: [ProductsController, CategoriesController, BrandsController], providers: [ProductsService, BrandsService, CategoriesService], exports: [ProductsService], }) export class ProductsModule {}
y en users.module.ts:
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { CustomerController } from './controllers/customers.controller'; import { CustomersService } from './services/customers.service'; import { Customer } from './entities/customer.entity'; import { UsersController } from './controllers/users.controller'; import { UsersService } from './services/users.service'; import { User } from './entities/user.entity'; import { ProductsModule } from '../products/products.module'; @Module({ imports: [ProductsModule, TypeOrmModule.forFeature([Customer, User])], controllers: [CustomerController, UsersController], providers: [CustomersService, UsersService], }) export class UsersModule {}
para que el servidor corriera
Para validar la existencia o no de algún registro, ya sea producto, categoría, etc cree una clase abstracta que ayuda a simplificar un poco el código ya que la puedo usar en todos los services
import { NotFoundException } from '@nestjs/common'; import { FindOneOptions, FindOptionsWhere, Repository } from 'typeorm'; export abstract class ValidateIfExist<T> { protected name: string; private repository: Repository<T>; constructor(name: string, repository: Repository<T>) { this.name = name; this.repository = repository; } protected async existEntry(id: number): Promise<T> { const findOptions: FindOneOptions<T> = { where: ({ id: id, } as unknown) as FindOptionsWhere<T>, }; const entry = await this.repository.findOne(findOptions); if (!entry) { throw new NotFoundException(`${this.name} #${id} not found`); } return entry; } }
como por ejemplo:
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Category } from '../entities/category.entity'; import { CreateCategoryDto, UpdateCategoryDto } from '../dtos/category.dtos'; import { ValidateIfExist } from '../../common/services/validate-if-exist'; @Injectable() export class CategoriesService extends ValidateIfExist<Category> { constructor( @InjectRepository(Category) private categoryRepository: Repository<Category>, ) { super('Category', categoryRepository); } async findAll() { return this.categoryRepository.find({}); } async findOne(id: number) { const category = this.existEntry(id); return category; } create(data: CreateCategoryDto) { const newCategory = this.categoryRepository.create(data); return this.categoryRepository.save(newCategory); } async update(id: number, changes: UpdateCategoryDto) { const category = await this.existEntry(id); this.categoryRepository.merge(category, changes); return this.categoryRepository.save(category); } async remove(id: number) { this.existEntry(id); return this.categoryRepository.delete(id); } }
Delete ok Create ok update ok
create(data: CreateProductDto) { const newProduct = { ...data, }; this.productRepository.create(newProduct);//No estoy seguro si eliminar esta linea, funciona sin ella pero no estoy seguro return this.productRepository.save(newProduct); } async update(id: number, changes: UpdateProductDto) { const product = await this.productRepository.findOne(id); this.productRepository.merge(product, changes) return this.productRepository.save(product); } async remove(id: number) { const index = await this.productRepository.findOne(id); this.productRepository.delete(index); return true; }
Algunos cambios
async update(id: number, changes: UpdateProductDto) { const product = await this.productRepository.findOne(id); if (!product) { throw new NotFoundException(`Product #${id} not found`); } this.productRepository.merge(product, changes) return this.productRepository.save(product); } async remove(id: number) { const index = await this.productRepository.findOne(id); if (!index) { throw new NotFoundException(`Product #${id} not found`); } return this.productRepository.remove(index); }
Le agregue una validación al create para que no de un error 500.
async findOne(id: number) { const product = await this.productRepo.findOne(id); if (!product) { throw new NotFoundException(`Product with id ${id} not found`); } return product; } create(payload: CreateProductDto) { const newProduct = this.productRepo.create(payload); const product = this.productRepo .save(newProduct) .then((res) => { return res; }) .catch((err) => { throw new BadRequestException(`${err.message || 'Unexpected Error'}`); }); return product; } async update(id: number, payload: UpdateProductDto) { const product = await this.findOne(id); this.productRepo.merge(product, payload); return this.productRepo.save(product); } async delete(id: number) { await this.findOne(id); this.productRepo.delete(id); return { message: `Product with id ${id} deleted`, }; }