No tienes acceso a esta clase

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

Crear, actualizar y eliminar

12/33
Recursos

Aportes 8

Preguntas 3

Ordenar por:

Los aportes, preguntas y respuestas son vitales para aprender en comunidad. Regístrate o inicia sesión para participar.

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

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