yarn add typescript -D
yarn tsc --init # init ts
- TSC transpile ts to js
yarn tsc
tsconfig.json
> outDir => output path to js files
tsconfig.json
> strict => disabled
yarn add -D ts-node-dev
create a launch.json file
> node
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"outFiles": [
"${workspaceFolder}/**/*.js"
]
}
]
}
- Need insert
--inspect
to script dev in package.json
-
Camada responsável por fazer toda a manipulação de dados da nossa aplicação
-
Responsável por acessar o banco, editar, criar
- As rotas não devem conhecer a Model, quem deve conhecer são os repositórios
- DTO -> Receber os dados das rotas e receber nos repositórios
Conceito de código limpo
- S -> SRP - Single Responsability Principle
- O -> OCP - Open-Closed Principle
- L -> LSP - Liskov Substitution Principle
- I -> ISP - Interface Segregation Principle
- D -> DIP - Dependency Inversion Principle
- Cada rota deve ter uma única responsabilidade
Olhando para a rota de criação de categoria:
categoriesRoutes.post("/", (request, response) => {
const { name, description } = request.body;
const categoryAlreadyExists = categoriesRepository.findByName(name);
if (categoryAlreadyExists) {
return response.status(400).json({
error: "Category already exists",
});
}
categoriesRepository.create({ name, description });
return response.status(201).json({ success: true });
});
temos a responsabilidade de validar se a categoria já existe e de criar
- Devemos isolar a lógica da rota em um service, para que seja possível cadastrar a categoria, separando a responsabilidade do contexto
Olhando pra esse código:
class CreateCategoryService {
execute({ name, description }: IRequest) {
const categoriesRepository = new CategoriesRepository();
}
}
-
Imaginando que nós temos 3 classes: list, create e delete, todas elas instanciando o
CategoriesRepository
, logo teríamos um novo repositório e nunca usaríamos a mesma instância do repositório -
O código q implementa uma política de alto nível, não deve depender de um código q implementa detalhes de baixo nível, ou seja, o
service
(alto nível - mais próximo do domínio) não deve conhecer o tipo dorepositório
. As rotas são os de baixo nível, pois estão mais perto do contato com o usuário -
A responsabilidade passa a ser de quem chama o service
-
Tirar a responsabilidade da rota de fazer a regra de negócio (responsabilidade da rota: receber a request, chamar o serviço, executar a função para retornar algo)
-
Separar a responsabilidade em um serviço, para criar uma categoria
- Permitir que as partes do programa sejam substituídos sem que gere impacto na aplicação
interface ICategoriesRepository {
findByName(name: string): Category;
list(): Category[];
create({ name, description }: ICreateCategoryDTO): void;
}
-
Basta que os repositórios recebam o mesmo contrato (interface)
-
O repositório se torna um subtipo da interface
// Antes
class CreateCategoryService {
constructor(private categoriesRepository: CategoriesRepository) {}
}
// Depois
class CreateCategoryService {
constructor(private categoriesRepository: ICategoriesRepository) {}
}
- Ao invés de receber o repositório, travando o service, agora a gente recebe a interface, removendo a definição do tipo que a gente esperava no
constructor
e agora utiliza o subtipo. Qualquer classe que implementar a interface, pode ser implementada nas routes e substituir q vai continuar funcionando
- Separação de operações (separar arquivos de criação de categoria, listagem de categoria, criação de especificação, etc)
- Classes que recebem nossa requisição e retornam a resposta pra quem está chamando elas
- Criando apenas uma instância global
- Ponto de atenção: verificar se a instância deve ser única em todo o projeto
no arquivo CategoriesRepository.ts:
private static INSTANCE: CategoriesRepository;
- somente a class
CategoriesRepository
vai poder criar a instância
yarn add multer # lib para leitura de arquivos
// TODO =
- Receber o arquivo de upload
- Armazenar em uma pasta temporária
- Fazer a leitura desses arquivos
- Deletar os arquivos dessa pasta
-
ReadFile
faz a leitura de uma vez do arquivo, se o arquivo tiver muitas linhas, será uma leitura pesada e a aplicação começa a consumir muita memória do servidor -
Conceito de stream: permite ler o arquivo em partes (chunks)
-
pipe
, pega as informações e passa pra algum lugar
- O back-end deve ser desenvolvido pensando em quem vai consumir ele, por isso é importante ter uma documentação
yarn add swagger-ui-express && yarn add -D @types/swagger-ui-express
no arquivo server da aplicação
/**
* "/api-docs" -> rota pra acessar a documentação
* swagger.serve -> chamando o servidor
* swagger.setup() -> arquivo json com todas as informações da nossa aplicação, toda a parte de documentação
*/
app.use("/api-docs", swagger.serve, swagger.setup());
liberando a importação de json na aplicação // tsconfig
{
"resolveJsonModule": true
}
-
Instalando as ferramentas e configurando tudo no docker
-
Criar o arquivo
Dockerfile
, que terá todo o passo a passo para rodar a aplicação dentro do docker
Imagens do docker Hub Docker
FROM node:latest # de qual imagem vai rodar
WORKDIR /path # diretório q as informações estão contidas
COPY package.json ./ # copiando o package pro workdir
RUN npm install # nem sempre as imagens vem com o yarn instalado
COPY . . # copiando tudo, para a pasta raíz
EXPOSE 3333
CMD ["npm", "run", "dev"] # comandos a serem rodados, precisa ser em itens do array
Rodando o Dockerfile:
docker build -t nome_da_imagem_a_ser_criada . # . -> raiz do projeto
Rodando o container:
docker run -p 3333:3333 nome_da_imagem_a_ser_criada # toda vez q chamar no localhost 3333, dentro do docker, será buscada a porta 3333
docker ps
acessando o container:
docker exec -it container_id /bin/bash # cairá no workdir
- Orquestrador de container, define os serviços necessários para rodar a aplicação
docker-compose.yml
version: "3.7" # versão do compose
services:
app: # nome do serviço
build: . # diretório local
container_name: rentx # nome do container
ports:
- 3333:3333 # acessando a 3333, será feito o mapeamento da porta 3333 do container
volumes:
- .:/usr/app # como se fosse o workdir, pegando tudo q está na aplicação e jogando pra /usr/app
Rodando o compose
docker-compose up
docker-compose up -d # roda em background
docker logs container_name -f
docker-compose stop # para o container
docker-compose start # inicia o container
docker-compose down # remove o container
npm i pg # driver do postgres
problema de usar dessa forma:
-
Pra cada banco que fossemos usar, teríamos q instalar o nosso driver
-
Por exemplo, a forma de fazer uma query em postgres pode ser diferente do MySQL, o que seria mais cansativo de dar manutenção
Knex.js
-
Mistura o SQL puro com js
-
Precisa instalar os drivers nativos, mas possui um padrão para as queries
Model <-> ORM <-> Banco de dados
- Pega o código em js e converte pra uma maneira que o banco de dados entende
Search Installation Installation
yarn add typeorm reflect-metadata pg
tsconfig
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
- Dentro de
src
, criar a pastadatabase
- Criar o arquivo ormconfig.json
{
"type": "postgres",
"port":5432,
"host": "localhost",
"username":"docker",
"password":"database_ignite",
"database":"rentx",
"entities": ["src/modules/**/entities/*.ts", "./build/src/modules/**/entities/*.js"], // https://stackoverflow.com/questions/65336801/repositorynotfounderror-no-repository-for-user-was-found-looks-like-this-ent
"migrations":["./src/database/migrations/*.ts"], // onde q estão as migrations para serem rodadas
"cli": {
"migrationsDir":"./src/database/migrations" // onde as migrations serão salvas
}
}
incluir script no package
"typeorm":"ts-node-dev ./node_modules/typeorm/cli"
yarn typeorm migration:create -n CreateCategory
yarn typeorm migration:run # rodar as migrations
yarn typeorm migration:revert # reverter a última migration
- Ferramenta para ajudar na inversão de dependências
- Facilitador de injeção de dependências
yarn add tsyringe
// container.registerSingleto<Interface>("nome do container", classe que será chamada)
container.registerSingleton<ICategoriesRepository>("", CategoriesRepository);
server.ts:
// se o erro for mapeado, ele cairá no if, caso contrário, será um erro do sistema
app.use(
(err: Error, request: Request, response: Response, next: NextFunction) => {
if (err instanceof AppError) {
return response.status(err.status).json({
message: err.message,
});
}
return response.status(500).json({
status: "error",
message: `Internal server error - ${err.message}`,
});
}
);
- Necessário instalar uma lib para conseguir passar os erros pra frente
yarn add express-async-errors # o express não sabe lidar com throws
Como user não é tipado na request, pelo express, precisamos criar um arquivo tipando user
ensureAuthenticated:
request.user = {
id: user_id,
};
src/@types/express/index.d.ts:
declare namespace Express {
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface Request { // A regra do eslint foi desabilitada, clicando em quickfix add to this line
user: {
id: string;
};
}
}
usando o jest
yarn add jest && yarn add @types/jest
yarn jest --init
preset ṕra trabalhar com jest e ts:
yarn add ts-jest -D
jest.config.ts:
// Stop running tests after `n` failures
bail: true,
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// The glob patterns Jest uses to detect test files
testMatch: ["**/*.spec.ts"],
- Como funciona o teste? : É a comparação do resultado q se espera do teste com o real resultado da requisição
describe() // serve para agrupar os testes
beforeEach(() => {}) // antes de determinado teste, roda uma função
expect(async () => {
const category: IRequest = {
name: "Category Test",
description: "Category description test",
};
await createCategoryUseCase.execute(category);
await createCategoryUseCase.execute(category);
}).rejects.toBeInstanceOf(AppError); // espera-se que dê erro, pois não pode ter 2 categoruas com o mesmo nome. O erro precisa ser uma instância de apperror
-
Os testes não devem conectar com o banco, para contornar isso, podemos utilizar as interfaces criadas e criar nosso próprio repositório
-
Os testes tmb devem seguir o fluxo natural da aplicação, por exemplo: pra gerar o jwt, é necessário q o usuário exista na base de dados e ele informe as credencias válidas
Ao invés de acessar a pasta através de
../
, agora utilizamos@diretorio
tsconfig.json:
"baseUrl": "./src",
"paths": {
"@modules/*": ["modules/*"],
"@config/*": ["config/*"],
"@shared/*": ["shared/*"],
"@errors/*": ["errors/*"],
},
-
feito isso, precisa dar um reload na aplicação
-
Impedindo q jogue nossos repositórios pra cima das dependências eslint.json (antes estava como
/^@shared/
)
"import-helpers/order-imports": [
"warn",
{
"newlinesBetween": "always",
"groups": ["module", "/^@/", ["parent", "sibling", "index"]],
"alphabetize": { "order": "asc", "ignoreCase": true }
}
],
yarn add tsconfig-paths -D # responsável por fazer a tradução dos imports com @, precisa passar no script de dev tmb
"scripts": {
"dev": "ts-node-dev -r tsconfig-paths/register --inspect --transpile-only --ignore-watch node_modules --respawn src/server.ts",
"typeorm": "ts-node-dev -r tsconfig-paths/register ./node_modules/typeorm/cli",
"test": "jest"
},
RF => Requisitos funcionais RNF => Requisitos não funcionais RN => Regra de negócio
RF
- Deve ser possível cadastrar um novo carro.
- Deve ser possível listar todas as categorias
RN
-
Não deve ser possível cadastrar um carro com uma placa já existente
-
Não deve ser possível alterar a placa de um carro já cadastrado.
-
O carro deve ser cadastrado com disponibilidade, por padrão
-
O usuário responsável pelo cadastro deve ser um usuário admin
RF
- Deve ser possível listar todos os carros disponíveis
- Deve ser possível listar todas as categorias
- Deve ser possível listar pelo nome da marca
- Deve ser possível listar pelo nome do carro
RN
- O usuário não precia estar logado no sistema
RF
-
Deve ser possível cadastrar uma especificação par um carro
-
Deve ser possível listar todas as especificações
-
Deve ser possível listar todos os carros
RN
- Não deve ser possível cadastrar uma especificação já existente para o mesmo carro
- Não deve ser possível cadastrar uma especificação já existente para o mesmo carro
- O usuário responsável pelo cadastro deve ser um usuário admin
RF
- Deve ser possível cadastrar a imagem do carro
RNF
- Utilizar o multer para upload dos arquivos
RN
- O usuário deve poder cadastrar mais de uma imagem para o mesmo carro
- O usuário responsável pelo cadastro deve ser um usuário admin
RF
- Deve ser possível cadastrar um aluguel
RNF
RN
- O aluguel deve ter no mínimo de 24 horas
- Não deve ser possível cadastrar um novo aluguel caso já exista um aberto para para o mesmo usuário
- Não deve ser possível cadastrar um novo aluguel caso já exista um aberto para para o mesmo carro
// forçando tipo das variáveis
const cars = await listAvailableCarsUseCase.execute({
brand: brand as string,
category_id: category_id as string,
name: name as string,
});
const carsQuery = await this.repository
.createQueryBuilder("c")// c é um alias referenciando cars
.where("available = :available", {
available: true,
}); // :available é o parâmetro que irá receber
- Teste de integração
cria basicamente um servidor http
yarn add supertest && yarn add @types/supertest -D
-
separando app de server (criando um arquivo q da o listen do app), pra testar sem ter q iniciar o servidor
-
Necessário criar uma nova estrutura de banco de dados
Na query do postbird ou dbeaver:
create database rentx_test;
- passando a env no package.json
{
"test": "NODE_ENV=test jest --detectOpenHandles",
}
// detectOpenHandles: detecta se tem teste em aberto
// runInBand: evitando dados duplicados nos testes, rodando um teste de cada vez
"test": "NODE_ENV=test jest --detectOpenHandles --runInBand",
beforeEach vs beforeAll
- beforeEach zera antes de cada teste
- beforeAll monta uma vez só e utiliza pra todos os testes
useCase > IRepository > Implementação (Repository)
Criar uma tabela de tokens para o usuário
O EtherealMailProvider precisa ser injetado assim q a aplicação é iniciada, pra conseguir criar o client antes de chamar o sendMail
container.registerInstance<IMailProvider>(
"EtherealMailProvider",
new EtherealMailProvider()
);
"Jest has detected the following 2 open handles potentially keeping Jest from exiting"
"test": "NODE_ENV=test jest --runInBand --forceExit"
// spy do jest -> verifica se algum método da classe foi chamado
const sendMail = jest.spyOn(mailProvider, "sendMail");
await usersRepositoryInMemory.create({
driver_license: "123456",
email: "email@user.com",
name: "John Doe",
password: "1234",
});
expect(sendMail).toHaveBeenCalled();
jest.config.ts
{
collectCoverage: true,
collectCoverageFrom: ["<rootDir>/src/modules/**/useCases/**/*.ts"], // arquivos que serão mapeados
coverageDirectory: "coverage" // pasta com todas as informações do coverage
coverageReporters: [ "text-summary", "lcov"],
}
Quando a gente quer transformar o objeto q temos em um objeto mais adequado pro usuário (tirando senha, tirando informações desnecessárias)
yarn add class-transformer
docker run --name redis-rentx -p 6379:6379 -d -t redis:alpine