DEV Community

Jhones Gonçalves
Jhones Gonçalves

Posted on

Criando um Repositório Genérico de Leitura com Generics, TypeORM e SQLite no NestJS 🚀

Neste artigo, vamos explorar a criação de um repositório genérico de leitura no contexto do NestJS, uma estrutura de aplicativo Node.js para construção de aplicativos escaláveis e eficientes. Utilizaremos Generics, TypeORM (um popular ORM para TypeScript/JavaScript) e SQLite (um banco de dados leve e eficiente) para criar um repositório de leitura flexível e reutilizável.

Iniciando um Projeto NestJS 🚀

Para começar um novo projeto NestJS, certifique-se de ter o Node.js e o npm instalados em sua máquina. Em seguida, abra o terminal e execute os seguintes comandos:

# Instale o NestJS CLI globalmente
npm install -g @nestjs/cli

# Crie um novo projeto NestJS
nest new nome-do-projeto
Enter fullscreen mode Exit fullscreen mode

Isso criará uma estrutura básica para o seu projeto NestJS. Navegue até o diretório recém-criado usando cd nome-do-projeto e continue com as próximas etapas da implementação do repositório genérico de leitura.

Agora, vamos começar a implementar o repositório genérico de leitura usando Generics, TypeORM e SQLite. Fique à vontade para adicionar seu código conforme avançamos no desenvolvimento.

Instalando SQLite e TypeORM no NestJS

Para o nosso projeto NestJS, vamos utilizar o SQLite como banco de dados e o TypeORM como ORM para interagir com o banco de dados. Siga os passos abaixo para configurar essas dependências:

1 - Instalando o SQLite:

  • O SQLite é um banco de dados leve e eficiente, perfeito para ambientes de desenvolvimento. Para instalá-lo, utilize o seguinte comando:
   npm install --save sqlite3
Enter fullscreen mode Exit fullscreen mode

2 - Instalando o TypeORM:

  • O TypeORM é um ORM (Object-Relational Mapping) para TypeScript e JavaScript. Ele facilita a interação com o banco de dados SQLite. Instale o TypeORM usando o comando:
npm install --save @nestjs/typeorm typeorm
Enter fullscreen mode Exit fullscreen mode

3 - Configurando a Conexão com o Banco de Dados:

  • No seu projeto NestJS, vá até o arquivo app.module.ts e importe os módulos do TypeORM e SQLite. Adicione a configuração da conexão ao banco de dados. Seu arquivo app.module.ts pode se parecer com algo assim:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'sqlite',
      database: 'db/sql',
      synchronize: true,
      entities: [User], // já vamos criar a entidade
    }),
  ],
})
export class AppModule {}

Enter fullscreen mode Exit fullscreen mode

4 - Criando uma Entidade:

  • No diretório do seu projeto, crie uma pasta entities. Dentro dessa pasta, crie um arquivo user.entity.ts para representar a entidade do usuário. Exemplo:
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  email: string;
}

Enter fullscreen mode Exit fullscreen mode

5 - Criando a tabela e inserindo o dado:

  • Rode o comando:
npm run start
Enter fullscreen mode Exit fullscreen mode

Com isso deve criar o arquivo db/sql.

  • Acesse o banco, no terminal rode o comando abaixo e entrará dentro do sqlite:
sqlite3 db/sql 
Enter fullscreen mode Exit fullscreen mode
  • Faça um insert na base para ter as informações para recuperar:
INSERT INTO user (name,email) VALUES ('Jhones', 'email@dominio.com');
Enter fullscreen mode Exit fullscreen mode

Criando uma Pasta para ViewModels

Para manter uma estrutura organizada e reutilizável em nosso projeto, vamos criar uma pasta chamada viewmodels. Nela, teremos um arquivo chamado base.viewmodel.ts que conterá a classe BaseViewModel. Esta classe fornecerá um modelo base para as ViewModels do nosso sistema.

  1. Criando a Pasta e o Arquivo:

    • No diretório do seu projeto, crie uma nova pasta chamada viewmodels. Dentro dessa pasta, crie um arquivo chamado base.viewmodel.ts.
  2. Conteúdo do base.viewmodel.ts:

    • Adicione o seguinte conteúdo ao arquivo base.viewmodel.ts:
   export default class BaseViewModel<T extends Record<string, any>> {
     constructor(data?: Partial<T>) {
       this.initializeDefaultValues();
       Object.assign(this, data);
     }

     private initializeDefaultValues(): void {
       const prototype = Object.getPrototypeOf(this);
       const properties = Object.getOwnPropertyNames(prototype);

       for (const property of properties) {
         const defaultValue = this[property];
         if (defaultValue !== undefined) {
           this[property] = defaultValue;
         }
       }
     }
   }
Enter fullscreen mode Exit fullscreen mode

Esta classe BaseViewModel aceita um objeto parcial durante a inicialização e preenche suas propriedades com valores padrão, garantindo uma consistência em todas as ViewModels.

Agora, estamos prontos para utilizar a BaseViewModel como base para as ViewModels específicas do nosso sistema. Isso proporcionará uma estrutura coesa e reutilizável para representar dados no nosso contexto.

