feat(infra): script de provisionamento de workspace — C9
pnpm workspace:provision --id <id> [--name <nome>] [--with-seed]
Cria banco sar_workspace_{id}, habilita extensões, aplica todas as
migrations e opcionalmente popula dados demo. Sem master DB necessário
— JwtAuthGuard resolve a URL pela convenção de nome (ADR 0006).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"e2e": "nx run-many -t e2e",
|
||||
"dev:api": "nx run api:serve",
|
||||
"dev:web": "nx run web:serve",
|
||||
"workspace:provision": "tsx scripts/provision-workspace.ts",
|
||||
"dev:up": "docker compose -f docker-compose.dev.yml up -d",
|
||||
"dev:down": "docker compose -f docker-compose.dev.yml down",
|
||||
"dev:logs": "docker compose -f docker-compose.dev.yml logs -f",
|
||||
|
||||
167
scripts/provision-workspace.ts
Normal file
167
scripts/provision-workspace.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* SAR — Provisionamento de workspace (C9)
|
||||
*
|
||||
* Uso:
|
||||
* pnpm workspace:provision --id <workspace-id> [--name <nome>] [--with-seed]
|
||||
*
|
||||
* O que faz:
|
||||
* 1. Cria o banco sar_workspace_<id> (ignora se já existe)
|
||||
* 2. Habilita pgcrypto + uuid-ossp
|
||||
* 3. Roda `prisma migrate deploy` no novo banco
|
||||
* 4. Se --with-seed: popula dados demo (mesmos do seed de dev)
|
||||
*
|
||||
* Variáveis de ambiente (todas opcionais — padrões para dev local):
|
||||
* PG_HOST (default: localhost)
|
||||
* PG_PORT (default: 5432)
|
||||
* PG_USER (default: sar)
|
||||
* PG_PASSWORD (default: sar_dev_password)
|
||||
* PG_ADMIN_DB (default: postgres)
|
||||
*
|
||||
* Convenção BD-por-workspace (ADR 0006):
|
||||
* O JwtAuthGuard constrói a URL como sar_workspace_{workspaceId} automaticamente.
|
||||
* Nenhuma entrada em master DB é necessária para o MVP.
|
||||
*/
|
||||
|
||||
import { execSync } from 'child_process';
|
||||
import { resolve } from 'path';
|
||||
import pg from 'pg';
|
||||
|
||||
// ─── CLI args ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function arg(flag: string): string | undefined {
|
||||
const idx = process.argv.indexOf(flag);
|
||||
return idx !== -1 ? process.argv[idx + 1] : undefined;
|
||||
}
|
||||
|
||||
const workspaceId = arg('--id');
|
||||
const workspaceName = arg('--name') ?? workspaceId;
|
||||
const withSeed = process.argv.includes('--with-seed');
|
||||
|
||||
if (!workspaceId) {
|
||||
console.error('Uso: pnpm workspace:provision --id <workspace-id> [--name <nome>] [--with-seed]');
|
||||
console.error('Exemplo: pnpm workspace:provision --id acme-001 --name "Acme Distribuidora"');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/.test(workspaceId)) {
|
||||
console.error('Erro: --id deve conter apenas letras minúsculas, números e hífens (3-64 chars).');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ─── Config ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const PG_HOST = process.env['PG_HOST'] ?? 'localhost';
|
||||
const PG_PORT = parseInt(process.env['PG_PORT'] ?? '5432', 10);
|
||||
const PG_USER = process.env['PG_USER'] ?? 'sar';
|
||||
const PG_PASSWORD = process.env['PG_PASSWORD'] ?? 'sar_dev_password';
|
||||
const PG_ADMIN_DB = process.env['PG_ADMIN_DB'] ?? 'postgres';
|
||||
|
||||
const DB_NAME = `sar_workspace_${workspaceId}`;
|
||||
const DB_URL = `postgresql://${PG_USER}:${PG_PASSWORD}@${PG_HOST}:${PG_PORT}/${DB_NAME}`;
|
||||
|
||||
const MONOREPO_ROOT = resolve(import.meta.dirname, '..');
|
||||
const API_DIR = resolve(MONOREPO_ROOT, 'apps/api');
|
||||
|
||||
// ─── Steps ────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function createDatabase(): Promise<void> {
|
||||
const pool = new pg.Pool({
|
||||
host: PG_HOST,
|
||||
port: PG_PORT,
|
||||
user: PG_USER,
|
||||
password: PG_PASSWORD,
|
||||
database: PG_ADMIN_DB,
|
||||
});
|
||||
|
||||
try {
|
||||
await pool.query(`CREATE DATABASE "${DB_NAME}" OWNER ${PG_USER}`);
|
||||
console.log(` ✓ Banco criado: ${DB_NAME}`);
|
||||
} catch (e: unknown) {
|
||||
if ((e as { code?: string }).code === '42P04') {
|
||||
console.log(` ~ Banco já existe: ${DB_NAME} — pulando criação`);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
|
||||
// Extensões no novo banco
|
||||
const wsPool = new pg.Pool({
|
||||
host: PG_HOST,
|
||||
port: PG_PORT,
|
||||
user: PG_USER,
|
||||
password: PG_PASSWORD,
|
||||
database: DB_NAME,
|
||||
});
|
||||
try {
|
||||
await wsPool.query(`CREATE EXTENSION IF NOT EXISTS pgcrypto`);
|
||||
await wsPool.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
||||
console.log(` ✓ Extensões habilitadas (pgcrypto, uuid-ossp)`);
|
||||
} finally {
|
||||
await wsPool.end();
|
||||
}
|
||||
}
|
||||
|
||||
function runMigrations(): void {
|
||||
execSync(`pnpm exec prisma migrate deploy`, {
|
||||
cwd: API_DIR,
|
||||
env: { ...process.env, DATABASE_URL: DB_URL },
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log(` ✓ Migrations aplicadas`);
|
||||
}
|
||||
|
||||
function runSeed(): void {
|
||||
execSync(`pnpm exec prisma db seed`, {
|
||||
cwd: API_DIR,
|
||||
env: { ...process.env, DATABASE_URL: DB_URL },
|
||||
stdio: 'inherit',
|
||||
});
|
||||
console.log(` ✓ Seed de demo executado`);
|
||||
}
|
||||
|
||||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function main(): Promise<void> {
|
||||
console.log(`\nSAR — Provisionamento de Workspace`);
|
||||
console.log(`────────────────────────────────────`);
|
||||
console.log(`ID: ${workspaceId}`);
|
||||
console.log(`Nome: ${workspaceName}`);
|
||||
console.log(`Banco: ${DB_NAME}`);
|
||||
console.log(`Seed: ${withSeed ? 'sim (--with-seed)' : 'não'}`);
|
||||
console.log('');
|
||||
|
||||
console.log(`[1/3] Criando banco de dados...`);
|
||||
await createDatabase();
|
||||
|
||||
console.log(`[2/3] Aplicando migrations...`);
|
||||
runMigrations();
|
||||
|
||||
if (withSeed) {
|
||||
console.log(`[3/3] Populando dados demo...`);
|
||||
runSeed();
|
||||
} else {
|
||||
console.log(`[3/3] Seed pulado (use --with-seed para dados demo)`);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
console.log(`════════════════════════════════════`);
|
||||
console.log(`✓ Workspace provisionado com sucesso`);
|
||||
console.log(`════════════════════════════════════`);
|
||||
console.log('');
|
||||
console.log(`Próximos passos:`);
|
||||
console.log(` 1. Emita um JWT com workspace_id = "${workspaceId}"`);
|
||||
console.log(` usando MASTER_LOGIN_JWT_SECRET do ambiente alvo`);
|
||||
console.log(` 2. O JwtAuthGuard conecta automaticamente ao banco`);
|
||||
console.log(` via convenção sar_workspace_{id} (ADR 0006)`);
|
||||
console.log(` 3. Em produção, defina DATABASE_URL no ambiente`);
|
||||
console.log(` se o banco não seguir a convenção de nome padrão`);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
main().catch((e: unknown) => {
|
||||
console.error('\nErro durante provisionamento:', e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user