F5.1: reset de datos y jerarquía manual con Departamento
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class F51CleanManualInventory1790087300000 implements MigrationInterface {
|
||||
name = 'F51CleanManualInventory1790087300000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// F5.1 is an intentional clean-start cut. The deployment process creates a
|
||||
// full database backup before migrations, so old domain data is recovered
|
||||
// from that backup rather than by pretending a destructive migration can
|
||||
// reconstruct historical rows.
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_f5_canonical_asset_hierarchy ON assets');
|
||||
|
||||
// Remove all operational/domain instances and every table that depends on
|
||||
// them (visits, acts, findings, reports, versions, media, relations, etc.).
|
||||
// Users/roles/permissions and technical configuration are deliberately not
|
||||
// part of this TRUNCATE.
|
||||
await queryRunner.query('TRUNCATE TABLE assets CASCADE');
|
||||
|
||||
// Imported territorial/source material must not silently repopulate or
|
||||
// influence the new manually curated structure.
|
||||
await queryRunner.query('TRUNCATE TABLE administrative_departments CASCADE');
|
||||
await queryRunner.query('TRUNCATE TABLE source_documents CASCADE');
|
||||
|
||||
// Start the classification ↔ finding applicability review from zero while
|
||||
// preserving both master catalogs themselves.
|
||||
await queryRunner.query('TRUNCATE TABLE finding_catalog_item_inventory_families');
|
||||
|
||||
// Explicitly clear audit history, including authentication/admin events
|
||||
// accumulated during development. New events continue to be recorded after
|
||||
// this migration.
|
||||
await queryRunner.query('TRUNCATE TABLE audit_events');
|
||||
|
||||
// Import/reconciliation tables can contain rows not connected to a current
|
||||
// Asset. Clear every asset_import_* data table without coupling this cut to
|
||||
// one historical import implementation.
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
DECLARE table_name text;
|
||||
BEGIN
|
||||
FOR table_name IN
|
||||
SELECT tablename
|
||||
FROM pg_tables
|
||||
WHERE schemaname = current_schema()
|
||||
AND tablename LIKE 'asset_import_%'
|
||||
LOOP
|
||||
EXECUTE format('TRUNCATE TABLE %I CASCADE', table_name);
|
||||
END LOOP;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
// Departamento becomes the real root of the physical Inventory tree.
|
||||
await queryRunner.query(`
|
||||
INSERT INTO asset_types(code,name,description,can_be_root,is_active,operational_role)
|
||||
SELECT 'departamento','Departamento','Departamento administrativo que contiene Áreas.',true,true,'GENERIC'
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM asset_types WHERE lower(code)='departamento'
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE asset_types
|
||||
SET name='Departamento',
|
||||
description='Departamento administrativo que contiene Áreas.',
|
||||
can_be_root=true,
|
||||
is_active=true,
|
||||
operational_role='GENERIC',
|
||||
updated_at=CURRENT_TIMESTAMP
|
||||
WHERE lower(code)='departamento'
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE asset_types
|
||||
SET can_be_root=false,updated_at=CURRENT_TIMESTAMP
|
||||
WHERE lower(code)='area'
|
||||
`);
|
||||
|
||||
// Area has exactly one canonical structural parent kind: Departamento.
|
||||
await queryRunner.query(`
|
||||
DELETE FROM asset_type_parent_rules rule
|
||||
USING asset_types child
|
||||
WHERE rule.child_type_id=child.id AND lower(child.code)='area'
|
||||
`);
|
||||
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)='area' AND lower(parent.code)='departamento'
|
||||
ON CONFLICT (child_type_id,parent_type_id) DO NOTHING
|
||||
`);
|
||||
|
||||
// Database-level guard: UI/API bugs cannot create an invalid physical tree.
|
||||
await queryRunner.query(`
|
||||
CREATE OR REPLACE FUNCTION enforce_f5_canonical_asset_hierarchy()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE child_code text; parent_code text;
|
||||
BEGIN
|
||||
SELECT lower(code) INTO child_code FROM asset_types WHERE id=NEW.asset_type_id;
|
||||
|
||||
IF child_code IN ('empresa','organizacion','departamento') THEN
|
||||
IF NEW.parent_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Empresa y Departamento son maestros raíz independientes';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF child_code NOT IN ('area','yacimiento','instalacion','subinstalacion') THEN
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF NEW.parent_id IS NULL THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La estructura requiere Departamento → Área → Yacimiento → Instalación → Subinstalación';
|
||||
END IF;
|
||||
|
||||
SELECT lower(type.code) INTO parent_code
|
||||
FROM assets parent
|
||||
JOIN asset_types type ON type.id=parent.asset_type_id
|
||||
WHERE parent.id=NEW.parent_id;
|
||||
|
||||
IF (child_code='area' AND parent_code<>'departamento')
|
||||
OR (child_code='yacimiento' AND parent_code<>'area')
|
||||
OR (child_code='instalacion' AND parent_code<>'yacimiento')
|
||||
OR (child_code='subinstalacion' AND parent_code<>'instalacion') THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Jerarquía inválida: Departamento → Área → Yacimiento → Instalación → Subinstalación';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE TRIGGER trg_f5_canonical_asset_hierarchy
|
||||
BEFORE INSERT OR UPDATE OF asset_type_id,parent_id ON assets
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_f5_canonical_asset_hierarchy()
|
||||
`);
|
||||
|
||||
const rows = (await queryRunner.query(`
|
||||
SELECT
|
||||
(SELECT COUNT(*)::integer FROM assets) AS assets,
|
||||
(SELECT COUNT(*)::integer FROM audit_events) AS audits,
|
||||
(SELECT COUNT(*)::integer FROM finding_catalog_item_inventory_families) AS applicability,
|
||||
(SELECT COUNT(*)::integer FROM asset_types WHERE lower(code)='departamento' AND can_be_root=true AND is_active=true) AS departments,
|
||||
(SELECT COUNT(*)::integer
|
||||
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)='area' AND lower(parent.code)='departamento') AS area_rules
|
||||
`)) as Array<{ assets: number; audits: number; applicability: number; departments: number; area_rules: number }>;
|
||||
const check = rows[0];
|
||||
if (!check || Number(check.assets) !== 0 || Number(check.audits) !== 0 || Number(check.applicability) !== 0
|
||||
|| Number(check.departments) !== 1 || Number(check.area_rules) !== 1) {
|
||||
throw new Error(`F5.1 clean-start verification failed: ${JSON.stringify(check ?? {})}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
throw new Error('F5.1 is an intentional destructive clean-start migration. Restore the pre-deploy database backup to recover previous data.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user