193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
type TypeRow = {
|
|
id: string;
|
|
code: string;
|
|
role: string;
|
|
active: boolean;
|
|
canBeRoot: boolean;
|
|
};
|
|
|
|
type RuleRow = {
|
|
childTypeId: string;
|
|
parentTypeId: string;
|
|
};
|
|
|
|
type CountRow = { total: number };
|
|
|
|
const CREATED_TYPES_TABLE = 'f5_canonical_hierarchy_created_types';
|
|
const CREATED_RULES_TABLE = 'f5_canonical_hierarchy_created_rules';
|
|
|
|
const CANONICAL_TYPES = [
|
|
{
|
|
code: 'yacimiento',
|
|
name: 'Yacimiento',
|
|
description: 'Yacimiento perteneciente a un Área.',
|
|
},
|
|
{
|
|
code: 'instalacion',
|
|
name: 'Instalación',
|
|
description: 'Instancia física de una Instalación dentro de un Yacimiento.',
|
|
},
|
|
{
|
|
code: 'subinstalacion',
|
|
name: 'Subinstalación',
|
|
description: 'Instancia física subordinada a una Instalación.',
|
|
},
|
|
] as const;
|
|
|
|
const CANONICAL_RULES = [
|
|
['yacimiento', 'area'],
|
|
['instalacion', 'yacimiento'],
|
|
['subinstalacion', 'instalacion'],
|
|
] as const;
|
|
|
|
export class F5CanonicalInventoryHierarchy1790087150000 implements MigrationInterface {
|
|
name = 'F5CanonicalInventoryHierarchy1790087150000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE ${CREATED_TYPES_TABLE} (
|
|
type_id uuid PRIMARY KEY,
|
|
code varchar(80) NOT NULL UNIQUE,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_f5_canonical_created_type FOREIGN KEY (type_id)
|
|
REFERENCES asset_types(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE TABLE ${CREATED_RULES_TABLE} (
|
|
child_type_id uuid NOT NULL,
|
|
parent_type_id uuid NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (child_type_id,parent_type_id),
|
|
CONSTRAINT fk_f5_canonical_created_rule_child FOREIGN KEY (child_type_id)
|
|
REFERENCES asset_types(id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_f5_canonical_created_rule_parent FOREIGN KEY (parent_type_id)
|
|
REFERENCES asset_types(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
const area = await this.requireType(queryRunner, 'area');
|
|
if (area.role !== 'AREA' || !area.active || !area.canBeRoot) {
|
|
throw new Error('F5 requires canonical active root type area with AREA operational role');
|
|
}
|
|
|
|
for (const definition of CANONICAL_TYPES) {
|
|
const inserted = (await queryRunner.query(`
|
|
INSERT INTO asset_types(code,name,description,can_be_root,is_active,operational_role)
|
|
SELECT $1::varchar,$2::varchar,$3::text,false,true,'GENERIC'
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM asset_types WHERE lower(code)=lower($1::varchar)
|
|
)
|
|
RETURNING id,code,operational_role AS role,is_active AS active,can_be_root AS "canBeRoot"
|
|
`, [definition.code, definition.name, definition.description])) as TypeRow[];
|
|
|
|
if (inserted[0]?.id) {
|
|
await queryRunner.query(`
|
|
INSERT INTO ${CREATED_TYPES_TABLE}(type_id,code)
|
|
VALUES ($1::uuid,$2::varchar)
|
|
`, [inserted[0].id, definition.code]);
|
|
}
|
|
|
|
const type = await this.requireType(queryRunner, definition.code);
|
|
if (type.role !== 'GENERIC' || !type.active || type.canBeRoot) {
|
|
throw new Error(`F5 incompatible canonical type configuration: ${definition.code}`);
|
|
}
|
|
}
|
|
|
|
for (const [childCode, parentCode] of CANONICAL_RULES) {
|
|
const inserted = (await queryRunner.query(`
|
|
INSERT INTO asset_type_parent_rules(child_type_id,parent_type_id)
|
|
SELECT child.id,parent.id
|
|
FROM asset_types child CROSS JOIN asset_types parent
|
|
WHERE lower(child.code)=lower($1::varchar)
|
|
AND lower(parent.code)=lower($2::varchar)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM asset_type_parent_rules existing
|
|
WHERE existing.child_type_id=child.id AND existing.parent_type_id=parent.id
|
|
)
|
|
RETURNING child_type_id AS "childTypeId",parent_type_id AS "parentTypeId"
|
|
`, [childCode, parentCode])) as RuleRow[];
|
|
|
|
if (inserted[0]?.childTypeId && inserted[0]?.parentTypeId) {
|
|
await queryRunner.query(`
|
|
INSERT INTO ${CREATED_RULES_TABLE}(child_type_id,parent_type_id)
|
|
VALUES ($1::uuid,$2::uuid)
|
|
`, [inserted[0].childTypeId, inserted[0].parentTypeId]);
|
|
}
|
|
}
|
|
|
|
const [verified] = (await queryRunner.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM asset_type_parent_rules rule
|
|
JOIN asset_types child ON child.id=rule.child_type_id
|
|
JOIN asset_types parent ON parent.id=rule.parent_type_id
|
|
WHERE (lower(child.code)='yacimiento' AND lower(parent.code)='area')
|
|
OR (lower(child.code)='instalacion' AND lower(parent.code)='yacimiento')
|
|
OR (lower(child.code)='subinstalacion' AND lower(parent.code)='instalacion')
|
|
`)) as CountRow[];
|
|
if (Number(verified?.total ?? 0) !== 3) {
|
|
throw new Error(`F5 canonical hierarchy verification failed: rules=${Number(verified?.total ?? 0)}`);
|
|
}
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
const [usedCreatedTypes] = (await queryRunner.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM assets asset
|
|
JOIN ${CREATED_TYPES_TABLE} owned ON owned.type_id=asset.asset_type_id
|
|
`)) as CountRow[];
|
|
if (Number(usedCreatedTypes?.total ?? 0) !== 0) {
|
|
throw new Error('Cannot safely rollback F5 canonical hierarchy: an F5-created type is already used by Inventory');
|
|
}
|
|
|
|
const [foreignRules] = (await queryRunner.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM asset_type_parent_rules rule
|
|
WHERE (
|
|
rule.child_type_id IN (SELECT type_id FROM ${CREATED_TYPES_TABLE})
|
|
OR rule.parent_type_id IN (SELECT type_id FROM ${CREATED_TYPES_TABLE})
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM ${CREATED_RULES_TABLE} owned
|
|
WHERE owned.child_type_id=rule.child_type_id
|
|
AND owned.parent_type_id=rule.parent_type_id
|
|
)
|
|
`)) as CountRow[];
|
|
if (Number(foreignRules?.total ?? 0) !== 0) {
|
|
throw new Error('Cannot safely rollback F5 canonical hierarchy: an F5-created type gained external parent rules');
|
|
}
|
|
|
|
await queryRunner.query(`
|
|
DELETE FROM asset_type_parent_rules rule
|
|
USING ${CREATED_RULES_TABLE} owned
|
|
WHERE rule.child_type_id=owned.child_type_id
|
|
AND rule.parent_type_id=owned.parent_type_id
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
DELETE FROM asset_types type
|
|
USING ${CREATED_TYPES_TABLE} owned
|
|
WHERE type.id=owned.type_id
|
|
`);
|
|
|
|
await queryRunner.query(`DROP TABLE ${CREATED_RULES_TABLE}`);
|
|
await queryRunner.query(`DROP TABLE ${CREATED_TYPES_TABLE}`);
|
|
}
|
|
|
|
private async requireType(queryRunner: QueryRunner, code: string): Promise<TypeRow> {
|
|
const rows = (await queryRunner.query(`
|
|
SELECT id,code,operational_role AS role,is_active AS active,can_be_root AS "canBeRoot"
|
|
FROM asset_types
|
|
WHERE lower(code)=lower($1::varchar)
|
|
ORDER BY created_at
|
|
`, [code])) as TypeRow[];
|
|
|
|
if (rows.length !== 1) {
|
|
throw new Error(`F5 requires exactly one canonical asset type ${code}; found ${rows.length}`);
|
|
}
|
|
return rows[0];
|
|
}
|
|
}
|