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

Crear, actualizar y eliminar

12/36
Recursos

Aportes 18

Preguntas 4

Ordenar por:

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

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:

  • ENTITY: Representa la entidad del servicio
  • ID: Representa el tipo del Primary Key del Repositorio (no siempre es un number, por eso lo coloco como dinámico)
  • DTO: Representa la clase del DTO (necesaria para recibir los datos desde el body)
  • PARTIAL_DTO: Representa la misma clase del DTO pero con sus atributos como opcionales

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.

El mejor profesor libra por libra

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 } })

En versiones recientes se utiliza

findOneBy({ id })

Ya que findOne() fue eliminada de typeorm

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`,
    };
 }

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"
}

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

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.

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.ts
export 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.

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);
  }
}

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).

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;
  }

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;
  }

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);
  }

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`,
    };
  }

No habia terminado de ver el video, al hacer el await en el findOne ya no es necesario usarlo en el remove, el codigo quedaria asi…

remove(id: number) {
    //Si no existe, damos error.
    if (this.findOne(id)) {
      throw new NotFoundException();
    }
    return this.productRepo.delete(id);
  }

Crear, actualizar y eliminar

//src/products/services/products.service.ts

@Injectable()
export class ProductsService {

......


async findOne(id: number) {
    const product = await this.productRepo.findOne(id); // 👈 use repo
    if (!product) {
      throw new NotFoundException(`Product #${id} not found`);
    }
    return product;
  }

  create(data: CreateProductDto) {
    // Forma 1 - Declarando un por uno
    // const newProduct = new Product();
    // newProduct.name = data.image;
    // newProduct.description = data.description;
    // newProduct.price = data.price;
    // newProduct.stock = data.stock;
    // newProduct.image = data.image;

    // Forma 1 - Creando un instancia
    // De esta manera podemos instancia de manera mas facil todos los elementos de un producto
    const newProduct = this.productRepo.create(data);
    return this.productRepo.save(newProduct); // Guradamos en la base de datos
  }

  async update(id: number, changes: UpdateProductDto) {
    const product = await this.productRepo.findOne(id); // Obtenemos el producto a actualizar
    this.productRepo.merge(product, changes);

    return this.productRepo.save(product); // Guradamos en la base de datos
  }

  remove(id: number) {
    return this.productRepo.delete(id);
  }
}
//src/products/controllers/products.controller.ts

@Controller('products')
export class ProductsController {

.......

  @Post()
  create(@Body() payload: CreateProductDto) {
    return this.productsService.create(payload);
  }

  @Put(':id')
  update(@Param('id') id: number, @Body() payload: UpdateProductDto) {
    return this.productsService.update(id, payload);
  }

  @Delete(':id')
  delete(@Param('id') id: number) {
    return this.productsService.remove(id);
  }
}
//src/products/dtos/products.dtos.ts
.....
import { PartialType, ApiProperty } from '@nestjs/swagger';

export class CreateProductDto {
  @IsString()
  @IsNotEmpty()
  @ApiProperty({ description: `product's name` }) // 👈 new Decorator
  readonly name: string;

  @IsString()
  @IsNotEmpty()
  @ApiProperty() // 👈 new Decorator
  readonly description: string;

  @IsNumber()
  @IsNotEmpty()
  @IsPositive()
  @ApiProperty() // 👈 new Decorator
  readonly price: number;

  @IsNumber()
  @IsNotEmpty()
  @ApiProperty() // 👈 new Decorator
  readonly stock: number;

  @IsUrl()
  @IsNotEmpty()
  @ApiProperty() // 👈 new Decorator
  readonly image: string;
}

export class UpdateProductDto extends PartialType(CreateProductDto) {}

Lo que vimos:

products.controller.ts

async findOne(id: number) {
    const product = await this.productRepo.findOne(id);
    if (!product) {
      throw new NotFoundException(`Product #${id} not found`);
    }
    return product;
  }
 create(data: CreateProductDto) {
    const newProduct = this.productRepo.create(data);
    return this.productRepo.save(newProduct);
  }

  async update(id: number, changes: UpdateProductDto) {
    const product = await this.productRepo.findOne(id);
    this.productRepo.merge(product, changes);
    return this.productRepo.save(product);
  }

  remove(id: number) {
    return this.productRepo.delete(id);
  }

products.dtos.ts

export 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()
  @ApiProperty()
  readonly stock: number;

  @IsUrl()
  @IsNotEmpty()
  @ApiProperty()
  readonly image: string;
}