Files
sar/scripts/provision-client.ts
julian bf6e21ce47 refactor(infra)!: schema sar no banco ERP substitui banco-por-workspace
Revoga o ADR 0006. O isolamento multi-tenant deixa de ser um banco por
workspace e passa a ser o schema `sar` dentro do proprio banco ERP do
cliente, com `id_empresa` discriminando o tenant em todas as tabelas.

Provisionamento:
- `provision-client.ts` substitui `provision-workspace.ts`. Nao cria mais
  banco: aplica schema + views + triggers num banco ERP ja existente.
  Trata o encoding LATIN1 dos bancos ERP via `toLatinSafe()`.
- `sar-erp-schema.sql` vira a fonte unica: absorve as views que estavam em
  `sarweb_views.sql` e passa a conter tambem `sar.chamados` /
  `sar.chamado_mensagens`, que so existiam via migration.

Correcoes necessarias para o provisionamento rodar ponta a ponta:
- `migrate deploy` abortava com P3005, porque o schema SQL roda antes do
  Prisma e o schema `sar` nunca esta vazio. O script agora detecta banco
  pre-existente e faz o baseline sozinho.
- `_prisma_migrations` ia parar no schema `public` do ERP do cliente; a URL
  de migracao agora leva `?schema=sar`.
- `sar.pedidos` nao tinha `end_entrega`, coluna que o schema.prisma ja
  declarava desde a feature de endereco de entrega, e que derrubava
  GET /orders com 500. Migration adicionada.
- `vw_sitpedido` se perdeu junto com `sarweb_views.sql` e e usada pela
  consulta de pedidos ERP. Restaurada.

Remove o seed de dados demo e os fallbacks de DATABASE_URL hardcoded: sem
a variavel configurada a API agora falha explicitamente, em vez de apontar
silenciosamente para um banco de desenvolvimento inexistente.

Verificado ponta a ponta contra um banco ERP real: schema aplicado, 13
tabelas + 20 views, migrations registradas, e segunda execucao idempotente.

BREAKING CHANGE: DATABASE_URL passa a ser obrigatoria. O comando de
provisionamento muda para `pnpm client:provision --host <host> --db <banco>`
(rename efetivado no commit de dependencias).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 18:11:35 +00:00

190 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <host> --db <banco> [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 <host> --db <banco> [--port 5432] [--user postgres] [--password ...] [--role <pg-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<void> {
console.log(` Executando ${label}...`);
await client.query(sql);
console.log(`${label} aplicado`);
}
async function applyGrants(client: pg.Client, appRole: string): Promise<void> {
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<void> {
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 <pg-role>');
}
console.log('');
}
main().catch((e: unknown) => {
console.error('\nErro durante provisionamento:', e);
process.exit(1);
});