/** * Carga la semilla de géneros: países, instrumentos, personas, obras de * referencia y el linaje. * * npm run db:generos * * Es el primer importador, y sigue el contrato que se decidió para las fichas: * entra desde un JSON, resuelve las relaciones por código, y **marca la * procedencia**. Todo lo que carga queda con `origen = 'importado'` y sin * revisar, porque lo escribió un modelo y no una persona: es musicología que se * publica con el nombre del titular, y esa diferencia tiene que estar en la * base y no en la memoria de nadie. * * Los géneros ya existen —los creó el volcado del catálogo a partir de las * fichas del taller—, así que aquí se completan, no se crean. Lo que sí se crea * es lo que no tenía de dónde salir. */ import fs from 'node:fs'; import pg from 'pg'; const SEMILLA = JSON.parse(fs.readFileSync('scripts/db/semilla-generos.json', 'utf8')); const MODELO = 'claude-opus-5'; const slugificar = (t) => t .toLowerCase() .normalize('NFD') .replace(/[̀-ͯ]/g, '') .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); const c = new pg.Client({ connectionString: process.env.DATABASE_URL, query_timeout: 60_000 }); await c.connect(); await c.query('BEGIN'); const contado = {}; const avisos = []; try { /* --- Países ------------------------------------------------------------ */ for (const p of SEMILLA.paises) { await c.query( `INSERT INTO pais (codigo, nombre, gentilicio, region) VALUES ($1,$2,$3,$4) ON CONFLICT (codigo) DO UPDATE SET nombre = EXCLUDED.nombre, gentilicio = EXCLUDED.gentilicio, region = EXCLUDED.region`, [p.codigo, p.nombre, p.gentilicio ?? null, p.region ?? null] ); } contado.pais = SEMILLA.paises.length; /* --- Instrumentos ------------------------------------------------------ */ const idInstrumento = new Map(); for (const i of SEMILLA.instrumentos) { const { rows } = await c.query( `INSERT INTO instrumento (id, slug, nombre, familia, subfamilia, pais_codigo, descripcion, afinacion, estado, origen, modelo_origen, importado_en) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,'publicado','importado',$9,now()) ON CONFLICT (slug) DO UPDATE SET nombre = EXCLUDED.nombre, familia = EXCLUDED.familia, subfamilia = EXCLUDED.subfamilia, pais_codigo = EXCLUDED.pais_codigo, descripcion = EXCLUDED.descripcion, afinacion = EXCLUDED.afinacion RETURNING id`, [ crypto.randomUUID(), i.slug, i.nombre, i.familia, i.subfamilia ?? null, i.paisCodigo ?? null, i.descripcion ?? null, i.afinacion ?? null, MODELO ] ); idInstrumento.set(i.slug, rows[0].id); } contado.instrumento = idInstrumento.size; /* --- Personas ---------------------------------------------------------- * * Se completan las que ya existan por nombre —el volcado creó las de los * créditos— y se crean las que falten. Que sea la misma tabla es justo el * motivo de no haber hecho una de «letristas» aparte. */ const idPersona = new Map(); for (const p of SEMILLA.personas) { const { rows } = await c.query( `INSERT INTO persona (id, slug, nombre, nacimiento, defuncion, pais_codigo, semblanza, estado, origen, modelo_origen, importado_en) VALUES ($1,$2,$3,$4,$5,$6,$7,'publicado','importado',$8,now()) ON CONFLICT (slug) DO UPDATE SET nacimiento = EXCLUDED.nacimiento, defuncion = EXCLUDED.defuncion, pais_codigo = EXCLUDED.pais_codigo, semblanza = EXCLUDED.semblanza RETURNING id`, [ crypto.randomUUID(), p.slug, p.nombre, p.nacimiento ?? null, p.defuncion ?? null, p.paisCodigo ?? null, p.semblanza ?? null, MODELO ] ); idPersona.set(p.slug, rows[0].id); } contado.persona = idPersona.size; /* --- Los géneros ------------------------------------------------------- */ const idGenero = new Map(); const { rows: existentes } = await c.query('SELECT id, slug FROM genero'); for (const g of existentes) idGenero.set(g.slug, g.id); contado.generoNuevo = 0; for (const [slug, datos] of Object.entries(SEMILLA.generos)) { if (!idGenero.has(slug)) { if (!datos.nombre) { avisos.push(`el género «${slug}» no existe y la semilla no le da nombre; lo salto`); continue; } const { rows } = await c.query( `INSERT INTO genero (id, slug, nombre, resumen, origen_ano, estado, origen, modelo_origen, importado_en) VALUES ($1,$2,$3,$4,$5,'publicado','importado',$6,now()) RETURNING id`, [ crypto.randomUUID(), slug, datos.nombre, datos.resumen ?? '', datos.origenAno ?? null, MODELO ] ); idGenero.set(slug, rows[0].id); contado.generoNuevo++; } else if (datos.origenAno) { await c.query('UPDATE genero SET origen_ano = $1 WHERE id = $2', [ datos.origenAno, idGenero.get(slug) ]); } } /* --- Y todo lo que cuelga de ellos -------------------------------------- */ const cuenta = (clave) => (contado[clave] = (contado[clave] ?? 0) + 1); for (const [slug, datos] of Object.entries(SEMILLA.generos)) { const generoId = idGenero.get(slug); if (!generoId) continue; for (const [pais, relacion] of datos.paises ?? []) { await c.query( `INSERT INTO genero_pais (genero_id, pais_codigo, relacion) VALUES ($1,$2,$3) ON CONFLICT (genero_id, pais_codigo) DO UPDATE SET relacion = EXCLUDED.relacion`, [generoId, pais, relacion] ); cuenta('generoPais'); } for (const [nombre, pais] of datos.alias ?? []) { await c.query( `INSERT INTO genero_alias (id, genero_id, nombre, pais_codigo) VALUES ($1,$2,$3,$4)`, [crypto.randomUUID(), generoId, nombre, pais] ); cuenta('alias'); } for (const [i, [inst, importancia, papel]] of (datos.instrumentos ?? []).entries()) { const instrumentoId = idInstrumento.get(inst); if (!instrumentoId) { avisos.push(`${slug}: no conozco el instrumento «${inst}»`); continue; } await c.query( `INSERT INTO genero_instrumento (genero_id, instrumento_id, importancia, papel, orden) VALUES ($1,$2,$3,$4,$5) ON CONFLICT (genero_id, instrumento_id) DO UPDATE SET importancia = EXCLUDED.importancia, papel = EXCLUDED.papel, orden = EXCLUDED.orden`, [generoId, instrumentoId, importancia, papel ?? null, i] ); cuenta('generoInstrumento'); } for (const [i, [persona, papel]] of (datos.personas ?? []).entries()) { const personaId = idPersona.get(persona); if (!personaId) { avisos.push(`${slug}: no conozco a «${persona}»`); continue; } await c.query( `INSERT INTO genero_persona (genero_id, persona_id, papel, orden) VALUES ($1,$2,$3,$4) ON CONFLICT (genero_id, persona_id, papel) DO UPDATE SET orden = EXCLUDED.orden`, [generoId, personaId, papel, i] ); cuenta('generoPersona'); } for (const [titulo, ano, autores, nota, cita] of datos.obras ?? []) { const obraSlug = slugificar(`${titulo}-${ano ?? ''}`); const { rows } = await c.query( `INSERT INTO obra_referencia (id, slug, titulo, ano, genero_id, nota, cita, estado, origen, modelo_origen, importado_en) VALUES ($1,$2,$3,$4,$5,$6,$7,'publicado','importado',$8,now()) ON CONFLICT (slug) DO UPDATE SET nota = EXCLUDED.nota, cita = EXCLUDED.cita RETURNING id`, [ crypto.randomUUID(), obraSlug, titulo, ano ?? null, generoId, nota ?? null, cita ?? null, MODELO ] ); cuenta('obra'); for (const [i, autor] of (autores ?? []).entries()) { const personaId = idPersona.get(autor); if (!personaId) { avisos.push(`«${titulo}»: no conozco a «${autor}»`); continue; } await c.query( `INSERT INTO obra_referencia_persona (obra_id, persona_id, papel, orden) VALUES ($1,$2,'letra',$3) ON CONFLICT DO NOTHING`, [rows[0].id, personaId, i] ); } } for (const [clave, titulo, cuerpo] of datos.secciones ?? []) { await c.query( `INSERT INTO genero_seccion (id, genero_id, clave, titulo, cuerpo, orden) VALUES ($1,$2,$3,$4,$5,0) ON CONFLICT (genero_id, clave) DO UPDATE SET titulo = EXCLUDED.titulo, cuerpo = EXCLUDED.cuerpo`, [crypto.randomUUID(), generoId, clave, titulo, cuerpo] ); cuenta('seccion'); } } /* * El linaje va al final, en una pasada aparte: una arista puede apuntar a un * género que se crea más abajo en el JSON, y el disparador de ciclos * necesita que existan los dos extremos. */ for (const [slug, datos] of Object.entries(SEMILLA.generos)) { const generoId = idGenero.get(slug); if (!generoId) continue; for (const [i, [origen, tipo, principal, nota]] of (datos.origenes ?? []).entries()) { const origenId = idGenero.get(origen); if (!origenId) { avisos.push(`${slug}: no conozco el género de origen «${origen}»`); continue; } await c.query( `INSERT INTO genero_origen (genero_id, origen_id, tipo, principal, nota, orden) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT (genero_id, origen_id, tipo) DO UPDATE SET principal = EXCLUDED.principal, nota = EXCLUDED.nota`, [generoId, origenId, tipo, principal ?? false, nota ?? null, i] ); cuenta('linaje'); } } await c.query('COMMIT'); } catch (fallo) { await c.query('ROLLBACK').catch(() => {}); console.error('\nFalló, no se ha escrito nada:\n ' + fallo.message); await c.end(); process.exit(1); } await c.end(); for (const [que, n] of Object.entries(contado)) console.log(` ${que.padEnd(20)} ${n}`); for (const aviso of avisos) console.log(` aviso: ${aviso}`); console.log('\nSembrado. Todo entra como «importado» y sin revisar.');