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.
644 lines
19 KiB
644 lines
19 KiB
/**
|
|
* Vuelca el contenido de `src/content/**` a PostgreSQL.
|
|
*
|
|
* npm run db:volcar # vuelca y comprueba
|
|
* npm run db:volcar -- --seco # solo dice qué haría
|
|
*
|
|
* ## Por qué reutiliza los analizadores del sitio
|
|
*
|
|
* En vez de volver a leer el frontmatter a mi manera, este script pasa cada
|
|
* archivo por `src/lib/content/schema.ts`, que es exactamente lo que hace la
|
|
* web hoy. Así lo volcado no puede diferir de lo que se ve: si un valor por
|
|
* defecto o una normalización cambian, cambian para los dos. Escribir un
|
|
* segundo analizador «parecido» es la forma más fácil de que la base y la web
|
|
* cuenten cosas distintas y nadie lo note.
|
|
*
|
|
* ## Es idempotente
|
|
*
|
|
* Se puede lanzar las veces que haga falta. Borra lo volcado antes de volver a
|
|
* escribirlo, y no toca nada de lo que generan las personas —cuentas, pedidos,
|
|
* compras—: esas tablas ni se miran.
|
|
*
|
|
* ## Todo en una transacción
|
|
*
|
|
* Si algo falla a mitad, no queda medio catálogo dentro. Y al final comprueba
|
|
* que lo escrito coincide con lo leído, porque «no dio error» no es lo mismo
|
|
* que «está bien».
|
|
*/
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
// Con nombre y no por defecto: js-yaml 4 es CommonJS y su interoperabilidad
|
|
// con modulos no expone un `default`.
|
|
import { load as leerYaml } from 'js-yaml';
|
|
import pg from 'pg';
|
|
|
|
import {
|
|
parsearAlbum,
|
|
parsearCancion,
|
|
parsearCapitulo,
|
|
parsearEntrada,
|
|
parsearEstilo,
|
|
parsearLegal,
|
|
parsearPagina
|
|
} from '../../src/lib/content/schema.ts';
|
|
import { extraerLineas } from '../../src/lib/content/extracto.ts';
|
|
|
|
const SECO = process.argv.includes('--seco');
|
|
const RAIZ = 'src/content';
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Leer los archivos
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
/** Separa el frontmatter del cuerpo. */
|
|
function partir(texto) {
|
|
const lineas = texto.split(/\r?\n/);
|
|
if (lineas[0]?.trim() !== '---') return { datos: {}, cuerpo: texto.trim() };
|
|
|
|
const cierre = lineas.indexOf('---', 1);
|
|
if (cierre === -1) return { datos: {}, cuerpo: texto.trim() };
|
|
|
|
return {
|
|
datos: leerYaml(lineas.slice(1, cierre).join('\n')) ?? {},
|
|
cuerpo: lineas
|
|
.slice(cierre + 1)
|
|
.join('\n')
|
|
.trim()
|
|
};
|
|
}
|
|
|
|
function leerCarpeta(carpeta, parser) {
|
|
const dir = path.join(RAIZ, carpeta);
|
|
if (!fs.existsSync(dir)) return [];
|
|
|
|
return fs
|
|
.readdirSync(dir)
|
|
.filter((n) => n.endsWith('.md'))
|
|
.sort()
|
|
.map((nombre) => {
|
|
const ruta = path.join(dir, nombre);
|
|
const { datos, cuerpo } = partir(fs.readFileSync(ruta, 'utf8'));
|
|
const slug = nombre.replace(/\.md$/, '');
|
|
return { slug, cuerpo, ruta, datos: parser(datos, slug, ruta) };
|
|
});
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Escribir
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
const nuevoId = () => crypto.randomUUID();
|
|
|
|
/** Normaliza una etiqueta a slug, como hace el sitio. */
|
|
const slugificar = (nombre) =>
|
|
nombre
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/[̀-ͯ]/g, '')
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '');
|
|
|
|
async function volcar(c, contenido, informe) {
|
|
const { estilos, albumes, canciones, capitulos, entradas, legales, paginas } = contenido;
|
|
|
|
/* --- Se borra lo volcado antes de reescribirlo -----------------------
|
|
*
|
|
* En orden inverso a las dependencias. Las tablas de cuentas, pedidos y
|
|
* compras no aparecen aquí ni por asomo: esas las llenan las personas.
|
|
*/
|
|
const aVaciar = [
|
|
'capitulo_acepcion',
|
|
'genero_tema',
|
|
'cancion_genero',
|
|
'genero_seccion',
|
|
'genero_alias',
|
|
'genero_pais',
|
|
'genero_origen',
|
|
'genero_metro',
|
|
'genero_compas',
|
|
'genero_instrumento',
|
|
'genero_persona',
|
|
'obra_referencia_persona',
|
|
'obra_referencia',
|
|
'capitulo',
|
|
'genero',
|
|
'entrada_cancion',
|
|
'entrada_etiqueta',
|
|
'entrada',
|
|
'version_legal',
|
|
'documento_legal',
|
|
'pagina',
|
|
'autoria',
|
|
'registro_obra',
|
|
'cancion_persona',
|
|
'cancion_enlace',
|
|
'cancion_etiqueta',
|
|
'traduccion',
|
|
'video',
|
|
'cancion',
|
|
'album_enlace',
|
|
'credito_album',
|
|
'album',
|
|
'etiqueta',
|
|
'estilo',
|
|
'persona'
|
|
];
|
|
for (const tabla of aVaciar) await c.query(`DELETE FROM "${tabla}"`);
|
|
|
|
/* --- Personas ---------------------------------------------------------
|
|
*
|
|
* Salen de los créditos, que hoy son cadenas sueltas repetidas en cada
|
|
* archivo. Aquí se unifican por nombre: es justo lo que la tabla existe
|
|
* para arreglar.
|
|
*/
|
|
const personas = new Map();
|
|
const persona = async (nombre) => {
|
|
const limpio = nombre.trim();
|
|
if (personas.has(limpio)) return personas.get(limpio);
|
|
const id = nuevoId();
|
|
await c.query('INSERT INTO persona (id, slug, nombre, estado) VALUES ($1, $2, $3, $4)', [
|
|
id,
|
|
slugificar(limpio),
|
|
limpio,
|
|
'publicado'
|
|
]);
|
|
personas.set(limpio, id);
|
|
return id;
|
|
};
|
|
|
|
/* --- Etiquetas ------------------------------------------------------- */
|
|
const etiquetas = new Map();
|
|
const etiqueta = async (nombre) => {
|
|
const slug = slugificar(nombre);
|
|
if (etiquetas.has(slug)) return etiquetas.get(slug);
|
|
const id = nuevoId();
|
|
await c.query('INSERT INTO etiqueta (id, slug, nombre) VALUES ($1, $2, $3)', [
|
|
id,
|
|
slug,
|
|
nombre.trim()
|
|
]);
|
|
etiquetas.set(slug, id);
|
|
return id;
|
|
};
|
|
|
|
/* --- Estilos ---------------------------------------------------------- */
|
|
const idEstilo = new Map();
|
|
for (const { datos } of estilos) {
|
|
const id = nuevoId();
|
|
await c.query(
|
|
`INSERT INTO estilo (id, slug, nombre, resumen, orden, principal, estado)
|
|
VALUES ($1, $2, $3, $4, $5, $6, 'publicado')`,
|
|
[id, datos.slug, datos.nombre, datos.resumen, datos.orden, datos.principal]
|
|
);
|
|
idEstilo.set(datos.slug, id);
|
|
}
|
|
informe.estilo = estilos.length;
|
|
|
|
/* --- Álbumes ----------------------------------------------------------- */
|
|
const idAlbum = new Map();
|
|
for (const { datos } of albumes) {
|
|
const id = nuevoId();
|
|
await c.query(
|
|
`INSERT INTO album (id, slug, titulo, estilo_id, tipo, fecha, sello, resumen,
|
|
destacado, numero_catalogo, formatos, estado)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'publicado')`,
|
|
[
|
|
id,
|
|
datos.slug,
|
|
datos.titulo,
|
|
idEstilo.get(datos.estilo),
|
|
datos.tipo,
|
|
datos.fecha,
|
|
datos.sello ?? null,
|
|
datos.resumen,
|
|
datos.destacado,
|
|
datos.edicion?.numeroCatalogo ?? null,
|
|
datos.edicion?.formatos?.length ? datos.edicion.formatos : null
|
|
]
|
|
);
|
|
idAlbum.set(datos.slug, id);
|
|
|
|
for (const [plataforma, url] of Object.entries(datos.enlaces ?? {})) {
|
|
await c.query('INSERT INTO album_enlace (album_id, plataforma, url) VALUES ($1,$2,$3)', [
|
|
id,
|
|
plataforma,
|
|
url
|
|
]);
|
|
}
|
|
for (const [i, credito] of (datos.edicion?.creditos ?? []).entries()) {
|
|
await c.query(
|
|
`INSERT INTO credito_album (id, album_id, persona_id, nombre, rol, orden)
|
|
VALUES ($1,$2,$3,$4,$5,$6)`,
|
|
[nuevoId(), id, await persona(credito.nombre), credito.nombre, credito.rol, i]
|
|
);
|
|
}
|
|
}
|
|
informe.album = albumes.length;
|
|
|
|
/* --- Canciones, y sus versiones como filas hermanas -------------------- */
|
|
let versiones = 0;
|
|
for (const { datos, cuerpo } of canciones) {
|
|
const id = nuevoId();
|
|
await c.query(
|
|
`INSERT INTO cancion (id, slug, titulo, estilo_id, album_id, numero, duracion_s, ano,
|
|
letra, idioma, inspiracion, instrumental, destacada, ala_venta, estado)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,'publicado')`,
|
|
[
|
|
id,
|
|
datos.slug,
|
|
datos.titulo,
|
|
idEstilo.get(datos.estilo),
|
|
datos.album ? (idAlbum.get(datos.album) ?? null) : null,
|
|
datos.numero,
|
|
datos.duracion ?? null,
|
|
datos.ano ?? null,
|
|
cuerpo || null,
|
|
datos.idioma,
|
|
datos.inspiracion?.length ? datos.inspiracion.join('\n\n') : null,
|
|
datos.instrumental,
|
|
datos.destacada,
|
|
datos.alaVenta
|
|
]
|
|
);
|
|
|
|
/* Créditos: letra, música e intérpretes. */
|
|
for (const [rol, nombres] of [
|
|
['letra', datos.letra],
|
|
['musica', datos.musica],
|
|
['interprete', datos.interpretes]
|
|
]) {
|
|
for (const [i, nombre] of (nombres ?? []).entries()) {
|
|
await c.query(
|
|
`INSERT INTO cancion_persona (cancion_id, persona_id, rol, orden)
|
|
VALUES ($1,$2,$3,$4) ON CONFLICT DO NOTHING`,
|
|
[id, await persona(nombre), rol, i]
|
|
);
|
|
}
|
|
}
|
|
|
|
for (const nombre of datos.etiquetas ?? []) {
|
|
await c.query(
|
|
`INSERT INTO cancion_etiqueta (cancion_id, etiqueta_id) VALUES ($1,$2)
|
|
ON CONFLICT DO NOTHING`,
|
|
[id, await etiqueta(nombre)]
|
|
);
|
|
}
|
|
|
|
for (const [plataforma, url] of Object.entries(datos.enlaces ?? {})) {
|
|
await c.query('INSERT INTO cancion_enlace (cancion_id, plataforma, url) VALUES ($1,$2,$3)', [
|
|
id,
|
|
plataforma,
|
|
url
|
|
]);
|
|
}
|
|
|
|
for (const [i, v] of (datos.videos ?? []).entries()) {
|
|
await c.query(
|
|
'INSERT INTO video (id, cancion_id, youtube_id, titulo, orden) VALUES ($1,$2,$3,$4,$5)',
|
|
[nuevoId(), id, v.youtube, v.titulo, i]
|
|
);
|
|
}
|
|
|
|
if (datos.traduccion) {
|
|
await c.query('INSERT INTO traduccion (cancion_id, idioma, texto) VALUES ($1,$2,$3)', [
|
|
id,
|
|
'es',
|
|
datos.traduccion
|
|
]);
|
|
}
|
|
|
|
const r = datos.registro ?? {};
|
|
if (r.iswc || r.isrc || r.entidad || r.numero || r.editorial) {
|
|
await c.query(
|
|
`INSERT INTO registro_obra (cancion_id, iswc, isrc, entidad, numero, editorial)
|
|
VALUES ($1,$2,$3,$4,$5,$6)`,
|
|
[
|
|
id,
|
|
r.iswc ?? null,
|
|
r.isrc ?? null,
|
|
r.entidad ?? null,
|
|
r.numero ?? null,
|
|
r.editorial ?? null
|
|
]
|
|
);
|
|
}
|
|
for (const parte of r.reparto ?? []) {
|
|
await c.query(
|
|
`INSERT INTO autoria (cancion_id, persona_id, rol, porcentaje) VALUES ($1,$2,$3,$4)
|
|
ON CONFLICT DO NOTHING`,
|
|
[id, await persona(parte.nombre), parte.rol, parte.porcentaje]
|
|
);
|
|
}
|
|
|
|
/*
|
|
* Las versiones pasan a ser filas hermanas, con los campos COPIADOS de la
|
|
* principal. No se heredan al leer: un nulo heredado nunca se sabe si es
|
|
* «no lo sé» o «lo mismo de arriba».
|
|
*/
|
|
for (const [i, v] of (datos.versiones ?? []).entries()) {
|
|
await c.query(
|
|
`INSERT INTO cancion (id, slug, titulo, estilo_id, version_de_id, version_nombre,
|
|
version_orden, duracion_s, ano, letra, idioma, instrumental, estado)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,'publicado')`,
|
|
[
|
|
nuevoId(),
|
|
`${datos.slug}-${v.id}`,
|
|
datos.titulo,
|
|
idEstilo.get(datos.estilo),
|
|
id,
|
|
v.nombre,
|
|
i,
|
|
v.duracion ?? null,
|
|
datos.ano ?? null,
|
|
cuerpo || null,
|
|
datos.idioma,
|
|
datos.instrumental
|
|
]
|
|
);
|
|
versiones++;
|
|
}
|
|
}
|
|
informe.cancion = canciones.length;
|
|
informe.version = versiones;
|
|
|
|
/* --- Taller: géneros y capítulos ---------------------------------------
|
|
*
|
|
* Cada ficha del taller da un `genero` y un `capitulo` que lo describe. Los
|
|
* campos comparables se quedan de momento en las notas en prosa: pasarlos a
|
|
* `metro` y `compas` exige decidir a mano a qué fila corresponde cada frase,
|
|
* y eso no lo puede adivinar un volcado.
|
|
*/
|
|
const idGenero = new Map();
|
|
for (const { datos } of capitulos.filter((c) => c.datos.tipo === 'ficha')) {
|
|
const id = nuevoId();
|
|
const f = datos.ficha;
|
|
await c.query(
|
|
`INSERT INTO genero (id, slug, nombre, resumen, origen_lugar, tempo, rima,
|
|
metrica_nota, compas_nota, rasgos, estado, origen)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,'publicado','manual')`,
|
|
[
|
|
id,
|
|
datos.slug,
|
|
datos.titulo,
|
|
datos.resumen,
|
|
f.origen ?? null,
|
|
f.tempo ?? null,
|
|
f.rima ?? null,
|
|
f.metrica ?? null,
|
|
f.compas ?? null,
|
|
f.rasgos?.length ? f.rasgos : null
|
|
]
|
|
);
|
|
idGenero.set(datos.slug, id);
|
|
|
|
if (f.estructura) {
|
|
await c.query(
|
|
`INSERT INTO genero_seccion (id, genero_id, clave, titulo, cuerpo, orden)
|
|
VALUES ($1,$2,'estructura','Estructura de las canciones',$3,0)`,
|
|
[nuevoId(), id, f.estructura]
|
|
);
|
|
}
|
|
for (const [i, tema] of (f.temas ?? []).entries()) {
|
|
await c.query(
|
|
`INSERT INTO genero_tema (genero_id, etiqueta_id, orden) VALUES ($1,$2,$3)
|
|
ON CONFLICT DO NOTHING`,
|
|
[id, await etiqueta(tema), i]
|
|
);
|
|
}
|
|
}
|
|
informe.genero = idGenero.size;
|
|
void personas;
|
|
|
|
for (const { datos, cuerpo } of capitulos) {
|
|
await c.query(
|
|
`INSERT INTO capitulo (id, slug, titulo, tipo, genero_id, orden, resumen, cuerpo, estado)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'publicado')`,
|
|
[
|
|
nuevoId(),
|
|
datos.slug,
|
|
datos.titulo,
|
|
datos.tipo,
|
|
idGenero.get(datos.slug) ?? null,
|
|
datos.orden,
|
|
datos.resumen,
|
|
cuerpo
|
|
]
|
|
);
|
|
}
|
|
informe.capitulo = capitulos.length;
|
|
|
|
/* --- Blog --------------------------------------------------------------- */
|
|
for (const { datos, cuerpo } of entradas) {
|
|
const id = nuevoId();
|
|
await c.query(
|
|
`INSERT INTO entrada (id, slug, titulo, fecha, resumen, cuerpo, estado)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
|
[
|
|
id,
|
|
datos.slug,
|
|
datos.titulo,
|
|
datos.fecha,
|
|
datos.resumen,
|
|
cuerpo,
|
|
datos.borrador ? 'borrador' : 'publicado'
|
|
]
|
|
);
|
|
for (const nombre of datos.etiquetas ?? []) {
|
|
await c.query(
|
|
`INSERT INTO entrada_etiqueta (entrada_id, etiqueta_id) VALUES ($1,$2)
|
|
ON CONFLICT DO NOTHING`,
|
|
[id, await etiqueta(nombre)]
|
|
);
|
|
}
|
|
for (const slug of datos.canciones ?? []) {
|
|
const { rows } = await c.query('SELECT id FROM cancion WHERE slug = $1', [slug]);
|
|
if (rows[0]) {
|
|
await c.query(
|
|
`INSERT INTO entrada_cancion (entrada_id, cancion_id) VALUES ($1,$2)
|
|
ON CONFLICT DO NOTHING`,
|
|
[id, rows[0].id]
|
|
);
|
|
} else {
|
|
informe.avisos.push(`la entrada «${datos.slug}» cita un tema que no existe: ${slug}`);
|
|
}
|
|
}
|
|
}
|
|
informe.entrada = entradas.length;
|
|
|
|
/* --- Legales, con su primera versión ------------------------------------ */
|
|
for (const { datos, cuerpo } of legales) {
|
|
const id = nuevoId();
|
|
await c.query(
|
|
`INSERT INTO documento_legal (id, slug, titulo, resumen, orden) VALUES ($1,$2,$3,$4,$5)`,
|
|
[id, datos.slug, datos.titulo, datos.resumen, datos.orden]
|
|
);
|
|
const version = nuevoId();
|
|
await c.query(
|
|
`INSERT INTO version_legal (id, documento_id, cuerpo, vigente_desde, resumen_cambios)
|
|
VALUES ($1,$2,$3,$4,$5)`,
|
|
[version, id, cuerpo, new Date(datos.actualizada), 'Primera versión, volcada del repositorio']
|
|
);
|
|
await c.query('UPDATE documento_legal SET version_vigente_id = $1 WHERE id = $2', [
|
|
version,
|
|
id
|
|
]);
|
|
}
|
|
informe.legal = legales.length;
|
|
|
|
/* --- Páginas sueltas ------------------------------------------------------ */
|
|
for (const { datos, cuerpo } of paginas) {
|
|
await c.query(
|
|
`INSERT INTO pagina (id, slug, titulo, descripcion, cuerpo, estado)
|
|
VALUES ($1,$2,$3,$4,$5,'publicado')`,
|
|
[nuevoId(), datos.slug, datos.titulo, datos.descripcion, cuerpo]
|
|
);
|
|
}
|
|
informe.pagina = paginas.length;
|
|
|
|
/*
|
|
* Personas y etiquetas se cuentan AL FINAL y desde la base. Apuntar
|
|
* `etiquetas.size` justo después de las canciones daba 18 cuando acaban
|
|
* siendo 53: después se crean más con los temas de los géneros y con las del
|
|
* blog. Un informe con un número que no es el de la tabla es peor que sin
|
|
* número, porque parece comprobado.
|
|
*/
|
|
for (const tabla of ['persona', 'etiqueta']) {
|
|
const { rows } = await c.query(`SELECT count(*)::int n FROM "${tabla}"`);
|
|
informe[tabla] = rows[0].n;
|
|
}
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Comprobar
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Comprueba que lo escrito coincide con lo leído.
|
|
*
|
|
* «No dio error» no es lo mismo que «está bien»: un `INSERT` puede meter un
|
|
* nulo donde había un texto sin quejarse.
|
|
*/
|
|
async function comprobar(c, contenido, informe) {
|
|
const fallos = [];
|
|
|
|
const cuenta = async (tabla, esperado, condicion = '') => {
|
|
const { rows } = await c.query(`SELECT count(*)::int n FROM "${tabla}" ${condicion}`);
|
|
if (rows[0].n !== esperado) {
|
|
fallos.push(`${tabla}: hay ${rows[0].n} y se esperaban ${esperado}`);
|
|
}
|
|
return rows[0].n;
|
|
};
|
|
|
|
await cuenta('estilo', contenido.estilos.length);
|
|
await cuenta('album', contenido.albumes.length);
|
|
await cuenta('cancion', contenido.canciones.length, 'WHERE version_de_id IS NULL');
|
|
await cuenta('cancion_principal', contenido.canciones.length);
|
|
await cuenta('capitulo', contenido.capitulos.length);
|
|
await cuenta('entrada', contenido.entradas.length);
|
|
await cuenta('documento_legal', contenido.legales.length);
|
|
await cuenta('pagina', contenido.paginas.length);
|
|
|
|
/* La letra de cada tema, carácter a carácter. */
|
|
for (const { datos, cuerpo } of contenido.canciones) {
|
|
const { rows } = await c.query(
|
|
'SELECT titulo, letra, duracion_s, ano FROM cancion WHERE slug = $1',
|
|
[datos.slug]
|
|
);
|
|
const fila = rows[0];
|
|
if (!fila) {
|
|
fallos.push(`falta la canción ${datos.slug}`);
|
|
continue;
|
|
}
|
|
if (fila.titulo !== datos.titulo) fallos.push(`${datos.slug}: el título no coincide`);
|
|
if ((fila.letra ?? '') !== (cuerpo || '')) {
|
|
fallos.push(`${datos.slug}: la letra no coincide`);
|
|
}
|
|
if ((fila.duracion_s ?? null) !== (datos.duracion ?? null)) {
|
|
fallos.push(`${datos.slug}: la duración no coincide`);
|
|
}
|
|
if ((fila.ano ?? null) !== (datos.ano ?? null)) {
|
|
fallos.push(`${datos.slug}: el año no coincide`);
|
|
}
|
|
}
|
|
|
|
/* Que el extracto siga saliendo igual de la letra guardada. */
|
|
const conLetra = contenido.canciones.find((c) => c.cuerpo);
|
|
if (conLetra) {
|
|
const { rows } = await c.query('SELECT letra FROM cancion WHERE slug = $1', [conLetra.slug]);
|
|
const deLaBase = extraerLineas(rows[0].letra ?? '');
|
|
const delArchivo = extraerLineas(conLetra.cuerpo);
|
|
if (deLaBase.join('|') !== delArchivo.join('|')) {
|
|
fallos.push('el extracto de la letra no sale igual desde la base');
|
|
}
|
|
}
|
|
|
|
informe.fallos = fallos;
|
|
return fallos.length === 0;
|
|
}
|
|
|
|
/* -------------------------------------------------------------------------
|
|
* Adelante
|
|
* ---------------------------------------------------------------------- */
|
|
|
|
const contenido = {
|
|
estilos: leerCarpeta('estilos', parsearEstilo),
|
|
albumes: leerCarpeta('albumes', parsearAlbum),
|
|
canciones: leerCarpeta('canciones', parsearCancion),
|
|
capitulos: leerCarpeta('taller', parsearCapitulo),
|
|
entradas: leerCarpeta('blog', parsearEntrada),
|
|
legales: leerCarpeta('legal', parsearLegal),
|
|
paginas: leerCarpeta('paginas', parsearPagina)
|
|
};
|
|
|
|
const total = Object.values(contenido).reduce((n, l) => n + l.length, 0);
|
|
console.log(`Leídos ${total} archivos de ${RAIZ}\n`);
|
|
|
|
if (SECO) {
|
|
for (const [nombre, lista] of Object.entries(contenido)) {
|
|
console.log(` ${nombre.padEnd(12)} ${lista.length}`);
|
|
}
|
|
console.log('\n(en seco: no se ha tocado la base)');
|
|
process.exit(0);
|
|
}
|
|
|
|
const cliente = new pg.Client({
|
|
connectionString: process.env.DATABASE_URL,
|
|
query_timeout: 60_000
|
|
});
|
|
await cliente.connect();
|
|
await cliente.query('BEGIN');
|
|
|
|
const informe = { avisos: [], fallos: [] };
|
|
let bien = false;
|
|
|
|
try {
|
|
await volcar(cliente, contenido, informe);
|
|
bien = await comprobar(cliente, contenido, informe);
|
|
await cliente.query(bien ? 'COMMIT' : 'ROLLBACK');
|
|
} catch (fallo) {
|
|
await cliente.query('ROLLBACK').catch(() => {});
|
|
console.error('\nFalló el volcado, no se ha escrito nada:\n');
|
|
console.error(' ' + fallo.message);
|
|
await cliente.end();
|
|
process.exit(1);
|
|
}
|
|
|
|
await cliente.end();
|
|
|
|
for (const [tabla, n] of Object.entries(informe)) {
|
|
if (typeof n === 'number') console.log(` ${tabla.padEnd(12)} ${n}`);
|
|
}
|
|
for (const aviso of informe.avisos) console.log(` aviso: ${aviso}`);
|
|
|
|
if (bien) {
|
|
console.log('\nVolcado y comprobado.');
|
|
} else {
|
|
console.log('\nNo cuadra, así que se ha deshecho:');
|
|
for (const f of informe.fallos) console.log(` ✗ ${f}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
void createHash;
|