import pg from 'pg'; import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { TABLAS_FUENTE, huella, leerFuente } from './fuente.mjs'; const accion = process.argv[2] ?? 'ver'; if (!['ver', 'exportar', 'activar', 'desactivar'].includes(accion)) throw Error('Usa ver, exportar, activar o desactivar.'); const c = new pg.Client({ connectionString: process.env.DATABASE_URL }); await c.connect(); const tablas = [ 'estudio_control', 'estudio_documento', 'estudio_vinculo', 'estudio_historial', 'estudio_archivo', 'estudio_trabajo', 'estudio_pista_publicada', 'estudio_alias', 'estudio_migracion' ]; const q = (s) => '"' + s.replaceAll('"', '""') + '"'; try { const { rows: [s] } = await c.query('SELECT current_schema() AS esquema'); const { rows: [control] } = await c.query('SELECT * FROM estudio_control WHERE id=1'); if (accion === 'ver') { console.log(`Modo: ${control.modo}. Esquema: ${s.esquema}.`); const { rows } = await c.query( 'SELECT tipo,count(*)::int AS total FROM estudio_documento GROUP BY tipo ORDER BY tipo' ); console.table(rows); console.log( `Fuente ${control.huella_fuente && huella(await leerFuente(c)) === control.huella_fuente ? 'sin cambios desde la importación' : 'modificada o pendiente de comprobación'}.` ); } else if (accion === 'exportar') { const destino = resolve(process.argv[3] ?? `exports/estudio-${Date.now()}.json`); const datos = {}; await c.query('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'); for (const t of tablas) datos[t] = (await c.query(`SELECT * FROM ${t}`)).rows; await c.query('COMMIT'); mkdirSync(dirname(destino), { recursive: true }); writeFileSync( destino, JSON.stringify( { version: 1, esquema: s.esquema, fecha: new Date().toISOString(), datos }, null, 2 ), { flag: 'wx' } ); console.log(`Exportación conservada en ${destino}`); } else if (accion === 'activar') { if (control.modo === 'activo') throw Error('Este almacén ya está activo.'); const respaldo = process.argv[3]; if (!respaldo) throw Error('Exporta primero y pasa la ruta del respaldo a activar.'); const copia = JSON.parse(readFileSync(respaldo, 'utf8')); if (copia.version !== 1 || copia.esquema !== s.esquema) throw Error('El respaldo no corresponde a este almacén.'); await c.query('BEGIN'); await c.query("SELECT pg_advisory_xact_lock(hashtext('estudio:publicar'))"); for (const t of [...TABLAS_FUENTE, 'estudio_documento']) await c.query(`LOCK TABLE ${q(t)} IN SHARE ROW EXCLUSIVE MODE`); const actuales = (await c.query('SELECT * FROM estudio_documento')).rows; if (huella({ documentos: actuales }) !== huella({ documentos: copia.datos.estudio_documento })) throw Error('El almacén cambió después de exportarlo. Genera un respaldo nuevo.'); if (!control.huella_fuente || huella(await leerFuente(c)) !== control.huella_fuente) throw Error( 'El catálogo original cambió desde la importación. Concilia esos cambios antes de activar.' ); const funcion = `${q(s.esquema)}.estudio_guardar_escritor`; await c.query(`CREATE OR REPLACE FUNCTION ${funcion}() RETURNS trigger LANGUAGE plpgsql SET search_path=${q(s.esquema)},pg_temp AS $guard$ BEGIN IF current_setting('senzapaura.editor',true) IS DISTINCT FROM 'estudio' AND EXISTS(SELECT 1 FROM estudio_control WHERE id=1 AND modo='activo') THEN RAISE EXCEPTION 'La edición del catálogo se realiza desde Estudio'; END IF; IF TG_OP='DELETE' THEN RETURN OLD; END IF; RETURN NEW; END; $guard$`); for (const t of TABLAS_FUENTE) { await c.query(`DROP TRIGGER IF EXISTS estudio_escritor_unico ON ${q(t)}`); await c.query( `CREATE TRIGGER estudio_escritor_unico BEFORE INSERT OR UPDATE OR DELETE ON ${q(t)} FOR EACH ROW EXECUTE FUNCTION ${funcion}()` ); } await c.query("UPDATE estudio_control SET modo='activo' WHERE id=1"); await c.query('COMMIT'); console.log( 'Estudio activado. El catálogo tiene un único escritor; las compras conservan sus tablas y contratos.' ); } else { await c.query("UPDATE estudio_control SET modo='preparacion' WHERE id=1"); console.log( 'Publicación desde Estudio suspendida. Los datos ya publicados se conservan; no se han revertido ni eliminado.' ); } } catch (e) { await c.query('ROLLBACK').catch(() => {}); console.error(e.message); process.exitCode = 1; } finally { await c.end(); }