metis-desk/modules/assets/src/api/infrastructure/persistence/asset.model.ts

202 lines
7.8 KiB
TypeScript
Raw Normal View History

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";
const id = { type: DataTypes.UUID, primaryKey: true };
const fk = (model: string, nullable = false) => ({
type: DataTypes.UUID,
allowNull: nullable,
references: { model, key: "id" },
onDelete: "RESTRICT",
});
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,
};
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),
recordedAt: { type: DataTypes.STRING(30), allowNull: false },
2026-10-10 09:41:42 +00:00
...inventoryColumns,
templateId: fk("asset_definitions"),
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));
}
}
}
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;
}
const legacyAssignments = await database.query<LegacyAssignment>(
"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 }
);
const activeWorkstationAssets = await database.query<ActiveWorkstationAsset>(
"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 }
);
const activeByAsset = new Map(activeWorkstationAssets.map((item) => [item.assetId, item]));
const groups = new Map<string, LegacyAssignment[]>();
for (const assignment of legacyAssignments) {
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,
});
});
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("`", "``")}\``
);
}
// QueryInterface.removeColumn is incompatible with Sequelize 6 and mariadb 3 here.
await database.query(`ALTER TABLE \`${table}\` DROP COLUMN personId`);
}