Adicionando um Decorator DefaultValue

Decorators são uma poderosa ferramenta em TypeScript, permitindo-nos adicionar metadados e comportamentos adicionais às nossas classes e propriedades. Vamos criar um decorator chamado DefaultValue que garantirá valores padrão para propriedades específicas em nossas ViewModels.

  1. Criando a Pasta e o Arquivo:

    • No diretório do seu projeto, crie uma nova pasta chamada decorators. Dentro dessa pasta, crie um arquivo chamado default-value.decorator.ts.
  2. Conteúdo do default-value.decorator.ts:

    • Adicione o seguinte conteúdo ao arquivo default-value.decorator.ts:
   export default function DefaultValue(defaultValue: any) {
     return function (target: any, propertyKey: string) {
       let value = defaultValue;

       Object.defineProperty(target, propertyKey, {
         get() {
           return value;
         },
         set(newValue) {
           value = newValue !== undefined ? newValue : defaultValue;
         },
         enumerable: true,
         configurable: true,
       });
     };
   }
Enter fullscreen mode Exit fullscreen mode

Este decorator DefaultValue permite definir um valor padrão para propriedades específicas em uma classe. Se a propriedade for configurada como undefined, o valor padrão será aplicado.

Criando uma ViewModel para Usuário

Agora que temos nossa classe base BaseViewModel, podemos criar ViewModels específicas para diferentes entidades do sistema. Vamos começar com uma UserViewModel que representa um usuário.

  1. Criando o Arquivo user.viewmodel.ts:

    • Dentro da pasta viewmodels, crie um arquivo chamado user.viewmodel.ts.
  2. Conteúdo do user.viewmodel.ts:

    • Adicione o seguinte conteúdo ao arquivo user.viewmodel.ts:
   import DefaultValue from 'src/decorators/default-value.decorator';
   import BaseViewModel from './base.viewmodel';

   export default class UserViewModel extends BaseViewModel<UserViewModel> {
     constructor(data?: Partial<UserViewModel>) {
       super(data);
     }

     @DefaultValue('')
     name: string;

     @DefaultValue('')
     email: string;
   }
Enter fullscreen mode Exit fullscreen mode

Nesta UserViewModel, estamos estendendo a BaseViewModel e definindo duas propriedades: name e email. Além disso, estamos utilizando um decorator @DefaultValue('') que é necessário para quando inicializar a classe o repositório reconheça as propriedades.

Criando uma Pasta para Repositórios

Agora, vamos organizar nossos repositórios em uma pasta chamada repositories. Dentro dessa pasta, criaremos um arquivo chamado read.repository.ts que conterá nosso repositório genérico de leitura.

  1. Criando a Pasta e o Arquivo:

    • No diretório do seu projeto, crie uma nova pasta chamada repositories. Dentro dessa pasta, crie um arquivo chamado read.repository.ts.
  2. Conteúdo do read.repository.ts:

    • Adicione o seguinte conteúdo ao arquivo read.repository.ts:
   import { InjectEntityManager } from '@nestjs/typeorm';
   import BaseViewModel from 'src/viewmodels/base.viewmodel';
   import { EntityManager } from 'typeorm';

   export class ReadRepository {
     constructor(
       @InjectEntityManager()
       private readonly entityManager: EntityManager,
     ) {}

     public async getAllOrBy<TModel extends BaseViewModel<TModel>>(
       modelType: new () => TModel,
       query: string,
       parameters?: any[],
     ): Promise<TModel[]> {
       const result = await this.entityManager.query(query, parameters);
       return result.map((row: any) => this.mapper<TModel>(row, modelType));
     }

     public async getOne<TModel extends BaseViewModel<TModel>>(
       modelType: new () => TModel,
       query: string,
       parameters?: any[],
     ): Promise<TModel> {
       const result = await this.entityManager.query(query, parameters);
       const resultMapped = result.map((row) =>
         this.mapper<TModel>(row, modelType),
       );
       return resultMapped[0] ?? null;
     }

     private mapper<TModel extends BaseViewModel<TModel>>(
       row: any,
       modelType: new () => TModel,
     ): TModel {
       const instance = new modelType();
       const prototype = Object.getPrototypeOf(instance);

       Object.keys(prototype).forEach((key) => {
         prototype[key] = row[key];
       });

       return prototype;
     }
   }
Enter fullscreen mode Exit fullscreen mode

Este arquivo contém nosso repositório genérico de leitura (ReadRepository). Ele utiliza o TypeORM e o SQLite para realizar operações de leitura no banco de dados e mapeia os resultados para as ViewModels correspondentes.

Agora, estamos prontos para utilizar o ReadRepository em nosso serviço para interações de leitura com o banco de dados.

Utilizando o Repositório Genérico no Controller

Agora que temos nosso repositório genérico de leitura, podemos integrá-lo ao nosso controlador (AppController). Vamos fazer algumas alterações no app.controller.ts para utilizar o ReadRepository e realizar operações de leitura no banco de dados.

  1. Conteúdo Atualizado do app.controller.ts:
    • Altere o conteúdo do arquivo app.controller.ts para o seguinte:
   import { Controller, Get, Param } from '@nestjs/common';
