Migrations
Com o modelo definido, precisamos criar essas tabelas no banco de dados.
Poderíamos abrir um terminal, conectar no PostgreSQL e executar os comandos
CREATE TABLE manualmente. Funciona, uma vez. Mas quando um colega clonar o
repositório e precisar criar o mesmo banco, ele precisaria repetir tudo.
Para resolver isso, usamos migrations (migrações) — arquivos SQL versionados que descrevem mudanças na estrutura do banco de dados. Cada migration é um passo: criar uma tabela, adicionar uma coluna, remover um constraint. A sequência completa das migrations reproduz o banco inteiro do zero.
A primeira migration: events
A primeira tabela que vamos criar é events — o recurso central do TamoJunto.
Por enquanto, sem a coluna organizer_id: a tabela users ainda não existe, e
o foreign key precisa referenciar uma tabela que já esteja criada.
Crie um diretório backend/migrations/ e dentro dele o arquivo
001_create_events.sql:
CREATE TABLE events (
id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
location VARCHAR(200) NOT NULL,
date TIMESTAMP NOT NULL,
capacity INTEGER NOT NULL CHECK (capacity > 0),
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Código 5-1: Migration que cria a tabela events (sem foreign key por enquanto).
Algumas decisões nesse SQL:
SERIAL PRIMARY KEYgera um inteiro auto-incrementado e o marca como chave primária — cada registro recebe umidúnico automaticamente.NOT NULLimpede que a coluna fique vazia. Um evento sem título ou sem local não faz sentido no TamoJunto.CHECK (capacity > 0)é um constraint que impede eventos com capacidade zero ou negativa. O banco de dados valida isso a cada inserção e atualização.DEFAULT NOW()preenchecreated_atautomaticamente com a data/hora atual no momento da inserção.
A segunda migration: users e o foreign key
A segunda migration faz duas coisas: cria a tabela users e adiciona a coluna
organizer_id à tabela events já existente.
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(200) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
ALTER TABLE events
ADD COLUMN organizer_id INTEGER NOT NULL DEFAULT 1 REFERENCES users(id);
Código 5-2: Migration que cria users e adiciona o foreign key em events.
O ALTER TABLE mostra algo importante: migrations não servem apenas para criar
tabelas. Elas também modificam tabelas existentes — adicionar colunas,
remover constraints, alterar tipos. Essa é a natureza incremental das migrations:
cada arquivo descreve uma diferença em relação ao estado anterior do banco.
O DEFAULT 1 na coluna organizer_id existe porque a tabela events pode já
conter registros (se o seed tivesse rodado antes, por exemplo). Sem um valor
padrão, o ALTER TABLE ADD COLUMN ... NOT NULL falharia — o banco não saberia
qual valor colocar nas linhas existentes. Numa aplicação real, esse valor padrão
seria removido numa migration posterior; aqui, ele serve para que o SQL execute
sem erros.
A cláusula REFERENCES users(id) declara o foreign key. O PostgreSQL garante
que todo valor em organizer_id corresponde a um id existente na tabela
users.
Executando as migrations
Para executar as migrations, vamos usar um script simples que lê os arquivos
.sql do diretório migrations/ em ordem e executa cada um no banco. Crie o
arquivo backend/migrate.ts:
import { Client } from "pg";
import fs from "fs";
import path from "path";
async function migrate() {
const client = new Client({
connectionString: process.env.DATABASE_URL,
});
await client.connect();
const migrationsDir = path.join(__dirname, "migrations");
const files = fs.readdirSync(migrationsDir).sort();
for (const file of files) {
if (!file.endsWith(".sql")) continue;
const sql = fs.readFileSync(path.join(migrationsDir, file), "utf8");
console.log(`Executando: ${file}`);
await client.query(sql);
}
console.log("Migrations concluídas.");
await client.end();
}
migrate().catch((err) => {
console.error("Erro ao executar migrations:", err.message);
process.exit(1);
});
Código 5-3: Script que aplica migrations em ordem.
Esse script é propositalmente simples: lê os arquivos em ordem alfabética (por
isso o prefixo numérico 001_, 002_), executa cada SQL e pronto. Não rastreia
quais migrations já foram aplicadas — se rodar duas vezes, a segunda execução
falha porque as tabelas já existem. Para um projeto em sala de aula, isso é
aceitável. Recrie o banco quando precisar rodar as migrations novamente1.
Para executar, usamos o tsx, um executor de TypeScript que compila e roda num
passo só:
$ cd backend
$ DATABASE_URL=postgres://localhost:5432/tamojunto npx tsx migrate.ts
Executando: 001_create_events.sql
Executando: 002_create_users_add_fk.sql
Migrations concluídas.
As tabelas agora existem no banco. Verificando:
$ psql postgres://localhost:5432/tamojunto -c '\dt'
List of relations
Schema | Name | Type | Owner
--------+--------+-------+---------
public | events | table | pedrov
public | users | table | pedrov
(2 rows)