diff --git a/api-v3/src/database/migrations/1789844900000-phase-f3-1-finding-catalog-merge.ts b/api-v3/src/database/migrations/1789844900000-phase-f3-1-finding-catalog-merge.ts new file mode 100644 index 0000000..30b728d --- /dev/null +++ b/api-v3/src/database/migrations/1789844900000-phase-f3-1-finding-catalog-merge.ts @@ -0,0 +1,46 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PhaseF31FindingCatalogMerge1789844900000 implements MigrationInterface { + name = 'PhaseF31FindingCatalogMerge1789844900000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE finding_catalog_item_merges ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + source_item_id uuid NOT NULL REFERENCES finding_catalog_items(id) ON DELETE RESTRICT, + canonical_item_id uuid NOT NULL REFERENCES finding_catalog_items(id) ON DELETE RESTRICT, + reason text NOT NULL, + merged_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + merged_by uuid NULL REFERENCES users(id) ON DELETE SET NULL, + request_id varchar(128) NULL, + source_snapshot jsonb NOT NULL, + canonical_snapshot jsonb NOT NULL, + CONSTRAINT chk_finding_catalog_item_merges_distinct CHECK (source_item_id <> canonical_item_id), + CONSTRAINT uq_finding_catalog_item_merges_source UNIQUE (source_item_id) + ) + `); + await queryRunner.query(`CREATE INDEX idx_finding_catalog_item_merges_canonical ON finding_catalog_item_merges(canonical_item_id)`); + await queryRunner.query(`CREATE INDEX idx_finding_catalog_item_merges_at ON finding_catalog_item_merges(merged_at)`); + await queryRunner.query(` + CREATE OR REPLACE FUNCTION protect_finding_catalog_item_merges_append_only() + RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + RAISE EXCEPTION 'finding_catalog_item_merges es append-only; una fusión de catálogo no puede editarse ni borrarse'; + END; + $$ + `); + await queryRunner.query(` + CREATE TRIGGER trg_finding_catalog_item_merges_append_only + BEFORE UPDATE OR DELETE ON finding_catalog_item_merges + FOR EACH ROW EXECUTE FUNCTION protect_finding_catalog_item_merges_append_only() + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP TRIGGER IF EXISTS trg_finding_catalog_item_merges_append_only ON finding_catalog_item_merges'); + await queryRunner.query('DROP FUNCTION IF EXISTS protect_finding_catalog_item_merges_append_only()'); + await queryRunner.query('DROP TABLE IF EXISTS finding_catalog_item_merges'); + } +}