DEV Community

Jorge Carlos
Jorge Carlos

Posted on • Updated on

NestJs e Typeorm: Recuperando a entidade pelo parâmetro de rota

Nestjs é um framework progressivo em typescript para criação de aplicações api e aplicações MVC.

Usando o sistema de rotas do nestjs é possível definir parâmetros na uri para serem usados pela sua aplicação. Esses parâmetros são recuperados como uma string, e a validação é normalmente executada em seu controllers ou serviços.

Para recuperar o parâmetro de rota, usamos o decorator @param() que recebe dois (2) parâmetros, o nome do parâmetro e um transformer.

Os transformers são usados para tratar o dado que esta sendo recebido. Os transformers usados para receber identificadores de entidade pela rota são:

  • ParseIntPipe para identificadores numéricos
  • ParseUUIDPipe para os sistemas que usam uuid

Observação

Esse texto se destina a mostrar um recurso para serem usados em sistemas com NestJs com Typeorm, portanto, não vai ser mostrado como criar, instalar as libs relacionadas.

O Pipe criado abaixo usa o padrão ActiveRecord do Typeorm, no git do projeto tem uma versão com o padrão repository.

Obs.: Certifique-se ter instalado o cli do nestjs, caso não tenha veja na documentação de Fist Steps do NestJs

Obs.: É possível fazer com os orm/libs que o NestJs suporta para uso no banco de dados

Criando o Pipe

Para começar precisamos criar um pipe. Vamos criar o pipe com o nome GetEntity, é possível criar com o comando abaixo.

nest generate pipe GetEntity

# Ou

nest g pipe GetEntity
Enter fullscreen mode Exit fullscreen mode

Esse comando vai criar dois arquivos:

  • src/get-entity.pipe.ts: o próprio pipe com uma classe de nome GetEntityPipe
  • src/get-entity.pipe.spec.ts: o arquivo de teste para o novo pipe

O conteúdo do get-entity.pipe.ts é:

import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';

@Injectable()
export class GetEntityPipe implements PipeTransform {
  transform(value: any, metadata: ArgumentMetadata) {
    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode

Irei renomear a classe gerada, removendo o sufixo Pipe e fica somente GetEntity.

O construtor

Para começar vamos criar o construtor da classe e adicionar 3 (três) parâmetros.

  • entity: A entidade
    • Não é uma instância da classe, é a classe pura
    • A classe precisa extender BaseEntity
  • relations: Um array com strings para carregar as relações da entidade, valor padrão
    • O Typeorm só aceita as relações como array, por isso é necessário fazer essa conversão
  • field: O campo que será buscado no banco, padrão ‘id’

O novo construtor ficara assim:

import { BaseEntity } from 'typeorm';

export default class GetEntity implements PipeTransform {
  private readonly relations;

constructor(
    private readonly entity: typeof BaseEntity,
    relations?: string[] | string,
    private readonly field = 'id',
  ) {
    this.relations = typeof relations === 'string' ? [relations] : relations;
  }
//......
}
Enter fullscreen mode Exit fullscreen mode

O transformer

O método transformer vai receber o valor, validar e processar e retornar para o aplicativo.

Abaixo, o método transformer completo:

async transform(value: any) {
  if (
    !value ||
    value === '' ||
    value === 'undefined' ||
    !Number.isInteger(value)
  ) {
    throw newHttpErrorByCode[HttpStatus.BAD_REQUEST](
      `No entity ${this.field} provided`,
    );
  }

  const entity = await this.entity.findOne({
    where: { [this.field]: value },
    relations: this.relations,
  });

  if (!entity) {
    const entity = `${this.entity.toString()}`
      .match(/\w+/g)[1]
      .replace('Entity', '');

    throw newHttpErrorByCode[HttpStatus.NOT_FOUND](
      `Validation failed for ${entity} value`,
    );
  }

  return entity;
}
Enter fullscreen mode Exit fullscreen mode

Explicando o Transformer

if (!value || value === '' || value === 'undefined' || isNaN(value)) {
    throw newHttpErrorByCode[HttpStatus.BAD_REQUEST](
      `No entity ${this.field} provided`,
    );
  }
Enter fullscreen mode Exit fullscreen mode

Validação para impedir que valores errados cheguem até a consulta do banco, como o valor do parâmetro sempre será uma string é necessário fazer essas validações, caso verdadeiro dispara uma exceção HTTP com o status de BadRequest

const entity = await this.entity.findOne({
    where: { [this.field]: value },
    relations: this.relations,
  });
Enter fullscreen mode Exit fullscreen mode

Faz consulta ao banco de dados com os valores recebidos.

if (!entity) {
    const entity = `${this.entity.toString()}`
      .match(/\w+/g)[1]
      .replace('Entity', '');

    throw newHttpErrorByCode[HttpStatus.NOT_FOUND](
      `Validation failed for ${entity} value`,
    );
  }
Enter fullscreen mode Exit fullscreen mode

Caso a entidade não exista, dispara uma exceção HTTP com o status de NotFound.

Modo de uso

Para usar o novo Pipe criado, podemos criar ou utilizar uma rota existente, adicionar um novo parâmetro e utilizar o pipe. Veja alguns exemplos abaixo.

Recuperando a entidade

@Get(':id')
findOne(@Param('id', new GetEntity(UserEntity)) user: UserEntity) {
  return user;
}
Enter fullscreen mode Exit fullscreen mode

Recuperando a entidade com relações

@Get(':id/posts')
findOneWithPosts(
  @Param('id', new GetEntity(UserEntity, ['posts'])) user: UserEntity,
) {
  return user.posts;
}

// Ou

@Get(':id/posts')
findOneWithPosts(
  @Param('id', new GetEntity(UserEntity, 'posts')) user: UserEntity,
) {
  return user.posts;
}
Enter fullscreen mode Exit fullscreen mode

Recuperando várias entidades na mesma rota

@Get(':id/posts/:postId')
findOnePost(
  @Param('id', new GetEntity(UserEntity)) user: UserEntity,
  @Param('postId', new GetEntity(PostEntity, 'user')) post: PostEntity,
) {
  return { post, user };
}
Enter fullscreen mode Exit fullscreen mode

Conclusão

Usando esse Pipe evita a repetição de código para recuperação do banco de dados, centraliza a validação.

Veja o conteúdo desse post no Github, no projeto você vai encontrar:

  • Dois pipes, um com o modelo de repositório e com o modelo de ActiveRecord.
  • Arquivo docker-compose com mysql
  • Arquivo de exportação do Insomnia com as rotas do projeto

Projeto GitHub

Discussion (0)