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 } ;
}