Quando vamos trabalhar com banco de dados no back-end, existem 3 principais estratégias para manipular os dados:
-
Lidar direto com o driver nativo do banco de dados. Ex: node-postgres.
-
Utilizando um query builder: construir nossas queries com Javascript. Ex: Knex.js.
-
ORM (Object Relational Mapping): que é o maior nível de abstração, onde mapeamos registros do nosso banco de dados com objetos no Javascript, ou seja, vamos criar models no nosso Javascript ou Typescript. E esses models, sempre quando criarmos uma instância (criar, alterar, deletar), ele vai refletir no banco de dados.
- Sequelize para Javascript
- TypeORM para Typescript
TypeORM
Faz todas as instalações necessárias e dentro do nosso model
vamos adicionar como decorators
as @Entity()
e @Column()
.
Ao salvar no repository
, automaticamente vai salvar também no banco de dados.
Para buscar no banco, existem os métodos find()
ou findOne()
enviando id
ou um objeto com campos e valores do que queremos buscar. Então, usamos uma sintaxe Typescript para buscar informações no banco de dados.
TypeORM executa o Knex por baixo dos panos, o que permite também usar o Knex.
A abstração no TypeORM e do Knex permite que utilizemos qualquer banco de dados e mesmo se mudarmos de banco de dados, as queries continuarão funcionando.
O node-postgres, como é um driver nativo, ele vai quebrar se mudar o banco de dados.
- Criação de ambientes isolados (container);
- Containers expõe portas para comunicação;
- Imagem
- Container
- Docker Registry (Docker Hub)
- Dockerfile
- Receita de uma imagem
# Partimos de uma imagem existente
FROM node:10
# Definimos a pasta e copiamos os arquivox
WORKDIR /usr/app
COPY . ./
# Instalamos as dependências
RUN yarn
# Expomos a porta
EXPOSE 3333
# Executamos nossa aplicação
CMD yarn start
Procurar a imagem docker do postgres na internet.
Para verificar se a porta está disponível no mac,
lsof -i :5432
Executar no terminal:
docker run --name gostack_postgres -e POSTGRES_PASSWORD=docker -p 5432:5432 -d postgres
Para ver se o container está de pé
docker ps
Caso não esteja de pé, é possível ver todas as imagens que existem na sua máquina
docker ps -a
Para parar um container
docker stop <id>
Para iniciar um container
docker start <id>
- Instalar o DBeaver
- Criar nova conexão
- Escolher PostgreSQL
- Main:
- Host: localhost
- Port: 5432 (tem que ser a mesma porta informada no comando do docker run)
- Database: postgres
- Username: postgres
- Password: docker (que é a mesma senha criada no comando do docker run)
- PostgreSQL:
- Show all databases
- Main:
- Finish
Ir no site do TypeORM e seguir instruções de criar um arquivo de configuração do nosso banco de dados.
Instalar a dependência no projeto
yarn add typeorm pg
Criar na raiz do projeto o arquivo ormconfig.json
e adicionar type
, host
, port
, username
, password
, database
{
"type": "postgres",
"host": "localhost",
"port": "5432",
"username": "postgres",
"password": "docker",
"database": "gostack_gobarber"
}
Criar um arquivo de conexão com o banco de dados src/database/index.ts
import { createConnection } from 'typeorm';
createConnection();
Tanto o createConnection
quanto a cli
do typeorm
leem o arquivo ormconfig.json
Agora é só importar no server.ts
import express from 'express';
import routes from './routes';
import './database';
const app = express();
app.use(express.json());
app.use(routes);
app.listen(3333, () => {
console.log('🚀 Server started on port 3333!');
});
E rodar a aplicação
yarn dev:server
Deu um erro, pois temos que criar o banco de dados pelo DBeaver. E roda de novo a aplicação.
Em ormconfig.json
temos que configurar a pasta que vai armazenar as migrations
.
{
// ...
"migrations": [
"./src/database/migrations/*.ts"
],
"cli": {
"migrationsDir": "./src/database/migrations"
}
}
Como o typeorm entende tanto Typescript quanto Javascript e a cli faz as migrations em Javascript, criaremos um script novo para executar a cli do typeorm usando Typescript.
{
// ...
"scripts": {
"build": "tsc",
"dev:server": "ts-node-dev --inspect --transpileOnly --ignore-watch node_modules src/server.ts",
"typeorm": "ts-node-dev ./node_modules/typeorm/cli.js"
},
// ...
}
E executa no terminal
yarn typeorm migration:create -n CreateAppointments
E vamos ver que na pasta database/migration
foi criada uma tabela.
Migrations
é como se fosse o git do nosso banco de dados. Elas controlam a versão do banco de dados e controlam alterações simultâneas dentro do nosso banco de dados. Ao invés do desenvolvedor fazer as alterações diretamente no banco de dados, ele cria uma migration. E outro desenvolvedor que pegar o projeto, vai executar todas as migrations para ter sua base de dados atualizada. Isso evita que os bancos de dados estejam em versões diferentes em todos ambientes de desenvolvimento.
Funcionamento da Migration
Dentro do método up
vamos colocar o que queremos que seja feito no banco de dados quando essa migration for executada (alterações, criações).
O método down
utilizamos como um fallback para desfazer o que fizemos no método up
.
Arrumar o lint export default
, Promise<void>
e adiciona nova regra no eslintrc.json
para não ficar pedindo this
dentro da class
{
// ...
"rules": {
"prettier/prettier": "error",
"class-methods-use-this":"off",
// ...
}
}
Em src/database/migrations/1589991332947-CreateAppointments.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export default class CreateAppointments1589991332947
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'appointments',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{
name: 'provider',
type: 'varchar',
isNullable: false,
},
{
name: 'date',
type: 'timestamp with time zone',
isNullable: false,
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('appointments');
}
}
Para rodar a migration
yarn typeorm migration:run
Para verificar quais migrations rodaram
yarn typeorm migration:show
Importante
Só podemos alterar a migration se ela ainda não foi enviada para o controle de versão (git). Senão, obrigatoriamente precisa criar uma nova migration.
Para desfazer a migration
yarn typeorm migration:revert
Relacionar o model de agendamentos com o banco de dados.
Entity
é um model
que vai ser salvo no banco de dados.
Em tsconfig.json
habilitar as opções de decorator
no Typescript
{
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
}
Aviso que toda vez que for salvo Appointment
, será na tabela appointments
. Adicionar Entity
, Column
e PrimaryGeneratedColumn
. Agora não precisaremos mais do constructor
pois a Entity
já cria automaticamente. E desabilitar no tsconfig.json
{
"strictPropertyInitialization": false,
}
Em src/models/Appointment.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity('appointments')
class Appointment {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
provider: string;
@Column('timestamp with time zone')
date: Date;
}
export default Appointment;
O TypeORM já vem com um repository
, então podemos excluir do código a parte do constructor
e métodos all
e create
.
Só deixamos o método findByDate
por enquanto e importamos EntityRepository
e Repository
de typeorm
.
Fazendo a alteração do método findByDate
import { EntityRepository, Repository } from 'typeorm';
import Appointment from '../models/Appointment';
@EntityRepository(Appointment)
class AppointmentsRepository extends Repository<Appointment> {
/**
* find an appointment by given date
*/
public async findByDate(date: Date): Promise<Appointment | null> {
const findAppointment = await this.findOne({
where: { date },
});
return findAppointment || null;
}
}
export default AppointmentsRepository;
Agora vamos alterar o src/services/CreateAppointmentService.ts
import { startOfHour } from 'date-fns';
import { getCustomRepository } from 'typeorm';
import Appointment from '../models/Appointment';
import AppointmentsRepository from '../repositories/AppointmentsRepository';
interface Request {
provider: string;
date: Date;
}
class CreateAppointmentService {
/**
* execute
*/
public async execute({ provider, date }: Request): Promise<Appointment> {
const appointmentsRepository = getCustomRepository(AppointmentsRepository);
const appointmentDate = startOfHour(date);
const findAppointmentInSameDate = appointmentsRepository.findByDate(
appointmentDate,
);
if (findAppointmentInSameDate) {
throw Error('The appointment hour is not available.');
}
const appointment = appointmentsRepository.create({
provider,
date: appointmentDate,
});
await appointmentsRepository.save(appointment);
return appointment;
}
}
export default CreateAppointmentService;
Agora vamos alterar a rota src/routes/appointments.routes.ts
import { Router } from 'express';
import { parseISO } from 'date-fns';
import { getCustomRepository } from 'typeorm';
import AppointmentsRepository from '../repositories/AppointmentsRepository';
import CreateAppointmentService from '../services/CreateAppointmentService';
const appointmentsRouter = Router();
appointmentsRouter.get('/', async (request, response) => {
const appointmentsRepository = getCustomRepository(AppointmentsRepository);
const appointments = await appointmentsRepository.find();
return response.json(appointments);
});
appointmentsRouter.post('/', async (request, response) => {
try {
const { provider, date } = request.body;
const parsedDate = parseISO(date);
const createAppointment = new CreateAppointmentService();
const appointment = await createAppointment.execute({
provider,
date: parsedDate,
});
return response.json(appointment);
} catch (err) {
return response.status(400).json({ error: err.message });
}
});
export default appointmentsRouter;
E agora vamos rodar a aplicação
yarn dev:server
Deu um erro, pois precisamos instalar uma dependência
yarn add reflect-metadata
E adicionar no server.ts
na primeira linha
import 'reflect-metadata';
E adicionar em ormconfig.json
{
"entities": [
"./src/models/*.ts"
],
}
Roda a aplicação e confere a criação e listagem dos appointments
no banco. =)
Vamos criar a tabela de Users
yarn typeorm migration:create -n CreateUsers
E vamos configurar a tabela no arquivo src/database/migrations/1590094902911-CreateUsers.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export default class CreateUsers1590094902911 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'varchar',
isPrimary: true,
generationStrategy: 'uuid',
default: 'uuid_generate_v4()',
},
{
name: 'name',
type: 'varchar',
},
{
name: 'email',
type: 'varchar',
isUnique: true,
},
{
name: 'password',
type: 'varchar',
},
{
name: 'created_at',
type: 'timestamp',
default: 'now()',
},
{
name: 'updated_at',
type: 'timestamp',
default: 'now()',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}
Criar o arquivo src/models/User.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('users')
export default class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column()
email: string;
@Column()
password: string;
@CreateDateColumn()
created_at: Date;
@UpdateDateColumn()
updated_at: Date;
}
E aproveitar para adicionar esses campos de created_at
e updated_at
na migration da tabela appointments
assim como em src/models/Appointment.ts
.
Rodar o revert
até que delete a tabela de appointments
também.
yarn typeorm migration:revert
Rodar o run
novamente
yarn typeorm migration:run
Ao invés de guardar o nome do prestador de serviços (provider), é melhor guardar a referência para o prestador de serviços. Nos bancos relacionais sempre colocamos o id da referência daquele usuário. Então, vamos trocar o provider
por provider_id
no model de Appointment
@Column()
provider_id: string;
E iremos criar outra migration para fazer essa alteração
yarn typeorm migration:create -n AlterProviderFieldToProvideId
Como o provider
pode um dia querer deletar sua conta, mas é importante manter o histórico os clientes, então mantemos o registro do provider_id
e permitimos que seja isNullable
. E precisamos também criar a foreignKey
.
onDelete
pode ter 3 opções:
- RESTRICT: não deixa o usuário ser deletado
- SET NULL: vai setar a variável provider_id como `null``
- CASCADE: deletou o usuário e todos agendamos relacionados a ele
onUpdate
caso um id seja alterado por qualquer razão, deixar como CASCADE
para que se isso um dia acontecer, todas as tabelas relacionadas sejam alteradas também.
No método down
, temos que fazer a ordem contrária para conseguir reverter tudo o que fizemos no método up
.
import {
MigrationInterface,
QueryRunner,
TableColumn,
TableForeignKey,
} from 'typeorm';
export default class AlterProviderFieldToProviderId1590196293971
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('appointments', 'provider');
await queryRunner.addColumn(
'appointments',
new TableColumn({
name: 'provider_id',
type: 'uuid',
isNullable: true,
}),
);
await queryRunner.createForeignKey(
'appointments',
new TableForeignKey({
name: 'AppointmentProvider',
columnNames: ['provider_id'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropForeignKey('appointments', 'AppointmentProvider');
await queryRunner.dropColumn('appointments', 'provider_id');
await queryRunner.addColumn(
'appointments',
new TableColumn({
name: 'provider',
type: 'varchar',
}),
);
}
}
Mudar o type da coluna id
das outras migrations, e por causa disso o run
deu erro. Precisamos fazer o revert
antes de rodar o run
de novo.
No DBeaver já é possívem ver em ERDiagram
que tabela de usuários agrega a tabela de agendamentos.
Existem 3 tipos de relacionamentos:
- OneToOne: um usuário tem no máximo um agendamento
- OneToMany: um usuário tem muitos agendamentos
- ManyToMany: muitos usuários participam de muitos agendamentos (se mais de um prestador de serviço pudesse participar do mesmo serviço)
No src/models/Appointments.ts
fazer o relacionamento com user, sendo ManyToOne, pois são muitos agendamentos para um usuário. A arrow function retorna o model relacionado. E coloca o JoinColumn
que vai identificar qual coluna vai identificar esse usuário.
// ...
@ManyToOne(() => User)
@JoinColumn({ name: 'provider_id' })
provider: User;
// ...
KISS - Keep It Simple & Stupid
Criação da rota de users
em src/routes/users.routes.ts
bem simples
import { Router } from 'express';
const usersRouter = Router();
usersRouter.post('/', async (request, response) => {
try {
return response.send();
} catch (err) {
return response.status(400).json({ error: err.message });
}
});
export default usersRouter;
Importa no src/routes/index.ts
// ...
import usersRouter from './users.routes';
// ...
routes.use('/users', usersRouter);
// ...
Criar o service src/services/CreateUserService.ts
.
Se eu não tenho nenhum método personalizado, eu não preciso criar o Repository. Basta de dentro do Service importar o typeorm
com o getRepository
. Mesmo que exista a regra de negócios no banco de dados, essa regra tem que estar na nossa aplicação. Então, para validar se o e-mail já existe ou não, colocamos no Service essa regra de verificação.
import { getRepository } from 'typeorm';
import User from '../models/User';
interface Request {
name: string;
email: string;
password: string;
}
class CreateUserService {
public async execute({ name, email, password }: Request): Promise<User> {
const userRepository = getRepository(User);
const checkUserExists = await userRepository.findOne({
where: { email },
});
if (checkUserExists) {
throw new Error('E-mail address already used.');
}
const user = userRepository.create({
name,
email,
password,
});
await userRepository.save(user);
return user;
}
}
export default CreateUserService;
E agora importo o createUserService
dentro do routes
import { Router } from 'express';
import CreateUserService from '../services/CreateUserService';
const usersRouter = Router();
usersRouter.post('/', async (request, response) => {
try {
const { name, email, password } = request.body;
const createUser = new CreateUserService();
const user = await createUser.execute({
name,
email,
password,
});
return response.json(user);
} catch (err) {
return response.status(400).json({ error: err.message });
}
});
export default usersRouter;
Configurar o Insomnia para criar um user
: requisição de POST na rota '/users'
e com body
{
"name": "Cintia",
"email": "cintiafumi@gmail.com",
"password": "123456"
}
Verifica no DBeaver e está lá na tabela de users
.
Para criar um appointment
temos que alterar provider
para provider_id
no CreateAppointmentService
e no appointments.routes
import { startOfHour } from 'date-fns';
import { getCustomRepository } from 'typeorm';
import Appointment from '../models/Appointment';
import AppointmentsRepository from '../repositories/AppointmentsRepository';
interface Request {
provider_id: string;
date: Date;
}
class CreateAppointmentService {
public async execute({ provider_id, date }: Request): Promise<Appointment> {
const appointmentsRepository = getCustomRepository(AppointmentsRepository);
const appointmentDate = startOfHour(date);
const findAppointmentInSameDate = await appointmentsRepository.findByDate(
appointmentDate,
);
if (findAppointmentInSameDate) {
throw Error('The appointment hour is not available.');
}
const appointment = appointmentsRepository.create({
provider_id,
date: appointmentDate,
});
await appointmentsRepository.save(appointment);
return appointment;
}
}
export default CreateAppointmentService;
import { Router } from 'express';
import { parseISO } from 'date-fns';
import { getCustomRepository } from 'typeorm';
import AppointmentsRepository from '../repositories/AppointmentsRepository';
import CreateAppointmentService from '../services/CreateAppointmentService';
const appointmentsRouter = Router();
appointmentsRouter.get('/', async (request, response) => {
const appointmentsRepository = getCustomRepository(AppointmentsRepository);
const appointments = await appointmentsRepository.find();
return response.json(appointments);
});
appointmentsRouter.post('/', async (request, response) => {
try {
const { provider_id, date } = request.body;
const parsedDate = parseISO(date);
const createAppointment = new CreateAppointmentService();
const appointment = await createAppointment.execute({
provider_id,
date: parsedDate,
});
return response.json(appointment);
} catch (err) {
return response.status(400).json({ error: err.message });
}
});
export default appointmentsRouter;
Adiciona no eslint para aceitar provider_id
sem ser camel case:
{
"rules": {
"@typescript-eslint/camelcase": "off",
}
}
Faz um POST de um appointment
agora com provider_id
ao invés de provider
no body
da requisição da rota '/appointments'
Instalar biblioteca bcryptjs
e seu types
yarn add bcryptjs
yarn add -D @types/bcryptjs
Importar o hash
em src/services/CreateUserService.ts
import { hash } from 'bcryptjs';
// ...
const hashedPassword = await hash(password, 8);
const user = usersRepository.create({
name,
email,
password: hashedPassword,
});
// ...
Criar um user pelo Insomnia com email diferente e verificar se encriptou a senha antes de salvar no banco. Como não é bom trazer a senha no retorno do request, remover das rotas o password antes de trazer o retorno. Mas se ver no DBeaver, a senha estará lá.
Para zerar a base de dados pelo DBeaver, selecione e delete as 3 linhas de users criado, e salva a tabela. E cria um user novo 'limpo'.
JWT: JSON Web Token
Nas rotas, criamos o src/routes/sessions.routes.ts
no sentido de ser uma sessão criada pela autenticação de um usuário.
import { Router } from 'express';
const sessionsRouter = Router();
sessionsRouter.post('/', async (request, response) => {
try {
const { email, password } = request.body;
return response.json({ ok: true });
} catch (err) {
return response.status(400).json({ error: err.message });
}
});
export default sessionsRouter;
Adicionar em src/routes/index.ts
import sessionsRouter from './sessions.routes';
// ...
routes.use('/sessions', sessionsRouter);
Como vai ter que verificar se o e-mail existe e se a senha está correta, temos que criar também um service src/services/AuthenticateUserService.ts
import { getRepository } from 'typeorm';
import { compare } from 'bcryptjs';
import User from '../models/User';
interface Request {
email: string;
password: string;
}
interface Response {
user: User;
}
export default class AuthenticateUserService {
public async execute({ email, password }: Request): Promise<Response> {
const usersRepository = getRepository(User);
const user = await usersRepository.findOne({ where: { email } });
if (!user) {
throw new Error('Incorrect email/password combination.');
}
const passwordMatched = await compare(password, user.password);
if (!passwordMatched) {
throw new Error('Incorrect email/password combination.');
}
return { user };
}
}
Alterar src/routes/sessions.routes.ts
import { Router } from 'express';
import AuthenticateUserService from '../services/AuthenticateUserService';
const sessionsRouter = Router();
sessionsRouter.post('/', async (request, response) => {
try {
const { email, password } = request.body;
const authenticateUserService = new AuthenticateUserService();
const { user } = await authenticateUserService.execute({
email,
password,
});
delete user.password;
return response.json({ user });
} catch (err) {
return response.status(400).json({ error: err.message });
}
});
export default sessionsRouter;
Fazer request de criar sessão no Insomnia, rota '/sessions'
com body. Verificar com e-mail e senha errados se está retornando o erro
{
"email": "cintiafumi@gmail.com",
"password": "123456"
}
Instalar pacote
yarn add jsonwebtoken
yarn add -D @types/jsonwebtoken
Adicionando o token em src/services/AuthenticateUserService.ts
import { sign } from 'jsonwebtoken';
// ...
interface Response {
user: User;
token: string;
}
export default class AuthenticateUserService {
public async execute({ email, password }: Request): Promise<Response> {
// ...
const token = sign({}, 'c038xxxxxxxx', {
subject: user.id,
expiresIn: '1d',
});
return { user, token };
}
}
Na rota, também trazer na resposta o token
// ...
const { user, token } = await authenticateUserService.execute({
email,
password,
});
delete user.password;
return response.json({ user, token });
// ...
Fazer uma requisição de criar session no Insomnia e verificar o retorno do jwt. Conferir em jwt.io
Esse middleware vai evitar dos usuários acessarem algumas rotas sem autenticação.
Inserir no Header da requisição o Authorization
com Bearer <token>
, que pode ser feito no Insomnia de maneira mais rápida na aba Auth
e selecionar a opção Bearer token
.
Vamos criar uma variável de ambiente no Insomnia chamada token
e adicionar então em Auth
nas rotas de '/appointments'
GET e POST.
Criar o arquivo src/middlewares/ensureAuthenticated.ts
Como já vai usar a secret
para a verificação do token JWT, então, vamos passar essa secret para um arquivo `src/config/auth.ts``
export default {
jwt: {
secret: 'c038xxxxxxxx',
expiresIn: '1d',
},
};
Alterar src/services/AuthenticateUserService.ts
import authConfig from '../config/auth';
// ...
const { secret, expiresIn } = authConfig.jwt;
const token = sign({}, secret, {
subject: user.id,
expiresIn,
});
// ...
Em src/middlewares/ensureAuthenticated.ts
import { NextFunction, Response, Request } from 'express';
import { verify } from 'jsonwebtoken';
import authConfig from '../config/auth';
export default function ensureAuthenticated(
request: Request,
response: Response,
next: NextFunction,
): void {
const authHeader = request.headers.authorization;
if (!authHeader) {
throw new Error('JWT token is missing.');
}
const [, token] = authHeader.split(' ');
try {
const decoded = verify(token, authConfig.jwt.secret);
console.log(decoded);
next();
} catch {
throw new Error('Invalid JWT token.');
}
}
Aplicar esse middleware na rota de appointments
// ...
import ensureAuthenticated from '../middlewares/ensureAuthenticated';
const appointmentsRouter = Router();
appointmentsRouter.use(ensureAuthenticated);
// ...
Testar no Insomnia o GET na rota de appointments
:
- sem token:
Error: JWT token is missing.
- com token errado:
Error: Invalid JWT token.
- com token certo: retorna todos appointments
E no console.log
também retornou.
No src/middlewares/ensureAuthenticated.ts
também seria legal incluir o id do user
para que na listagem dos agendamentos, o usuários só tivesse acesso aos seus agendamentos. Então, todas informações no request e no response do middleware será carregado para frente.
Forçar a tipagem do payload do token e sobrescrever o request e sua tipagem. Criar um arquivo src/@types/express.d.ts
para sobrescrever a biblioteca do express
. E ao inserir o interface do Request, isso será anexado ao tipo já existente.
declare namespace Express {
export interface Request {
user: {
id: string;
};
}
}
Então, src/middlewares/ensureAuthenticated.ts
conserta o tipo
import { NextFunction, Response, Request } from 'express';
import { verify } from 'jsonwebtoken';
import authConfig from '../config/auth';
interface TokenPayload {
iat: number;
exp: number;
sub: string;
}
export default function ensureAuthenticated(
request: Request,
response: Response,
next: NextFunction,
): void {
const authHeader = request.headers.authorization;
if (!authHeader) {
throw new Error('JWT token is missing.');
}
const [, token] = authHeader.split(' ');
try {
const decoded = verify(token, authConfig.jwt.secret);
const { sub } = decoded as TokenPayload;
request.user = {
id: sub,
};
next();
} catch {
throw new Error('Invalid JWT token.');
}
}
E agora temos o id do usuário disponível em todas as rotas autenticadas por esse middleware.
Adicionar uma coluna de avatar
em users
para armazenar o caminho da imagem. Criar a migration
yarn typeorm migration:create -n AddAvatarFieldToUsers
Nesse caso, a coluna pode ser nula caso já existam usuários cadastrados na base de dados.
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export default class AddAvatarFieldToUsers1590278682186
implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'users',
new TableColumn({
name: 'avatar',
type: 'varchar',
isNullable: true,
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('users', 'avatar');
}
}
Criar a rota de PATCH que vai fazer alteração somente nesse campo. Em src/routes/users.routes.ts
// ...
usersRouter.patch('/avatar', ensureAuthenticated, async (request, response) => {
return response.json({ ok: true });
});
// ,,,
Rodar a aplicação e criar no Insomnia essa request.
Instalar o pacote que é um middleware que faz upload de arquivos do express
yarn add multer
yarn add -D @types/multer
Criar um arquivo src/config/upload.ts
. O multer.diskStorage
é para salvar os arquivos localmente.
Criar a pasta /tmp na raiz do projeto (fora da src). E um arquivo .gitkeep
dentro que não quero que seja ignorada, garantindo que a pasta seja criada e suba vazia para o Github. No nosso .gitignore
adicionar
.DS_Store
.vscode/
.idea/
node_modules/
build/
temp/
tmp/*
!tmp/.gitkeep
Em src/config/upload.ts
import path from 'path';
import crypto from 'crypto';
import multer from 'multer';
export default {
storage: multer.diskStorage({
destination: path.resolve(__dirname, '..', '..', 'tmp'),
filename(request, file, callback) {
const fileHash = crypto.randomBytes(10).toString('hex');
const filename = `${fileHash}-${file.originalname}`;
return callback(null, filename);
},
}),
};
Em src/routes/users.routes.ts
import { Router } from 'express';
import multer from 'multer';
import uploadConfig from '../config/upload';
import CreateUserService from '../services/CreateUserService';
import ensureAuthenticated from '../middlewares/ensureAuthenticated';
const usersRouter = Router();
const upload = multer(uploadConfig);
// ...
usersRouter.patch(
'/avatar',
ensureAuthenticated,
upload.single('avatar'),
async (request, response) => {
return response.json({ ok: true });
},
);
Configurar o Insomnia para fazer um PATCH de uma foto na rota /users/avatar
. Ao invés de Body
, enviar Multipart form
de name
igual a avatar
e tipo file
. Escolher uma foto e fazer a requisição. Na pasta /tmp já apareceu a foto. Os dados desse arquivo são acessíveis pelo request.file
no arquivo de rotas src/routes/users.routes.ts
.
Criar um service de upload do avatar src/services/UpdateUserAvatarService.ts
que tem que deletar o avatar antigo e salvar o avatar novo.
interface Request {
user_id: string;
avatarFilename: string;
}
class UpdateUserAvatarService {
public async execute({ user_id, avatarFilename }: Request): Promise<void> {}
}
export default UpdateUserAvatarService;
Na rota src/routes/users.routes.ts
, importar o service e alterar
import UpdateUserAvatarService from '../services/UpdateUserAvatarService';
// ...
usersRouter.patch(
'/avatar',
ensureAuthenticated,
upload.single('avatar'),
async (request, response) => {
try {
const updateUserAvatarService = new UpdateUserAvatarService();
await updateUserAvatarService.execute({
user_id: request.user.id,
avatarFilename: request.file.filename,
});
return response.json({ ok: true });
} catch (err) {
return response.status(400).json({ error: err.message });
}
},
);
Voltar para o Service e importar o getRepository
com o model User
. Nesse ponto, verificamos que esquecemos de adicionar o avatar
no model do User
// ...
@Column()
avatar: string;
// ...
Voltando ao service, primeiro temos que verificar se o user existe e se ele já tem um avatar. Como estamos salvando o arquivo localmente, então, temos que procurar se esse arquivo existe na pasta /tmp
para então remover. Para não ter que escrever novamente a parte do path, vamos alterar o src/config/upload.ts
// ...
const tmpFolder = path.resolve(__dirname, '..', '..', 'tmp');
export default {
directory: tmpFolder,
storage: multer.diskStorage({
destination: tmpFolder,
// ...
}),
};
Importar o uploadConfig para pegar a propriedade directory. Então, verificar o status desse file para ver se existe.
import { getRepository } from 'typeorm';
import path from 'path';
import fs from 'fs';
import uploadConfig from '../config/upload';
import User from '../models/User';
interface Request {
user_id: string;
avatarFilename: string;
}
class UpdateUserAvatarService {
public async execute({ user_id, avatarFilename }: Request): Promise<User> {
const userRepository = getRepository(User);
const user = await userRepository.findOne(user_id);
if (!user) {
throw new Error('Only authenticated users can change avatar.');
}
if (user.avatar) {
const userAvatarFilePath = path.join(uploadConfig.directory, user.avatar);
const userAvatarExists = await fs.promises.stat(userAvatarFilePath);
if (userAvatarExists) {
await fs.promises.unlink(userAvatarFilePath);
}
}
user.avatar = avatarFilename;
await userRepository.save(user);
return user;
}
}
export default UpdateUserAvatarService;
Atualizar então a response
de src/routes/users.routes.ts
// ...
const user = await updateUserAvatarService.execute({
user_id: request.user.id,
avatarFilename: request.file.filename,
});
delete user.password;
return response.json(user);
// ...
Fazer o PATCH pelo Insomnia e verificar tanto na pasta /tmp e no banco se a imagem foi alterada.
Configurar uma rota para visualização dos arquivos de avatar de maneira estática. E para isso, vamos em src/server.ts
e adicionamos o uploadConfig
// ...
import uploadConfig from './config/upload';
// ...
app.use('/files', express.static(uploadConfig.directory));
// ...
Rodar a aplicação de novo e no browser já é possível ver esses arquivos pela rota http://localhost:3333/files/<filename>
Criar o arquivo src/errors/AppError.ts
class AppError {
public readonly message: string;
public readonly statusCode: number;
constructor(message: string, statusCode = 400) {
this.message = message;
this.statusCode = statusCode;
}
}
export default AppError;
Importar essa classe em todos os services e trocar onde estava Error
por new AppError(message, statusCode)
. E no middleware também
import AppError from '../errors/AppError';
E poderíamos já trocar no response
do try/catch
o status code como err.statusCode
, mas como iremos tratar globalmente os erros, podemos deixar assim por enquanto.
É um middleware que vai capturar todos os erros de nossa aplicação independente da rota, do service, do middleware. Para isso, tirar todos os try/catch
de todas as rotas. Vai no server.ts
e coloca depois das rotas a tratativa dos erros
// ...
app.use(routes);
app.use(
(err: Error, request: Request, response: Response, next: NextFunction) => {
if (err instanceof AppError) {
return response.status(err.statusCode).json({
status: 'error',
message: err.message,
});
}
console.error(err);
return response.status(500).json({
status: 'error',
message: 'Internal server error.',
});
},
);
// ...
Porém, ao fazer um POST de Session com um user inválido no Insomnia, o express
não conseguiu lidar direito com as funções assíncronas das rotas. Então, vamos instalar o pacote express-async-errors
e importar logo após a importação do express
no server.ts
yarn add express-async-errors
E no server.ts
// ...
import express, { Request, Response, NextFunction } from 'express';
import 'express-async-errors';
// ...
Agora o Insomnia já traz o erro.
O eslint reclama que o parâmetro next
não está sendo usado, e para isso, vamos alterar o server.ts
// ...
app.use((err: Error, request: Request, response: Response, _: NextFunction) => {
// ...
E adicionar uma regra no eslintrc.json
{
"rules": {
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "_"
}],
}
}
Voltamos para o projeto de backend
yarn add cors
yarn add -D @types/cors
Em src/server.ts
//...
import cors from 'cors';
//...
app.use(cors());
//...
Rodar o banco no docker e o server e conferir com o Insomnia se está funcionando.