Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras
This commit is contained in:
733
database-novo/db.cjs
Normal file
733
database-novo/db.cjs
Normal file
@@ -0,0 +1,733 @@
|
||||
#!/usr/bin/env node
|
||||
// =============================================================================
|
||||
// AgenciaPsi — Database CLI
|
||||
// =============================================================================
|
||||
// Uso: node db.cjs <comando> [opcoes]
|
||||
//
|
||||
// Comandos:
|
||||
// setup Instalação do zero (schema + seeds)
|
||||
// backup Exporta backup com data atual
|
||||
// restore [data] Restaura de um backup (ex: 2026-03-23)
|
||||
// migrate Aplica migrations pendentes
|
||||
// seed [grupo] Roda seeds (all|users|system|test_data)
|
||||
// status Mostra estado atual do banco
|
||||
// diff Compara schema atual vs último backup
|
||||
// reset Reseta o banco e reinstala tudo
|
||||
// verify Verifica integridade dos dados essenciais
|
||||
// help Mostra ajuda
|
||||
// =============================================================================
|
||||
|
||||
const { execSync, spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
const ROOT = __dirname;
|
||||
const CONFIG = JSON.parse(fs.readFileSync(path.join(ROOT, 'db.config.json'), 'utf8'));
|
||||
const CONTAINER = CONFIG.container;
|
||||
const DB = CONFIG.database;
|
||||
const USER = CONFIG.user;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Colors (sem dependências externas)
|
||||
// ---------------------------------------------------------------------------
|
||||
const c = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
cyan: '\x1b[36m',
|
||||
gray: '\x1b[90m'
|
||||
};
|
||||
|
||||
function log(msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
function info(msg) {
|
||||
log(`${c.cyan}ℹ${c.reset} ${msg}`);
|
||||
}
|
||||
function ok(msg) {
|
||||
log(`${c.green}✔${c.reset} ${msg}`);
|
||||
}
|
||||
function warn(msg) {
|
||||
log(`${c.yellow}⚠${c.reset} ${msg}`);
|
||||
}
|
||||
function err(msg) {
|
||||
log(`${c.red}✖${c.reset} ${msg}`);
|
||||
}
|
||||
function title(msg) {
|
||||
log(`\n${c.bold}${c.blue}═══ ${msg} ═══${c.reset}\n`);
|
||||
}
|
||||
function step(msg) {
|
||||
log(`${c.gray} →${c.reset} ${msg}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function dockerRunning() {
|
||||
try {
|
||||
const result = spawnSync('docker', ['inspect', '-f', '{{.State.Running}}', CONTAINER], {
|
||||
encoding: 'utf8',
|
||||
timeout: 10000,
|
||||
stdio: ['pipe', 'pipe', 'pipe']
|
||||
});
|
||||
return result.stdout.trim() === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function psql(sql, opts = {}) {
|
||||
const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} ${opts.tuples ? '-t' : ''} ${opts.quiet ? '-q' : ''} -c "${sql.replace(/"/g, '\\"')}"`;
|
||||
return execSync(cmd, { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' } }).trim();
|
||||
}
|
||||
|
||||
function psqlFile(filePath) {
|
||||
const absPath = path.resolve(filePath);
|
||||
const content = fs.readFileSync(absPath, 'utf8');
|
||||
// Prepend SET client_encoding to ensure UTF-8 inside the session
|
||||
const utf8Content = "SET client_encoding TO 'UTF8';\n" + content;
|
||||
const cmd = `docker exec -i -e PGCLIENTENCODING=UTF8 ${CONTAINER} psql -U ${USER} -d ${DB} -q`;
|
||||
return execSync(cmd, { input: utf8Content, encoding: 'utf8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONIOENCODING: 'utf-8', LANG: 'C.UTF-8' } });
|
||||
}
|
||||
|
||||
function pgDump(args) {
|
||||
const cmd = `docker exec -e PGCLIENTENCODING=UTF8 ${CONTAINER} pg_dump -U ${USER} -d ${DB} ${args}`;
|
||||
return execSync(cmd, { encoding: 'utf8', timeout: 120000, maxBuffer: 50 * 1024 * 1024 });
|
||||
}
|
||||
|
||||
function today() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function fileHash(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function requireDocker() {
|
||||
if (!dockerRunning()) {
|
||||
err(`Container "${CONTAINER}" não está rodando.`);
|
||||
log(`\n Inicie o Supabase primeiro:`);
|
||||
log(` ${c.cyan}npx supabase start${c.reset}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function listBackups() {
|
||||
const dir = path.join(ROOT, 'backups');
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => /^\d{4}-\d{2}-\d{2}$/.test(f))
|
||||
.sort()
|
||||
.reverse();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Migration tracking table
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ensureMigrationTable() {
|
||||
psql(`
|
||||
CREATE TABLE IF NOT EXISTS _db_migrations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
hash TEXT NOT NULL,
|
||||
category TEXT NOT NULL DEFAULT 'migration',
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
function getAppliedMigrations() {
|
||||
ensureMigrationTable();
|
||||
const result = psql('SELECT filename, hash, category, applied_at::text FROM _db_migrations ORDER BY id;', { tuples: true });
|
||||
if (!result) return [];
|
||||
return result
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
const [filename, hash, category, applied_at] = line.split('|').map((s) => s.trim());
|
||||
return { filename, hash, category, applied_at };
|
||||
});
|
||||
}
|
||||
|
||||
function recordMigration(filename, hash, category) {
|
||||
psql(`INSERT INTO _db_migrations (filename, hash, category) VALUES ('${filename}', '${hash}', '${category}') ON CONFLICT (filename) DO UPDATE SET hash = EXCLUDED.hash, applied_at = now();`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const commands = {};
|
||||
|
||||
// ---- SETUP ----
|
||||
commands.setup = function () {
|
||||
title('Setup — Instalação do zero');
|
||||
requireDocker();
|
||||
|
||||
// 1. Schema
|
||||
const schemaFile = path.join(ROOT, CONFIG.schema);
|
||||
if (!fs.existsSync(schemaFile)) {
|
||||
err(`Schema não encontrado: ${schemaFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
info('Aplicando schema...');
|
||||
psqlFile(schemaFile);
|
||||
ok('Schema aplicado');
|
||||
|
||||
// 2. Fixes
|
||||
info('Aplicando fixes...');
|
||||
for (const fix of CONFIG.fixes) {
|
||||
const fixPath = path.join(ROOT, 'fixes', fix);
|
||||
if (fs.existsSync(fixPath)) {
|
||||
step(fix);
|
||||
psqlFile(fixPath);
|
||||
}
|
||||
}
|
||||
ok(`${CONFIG.fixes.length} fixes aplicados`);
|
||||
|
||||
// 3. Seeds
|
||||
commands.seed('all');
|
||||
|
||||
// 4. Migration table
|
||||
ensureMigrationTable();
|
||||
|
||||
// 5. Record seeds as applied
|
||||
const allSeeds = [...CONFIG.seeds.users, ...CONFIG.seeds.system];
|
||||
for (const seed of allSeeds) {
|
||||
const seedPath = path.join(ROOT, 'seeds', seed);
|
||||
if (fs.existsSync(seedPath)) {
|
||||
recordMigration(seed, fileHash(seedPath), 'seed');
|
||||
}
|
||||
}
|
||||
for (const fix of CONFIG.fixes) {
|
||||
const fixPath = path.join(ROOT, 'fixes', fix);
|
||||
if (fs.existsSync(fixPath)) {
|
||||
recordMigration(fix, fileHash(fixPath), 'fix');
|
||||
}
|
||||
}
|
||||
|
||||
ok('Setup completo!');
|
||||
log('');
|
||||
|
||||
// 6. Auto-backup
|
||||
info('Criando backup pós-setup...');
|
||||
commands.backup();
|
||||
|
||||
// 7. Verify
|
||||
commands.verify();
|
||||
};
|
||||
|
||||
// ---- BACKUP ----
|
||||
commands.backup = function () {
|
||||
title('Backup');
|
||||
requireDocker();
|
||||
|
||||
const date = today();
|
||||
const dir = path.join(ROOT, 'backups', date);
|
||||
ensureDir(dir);
|
||||
|
||||
const infraSchemas = ['storage', 'realtime', '_realtime', 'supabase_functions', 'extensions', 'graphql', 'graphql_public', 'pgsodium', 'vault', 'net', '_analytics'];
|
||||
const excludeFlags = infraSchemas.map((s) => `--exclude-schema=${s}`).join(' ');
|
||||
|
||||
step('Exportando schema...');
|
||||
const schema = pgDump('--schema-only --no-owner --no-privileges');
|
||||
fs.writeFileSync(path.join(dir, 'schema.sql'), schema);
|
||||
|
||||
step('Exportando dados...');
|
||||
const data = pgDump(`--data-only --no-owner --no-privileges ${excludeFlags}`);
|
||||
fs.writeFileSync(path.join(dir, 'data.sql'), data);
|
||||
|
||||
step('Exportando dump completo...');
|
||||
const full = pgDump('--no-owner --no-privileges');
|
||||
fs.writeFileSync(path.join(dir, 'full_dump.sql'), full);
|
||||
|
||||
const sizes = ['schema.sql', 'data.sql', 'full_dump.sql'].map((f) => {
|
||||
const stat = fs.statSync(path.join(dir, f));
|
||||
return `${f}: ${(stat.size / 1024).toFixed(0)}KB`;
|
||||
});
|
||||
|
||||
ok(`Backup salvo em backups/${date}/`);
|
||||
sizes.forEach((s) => step(s));
|
||||
|
||||
// Cleanup old backups
|
||||
cleanupBackups();
|
||||
};
|
||||
|
||||
function cleanupBackups() {
|
||||
const backups = listBackups();
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - CONFIG.backupRetentionDays);
|
||||
const cutoffStr = cutoff.toISOString().slice(0, 10);
|
||||
|
||||
let removed = 0;
|
||||
for (const b of backups) {
|
||||
if (b < cutoffStr) {
|
||||
const dir = path.join(ROOT, 'backups', b);
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
if (removed > 0) {
|
||||
info(`${removed} backup(s) antigo(s) removido(s) (>${CONFIG.backupRetentionDays} dias)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- RESTORE ----
|
||||
commands.restore = function (dateArg) {
|
||||
title('Restore');
|
||||
requireDocker();
|
||||
|
||||
const backups = listBackups();
|
||||
if (backups.length === 0) {
|
||||
err('Nenhum backup encontrado.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const date = dateArg || backups[0];
|
||||
const dir = path.join(ROOT, 'backups', date);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
err(`Backup não encontrado: ${date}`);
|
||||
log(`\n Backups disponíveis:`);
|
||||
backups.forEach((b) => log(` ${c.cyan}${b}${c.reset}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const fullDump = path.join(dir, 'full_dump.sql');
|
||||
const schemaFile = path.join(dir, 'schema.sql');
|
||||
const dataFile = path.join(dir, 'data.sql');
|
||||
|
||||
// Safety backup before restore
|
||||
info('Criando backup de segurança antes do restore...');
|
||||
try {
|
||||
commands.backup();
|
||||
} catch {
|
||||
warn('Não foi possível criar backup de segurança (banco pode estar vazio)');
|
||||
}
|
||||
|
||||
if (fs.existsSync(fullDump)) {
|
||||
info(`Restaurando de backups/${date}/full_dump.sql ...`);
|
||||
|
||||
// Drop and recreate public schema
|
||||
step('Limpando schema public...');
|
||||
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||
|
||||
step('Aplicando full dump...');
|
||||
psqlFile(fullDump);
|
||||
} else if (fs.existsSync(schemaFile) && fs.existsSync(dataFile)) {
|
||||
info(`Restaurando de backups/${date}/ (schema + data)...`);
|
||||
|
||||
step('Limpando schema public...');
|
||||
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||
|
||||
step('Aplicando schema...');
|
||||
psqlFile(schemaFile);
|
||||
|
||||
step('Aplicando dados...');
|
||||
psqlFile(dataFile);
|
||||
} else {
|
||||
err(`Backup incompleto em ${date}/`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
ok(`Banco restaurado de backups/${date}/`);
|
||||
|
||||
// Verify
|
||||
commands.verify();
|
||||
};
|
||||
|
||||
// ---- MIGRATE ----
|
||||
commands.migrate = function () {
|
||||
title('Migrate');
|
||||
requireDocker();
|
||||
ensureMigrationTable();
|
||||
|
||||
const migrationsDir = path.join(ROOT, 'migrations');
|
||||
if (!fs.existsSync(migrationsDir)) {
|
||||
info('Nenhuma pasta migrations/ encontrada.');
|
||||
return;
|
||||
}
|
||||
|
||||
const files = fs
|
||||
.readdirSync(migrationsDir)
|
||||
.filter((f) => f.endsWith('.sql'))
|
||||
.sort();
|
||||
|
||||
if (files.length === 0) {
|
||||
info('Nenhuma migration encontrada.');
|
||||
return;
|
||||
}
|
||||
|
||||
const applied = getAppliedMigrations().map((m) => m.filename);
|
||||
const pending = files.filter((f) => !applied.includes(f));
|
||||
|
||||
if (pending.length === 0) {
|
||||
ok('Todas as migrations já foram aplicadas.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-backup before migrating
|
||||
info('Criando backup antes de migrar...');
|
||||
commands.backup();
|
||||
|
||||
info(`${pending.length} migration(s) pendente(s):`);
|
||||
for (const file of pending) {
|
||||
step(`Aplicando ${file}...`);
|
||||
const filePath = path.join(migrationsDir, file);
|
||||
try {
|
||||
psqlFile(filePath);
|
||||
recordMigration(file, fileHash(filePath), 'migration');
|
||||
ok(` ${file}`);
|
||||
} catch (e) {
|
||||
err(` FALHA em ${file}: ${e.message}`);
|
||||
err('Migration abortada. Banco pode estar em estado parcial.');
|
||||
err('Use "node db.cjs restore" para voltar ao backup.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
ok(`${pending.length} migration(s) aplicada(s)`);
|
||||
};
|
||||
|
||||
// ---- SEED ----
|
||||
commands.seed = function (group) {
|
||||
const validGroups = ['all', 'users', 'system', 'test_data'];
|
||||
if (!group) group = 'all';
|
||||
|
||||
if (!validGroups.includes(group)) {
|
||||
err(`Grupo inválido: ${group}`);
|
||||
log(` Grupos válidos: ${validGroups.join(', ')}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
title(`Seeds — ${group}`);
|
||||
requireDocker();
|
||||
|
||||
const groups = group === 'all' ? ['users', 'system'] : [group];
|
||||
let total = 0;
|
||||
|
||||
for (const g of groups) {
|
||||
const seeds = CONFIG.seeds[g];
|
||||
if (!seeds || seeds.length === 0) continue;
|
||||
|
||||
info(`Grupo: ${g}`);
|
||||
for (const seed of seeds) {
|
||||
const seedPath = path.join(ROOT, 'seeds', seed);
|
||||
if (!fs.existsSync(seedPath)) {
|
||||
warn(` Arquivo não encontrado: ${seed}`);
|
||||
continue;
|
||||
}
|
||||
step(seed);
|
||||
try {
|
||||
psqlFile(seedPath);
|
||||
total++;
|
||||
} catch (e) {
|
||||
err(` FALHA em ${seed}: ${e.stderr || e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ok(`${total} seed(s) aplicado(s)`);
|
||||
};
|
||||
|
||||
// ---- STATUS ----
|
||||
commands.status = function () {
|
||||
title('Status');
|
||||
requireDocker();
|
||||
|
||||
// Docker
|
||||
ok(`Container: ${CONTAINER} (rodando)`);
|
||||
|
||||
// Backups
|
||||
const backups = listBackups();
|
||||
if (backups.length > 0) {
|
||||
ok(`Último backup: ${backups[0]}`);
|
||||
info(`Total de backups: ${backups.length}`);
|
||||
} else {
|
||||
warn('Nenhum backup encontrado');
|
||||
}
|
||||
|
||||
// Migrations
|
||||
try {
|
||||
const applied = getAppliedMigrations();
|
||||
if (applied.length > 0) {
|
||||
info(`Migrations aplicadas: ${applied.length}`);
|
||||
applied.slice(-5).forEach((m) => {
|
||||
step(`${m.filename} ${c.gray}(${m.category}, ${m.applied_at})${c.reset}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Pending
|
||||
const migrationsDir = path.join(ROOT, 'migrations');
|
||||
if (fs.existsSync(migrationsDir)) {
|
||||
const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.sql'));
|
||||
const pending = files.filter((f) => !applied.map((m) => m.filename).includes(f));
|
||||
if (pending.length > 0) {
|
||||
warn(`${pending.length} migration(s) pendente(s):`);
|
||||
pending.forEach((f) => step(`${c.yellow}${f}${c.reset}`));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
info('Tabela _db_migrations não existe (rode setup primeiro)');
|
||||
}
|
||||
|
||||
// DB counts
|
||||
log('');
|
||||
info('Dados no banco:');
|
||||
const counts = [
|
||||
['auth.users', 'SELECT count(*) FROM auth.users'],
|
||||
['profiles', 'SELECT count(*) FROM profiles'],
|
||||
['tenants', 'SELECT count(*) FROM tenants'],
|
||||
['plans', 'SELECT count(*) FROM plans'],
|
||||
['features', 'SELECT count(*) FROM features'],
|
||||
['plan_features', 'SELECT count(*) FROM plan_features'],
|
||||
['subscriptions', 'SELECT count(*) FROM subscriptions'],
|
||||
['email_templates_global', 'SELECT count(*) FROM email_templates_global'],
|
||||
['notification_templates', 'SELECT count(*) FROM notification_templates']
|
||||
];
|
||||
|
||||
for (const [label, sql] of counts) {
|
||||
try {
|
||||
const count = psql(sql, { tuples: true }).trim();
|
||||
const color = parseInt(count) > 0 ? c.green : c.red;
|
||||
step(`${label}: ${color}${count}${c.reset}`);
|
||||
} catch {
|
||||
step(`${label}: ${c.gray}(tabela não existe)${c.reset}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ---- DIFF ----
|
||||
commands.diff = function () {
|
||||
title('Diff — Schema');
|
||||
requireDocker();
|
||||
|
||||
const backups = listBackups();
|
||||
if (backups.length === 0) {
|
||||
err('Nenhum backup para comparar. Rode "node db.cjs backup" primeiro.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const lastBackup = backups[0];
|
||||
const lastSchemaPath = path.join(ROOT, 'backups', lastBackup, 'schema.sql');
|
||||
if (!fs.existsSync(lastSchemaPath)) {
|
||||
err(`Schema não encontrado no backup ${lastBackup}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
info('Exportando schema atual...');
|
||||
const currentSchema = pgDump('--schema-only --no-owner --no-privileges');
|
||||
|
||||
const lastSchema = fs.readFileSync(lastSchemaPath, 'utf8');
|
||||
|
||||
// Extract table definitions for comparison
|
||||
const extractTables = (sql) => {
|
||||
const tables = {};
|
||||
const regex = /CREATE TABLE (?:IF NOT EXISTS )?(\S+)\s*\(([\s\S]*?)\);/g;
|
||||
let match;
|
||||
while ((match = regex.exec(sql)) !== null) {
|
||||
tables[match[1]] = match[2].trim();
|
||||
}
|
||||
return tables;
|
||||
};
|
||||
|
||||
const currentTables = extractTables(currentSchema);
|
||||
const lastTables = extractTables(lastSchema);
|
||||
|
||||
const allTables = new Set([...Object.keys(currentTables), ...Object.keys(lastTables)]);
|
||||
|
||||
let added = 0,
|
||||
removed = 0,
|
||||
changed = 0,
|
||||
unchanged = 0;
|
||||
|
||||
for (const table of [...allTables].sort()) {
|
||||
if (!lastTables[table]) {
|
||||
log(` ${c.green}+ ${table}${c.reset} (nova)`);
|
||||
added++;
|
||||
} else if (!currentTables[table]) {
|
||||
log(` ${c.red}- ${table}${c.reset} (removida)`);
|
||||
removed++;
|
||||
} else if (currentTables[table] !== lastTables[table]) {
|
||||
log(` ${c.yellow}~ ${table}${c.reset} (alterada)`);
|
||||
changed++;
|
||||
} else {
|
||||
unchanged++;
|
||||
}
|
||||
}
|
||||
|
||||
log('');
|
||||
ok(`Comparado com backup de ${lastBackup}:`);
|
||||
step(`${added} nova(s), ${changed} alterada(s), ${removed} removida(s), ${unchanged} sem mudança`);
|
||||
};
|
||||
|
||||
// ---- RESET ----
|
||||
commands.reset = function () {
|
||||
title('Reset — CUIDADO');
|
||||
requireDocker();
|
||||
|
||||
// Safety backup
|
||||
info('Criando backup antes do reset...');
|
||||
try {
|
||||
commands.backup();
|
||||
} catch {
|
||||
warn('Não foi possível criar backup');
|
||||
}
|
||||
|
||||
warn('Resetando schema public...');
|
||||
psql('DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public; GRANT ALL ON SCHEMA public TO postgres; GRANT ALL ON SCHEMA public TO public;');
|
||||
ok('Schema public resetado');
|
||||
|
||||
// Re-run setup
|
||||
commands.setup();
|
||||
};
|
||||
|
||||
// ---- VERIFY ----
|
||||
commands.verify = function () {
|
||||
title('Verificação de integridade');
|
||||
requireDocker();
|
||||
|
||||
const checks = [
|
||||
{ name: 'auth.users', sql: 'SELECT count(*) FROM auth.users', min: 1 },
|
||||
{ name: 'profiles', sql: 'SELECT count(*) FROM profiles', min: 1 },
|
||||
{ name: 'tenants', sql: 'SELECT count(*) FROM tenants', min: 1 },
|
||||
{ name: 'plans', sql: 'SELECT count(*) FROM plans', min: 7 },
|
||||
{ name: 'features', sql: 'SELECT count(*) FROM features', min: 20 },
|
||||
{ name: 'plan_features', sql: 'SELECT count(*) FROM plan_features', min: 50 },
|
||||
{ name: 'subscriptions', sql: 'SELECT count(*) FROM subscriptions', min: 1 },
|
||||
{ name: 'email_templates', sql: 'SELECT count(*) FROM email_templates_global', min: 10 }
|
||||
];
|
||||
|
||||
let pass = 0,
|
||||
fail = 0;
|
||||
|
||||
for (const check of checks) {
|
||||
try {
|
||||
const count = parseInt(psql(check.sql, { tuples: true }).trim());
|
||||
if (count >= check.min) {
|
||||
ok(`${check.name}: ${count} (mín: ${check.min})`);
|
||||
pass++;
|
||||
} else {
|
||||
err(`${check.name}: ${count} (esperado ≥ ${check.min})`);
|
||||
fail++;
|
||||
}
|
||||
} catch {
|
||||
err(`${check.name}: tabela não existe`);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
// Check entitlements view
|
||||
try {
|
||||
const ent = psql('SELECT count(*) FROM v_tenant_entitlements;', { tuples: true }).trim();
|
||||
ok(`v_tenant_entitlements: ${ent} registros`);
|
||||
pass++;
|
||||
} catch {
|
||||
err('v_tenant_entitlements: view não existe');
|
||||
fail++;
|
||||
}
|
||||
|
||||
log('');
|
||||
if (fail === 0) {
|
||||
ok(`${c.bold}Todos os ${pass} checks passaram!${c.reset}`);
|
||||
} else {
|
||||
err(`${fail} check(s) falharam, ${pass} passaram`);
|
||||
}
|
||||
};
|
||||
|
||||
// ---- HELP ----
|
||||
commands.help = function () {
|
||||
log(`
|
||||
${c.bold}AgenciaPsi — Database CLI${c.reset}
|
||||
|
||||
${c.cyan}Uso:${c.reset} node db.cjs <comando> [opções]
|
||||
|
||||
${c.cyan}Comandos:${c.reset}
|
||||
|
||||
${c.bold}setup${c.reset} Instalação do zero (schema + fixes + seeds)
|
||||
Cria backup automático após concluir
|
||||
|
||||
${c.bold}backup${c.reset} Exporta banco para backups/YYYY-MM-DD/
|
||||
Gera: schema.sql, data.sql, full_dump.sql
|
||||
|
||||
${c.bold}restore [data]${c.reset} Restaura de um backup
|
||||
Sem data = último backup disponível
|
||||
Ex: node db.cjs restore 2026-03-23
|
||||
|
||||
${c.bold}migrate${c.reset} Aplica migrations pendentes (pasta migrations/)
|
||||
Backup automático antes de aplicar
|
||||
|
||||
${c.bold}seed [grupo]${c.reset} Roda seeds (all, users, system, test_data)
|
||||
Ex: node db.cjs seed system
|
||||
|
||||
${c.bold}status${c.reset} Mostra estado do banco, backups, migrations
|
||||
|
||||
${c.bold}diff${c.reset} Compara schema atual vs último backup
|
||||
|
||||
${c.bold}reset${c.reset} Reseta o banco e reinstala tudo do zero
|
||||
${c.yellow}⚠ Cria backup antes de resetar${c.reset}
|
||||
|
||||
${c.bold}verify${c.reset} Verifica integridade dos dados essenciais
|
||||
|
||||
${c.bold}help${c.reset} Mostra esta ajuda
|
||||
|
||||
${c.cyan}Exemplos:${c.reset}
|
||||
|
||||
${c.gray}# Primeira vez — instala tudo${c.reset}
|
||||
node db.cjs setup
|
||||
|
||||
${c.gray}# Backup diário${c.reset}
|
||||
node db.cjs backup
|
||||
|
||||
${c.gray}# Perdi o banco — restaurar${c.reset}
|
||||
node db.cjs restore
|
||||
|
||||
${c.gray}# Nova migration${c.reset}
|
||||
node db.cjs migrate
|
||||
|
||||
${c.gray}# Ver o que tem no banco${c.reset}
|
||||
node db.cjs status
|
||||
`);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const [, , cmd, ...args] = process.argv;
|
||||
|
||||
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
|
||||
commands.help();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!commands[cmd]) {
|
||||
err(`Comando desconhecido: ${cmd}`);
|
||||
log(` Use ${c.cyan}node db.cjs help${c.reset} para ver os comandos disponíveis.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
commands[cmd](...args);
|
||||
} catch (e) {
|
||||
err(`Erro: ${e.message}`);
|
||||
if (process.env.DEBUG) console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user