You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
100 lines
4.3 KiB
100 lines
4.3 KiB
|
4 weeks ago
|
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();
|
||
|
|
}
|