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 { 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 { 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 { 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]; } }