import { MigrationInterface, QueryRunner } from 'typeorm'; function quoteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } export class PhaseD52FindingCatalogAdministration1787335200000 implements MigrationInterface { name = 'PhaseD52FindingCatalogAdministration1787335200000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TABLE finding_catalog_item_versions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), item_id uuid NOT NULL, revision integer NOT NULL, snapshot jsonb NOT NULL, actor_user_id uuid, actor_username varchar(80), created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_finding_catalog_item_versions_revision UNIQUE (item_id, revision), CONSTRAINT chk_finding_catalog_item_versions_revision CHECK (revision > 0), CONSTRAINT chk_finding_catalog_item_versions_snapshot CHECK (jsonb_typeof(snapshot) = 'object'), CONSTRAINT fk_finding_catalog_item_versions_item FOREIGN KEY (item_id) REFERENCES finding_catalog_items(id) ON DELETE RESTRICT, CONSTRAINT fk_finding_catalog_item_versions_actor FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(` CREATE INDEX idx_finding_catalog_item_versions_item_revision ON finding_catalog_item_versions (item_id, revision DESC) `); await queryRunner.query(` CREATE INDEX idx_finding_catalog_item_versions_created_at ON finding_catalog_item_versions (created_at DESC) `); await queryRunner.query(` INSERT INTO finding_catalog_item_versions ( item_id, revision, snapshot, actor_username, created_at ) SELECT item.id, item.revision, JSONB_BUILD_OBJECT( 'id', item.id, 'categoryId', category.id, 'categoryCode', category.code, 'categoryName', category.name, 'code', item.code, 'sourceNumber', item.source_number, 'title', item.title, 'legalBasis', item.legal_basis, 'glossary', item.glossary, 'importNote', item.import_note, 'revision', item.revision, 'isActive', item.is_active ), 'migration:D5.2', item.updated_at FROM finding_catalog_items item INNER JOIN finding_categories category ON category.id = item.category_id `); const appRole = process.env.DB_APP_USER; if (!appRole) throw new Error('Missing required environment variable: DB_APP_USER'); const roleRows = (await queryRunner.query( 'SELECT 1 FROM pg_roles WHERE rolname = $1', [appRole], )) as unknown[]; if (roleRows.length !== 1) throw new Error('Configured DB_APP_USER does not exist'); const applicationRole = quoteIdentifier(appRole); await queryRunner.query(` GRANT SELECT, INSERT ON TABLE finding_catalog_item_versions TO ${applicationRole} `); await queryRunner.query(` REVOKE UPDATE, DELETE ON TABLE finding_catalog_item_versions FROM ${applicationRole} `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query('DROP TABLE finding_catalog_item_versions'); } }