import { randomUUID } from 'node:crypto'; /** Unifica únicamente identidades personales enlazadas de forma explícita y unívoca. */ export async function migrarPersonas(c) { await c.query(`CREATE TABLE IF NOT EXISTS estudio_alias (id text PRIMARY KEY,documento_id text NOT NULL REFERENCES estudio_documento(id) ON DELETE RESTRICT); CREATE TABLE IF NOT EXISTS estudio_migracion (clave text PRIMARY KEY,datos jsonb NOT NULL,fecha timestamptz NOT NULL DEFAULT now());`); await c.query("SELECT pg_advisory_xact_lock(hashtext('estudio:publicar'))"); const { rows: docs } = await c.query('SELECT * FROM estudio_documento FOR UPDATE'); const antiguas = docs.filter((d) => ['artistas', 'colaboradores'].includes(d.tipo)); if (!antiguas.length) return { convertidas: 0, unificadas: 0 }; const respaldo = { documentos: docs }; for (const tabla of ['estudio_vinculo', 'estudio_historial', 'estudio_trabajo']) respaldo[tabla] = (await c.query(`SELECT * FROM ${tabla}`)).rows; await c.query('INSERT INTO estudio_migracion(clave,datos) VALUES($1,$2)', [ 'personas-' + randomUUID(), respaldo ]); const { rows: identidades } = await c.query( "SELECT ap.* FROM artista_persona ap JOIN artista a ON a.id=ap.artista_id WHERE ap.relacion='identidad' AND a.tipo='persona'" ); const { rows: artistas } = await c.query('SELECT id,slug,tipo,es_virtual FROM artista'); const { rows: personas } = await c.query( 'SELECT id,slug,nombre_legal,ipi,nacimiento,defuncion FROM persona' ); const { rows: creditos } = await c.query( "SELECT persona_id,CASE rol WHEN 'letra' THEN 'letrista' WHEN 'musica' THEN 'compositor' WHEN 'produccion' THEN 'productor' END AS tipo FROM cancion_persona UNION SELECT persona_id,CASE papel WHEN 'letrista' THEN 'letrista' WHEN 'compositor' THEN 'compositor' WHEN 'interprete' THEN 'artista' END FROM genero_persona" ); const aliases = new Map(); for (const r of identidades) { if ( identidades.filter((x) => x.persona_id === r.persona_id).length !== 1 || identidades.filter((x) => x.artista_id === r.artista_id).length !== 1 ) continue; if ( docs.some((d) => d.id === r.artista_id && d.tipo === 'artistas') && docs.some((d) => d.id === r.persona_id && d.tipo === 'colaboradores') ) aliases.set(r.artista_id, r.persona_id); } const grupos = new Map(); for (const d of antiguas) { const id = aliases.get(d.id) ?? d.id; grupos.set(id, [...(grupos.get(id) ?? []), d]); } const reservados = new Set(docs.filter((d) => d.tipo === 'personas').map((d) => d.borrador.slug)); const nuevos = []; for (const [id, grupo] of grupos) { const a = grupo.find((d) => d.tipo === 'artistas'), p = grupo.find((d) => d.tipo === 'colaboradores'); const identidad = artistas.find((x) => x.id === a?.id), fisica = personas.find((x) => x.id === p?.id); const convertir = (edicion) => { const x = a?.[edicion], y = p?.[edicion]; if (!x && !y) return null; const f = { ...(y?.campos ?? {}), ...(x?.campos ?? {}) }; const roles = new Set([ ...(f.tiposPersona ?? []), ...(a ? [identidad?.es_virtual || identidad?.tipo === 'virtual' ? 'artista_virtual' : 'artista'] : []), ...creditos.filter((r) => r.persona_id === p?.id && r.tipo).map((r) => r.tipo) ]); f.tiposPersona = [...roles]; f.naturaleza = identidad?.tipo === 'virtual' ? 'proyecto' : (identidad?.tipo ?? 'persona'); f.biografia = String(x?.campos.biografia || y?.campos.semblanza || ''); f.secciones = x?.campos.secciones ?? (f.biografia ? [{ id: 'biografia-principal', tipo: 'texto', texto: f.biografia }] : []); f.resumen = String(x?.campos.resumen ?? ''); f.nombreLegal = y?.campos.nombreLegal ?? fisica?.nombre_legal ?? ''; f.ipi = y?.campos.ipi ?? fisica?.ipi ?? ''; f.nacimiento = fisica?.nacimiento ?? null; f.defuncion = fisica?.defuncion ?? null; f._artistaId = identidad?.id ?? ''; f._personaId = fisica?.id ?? ''; f._personaSlug = fisica?.slug ?? ''; f._importado = true; delete f.tipo; delete f.semblanza; return { titulo: (x ?? y).titulo, slug: (x ?? y).slug, campos: f, vinculos: [...(x?.vinculos ?? []), ...(y?.vinculos ?? [])] }; }; const borrador = convertir('borrador'), publicado = convertir('publicado'); if (reservados.has(borrador.slug)) { borrador.slug += '-' + id.slice(0, 8); } reservados.add(borrador.slug); const base = a ?? p; nuevos.push({ ...base, id, tipo: 'personas', estado: grupo.some((d) => d.estado === 'publicado') ? 'publicado' : base.estado, borrador, publicado, revision: Math.max(...grupo.map((d) => d.revision)) + 1 }); } // Los enlaces apuntan a la ficha unificada; cada crédito conserva su papel. const remap = (b) => { if (!b) return b; const vistos = new Set(); b.vinculos = (b.vinculos ?? []) .map((v) => ({ ...v, destino: aliases.get(v.destino) ?? v.destino })) .filter((v) => { const k = v.papel + ':' + v.destino; if (vistos.has(k)) return false; vistos.add(k); return true; }); return b; }; const todas = docs.filter((d) => !antiguas.some((x) => x.id === d.id)).concat(nuevos); for (const d of todas) { remap(d.borrador); remap(d.publicado); } await c.query('DELETE FROM estudio_vinculo'); for (const d of nuevos) { await c.query( 'UPDATE estudio_documento SET tipo=$2,borrador=$3,publicado=$4,revision=$5,estado=$6,actualizado=now(),autor=$7 WHERE id=$1', [d.id, d.tipo, d.borrador, d.publicado, d.revision, d.estado, 'Migración a Personas'] ); await c.query( 'INSERT INTO estudio_historial(documento_id,revision,accion,autor,contenido) VALUES($1,$2,$3,$4,$5)', [d.id, d.revision, 'unificar persona', 'Migración a Personas', d.borrador] ); } for (const d of todas.filter((d) => d.tipo !== 'personas')) await c.query('UPDATE estudio_documento SET borrador=$2,publicado=$3 WHERE id=$1', [ d.id, d.borrador, d.publicado ]); for (const [antes, despues] of aliases) { await c.query('UPDATE estudio_historial SET documento_id=$2 WHERE documento_id=$1', [ antes, despues ]); await c.query('UPDATE estudio_trabajo SET documento_id=$2 WHERE documento_id=$1', [ antes, despues ]); await c.query( 'INSERT INTO estudio_alias(id,documento_id) VALUES($1,$2) ON CONFLICT(id) DO UPDATE SET documento_id=EXCLUDED.documento_id', [antes, despues] ); await c.query('DELETE FROM estudio_documento WHERE id=$1', [antes]); } const referencias = [ 'principalId', 'cancionId', 'estiloId', 'generoId', 'modeloId', 'grabacionId' ]; for (const d of todas) for (const edicion of ['borrador', 'publicado']) { const b = d[edicion]; if (!b) continue; const vinculos = [ ...b.vinculos, ...referencias .filter((k) => b.campos[k]) .map((k) => ({ destino: b.campos[k], papel: k, detalle: '', orden: 0 })) ]; for (const v of vinculos) if (v.destino !== d.id) 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 ?? ''] ); } return { convertidas: nuevos.length, unificadas: aliases.size }; }