fix(f5): guarantee canonical inventory hierarchy before territory preload
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user