2026-10-09 23:37:10 +00:00
import { UniqueID } from "@repo/rdx-ddd" ;
2026-10-10 09:41:42 +00:00
import type { Sequelize } from "sequelize" ;
import { DataTypes , QueryTypes } from "sequelize" ;
import { inventoryColumns , specificationColumns } from "./asset-normalization-migration" ;
2026-10-09 23:37:10 +00:00
2026-10-04 16:36:09 +00:00
const id = { type : DataTypes . UUID , primaryKey : true } ;
const fk = ( model : string , nullable = false ) = > ( {
type : DataTypes . UUID ,
allowNull : nullable ,
references : { model , key : "id" } ,
onDelete : "RESTRICT" ,
} ) ;
2026-10-09 23:37:10 +00:00
2026-10-04 16:36:09 +00:00
export async function migrateInventory ( database : Sequelize ) {
const query = database . getQueryInterface ( ) ;
const tables = await database . query < { name : string } > (
"SELECT TABLE_NAME AS name FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE()" ,
{ type : QueryTypes . SELECT }
) ;
const asset = {
id ,
companyId : fk ( "companies" ) ,
templateId : fk ( "asset_definitions" ) ,
siteId : fk ( "company_sites" , true ) ,
2026-10-07 16:08:48 +00:00
brandId : fk ( "asset_brands" , true ) ,
2026-10-10 09:41:42 +00:00
. . . inventoryColumns ,
. . . specificationColumns ,
2026-10-04 16:36:09 +00:00
} ;
const revision = {
id ,
assetId : fk ( "assets" ) ,
actorId : fk ( "users" ) ,
siteId : fk ( "company_sites" , true ) ,
2026-10-07 16:08:48 +00:00
brandId : fk ( "asset_brands" , true ) ,
2026-10-04 16:36:09 +00:00
recordedAt : { type : DataTypes . STRING ( 30 ) , allowNull : false } ,
2026-10-10 09:41:42 +00:00
. . . inventoryColumns ,
templateId : fk ( "asset_definitions" ) ,
2026-10-04 16:36:09 +00:00
position : { type : DataTypes . INTEGER , allowNull : false } ,
} ;
if ( ! tables . some ( ( table ) = > table . name === "assets" ) ) {
await query . createTable ( "assets" , asset ) ;
}
if ( ! tables . some ( ( table ) = > table . name === "asset_revisions" ) ) {
await query . createTable ( "asset_revisions" , revision ) ;
}
2026-10-10 09:41:42 +00:00
const revisionColumns = await query . describeTable ( "asset_revisions" ) ;
if ( ! ( "siteId" in revisionColumns ) ) {
await query . addColumn ( "asset_revisions" , "siteId" , fk ( "company_sites" , true ) ) ;
if ( revisionColumns . values ) {
await database . query (
"UPDATE asset_revisions SET siteId=NULLIF(JSON_UNQUOTE(JSON_EXTRACT(`values`,'$.siteId')),'null')"
) ;
}
}
2026-10-07 16:08:48 +00:00
for ( const table of [ "assets" , "asset_revisions" ] ) {
const columns = await query . describeTable ( table ) ;
if ( ! ( "brandId" in columns ) ) {
await query . addColumn ( table , "brandId" , fk ( "asset_brands" , true ) ) ;
}
}
2026-10-04 16:36:09 +00:00
}
2026-10-09 23:37:10 +00:00
type LegacyAssignment = {
id : string ;
companyId : string ;
siteId : string | null ;
personId : string ;
personName : string ;
siteName : string | null ;
actorId : string | null ;
} ;
type ActiveWorkstationAsset = { assetId : string ; personId : string | null } ;
/ * *
* Convierte la asignación histórica directa activo - persona en puestos activos antes de
* retirar el campo . Si ya existe un puesto activo para el mismo activo y persona , la
* relación ya está representada y no se duplica .
* /
export async function migrateLegacyAssetAssignments ( database : Sequelize ) {
const query = database . getQueryInterface ( ) ;
const assetColumns = await query . describeTable ( "assets" ) ;
if ( ! ( "personId" in assetColumns ) ) {
return ;
}
2026-10-10 09:09:34 +00:00
const legacyAssignments = await database . query < LegacyAssignment > (
2026-10-09 23:37:10 +00:00
"SELECT a.id,a.companyId,a.siteId,a.personId,p.name AS personName,s.name AS siteName,(SELECT r.actorId FROM asset_revisions r WHERE r.assetId=a.id ORDER BY r.position DESC LIMIT 1) AS actorId FROM assets a JOIN company_contacts p ON p.id=a.personId AND p.companyId=a.companyId LEFT JOIN company_sites s ON s.id=a.siteId AND s.companyId=a.companyId WHERE a.personId IS NOT NULL AND JSON_UNQUOTE(JSON_EXTRACT(a.`values`,'$.status')) <> 'retired'" ,
{ type : QueryTypes . SELECT }
) ;
2026-10-10 09:09:34 +00:00
const activeWorkstationAssets = await database . query < ActiveWorkstationAsset > (
2026-10-09 23:37:10 +00:00
"SELECT wa.assetId,w.personId FROM workstation_assets wa JOIN workstations w ON w.id=wa.workstationId WHERE wa.activeKey IS NOT NULL" ,
{ type : QueryTypes . SELECT }
) ;
2026-10-10 09:09:34 +00:00
const activeByAsset = new Map ( activeWorkstationAssets . map ( ( item ) = > [ item . assetId , item ] ) ) ;
2026-10-09 23:37:10 +00:00
const groups = new Map < string , LegacyAssignment [ ] > ( ) ;
2026-10-10 09:09:34 +00:00
for ( const assignment of legacyAssignments ) {
2026-10-09 23:37:10 +00:00
const existing = activeByAsset . get ( assignment . id ) ;
if ( existing ) {
if ( existing . personId !== assignment . personId ) {
throw new Error (
` No se puede migrar el activo ${ assignment . id } : ya pertenece a un puesto de otra persona. `
) ;
}
continue ;
}
const key = [ assignment . companyId , assignment . personId , assignment . siteId ? ? "" ] . join ( ":" ) ;
const entries = groups . get ( key ) ? ? [ ] ;
entries . push ( assignment ) ;
groups . set ( key , entries ) ;
}
await database . transaction ( async ( transaction ) = > {
for ( const entries of groups . values ( ) ) {
const first = entries [ 0 ] ;
if ( ! ( first ? . actorId && first . personName ) ) {
throw new Error ( "No se puede reconstruir el autor de la asignación histórica del activo." ) ;
}
const workstationId = UniqueID . generateNewID ( ) . toPrimitive ( ) ;
const revisionId = UniqueID . generateNewID ( ) . toPrimitive ( ) ;
const values = {
name : ` Puesto migrado: ${ first . personName } ${ first . siteName ? ` · ${ first . siteName } ` : "" } ` . slice (
0 ,
100
) ,
active : true ,
siteId : first.siteId ,
personId : first.personId ,
assetIds : entries.map ( ( entry ) = > entry . id ) ,
sharedAssetIds : [ ] ,
} ;
const recordedAt = new Date ( ) . toISOString ( ) ;
await database . query (
"INSERT INTO workstations (id,companyId,siteId,personId,`values`) VALUES (:id,:companyId,:siteId,:personId,:values)" ,
{
replacements : {
id : workstationId ,
companyId : first.companyId ,
siteId : first.siteId ,
personId : first.personId ,
values : JSON.stringify ( values ) ,
} ,
transaction ,
}
) ;
await database . query (
"INSERT INTO workstation_revisions (id,workstationId,actorId,siteId,personId,`values`,recordedAt,position) VALUES (:id,:workstationId,:actorId,:siteId,:personId,:values,:recordedAt,0)" ,
{
replacements : {
id : revisionId ,
workstationId ,
actorId : first.actorId ,
siteId : first.siteId ,
personId : first.personId ,
values : JSON.stringify ( values ) ,
recordedAt ,
} ,
transaction ,
}
) ;
for ( const entry of entries ) {
await database . query (
"INSERT INTO workstation_assets (workstationId,companyId,assetId,activeKey) VALUES (:workstationId,:companyId,:assetId,:activeKey)" ,
{
replacements : {
workstationId ,
companyId : first.companyId ,
assetId : entry.id ,
activeKey : ` ${ first . companyId } : ${ entry . id } ` ,
} ,
transaction ,
}
) ;
}
}
await database . query ( "UPDATE assets SET `values`=JSON_REMOVE(`values`,'$.personId')" , {
transaction ,
} ) ;
await database . query ( "UPDATE asset_revisions SET `values`=JSON_REMOVE(`values`,'$.personId')" , {
transaction ,
} ) ;
} ) ;
2026-10-10 09:09:34 +00:00
await removeLegacyPersonColumn ( database , "asset_revisions" ) ;
await removeLegacyPersonColumn ( database , "assets" ) ;
}
async function removeLegacyPersonColumn ( database : Sequelize , table : "assets" | "asset_revisions" ) {
const columns = await database . getQueryInterface ( ) . describeTable ( table ) ;
if ( ! ( "personId" in columns ) ) {
return ;
}
const constraints = await database . query < { constraintName : string } > (
"SELECT CONSTRAINT_NAME AS constraintName FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:table AND COLUMN_NAME='personId' AND REFERENCED_TABLE_NAME IS NOT NULL" ,
{ replacements : { table } , type : QueryTypes . SELECT }
) ;
for ( const { constraintName } of constraints ) {
await database . query (
` ALTER TABLE \` ${ table } \` DROP FOREIGN KEY \` ${ constraintName . replaceAll ( "`" , "``" ) } \` `
2026-10-09 23:37:10 +00:00
) ;
}
2026-10-10 09:09:34 +00:00
// QueryInterface.removeColumn is incompatible with Sequelize 6 and mariadb 3 here.
await database . query ( ` ALTER TABLE \` ${ table } \` DROP COLUMN personId ` ) ;
2026-10-09 23:37:10 +00:00
}