feat(f6): solidificar compatibilidades y campos técnicos por clasificación
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class F6SolidInventoryModel1790091000000 implements MigrationInterface {
|
||||
name = 'F6SolidInventoryModel1790091000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// A technical Subinstallation family may be valid under several Installation
|
||||
// families. F3.1 used child_family_id as the PK, which made this relation
|
||||
// accidentally one-to-one from the child's point of view.
|
||||
await queryRunner.query(`
|
||||
DO $$
|
||||
DECLARE constraint_name text;
|
||||
BEGIN
|
||||
SELECT con.conname INTO constraint_name
|
||||
FROM pg_constraint con
|
||||
JOIN pg_class rel ON rel.oid=con.conrelid
|
||||
WHERE rel.relname='inventory_family_parent_rules' AND con.contype='p'
|
||||
LIMIT 1;
|
||||
IF constraint_name IS NOT NULL THEN
|
||||
EXECUTE format('ALTER TABLE inventory_family_parent_rules DROP CONSTRAINT %I',constraint_name);
|
||||
END IF;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE inventory_family_parent_rules
|
||||
ADD CONSTRAINT pk_inventory_family_parent_rules
|
||||
PRIMARY KEY (child_family_id,parent_family_id)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_family_parent_rules_parent
|
||||
ON inventory_family_parent_rules(parent_family_id,child_family_id)
|
||||
`);
|
||||
|
||||
// Protect compatibility semantics at database level.
|
||||
await queryRunner.query(`
|
||||
CREATE OR REPLACE FUNCTION enforce_inventory_family_compatibility_rule()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE child_level text; parent_level text;
|
||||
BEGIN
|
||||
SELECT level::text INTO child_level FROM inventory_families WHERE id=NEW.child_family_id;
|
||||
SELECT level::text INTO parent_level FROM inventory_families WHERE id=NEW.parent_family_id;
|
||||
IF child_level IS DISTINCT FROM 'SUBINSTALLATION' OR parent_level IS DISTINCT FROM 'INSTALLATION' THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La compatibilidad debe vincular Subinstalación con Instalación';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_inventory_family_compatibility_rule ON inventory_family_parent_rules');
|
||||
await queryRunner.query(`
|
||||
CREATE TRIGGER trg_inventory_family_compatibility_rule
|
||||
BEFORE INSERT OR UPDATE OF child_family_id,parent_family_id ON inventory_family_parent_rules
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_inventory_family_compatibility_rule()
|
||||
`);
|
||||
|
||||
// Family-specific technical fields. Generic asset type attributes remain for
|
||||
// data genuinely shared by a whole structural level.
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS inventory_family_attribute_definitions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
inventory_family_id uuid NOT NULL REFERENCES inventory_families(id) ON DELETE CASCADE,
|
||||
code varchar(80) NOT NULL,
|
||||
name varchar(160) NOT NULL,
|
||||
data_type asset_attribute_data_type NOT NULL,
|
||||
is_required boolean NOT NULL DEFAULT false,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
unit varchar(40) NULL,
|
||||
options jsonb NULL,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
created_by uuid NULL,
|
||||
updated_by uuid NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_inventory_family_attribute_code UNIQUE(inventory_family_id,code),
|
||||
CONSTRAINT chk_inventory_family_attribute_code CHECK (code ~ '^[a-z][a-z0-9_]*$'),
|
||||
CONSTRAINT chk_inventory_family_attribute_options CHECK (
|
||||
(data_type='SELECT' AND options IS NOT NULL AND jsonb_typeof(options)='array')
|
||||
OR (data_type<>'SELECT')
|
||||
)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_inventory_family_attributes_family
|
||||
ON inventory_family_attribute_definitions(inventory_family_id,is_active,sort_order,name)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS asset_inventory_attribute_values (
|
||||
asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||
definition_id uuid NOT NULL REFERENCES inventory_family_attribute_definitions(id) ON DELETE CASCADE,
|
||||
value jsonb NOT NULL,
|
||||
updated_by uuid NULL,
|
||||
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(asset_id,definition_id)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_inventory_attribute_values_definition
|
||||
ON asset_inventory_attribute_values(definition_id,asset_id)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE OR REPLACE FUNCTION enforce_asset_inventory_attribute_family()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE asset_family uuid; definition_family uuid;
|
||||
BEGIN
|
||||
SELECT inventory_family_id INTO asset_family FROM assets WHERE id=NEW.asset_id;
|
||||
SELECT inventory_family_id INTO definition_family
|
||||
FROM inventory_family_attribute_definitions WHERE id=NEW.definition_id AND is_active=true;
|
||||
IF asset_family IS NULL OR definition_family IS NULL OR asset_family<>definition_family THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='El campo técnico no pertenece a la clasificación del Inventario';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_asset_inventory_attribute_family ON asset_inventory_attribute_values');
|
||||
await queryRunner.query(`
|
||||
CREATE TRIGGER trg_asset_inventory_attribute_family
|
||||
BEFORE INSERT OR UPDATE OF asset_id,definition_id ON asset_inventory_attribute_values
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_asset_inventory_attribute_family()
|
||||
`);
|
||||
|
||||
// Enforce classification level and Subinstallation compatibility on every
|
||||
// physical Inventory write, not just through the current WEB/API.
|
||||
await queryRunner.query(`
|
||||
CREATE OR REPLACE FUNCTION enforce_f6_asset_family_contract()
|
||||
RETURNS trigger LANGUAGE plpgsql AS $$
|
||||
DECLARE child_type text; family_level text; parent_family uuid;
|
||||
BEGIN
|
||||
SELECT lower(code) INTO child_type FROM asset_types WHERE id=NEW.asset_type_id;
|
||||
|
||||
IF child_type IN ('departamento','area','yacimiento') THEN
|
||||
IF NEW.inventory_family_id IS NOT NULL THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='Departamento, Área y Yacimiento no llevan clasificación técnica';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF child_type='instalacion' THEN
|
||||
IF NEW.inventory_family_id IS NULL THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La Instalación requiere clasificación técnica';
|
||||
END IF;
|
||||
SELECT level::text INTO family_level FROM inventory_families WHERE id=NEW.inventory_family_id AND is_active=true;
|
||||
IF family_level IS DISTINCT FROM 'INSTALLATION' THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La clasificación elegida no corresponde a una Instalación';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
IF child_type='subinstalacion' THEN
|
||||
IF NEW.inventory_family_id IS NULL OR NEW.parent_id IS NULL THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La Subinstalación requiere Instalación padre y clasificación técnica';
|
||||
END IF;
|
||||
SELECT level::text INTO family_level FROM inventory_families WHERE id=NEW.inventory_family_id AND is_active=true;
|
||||
SELECT inventory_family_id INTO parent_family FROM assets WHERE id=NEW.parent_id;
|
||||
IF family_level IS DISTINCT FROM 'SUBINSTALLATION' THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La clasificación elegida no corresponde a una Subinstalación';
|
||||
END IF;
|
||||
IF parent_family IS NULL OR NOT EXISTS (
|
||||
SELECT 1 FROM inventory_family_parent_rules rule
|
||||
WHERE rule.child_family_id=NEW.inventory_family_id AND rule.parent_family_id=parent_family
|
||||
) THEN
|
||||
RAISE EXCEPTION USING ERRCODE='23514',MESSAGE='La clasificación de Subinstalación no es compatible con la Instalación padre';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_f6_asset_family_contract ON assets');
|
||||
await queryRunner.query(`
|
||||
CREATE TRIGGER trg_f6_asset_family_contract
|
||||
BEFORE INSERT OR UPDATE OF asset_type_id,parent_id,inventory_family_id ON assets
|
||||
FOR EACH ROW EXECUTE FUNCTION enforce_f6_asset_family_contract()
|
||||
`);
|
||||
|
||||
const [check] = (await queryRunner.query(`
|
||||
SELECT
|
||||
(SELECT COUNT(*)::integer FROM pg_constraint c JOIN pg_class r ON r.oid=c.conrelid
|
||||
WHERE r.relname='inventory_family_parent_rules' AND c.contype='p'
|
||||
AND pg_get_constraintdef(c.oid) LIKE '%child_family_id, parent_family_id%') AS composite_pk,
|
||||
to_regclass('inventory_family_attribute_definitions') IS NOT NULL AS family_attributes,
|
||||
to_regclass('asset_inventory_attribute_values') IS NOT NULL AS family_values
|
||||
`)) as Array<{ composite_pk:number; family_attributes:boolean; family_values:boolean }>;
|
||||
if (!check || Number(check.composite_pk)!==1 || !check.family_attributes || !check.family_values) {
|
||||
throw new Error(`F6 inventory model verification failed: ${JSON.stringify(check ?? {})}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_f6_asset_family_contract ON assets');
|
||||
await queryRunner.query('DROP FUNCTION IF EXISTS enforce_f6_asset_family_contract()');
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_asset_inventory_attribute_family ON asset_inventory_attribute_values');
|
||||
await queryRunner.query('DROP FUNCTION IF EXISTS enforce_asset_inventory_attribute_family()');
|
||||
await queryRunner.query('DROP TABLE IF EXISTS asset_inventory_attribute_values');
|
||||
await queryRunner.query('DROP TABLE IF EXISTS inventory_family_attribute_definitions');
|
||||
await queryRunner.query('DROP TRIGGER IF EXISTS trg_inventory_family_compatibility_rule ON inventory_family_parent_rules');
|
||||
await queryRunner.query('DROP FUNCTION IF EXISTS enforce_inventory_family_compatibility_rule()');
|
||||
await queryRunner.query('DROP INDEX IF EXISTS idx_inventory_family_parent_rules_parent');
|
||||
|
||||
// Old schema allowed only one Installation family per Subinstallation family.
|
||||
// Keep a deterministic first relation if F6 data must be rolled back.
|
||||
await queryRunner.query(`
|
||||
DELETE FROM inventory_family_parent_rules rule
|
||||
USING inventory_family_parent_rules keep
|
||||
WHERE rule.child_family_id=keep.child_family_id
|
||||
AND rule.parent_family_id>keep.parent_family_id
|
||||
`);
|
||||
await queryRunner.query('ALTER TABLE inventory_family_parent_rules DROP CONSTRAINT IF EXISTS pk_inventory_family_parent_rules');
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE inventory_family_parent_rules
|
||||
ADD CONSTRAINT inventory_family_parent_rules_pkey PRIMARY KEY(child_family_id)
|
||||
`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user