#!/usr/bin/env tsx /** * SAR — Provisionamento de cliente * * Aplica o schema SAR (views + tabelas + triggers) no banco ERP do cliente. * Não requer psql — usa a lib pg que já é dependência do projeto. * * Uso: * pnpm client:provision --host --db [opções] * * Opções: * --host Host do PostgreSQL (obrigatório) * --db Nome do banco ERP (obrigatório) * --port Porta (default: 5432) * --user Usuário (default: postgres) * --password Senha (default: lê PG_PASSWORD do env) * --role Role do app SAR — aplica GRANTs se informado (ex: sar_app) * * O que faz: * 1. Executa scripts/sar-erp-schema.sql (schema sar + views + tabelas SAR) * 2. Executa scripts/sar-triggers.sql (triggers de sync ERP + staging tables) * 3. Aplica GRANTs se --role foi informado * 4. Executa `prisma migrate deploy` (aplica migrations Prisma pendentes) */ import { readFileSync, readdirSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { execSync } from 'child_process'; 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 host = arg('--host'); const db = arg('--db'); const port = parseInt(arg('--port') ?? '5432', 10); const user = arg('--user') ?? 'postgres'; const password = arg('--password') ?? process.env['PG_PASSWORD'] ?? ''; const role = arg('--role'); if (!host || !db) { console.error( 'Uso: pnpm client:provision --host --db [--port 5432] [--user postgres] [--password ...] [--role ]', ); console.error( 'Exemplo: pnpm client:provision --host 192.168.0.43 --db libreplast --role sar_app', ); process.exit(1); } // ─── Paths ──────────────────────────────────────────────────────────────────── const SCRIPTS_DIR = dirname(fileURLToPath(import.meta.url)); const MONOREPO_ROOT = resolve(SCRIPTS_DIR, '..'); const API_DIR = resolve(MONOREPO_ROOT, 'apps/api'); // Banco ERP usa LATIN1 — strip chars fora do range 0x00-0xFF (decorações nos comentários SQL) function toLatinSafe(sql: string): string { // O range LATIN1 começa em \x00 por definição — control chars intencionais. // eslint-disable-next-line no-control-regex return sql.replace(/[^\x00-\xFF]/g, (c) => { if (c === '—' || c === '–') return '--'; // em dash / en dash return '-'; // box-drawing chars e outros }); } const SQL_SCHEMA = toLatinSafe(readFileSync(resolve(SCRIPTS_DIR, 'sar-erp-schema.sql'), 'utf8')); const SQL_TRIGGERS = toLatinSafe(readFileSync(resolve(SCRIPTS_DIR, 'sar-triggers.sql'), 'utf8')); const DATABASE_URL = `postgresql://${user}:${encodeURIComponent(password)}@${host}:${port}/${db}`; // ─── Helpers ────────────────────────────────────────────────────────────────── async function runSql(client: pg.Client, label: string, sql: string): Promise { console.log(` Executando ${label}...`); await client.query(sql); console.log(` ✓ ${label} aplicado`); } async function applyGrants(client: pg.Client, appRole: string): Promise { console.log(` Aplicando GRANTs para role "${appRole}"...`); await client.query(` GRANT USAGE ON SCHEMA sar TO ${appRole}; GRANT SELECT ON ALL TABLES IN SCHEMA sar TO ${appRole}; GRANT INSERT, UPDATE, DELETE ON sar.pedidos, sar.pedido_itens, sar.historico_pedido, sar.alcada_desconto, sar.meta_representante, sar.push_subscription, sar.promocoes, sar.clientes_novos, sar.contatos_novos TO ${appRole}; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA sar TO ${appRole}; `); console.log(` ✓ GRANTs aplicados`); } // ─── Main ───────────────────────────────────────────────────────────────────── async function main(): Promise { console.log('\nSAR — Provisionamento de Cliente'); console.log('──────────────────────────────────'); console.log(`Host: ${host}:${port}`); console.log(`Banco: ${db}`); console.log(`User: ${user}`); if (role) console.log(`Role: ${role} (GRANTs serão aplicados)`); console.log(''); const client = new pg.Client({ host, port, database: db, user, password }); await client.connect(); let needsBaseline = false; try { console.log('[1/3] Schema SAR...'); await runSql(client, 'sar-erp-schema.sql', SQL_SCHEMA); console.log('[2/3] Triggers e tabelas staging...'); await runSql(client, 'sar-triggers.sql', SQL_TRIGGERS); if (role) { console.log('[+] GRANTs...'); await applyGrants(client, role); } // O schema SQL acima já criou as tabelas, então o schema `sar` nunca está // vazio quando o Prisma roda. Sem histórico de migrations o `migrate deploy` // aborta com P3005 — é preciso marcar a migration baseline como aplicada. const { rows } = await client.query( `SELECT to_regclass('sar._prisma_migrations') IS NOT NULL AS existe`, ); needsBaseline = !rows[0]?.existe; } finally { await client.end(); } // _prisma_migrations precisa morar no schema sar, não no public do ERP. const MIGRATE_URL = `${DATABASE_URL}?schema=sar`; const prismaEnv = { ...process.env, DATABASE_URL: MIGRATE_URL }; console.log('[3/3] Migrations Prisma...'); if (needsBaseline) { const baseline = readdirSync(resolve(API_DIR, 'prisma/migrations'), { withFileTypes: true }) .filter((e) => e.isDirectory()) .map((e) => e.name) .sort()[0]; if (!baseline) throw new Error('nenhuma migration encontrada para baseline'); console.log(` Banco pré-existente: marcando "${baseline}" como aplicada (baseline)...`); execSync(`pnpm exec prisma migrate resolve --applied ${baseline}`, { cwd: API_DIR, env: prismaEnv, stdio: 'inherit', }); } execSync('pnpm exec prisma migrate deploy', { cwd: API_DIR, env: prismaEnv, stdio: 'inherit', }); console.log(' ✓ Migrations aplicadas'); console.log(''); console.log('════════════════════════════════════'); console.log('✓ Cliente provisionado com sucesso'); console.log('════════════════════════════════════'); console.log(''); console.log('Próximos passos:'); console.log(` 1. Configure DATABASE_URL=${DATABASE_URL}`); console.log(` no ambiente de produção (Vault ou .env do cliente)`); if (!role) { console.log(' 2. Execute os GRANTs manualmente ou rode novamente com --role '); } console.log(''); } main().catch((e: unknown) => { console.error('\nErro durante provisionamento:', e); process.exit(1); });