215 lines
7.6 KiB
TypeScript
215 lines
7.6 KiB
TypeScript
import { UniqueID } from "@repo/rdx-ddd";
|
|
import type { Sequelize } from "sequelize";
|
|
import { DataTypes, QueryTypes } from "sequelize";
|
|
|
|
import { inventoryColumns, specificationColumns } from "./asset-normalization-migration";
|
|
import { writeWorkstation } from "./workstation-relational";
|
|
|
|
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),
|
|
brandId: fk("asset_brands", true),
|
|
...inventoryColumns,
|
|
...specificationColumns,
|
|
};
|
|
const revision = {
|
|
id,
|
|
assetId: fk("assets"),
|
|
actorId: fk("users"),
|
|
siteId: fk("company_sites", true),
|
|
brandId: fk("asset_brands", true),
|
|
recordedAt: { type: DataTypes.STRING(30), allowNull: false },
|
|
...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);
|
|
}
|
|
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')"
|
|
);
|
|
}
|
|
}
|
|
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 writeWorkstation(
|
|
database,
|
|
{
|
|
workstation: {
|
|
id: workstationId,
|
|
companyId: first.companyId,
|
|
siteId: first.siteId,
|
|
personId: first.personId,
|
|
values,
|
|
},
|
|
revisions: [
|
|
{
|
|
id: revisionId,
|
|
workstationId,
|
|
actorId: first.actorId,
|
|
siteId: first.siteId,
|
|
personId: first.personId,
|
|
values,
|
|
recordedAt,
|
|
position: 0,
|
|
},
|
|
],
|
|
references: [],
|
|
},
|
|
new Set(),
|
|
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`);
|
|
}
|