Criando um Repositório Genérico de Leitura com Generics, TypeORM e SQLite no NestJS 🚀
Tags: javascript, nestjs, architecture, cqs
Published: 2024-01-19 03:16:32

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:

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

# Crie um novo projeto NestJS
nest new nome-do-projeto
```

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:

   ```bash
   npm install --save sqlite3
   ```
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:
```bash
npm install --save @nestjs/typeorm typeorm
```

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:

```ts
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 {}

```

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:

```ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

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

  @Column()
  name: string;

  @Column()
  email: string;
}

```

5 - Criando a tabela e inserindo o dado:

   - Rode o comando:
```sh
npm run start
```

Com isso deve criar o arquivo  db/sql.

   - Acesse o banco, no terminal rode o comando abaixo e entrará dentro do sqlite:
```sh
sqlite3 db/sql 
```

 - Faça um insert na base para ter as informações para recuperar:
```sql
INSERT INTO user (name,email) VALUES ('Jhones', 'email@dominio.com');
```

## 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`:

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

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`:

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

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`:

   ```typescript
   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;
   }
```
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`:

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

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:

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

```

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:

   ```typescript
   @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 {}
```

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:
```json

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

```

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! 🚀