223 lines
7.7 KiB
TypeScript
223 lines
7.7 KiB
TypeScript
import type { Sequelize, Transaction } from "sequelize";
|
|
import { DataTypes, QueryTypes } from "sequelize";
|
|
import { z } from "zod/v4";
|
|
|
|
import { Resource } from "../../domain/resource.aggregate";
|
|
import {
|
|
ResourceSpecification,
|
|
ResourceValues,
|
|
} from "../../domain/value-objects/resource-values.vo";
|
|
|
|
import { writeInventory } from "./resource-inventory-relational";
|
|
import { ResourcePersistenceMapper, inventoryIdentity } from "./resource-persistence.mapper";
|
|
|
|
const mappingTable = "support_tool_resource_migrations";
|
|
|
|
async function specification(database: Sequelize, transaction: Transaction) {
|
|
const templates = await database.query<{ id: string; name: string }>(
|
|
"SELECT id,name FROM resource_definitions WHERE stockKey='template:support'",
|
|
{ type: QueryTypes.SELECT, transaction }
|
|
);
|
|
const template = templates[0];
|
|
|
|
if (!template) {
|
|
throw new Error("Plantilla de asistencia ausente.");
|
|
}
|
|
const fields = await database.query<{ fieldId: string; name: string; unit: string }>(
|
|
"SELECT f.id AS fieldId,f.name,f.unit FROM resource_template_fields b JOIN resource_definitions f ON f.id=b.fieldId WHERE b.templateId=:id ORDER BY b.position",
|
|
{ replacements: { id: template.id }, type: QueryTypes.SELECT, transaction }
|
|
);
|
|
const result = ResourceSpecification.create({
|
|
templateId: template.id,
|
|
templateName: template.name,
|
|
fields: fields.map((field) => ({ ...field, type: "text", choices: [], required: false })),
|
|
});
|
|
|
|
if (!result.isSuccess) {
|
|
throw new Error("Especificación de asistencia inválida.");
|
|
}
|
|
|
|
return result.data;
|
|
}
|
|
|
|
/** Traslada el catálogo a recursos por compañía y conserva las conexiones y sus revisiones. */
|
|
export async function migrateSupportResources(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 }
|
|
);
|
|
|
|
if (!tables.some((table) => table.name === "support_tools")) {
|
|
if (tables.some((table) => table.name === "support_catalog_lock")) {
|
|
await query.dropTable("support_catalog_lock");
|
|
}
|
|
|
|
return;
|
|
}
|
|
if (!tables.some((table) => table.name === mappingTable)) {
|
|
await query.createTable(mappingTable, {
|
|
toolId: { type: DataTypes.UUID, primaryKey: true },
|
|
companyId: { type: DataTypes.UUID, primaryKey: true },
|
|
resourceId: {
|
|
type: DataTypes.UUID,
|
|
allowNull: false,
|
|
unique: true,
|
|
references: { model: "resources", key: "id" },
|
|
onDelete: "RESTRICT",
|
|
},
|
|
});
|
|
}
|
|
await database.transaction((transaction) => migrateTools(database, transaction));
|
|
for (const table of ["asset_support", "asset_support_revisions"] as const) {
|
|
if (!tables.some((item) => item.name === table)) {
|
|
continue;
|
|
}
|
|
await migrateConnectionReferences(database, table);
|
|
}
|
|
await query.dropTable(mappingTable);
|
|
await query.dropTable("support_tools");
|
|
if (tables.some((table) => table.name === "support_catalog_lock")) {
|
|
await query.dropTable("support_catalog_lock");
|
|
}
|
|
}
|
|
|
|
async function migrateConnectionReferences(
|
|
database: Sequelize,
|
|
table: "asset_support" | "asset_support_revisions"
|
|
) {
|
|
const query = database.getQueryInterface();
|
|
const columns = await query.describeTable(table);
|
|
|
|
if (!columns.toolId) {
|
|
return;
|
|
}
|
|
if (!columns.resourceId) {
|
|
await query.addColumn(table, "resourceId", { type: DataTypes.UUID, allowNull: true });
|
|
}
|
|
await database.query(
|
|
table === "asset_support"
|
|
? "UPDATE asset_support s JOIN support_tool_resource_migrations m ON m.companyId=s.companyId AND m.toolId=s.toolId SET s.resourceId=m.resourceId"
|
|
: "UPDATE asset_support_revisions h JOIN asset_support s ON s.id=h.connectionId JOIN support_tool_resource_migrations m ON m.companyId=s.companyId AND m.toolId=h.toolId SET h.resourceId=m.resourceId"
|
|
);
|
|
const missing = await database.query(
|
|
`SELECT id FROM \`${table}\` WHERE resourceId IS NULL LIMIT 1`,
|
|
{ type: QueryTypes.SELECT }
|
|
);
|
|
|
|
if (missing.length) {
|
|
throw new Error("Conexión de asistencia sin recurso migrado.");
|
|
}
|
|
await query.changeColumn(table, "resourceId", { type: DataTypes.UUID, allowNull: false });
|
|
const constraints = await database.query<{ name: string }>(
|
|
"SELECT CONSTRAINT_NAME AS name FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=:table AND COLUMN_NAME='toolId' AND REFERENCED_TABLE_NAME IS NOT NULL",
|
|
{ replacements: { table }, type: QueryTypes.SELECT }
|
|
);
|
|
|
|
for (const constraint of constraints) {
|
|
await query.removeConstraint(table, constraint.name);
|
|
}
|
|
await database.query(`ALTER TABLE ${table} DROP COLUMN toolId`);
|
|
}
|
|
|
|
const ToolSchema = z.object({
|
|
id: z.uuid(),
|
|
name: z.string(),
|
|
active: z.union([z.boolean(), z.literal(0), z.literal(1)]).transform(Boolean),
|
|
});
|
|
|
|
async function migrateTools(database: Sequelize, transaction: Transaction) {
|
|
const companies = await database.query<{ id: string }>(
|
|
"SELECT id FROM companies ORDER BY id FOR UPDATE",
|
|
{ type: QueryTypes.SELECT, transaction }
|
|
);
|
|
|
|
if (!companies.length) {
|
|
return;
|
|
}
|
|
const actors = await database.query<{ id: string }>(
|
|
"SELECT id FROM users ORDER BY isInitialAdmin DESC,id LIMIT 1",
|
|
{ type: QueryTypes.SELECT, transaction }
|
|
);
|
|
const actor = actors[0];
|
|
|
|
if (!actor) {
|
|
throw new Error("La migración necesita un usuario para la auditoría.");
|
|
}
|
|
const rows = await database.query<Record<string, unknown>>(
|
|
"SELECT id,name,active FROM support_tools ORDER BY id FOR UPDATE",
|
|
{ type: QueryTypes.SELECT, transaction }
|
|
);
|
|
const tools = rows.map((row) => ToolSchema.parse(row));
|
|
const spec = await specification(database, transaction);
|
|
|
|
for (const company of companies) {
|
|
for (const tool of tools) {
|
|
await migrateTool(database, company.id, tool, spec, actor.id, transaction);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function migrateTool(
|
|
database: Sequelize,
|
|
companyId: string,
|
|
tool: z.infer<typeof ToolSchema>,
|
|
spec: ResourceSpecification,
|
|
actorId: string,
|
|
transaction: Transaction
|
|
) {
|
|
const replacements = { toolId: tool.id, companyId };
|
|
const existing = await database.query(
|
|
"SELECT resourceId FROM support_tool_resource_migrations WHERE toolId=:toolId AND companyId=:companyId",
|
|
{ replacements, type: QueryTypes.SELECT, transaction }
|
|
);
|
|
|
|
if (existing.length) {
|
|
return;
|
|
}
|
|
const values = ResourceValues.create(
|
|
{
|
|
templateId: spec.templateId.toPrimitive(),
|
|
name: tool.name,
|
|
active: tool.active,
|
|
notes: "",
|
|
characteristics: {},
|
|
},
|
|
spec
|
|
);
|
|
|
|
if (!values.isSuccess) {
|
|
throw new Error("Herramienta de asistencia inválida.");
|
|
}
|
|
const resource = Resource.create(
|
|
inventoryIdentity(companyId),
|
|
spec,
|
|
values.data,
|
|
inventoryIdentity(actorId),
|
|
new Date().toISOString()
|
|
);
|
|
|
|
if (!resource.isSuccess) {
|
|
throw new Error("No se pudo crear el recurso de asistencia.");
|
|
}
|
|
const rows = new ResourcePersistenceMapper().toPersistence(resource.data);
|
|
// Conserva el UUID de la herramienta en la primera compañía que pueda utilizarlo.
|
|
const occupied = await database.query("SELECT id FROM resources WHERE id=:id", {
|
|
replacements: { id: tool.id },
|
|
type: QueryTypes.SELECT,
|
|
transaction,
|
|
});
|
|
|
|
if (!occupied.length) {
|
|
rows.resource.id = tool.id;
|
|
for (const revision of rows.revisions) {
|
|
revision.resourceId = tool.id;
|
|
}
|
|
}
|
|
await writeInventory(database, rows, new Set(), transaction);
|
|
await database.query(
|
|
"INSERT INTO support_tool_resource_migrations (toolId,companyId,resourceId) VALUES (:toolId,:companyId,:resourceId)",
|
|
{ replacements: { ...replacements, resourceId: rows.resource.id }, transaction }
|
|
);
|
|
}
|