298 lines
9.5 KiB
TypeScript
298 lines
9.5 KiB
TypeScript
import type { Sequelize, Transaction } from "sequelize";
|
|
import { DataTypes, QueryTypes } from "sequelize";
|
|
import { z } from "zod/v4";
|
|
|
|
import type { ResourceSpecificationData } from "../../domain/resource-values";
|
|
import { ResourceSpecificationSchema } from "../../domain/resource-values";
|
|
|
|
import { readResourceRows, writeResourceRow } from "./resource-relational-sql";
|
|
|
|
const bool = z.union([z.boolean(), z.literal(0), z.literal(1)]).transform(Boolean);
|
|
const identity = z.object({ revisionId: z.uuid(), resourceId: z.uuid() });
|
|
const headerSchema = identity.extend({
|
|
templateId: z.uuid(),
|
|
templateName: z.string(),
|
|
sectionsDefined: bool,
|
|
});
|
|
const fieldSchema = identity.extend({
|
|
fieldId: z.uuid(),
|
|
name: z.string(),
|
|
fieldType: z.enum(["text", "multiline", "number", "date", "boolean", "choice"]),
|
|
unit: z.string(),
|
|
required: bool,
|
|
sectionId: z.uuid().nullable(),
|
|
position: z.number().int(),
|
|
});
|
|
const sectionSchema = identity.extend({
|
|
id: z.uuid(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
position: z.number().int(),
|
|
});
|
|
const choiceSchema = identity.extend({
|
|
fieldId: z.uuid(),
|
|
position: z.number().int(),
|
|
value: z.string(),
|
|
});
|
|
|
|
/** Se ejecuta antes de que una estructura viva pueda cambiar; las revisiones anteriores reciben su instantánea original. */
|
|
export async function migrateResourceRevisionSpecifications(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 revision = {
|
|
type: DataTypes.UUID,
|
|
primaryKey: true,
|
|
references: { model: "resource_revisions", key: "id" },
|
|
onDelete: "CASCADE",
|
|
};
|
|
const parent = {
|
|
type: DataTypes.UUID,
|
|
allowNull: false,
|
|
references: { model: "resources", key: "id" },
|
|
onDelete: "RESTRICT",
|
|
};
|
|
const definitions = [
|
|
[
|
|
"resource_revision_specifications",
|
|
{
|
|
revisionId: revision,
|
|
resourceId: parent,
|
|
templateId: { type: DataTypes.UUID, allowNull: false },
|
|
templateName: { type: DataTypes.STRING(100), allowNull: false },
|
|
sectionsDefined: { type: DataTypes.BOOLEAN, allowNull: false },
|
|
},
|
|
],
|
|
[
|
|
"resource_revision_specification_sections",
|
|
{
|
|
revisionId: revision,
|
|
resourceId: parent,
|
|
id: { type: DataTypes.UUID, primaryKey: true },
|
|
name: { type: DataTypes.STRING(100), allowNull: false },
|
|
description: { type: DataTypes.TEXT, allowNull: false },
|
|
position: { type: DataTypes.INTEGER, allowNull: false },
|
|
},
|
|
],
|
|
[
|
|
"resource_revision_specification_fields",
|
|
{
|
|
revisionId: revision,
|
|
resourceId: parent,
|
|
fieldId: {
|
|
type: DataTypes.UUID,
|
|
primaryKey: true,
|
|
references: { model: "resource_definitions", key: "id" },
|
|
onDelete: "RESTRICT",
|
|
},
|
|
name: { type: DataTypes.STRING(100), allowNull: false },
|
|
fieldType: { type: DataTypes.STRING(12), allowNull: false },
|
|
unit: { type: DataTypes.STRING(30), allowNull: false },
|
|
required: { type: DataTypes.BOOLEAN, allowNull: false },
|
|
sectionId: { type: DataTypes.UUID, allowNull: true },
|
|
position: { type: DataTypes.INTEGER, allowNull: false },
|
|
},
|
|
],
|
|
[
|
|
"resource_revision_specification_choices",
|
|
{
|
|
revisionId: revision,
|
|
resourceId: parent,
|
|
fieldId: { type: DataTypes.UUID, primaryKey: true },
|
|
position: { type: DataTypes.INTEGER, primaryKey: true },
|
|
value: { type: DataTypes.STRING(100), allowNull: false },
|
|
},
|
|
],
|
|
] as const;
|
|
|
|
for (const [table, columns] of definitions) {
|
|
if (!tables.some((item) => item.name === table)) {
|
|
await query.createTable(table, columns);
|
|
}
|
|
}
|
|
await database.transaction(async (transaction) => {
|
|
const missing = await database.query<{ id: string }>(
|
|
"SELECT r.id FROM resource_revisions r LEFT JOIN resource_revision_specifications s ON s.revisionId=r.id WHERE s.revisionId IS NULL",
|
|
{ type: QueryTypes.SELECT, transaction }
|
|
);
|
|
|
|
for (let offset = 0; offset < missing.length; offset += 500) {
|
|
const ids = missing.slice(offset, offset + 500).map((item) => item.id);
|
|
const options = { replacements: { ids }, transaction };
|
|
|
|
await database.query(
|
|
"INSERT INTO resource_revision_specifications (revisionId,resourceId,templateId,templateName,sectionsDefined) SELECT r.id,a.id,a.templateId,a.templateName,a.sectionsDefined FROM resource_revisions r JOIN resources a ON a.id=r.resourceId WHERE r.id IN (:ids)",
|
|
options
|
|
);
|
|
for (const [suffix, columns] of [
|
|
["sections", "id,name,description,position"],
|
|
["fields", "fieldId,name,fieldType,unit,required,sectionId,position"],
|
|
["choices", "fieldId,position,value"],
|
|
] as const) {
|
|
const source =
|
|
suffix === "choices"
|
|
? "resource_specification_field_choices"
|
|
: `resource_specification_${suffix}`;
|
|
const selected = columns
|
|
.split(",")
|
|
.map((column) => `s.\`${column}\``)
|
|
.join(",");
|
|
|
|
await database.query(
|
|
`INSERT INTO resource_revision_specification_${suffix} (revisionId,resourceId,${columns
|
|
.split(",")
|
|
.map((column) => `\`${column}\``)
|
|
.join(
|
|
","
|
|
)}) SELECT r.id,r.resourceId,${selected} FROM resource_revisions r JOIN ${source} s ON s.resourceId=r.resourceId WHERE r.id IN (:ids)`,
|
|
options
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
export async function readResourceRevisionSpecifications(
|
|
database: Sequelize,
|
|
ids: readonly string[],
|
|
transaction: Transaction,
|
|
revisionIds?: readonly string[]
|
|
) {
|
|
const headers = z
|
|
.array(headerSchema)
|
|
.parse(
|
|
await readResourceRows(
|
|
database,
|
|
"resource_revision_specifications",
|
|
revisionIds ? "revisionId" : "resourceId",
|
|
revisionIds ?? ids,
|
|
transaction
|
|
)
|
|
);
|
|
const fields = z
|
|
.array(fieldSchema)
|
|
.parse(
|
|
await readResourceRows(
|
|
database,
|
|
"resource_revision_specification_fields",
|
|
revisionIds ? "revisionId" : "resourceId",
|
|
revisionIds ?? ids,
|
|
transaction
|
|
)
|
|
);
|
|
const sections = z
|
|
.array(sectionSchema)
|
|
.parse(
|
|
await readResourceRows(
|
|
database,
|
|
"resource_revision_specification_sections",
|
|
revisionIds ? "revisionId" : "resourceId",
|
|
revisionIds ?? ids,
|
|
transaction
|
|
)
|
|
);
|
|
const choices = z
|
|
.array(choiceSchema)
|
|
.parse(
|
|
await readResourceRows(
|
|
database,
|
|
"resource_revision_specification_choices",
|
|
revisionIds ? "revisionId" : "resourceId",
|
|
revisionIds ?? ids,
|
|
transaction
|
|
)
|
|
);
|
|
|
|
return new Map(
|
|
headers.map((header) => [
|
|
header.revisionId,
|
|
ResourceSpecificationSchema.parse({
|
|
templateId: header.templateId,
|
|
templateName: header.templateName,
|
|
...(header.sectionsDefined
|
|
? {
|
|
sections: sections
|
|
.filter((item) => item.revisionId === header.revisionId)
|
|
.sort((a, b) => a.position - b.position)
|
|
.map(({ id, name, description }) => ({ id, name, description })),
|
|
}
|
|
: {}),
|
|
fields: fields
|
|
.filter((item) => item.revisionId === header.revisionId)
|
|
.sort((a, b) => a.position - b.position)
|
|
.map((field) => ({
|
|
fieldId: field.fieldId,
|
|
name: field.name,
|
|
type: field.fieldType,
|
|
unit: field.unit,
|
|
required: field.required,
|
|
...(field.sectionId ? { sectionId: field.sectionId } : {}),
|
|
choices: choices
|
|
.filter(
|
|
(item) => item.revisionId === header.revisionId && item.fieldId === field.fieldId
|
|
)
|
|
.sort((a, b) => a.position - b.position)
|
|
.map((item) => item.value),
|
|
})),
|
|
}),
|
|
])
|
|
);
|
|
}
|
|
|
|
export async function writeResourceRevisionSpecification(
|
|
database: Sequelize,
|
|
resourceId: string,
|
|
revisionId: string,
|
|
specification: ResourceSpecificationData,
|
|
transaction: Transaction
|
|
) {
|
|
const identity = { resourceId, revisionId };
|
|
|
|
await writeResourceRow(
|
|
database,
|
|
"resource_revision_specifications",
|
|
{
|
|
...identity,
|
|
templateId: specification.templateId,
|
|
templateName: specification.templateName,
|
|
sectionsDefined: specification.sections !== undefined,
|
|
},
|
|
transaction
|
|
);
|
|
for (const [position, section] of (specification.sections ?? []).entries()) {
|
|
await writeResourceRow(
|
|
database,
|
|
"resource_revision_specification_sections",
|
|
{ ...identity, ...section, position },
|
|
transaction
|
|
);
|
|
}
|
|
for (const [position, field] of specification.fields.entries()) {
|
|
await writeResourceRow(
|
|
database,
|
|
"resource_revision_specification_fields",
|
|
{
|
|
...identity,
|
|
fieldId: field.fieldId,
|
|
name: field.name,
|
|
fieldType: field.type,
|
|
unit: field.unit,
|
|
required: field.required,
|
|
sectionId: field.sectionId ?? null,
|
|
position,
|
|
},
|
|
transaction
|
|
);
|
|
for (const [choicePosition, value] of field.choices.entries()) {
|
|
await writeResourceRow(
|
|
database,
|
|
"resource_revision_specification_choices",
|
|
{ ...identity, fieldId: field.fieldId, position: choicePosition, value },
|
|
transaction
|
|
);
|
|
}
|
|
}
|
|
}
|