import { ReadRepository } from './repositories/read.repository';
import UserViewModel from './viewmodels/user.viewmodel';

@Controller()
export class AppController {
  constructor(private readonly readRepository: ReadRepository) {}

  @Get('/users')
  async getUsers() {
    const query = 'SELECT * FROM user';

    const users = await this.readRepository.getAllOrBy<UserViewModel>(
      UserViewModel,
      query,
    );
    return users;
  }

  @Get('/users/:id')
  async getUser(@Param('id') id: number) {
    // pegar o id da rota
    const query = 'SELECT * FROM user WHERE id = ?';

    const users = await this.readRepository.getOne<UserViewModel>(
      UserViewModel,
      query,
      [id],
    );
    return users;
  }
}

Enter fullscreen mode Exit fullscreen mode

Neste arquivo atualizado, utilizamos o método getAllOrBy para obter todos os usuários e o método getOne para obter um usuário específico com base no ID fornecido na rota.

Essas alterações garantem que nosso controlador utilize o repositório genérico para interações de leitura com o banco de dados SQLite. Continue a implementação do seu projeto, ajustando conforme necessário para atender aos requisitos específicos.

Configurando o Módulo para Injeção de Dependência

Para garantir que o ReadRepository seja injetado corretamente no AppController, é necessário fazer algumas configurações no módulo principal (AppModule). Vamos ajustar o arquivo app.module.ts para incluir o ReadRepository como um provedor.

  1. Conteúdo Atualizado do app.module.ts:
    • No arquivo app.module.ts, atualize o conteúdo para incluir o ReadRepository como um provedor:
   @Module({
    imports: [
      TypeOrmModule.forRoot({
        type: 'sqlite',
        database: 'db/sql',
        synchronize: true,
        entities: [User],
    }),
    TypeOrmModule.forFeature([User]),
  ],
  controllers: [AppController],
  providers: [AppService, ReadRepository],
})
export class AppModule {}
   export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Essas alterações configuram o módulo para reconhecer e injetar corretamente o ReadRepository quando necessário.

Continue a desenvolver seu projeto e, conforme necessário, ajuste essas configurações para atender aos requisitos específicos do seu aplicativo.

Resultado: Personalizando o Retorno da API 🎨

Ao implementar o repositório genérico de leitura e mapear os resultados para objetos ViewModel, conseguimos personalizar o retorno da API de acordo com a estrutura definida no ViewModel. Notavelmente, o campo "id" foi omitido do resultado, proporcionando uma resposta mais alinhada com a estrutura desejada.

Exemplo de Retorno:


[
  {
    "name": "Jhones",
    "email": "email@dominio.com"
  }
]

Enter fullscreen mode Exit fullscreen mode

O mapeamento inteligente no repositório garante que apenas os campos definidos no ViewModel sejam apresentados no resultado final. Isso não apenas simplifica a resposta da API, mas também reforça a consistência nos dados retornados, contribuindo para uma experiência de desenvolvimento mais previsível.

Conclusão 🚀

A implementação de um repositório genérico de leitura com Generics, TypeORM e SQLite no NestJS oferece uma série de benefícios significativos para o desenvolvimento de aplicativos. Ao abstrair as operações de leitura do banco de dados para um repositório genérico, podemos obter ganhos notáveis em termos de reutilização de código, manutenção simplificada e maior coesão.

Principais Benefícios:

  1. Reutilização de Código: O repositório genérico permite que operações de leitura comuns sejam encapsuladas e reutilizadas em todo o aplicativo, reduzindo a duplicação de código. 🔄

  2. Manutenção Simplificada: Mudanças nas operações de leitura podem ser gerenciadas centralmente no repositório genérico, simplificando a manutenção e evitando alterações em várias partes do código. 🛠️

  3. Coerência no Acesso aos Dados: Ao padronizar o acesso aos dados por meio do repositório genérico, promovemos uma abordagem consistente na recuperação de informações do banco de dados. 📊

No exemplo apresentado, utilizamos o NestJS, TypeORM e SQLite para criar um repositório genérico de leitura. Este repositório foi integrado a um controlador que realiza operações de leitura em uma entidade User. Os benefícios proporcionados por essa implementação servem como uma base sólida para expandir e adaptar conforme as necessidades específicas do seu projeto.

Ao seguir as práticas apresentadas neste artigo, você estará apto a criar repositórios de leitura genéricos em seus próprios projetos, promovendo uma arquitetura mais limpa e modular.

Continue explorando e personalizando conforme necessário para atender aos requisitos específicos do seu aplicativo. 🌟

Espero que este artigo tenha sido útil na compreensão e implementação de repositórios genéricos de leitura no NestJS. Boas práticas de programação e sucesso no desenvolvimento do seu projeto! 🚀

Top comments (1)

Collapse
 
jpmvale profile image
jpmvale

Show demais, ficou muito fácil seguir esse tutorial. A escrita bem feita facilitou muito o entendimento dos conceitos aplicados