diff --git a/package.json b/package.json index 52785e2..422edc2 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/provision-workspace.ts b/scripts/provision-workspace.ts new file mode 100644 index 0000000..8528536 --- /dev/null +++ b/scripts/provision-workspace.ts @@ -0,0 +1,167 @@ +#!/usr/bin/env tsx +/** + * SAR — Provisionamento de workspace (C9) + * + * Uso: + * pnpm workspace:provision --id [--name ] [--with-seed] + * + * O que faz: + * 1. Cria o banco sar_workspace_ (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 [--name ] [--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 { + 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 { + 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); +});