#!/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); });