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
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
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
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 {}
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;
}
5 - Criando a tabela e inserindo o dado:
- Rode o comando:
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:
sqlite3 db/sql
- Faça um insert na base para ter as informações para recuperar:
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.
-
Criando a Pasta e o Arquivo:
- No diretório do seu projeto, crie uma nova pasta chamada
viewmodels
. Dentro dessa pasta, crie um arquivo chamadobase.viewmodel.ts
.
- No diretório do seu projeto, crie uma nova pasta chamada
-
Conteúdo do
base.viewmodel.ts
:- Adicione o seguinte conteúdo ao arquivo
base.viewmodel.ts
:
- Adicione o seguinte conteúdo ao arquivo
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.
-
Criando a Pasta e o Arquivo:
- No diretório do seu projeto, crie uma nova pasta chamada
decorators
. Dentro dessa pasta, crie um arquivo chamadodefault-value.decorator.ts
.
- No diretório do seu projeto, crie uma nova pasta chamada
-
Conteúdo do
default-value.decorator.ts
:- Adicione o seguinte conteúdo ao arquivo
default-value.decorator.ts
:
- Adicione o seguinte conteúdo ao arquivo
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.
-
Criando o Arquivo
user.viewmodel.ts
:- Dentro da pasta
viewmodels
, crie um arquivo chamadouser.viewmodel.ts
.
- Dentro da pasta
-
Conteúdo do
user.viewmodel.ts
:- Adicione o seguinte conteúdo ao arquivo
user.viewmodel.ts
:
- Adicione o seguinte conteúdo ao arquivo
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.
-
Criando a Pasta e o Arquivo:
- No diretório do seu projeto, crie uma nova pasta chamada
repositories
. Dentro dessa pasta, crie um arquivo chamadoread.repository.ts
.
- No diretório do seu projeto, crie uma nova pasta chamada
-
Conteúdo do
read.repository.ts
:- Adicione o seguinte conteúdo ao arquivo
read.repository.ts
:
- Adicione o seguinte conteúdo ao arquivo
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.
-
Conteúdo Atualizado do
app.controller.ts
:- Altere o conteúdo do arquivo
app.controller.ts
para o seguinte:
- Altere o conteúdo do arquivo
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.
-
Conteúdo Atualizado do
app.module.ts
:- No arquivo
app.module.ts
, atualize o conteúdo para incluir oReadRepository
como um provedor:
- No arquivo
@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:
[
{
"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:
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. 🔄
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. 🛠️
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)
Show demais, ficou muito fácil seguir esse tutorial. A escrita bem feita facilitou muito o entendimento dos conceitos aplicados