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 16

Preguntas 4

Ordenar por:

驴Quieres ver m谩s aportes, preguntas y respuestas de la comunidad?

o inicia sesi贸n.

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.

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

En versiones recientes se utiliza

findOneBy({ id })

Ya que findOne() fue eliminada de typeorm

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 鈥榙tos.ts鈥 y deber铆a ser 鈥榙to.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 鈥渢ransfiriendo鈥 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;
}