import { MigrationInterface, QueryRunner } from 'typeorm'; /** * F5 makes the physical hierarchy Area-owned. Empresa is never a parent nor a * required property of Yacimiento/Instalación/Subinstalación. The current and * historical operator/concession truth lives in area_company_relations and is * frozen separately by each Inspección/Acta. * * operator_company_id is retained only as a backwards-compatible creation/ * historical snapshot. Runtime ownership and search MUST NOT depend on it. */ export class F5OperationalContextCompatibility1790087250000 implements MigrationInterface { name = 'F5OperationalContextCompatibility1790087250000'; public async up(queryRunner: QueryRunner): Promise { await this.installAreaOwnedGuard(queryRunner); } public async down(queryRunner: QueryRunner): Promise { await this.installLegacyPairedGuard(queryRunner); } private async installAreaOwnedGuard(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE OR REPLACE FUNCTION enforce_asset_operational_context() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE asset_role asset_type_operational_role; area_role asset_type_operational_role; company_role asset_type_operational_role; active_relation_id uuid; BEGIN SELECT operational_role INTO asset_role FROM asset_types WHERE id=NEW.asset_type_id; IF asset_role <> 'GENERIC'::asset_type_operational_role THEN IF NEW.operational_area_id IS NOT NULL OR NEW.operator_company_id IS NOT NULL THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='Área y Empresa no reciben contexto operativo de Inventario'; END IF; RETURN NEW; END IF; IF NEW.operator_company_id IS NOT NULL AND NEW.operational_area_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='Un snapshot de Empresa requiere un Área física'; END IF; -- Once written, an old/current company snapshot cannot be repointed to -- simulate physical ownership. Company changes happen in the temporal -- Area↔Empresa relation instead. IF TG_OP='UPDATE' AND NEW.operator_company_id IS DISTINCT FROM OLD.operator_company_id THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='La Empresa se cambia en la relación temporal del Área, no en el Inventario'; END IF; IF NEW.operational_area_id IS NULL THEN RETURN NEW; END IF; SELECT type.operational_role INTO area_role FROM assets area JOIN asset_types type ON type.id=area.asset_type_id WHERE area.id=NEW.operational_area_id AND area.information_status<>'INACTIVE' AND type.is_active=true; IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='operational area must be an active AREA asset'; END IF; IF NEW.parent_id IS NULL OR NOT EXISTS ( WITH RECURSIVE ancestors AS ( SELECT id,parent_id FROM assets WHERE id=NEW.parent_id UNION ALL SELECT parent.id,parent.parent_id FROM assets parent JOIN ancestors child ON parent.id=child.parent_id ) SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1 ) THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='operational area must be an ancestor in the physical hierarchy'; END IF; -- A company value is allowed only as the context snapshot that was valid -- at creation time. It is never used to decide future membership. IF NEW.operator_company_id IS NOT NULL THEN SELECT type.operational_role INTO company_role FROM assets company JOIN asset_types type ON type.id=company.asset_type_id WHERE company.id=NEW.operator_company_id AND company.information_status<>'INACTIVE' AND type.is_active=true; IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='operator snapshot must reference an active COMPANY-role asset'; END IF; IF TG_OP='INSERT' THEN SELECT relation.id INTO active_relation_id FROM area_company_relations relation WHERE relation.area_id=NEW.operational_area_id AND relation.company_id=NEW.operator_company_id AND relation.relation_role='OPERATOR'::area_organization_role AND relation.valid_until IS NULL FOR KEY SHARE; IF active_relation_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='creation operator snapshot must be active for the selected Area'; END IF; END IF; END IF; RETURN NEW; END $$; `); } /** Restores the production F4-era paired Area+Empresa guard on rollback. */ private async installLegacyPairedGuard(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE OR REPLACE FUNCTION enforce_asset_operational_context() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE asset_role asset_type_operational_role; area_role asset_type_operational_role; company_role asset_type_operational_role; active_relation_id uuid; BEGIN IF NEW.operational_area_id IS NULL AND NEW.operator_company_id IS NULL THEN RETURN NEW; END IF; IF NEW.operational_area_id IS NULL OR NEW.operator_company_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514', MESSAGE='operational area and organization must be assigned together'; END IF; SELECT operational_role INTO asset_role FROM asset_types WHERE id=NEW.asset_type_id; IF asset_role <> 'GENERIC'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='area and organization assets cannot receive an operational assignment'; END IF; SELECT type.operational_role INTO area_role FROM assets area JOIN asset_types type ON type.id=area.asset_type_id WHERE area.id=NEW.operational_area_id AND area.information_status<>'INACTIVE' AND type.is_active=true; SELECT type.operational_role INTO company_role FROM assets company JOIN asset_types type ON type.id=company.asset_type_id WHERE company.id=NEW.operator_company_id AND company.information_status<>'INACTIVE' AND type.is_active=true; IF area_role IS DISTINCT FROM 'AREA'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an active AREA asset'; END IF; IF company_role IS DISTINCT FROM 'COMPANY'::asset_type_operational_role THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operator organization must be an active COMPANY-role asset'; END IF; SELECT relation.id INTO active_relation_id FROM area_company_relations relation WHERE relation.area_id=NEW.operational_area_id AND relation.company_id=NEW.operator_company_id AND relation.relation_role='OPERATOR'::area_organization_role AND relation.valid_until IS NULL FOR KEY SHARE; IF active_relation_id IS NULL THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area and organization do not have an active OPERATOR relation'; END IF; IF NEW.parent_id IS NULL OR NOT EXISTS ( WITH RECURSIVE ancestors AS ( SELECT id,parent_id FROM assets WHERE id=NEW.parent_id UNION ALL SELECT parent.id,parent.parent_id FROM assets parent JOIN ancestors child ON parent.id=child.parent_id ) SELECT 1 FROM ancestors WHERE id=NEW.operational_area_id LIMIT 1 ) THEN RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='operational area must be an ancestor in the physical hierarchy'; END IF; RETURN NEW; END $$; `); } }