/** Crea el almacén editorial e importa una instantánea, sin modificar el catálogo público. */ import pg from 'pg'; import { migrarPersonas } from './personas.mjs'; import { migrarInspiracion } from './inspiracion.mjs'; import { TABLAS_FUENTE, huella } from './fuente.mjs'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const c = new pg.Client({ connectionString: process.env.DATABASE_URL }); await c.connect(); const estructura = readFileSync( fileURLToPath(new URL('./estructura.sql', import.meta.url)), 'utf8' ); try { await c.query('BEGIN'); await c.query(estructura); const { rows: [control] } = await c.query('SELECT * FROM estudio_control WHERE id=1 FOR UPDATE'); if (control.importado_en) { console.log('El almacén ya está importado. No se sobrescriben borradores.'); await migrarPersonas(c); await migrarInspiracion(c); await c.query('COMMIT'); } else { const tablas = TABLAS_FUENTE; /** @type {Record} */ const datos = {}; for (const tabla of tablas) datos[tabla] = (await c.query(`SELECT * FROM ${tabla}`)).rows; /** @type {{id:string,tipo:string,estado:string,borrador:any}[]} */ const docs = []; const agregar = (tipo, f, campos = {}, vinculos = []) => docs.push({ id: f.id, tipo, estado: f.estado ?? 'publicado', borrador: { titulo: f.titulo ?? f.nombre ?? f.termino ?? f.codigo ?? f.slug, slug: f.slug ?? f.codigo ?? f.id, campos: { ...campos, _importado: true }, vinculos } }); const medios = new Map(datos.medio.map((m) => [m.id, m])); for (const m of datos.medio.filter((m) => m.clase === 'imagen')) await c.query('INSERT INTO estudio_archivo(id,clase,nombre,datos) VALUES($1,$2,$3,$4)', [ m.id, 'imagen', m.clave, { legacyId: m.id, clave: m.clave, mime: m.mime, bytes: m.bytes, ancho: m.ancho, alto: m.alto, sha256: m.sha256, alt: m.alt } ]); for (const s of datos.estilo) agregar('estilos', s, { resumen: s.resumen, cuerpo: s.cuerpo, orden: s.orden, principal: s.principal, imagenId: s.imagen_id ?? '' }); for (const a of datos.artista) agregar('artistas', a, { tipo: a.tipo, resumen: a.descripcion_corta, nombreVisual: a.nombre_visual ?? 'texto', plantilla: a.presentacion?.plantilla ?? 'perfil-artista', heroe: a.presentacion?.heroe ?? 'automatico', biografia: a.biografia, secciones: Array.isArray(a.secciones) && a.secciones.length ? a.secciones : a.biografia ? [{ id: 'biografia-principal', tipo: 'texto', texto: a.biografia }] : [], lugarOrigen: a.lugar_origen ?? '', sitioWeb: a.sitio_web ?? '', imagenId: a.imagen_id ?? '', logotipoId: a.logotipo_id ?? '', logotipoClaroId: a.logotipo_claro_id ?? '', logotipoOscuroId: a.logotipo_oscuro_id ?? '' }); for (const p of datos.persona) agregar('colaboradores', p, { nombreLegal: p.nombre_legal ?? '', ipi: p.ipi ?? '', semblanza: p.semblanza ?? '', imagenId: p.retrato_id ?? '' }); for (const e of datos.etiqueta) agregar('etiquetas', e); for (const f of datos.cancion) { const audios = datos.cancion_audio.filter((a) => a.cancion_id === f.id); const info = { legacyId: f.id, duracionS: f.duracion_s }; for (const a of audios) { const m = medios.get(a.medio_id); if (!m) throw Error('Audio con medio inexistente'); const nombre = a.uso === 'stream' ? 'escucha' : a.uso; info[nombre] = { clave: m.clave, mime: m.mime, bytes: m.bytes, sha256: m.sha256, formato: a.formato, bitrateKbps: a.bitrate_kbps, frecuenciaHz: a.frecuencia_hz, bitsMuestra: a.bits_muestra, canales: a.canales }; } if (audios.length) await c.query('INSERT INTO estudio_archivo(id,clase,nombre,datos) VALUES($1,$2,$3,$4)', [ `a:${f.id}`, 'audio', f.titulo, info ]); if (!f.version_de_id) agregar('canciones', f, { principalId: `g:${f.id}`, plantilla: f.presentacion?.plantilla ?? 'detalle-cancion', heroe: f.presentacion?.heroe ?? 'automatico', alaVenta: f.ala_venta, destacada: f.destacada, resumen: '', inspiracionSecciones: Array.isArray(f.inspiracion_secciones) && f.inspiracion_secciones.length ? f.inspiracion_secciones : f.inspiracion ? [{ id: 'texto-principal', tipo: 'texto', texto: f.inspiracion }] : [], _legacyId: f.id, _versionCodigo: f.version_codigo }); const creditos = datos.cancion_interprete .filter((x) => x.cancion_id === f.id) .map((x) => ({ destino: x.artista_id, papel: x.rol, detalle: x.instrumento ?? '', acreditadoComo: x.acreditado_como ?? '', orden: x.orden })); creditos.push( ...datos.cancion_persona .filter((x) => x.cancion_id === f.id && ['letra', 'musica', 'produccion'].includes(x.rol)) .map((x) => ({ destino: x.persona_id, papel: x.rol, detalle: '', orden: x.orden })), ...datos.cancion_etiqueta .filter((x) => x.cancion_id === f.id) .map((x) => ({ destino: x.etiqueta_id, papel: 'etiqueta', detalle: '', orden: x.orden })) ); agregar( 'grabaciones', { ...f, id: `g:${f.id}`, slug: f.version_de_id ? f.slug : `${f.slug}-grabacion` }, { cancionId: f.version_de_id ?? f.id, versionNombre: f.version_nombre ?? 'Estudio', estiloId: f.estilo_id, idioma: f.idioma, ano: f.ano, letra: f.letra ?? '', tieneLetra: f.tiene_letra, formacionVocal: f.formacion_vocal ?? '', acceso: f.acceso, imagenId: f.portada_id ?? '', audioId: audios.length ? `a:${f.id}` : '', _legacyId: f.id }, creditos ); } for (const a of datos.album) agregar( 'lanzamientos', a, { tipo: a.tipo, estiloId: a.estilo_id, fecha: a.fecha, resumen: a.resumen, cuerpo: a.cuerpo, secciones: Array.isArray(a.secciones) && a.secciones.length ? a.secciones : a.cuerpo ? [{ id: 'contenido-principal', tipo: 'texto', texto: a.cuerpo }] : [], plantilla: a.presentacion?.plantilla ?? 'detalle-album', heroe: a.presentacion?.heroe ?? 'automatico', sello: a.sello ?? '', numeroCatalogo: a.numero_catalogo ?? '', imagenId: a.portada_id ?? '' }, datos.cancion .filter((f) => f.album_id === a.id) .sort((a, b) => (a.numero ?? 0) - (b.numero ?? 0)) .map((f, i) => ({ destino: `g:${f.id}`, papel: 'pista', detalle: '', orden: i })) ); for (const e of datos.entrada) agregar( 'blog', e, { fecha: e.fecha, resumen: e.resumen, cuerpo: e.cuerpo, secciones: e.secciones, plantilla: e.presentacion?.plantilla ?? 'articulo-editorial', heroe: e.presentacion?.heroe ?? 'automatico', imagenId: e.imagen_id ?? '' }, [ ...datos.entrada_cancion .filter((x) => x.entrada_id === e.id) .map((x) => ({ destino: datos.cancion.find((f) => f.id === x.cancion_id)?.version_de_id ?? x.cancion_id, papel: 'cancion', detalle: '', orden: 0 })), ...datos.entrada_etiqueta .filter((x) => x.entrada_id === e.id) .map((x) => ({ destino: x.etiqueta_id, papel: 'etiqueta', detalle: '', orden: 0 })) ] ); for (const p of datos.pagina) agregar('paginas', p, { resumen: p.descripcion, cuerpo: p.cuerpo }); for (const l of datos.documento_legal) { const v = datos.version_legal.find((v) => v.id === l.version_vigente_id); agregar('legales', l, { resumen: l.resumen, cuerpo: v?.cuerpo ?? '', fecha: v ? new Date(v.vigente_desde).toISOString().slice(0, 10) : '', orden: l.orden }); } for (const g of datos.genero) agregar( 'generos', g, { resumen: g.resumen, origenLugar: g.origen_lugar ?? '', origenAno: g.origen_ano, tempo: g.tempo ?? '', rima: g.rima ?? '', metricaNota: g.metrica_nota ?? '', compasNota: g.compas_nota ?? '', cuerpo: datos.genero_seccion.find((s) => s.genero_id === g.id && s.clave === 'introduccion') ?.cuerpo ?? '' }, [ ...datos.genero_origen .filter((r) => r.genero_id === g.id) .map((r) => ({ destino: r.origen_id, papel: r.tipo, detalle: r.nota ?? '', orden: r.orden })), ...datos.genero_tema .filter((r) => r.genero_id === g.id) .map((r) => ({ destino: r.etiqueta_id, papel: 'etiqueta', detalle: '', orden: r.orden })) ] ); for (const cap of datos.capitulo) agregar('capitulos', cap, { tipo: cap.tipo, generoId: cap.genero_id ?? '', orden: cap.orden, resumen: cap.resumen, cuerpo: cap.cuerpo }); for (const t of datos.termino) { const a = datos.acepcion.find((a) => a.termino_id === t.id && a.numero === 1); agregar('glosario', t, { categoria: a?.categoria ?? 'musica', definicion: a?.definicion ?? '', cuerpo: a?.ampliacion ?? '' }); } for (const m of datos.modelo_ia) agregar('modelos', m, { familia: m.familia, version: m.version, vigente: m.vigente, esquema: m.esquema ? JSON.stringify(m.esquema, null, 2) : '' }); for (const p of datos.prompt) agregar( 'prompts', { ...p, titulo: p.notas?.slice(0, 90) || `Prompt ${p.id.slice(0, 8)}` }, { modeloId: p.modelo_ia_id, generoId: p.genero_id ?? '', grabacionId: p.cancion_id ? `g:${p.cancion_id}` : '', idioma: p.idioma, contenido: JSON.stringify(p.contenido, null, 2), notas: p.notas ?? '', visibilidad: p.visibilidad } ); for (const r of datos.redireccion) agregar( 'redirecciones', { id: `r:${r.desde}`, titulo: r.desde, slug: `redireccion-${docs.length}` }, { desde: r.desde, hacia: r.hacia, motivo: r.motivo ?? '' } ); for (const d of docs) await c.query( 'INSERT INTO estudio_documento(id,tipo,estado,borrador,publicado,autor) VALUES($1,$2,$3,$4,$5,$6)', [ d.id, d.tipo, d.estado, d.borrador, d.estado !== 'borrador' ? d.borrador : null, 'Importación inicial' ] ); const ids = new Set(docs.map((d) => d.id)); const camposReferencia = [ 'principalId', 'cancionId', 'estiloId', 'generoId', 'modeloId', 'grabacionId' ]; for (const d of docs) { const links = [ ...d.borrador.vinculos, ...camposReferencia .filter((k) => d.borrador.campos[k]) .map((k) => ({ destino: d.borrador.campos[k], papel: k, detalle: '', orden: 0 })) ]; for (const v of links) { if (!ids.has(v.destino)) throw Error(`Referencia no importada: ${d.tipo}/${d.id}`); for (const edicion of d.estado !== 'borrador' ? ['borrador', 'publicado'] : ['borrador']) await c.query( 'INSERT INTO estudio_vinculo(origen,destino,edicion,papel,orden,detalle) VALUES($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING', [d.id, v.destino, edicion, v.papel, v.orden, v.detalle] ); } } await c.query('UPDATE estudio_control SET importado_en=now(),huella_fuente=$1 WHERE id=1', [ huella(datos) ]); await migrarPersonas(c); await migrarInspiracion(c); await c.query('COMMIT'); console.log(`Importadas ${docs.length} fichas; catálogo público conservado. Modo preparación.`); } } catch (e) { await c.query('ROLLBACK'); console.error(e.message); process.exitCode = 1; } finally { await c.end(); }