Files
dh-inspeccion-v2/api-v3/src/database/migrations/1788976800000-phase-d5-4-contextual-finding-catalog.ts
T

210 lines
9.1 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD54ContextualFindingCatalog1788976800000 implements MigrationInterface {
name = 'PhaseD54ContextualFindingCatalog1788976800000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE finding_catalog_items
ADD COLUMN suggested_severity smallint,
ADD CONSTRAINT chk_finding_catalog_items_suggested_severity
CHECK (suggested_severity IS NULL OR suggested_severity BETWEEN 1 AND 10)
`);
await queryRunner.query(`
ALTER TABLE inspection_findings
ADD COLUMN suggested_severity smallint,
ADD COLUMN severity smallint,
ADD CONSTRAINT chk_inspection_findings_suggested_severity
CHECK (suggested_severity IS NULL OR suggested_severity BETWEEN 1 AND 10),
ADD CONSTRAINT chk_inspection_findings_severity
CHECK (severity IS NULL OR severity BETWEEN 1 AND 10)
`);
await queryRunner.query(`
CREATE TABLE finding_catalog_asset_type_profiles (
asset_type_id uuid PRIMARY KEY,
reason text NOT NULL,
updated_by uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_finding_catalog_asset_type_profiles_reason CHECK (char_length(btrim(reason)) >= 5),
CONSTRAINT fk_finding_catalog_asset_type_profiles_type FOREIGN KEY (asset_type_id)
REFERENCES asset_types(id) ON DELETE CASCADE,
CONSTRAINT fk_finding_catalog_asset_type_profiles_updated_by FOREIGN KEY (updated_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`
CREATE INDEX idx_finding_catalog_asset_type_profiles_updated
ON finding_catalog_asset_type_profiles (updated_at DESC)
`);
await queryRunner.query(`
CREATE TABLE finding_catalog_item_asset_types (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
catalog_item_id uuid NOT NULL,
asset_type_id uuid NOT NULL,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_finding_catalog_item_asset_types_pair UNIQUE (catalog_item_id, asset_type_id),
CONSTRAINT fk_finding_catalog_item_asset_types_item FOREIGN KEY (catalog_item_id)
REFERENCES finding_catalog_items(id) ON DELETE CASCADE,
CONSTRAINT fk_finding_catalog_item_asset_types_type FOREIGN KEY (asset_type_id)
REFERENCES asset_types(id) ON DELETE CASCADE,
CONSTRAINT fk_finding_catalog_item_asset_types_created_by FOREIGN KEY (created_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`
CREATE INDEX idx_finding_catalog_item_asset_types_type
ON finding_catalog_item_asset_types (asset_type_id, catalog_item_id)
`);
await queryRunner.query(`
CREATE TABLE finding_catalog_asset_overrides (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
asset_id uuid NOT NULL,
catalog_item_id uuid NOT NULL,
is_enabled boolean NOT NULL,
reason text NOT NULL,
created_by uuid,
updated_by uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_finding_catalog_asset_overrides_pair UNIQUE (asset_id, catalog_item_id),
CONSTRAINT chk_finding_catalog_asset_overrides_reason CHECK (char_length(btrim(reason)) >= 5),
CONSTRAINT fk_finding_catalog_asset_overrides_asset FOREIGN KEY (asset_id)
REFERENCES assets(id) ON DELETE CASCADE,
CONSTRAINT fk_finding_catalog_asset_overrides_item FOREIGN KEY (catalog_item_id)
REFERENCES finding_catalog_items(id) ON DELETE CASCADE,
CONSTRAINT fk_finding_catalog_asset_overrides_created_by FOREIGN KEY (created_by)
REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_finding_catalog_asset_overrides_updated_by FOREIGN KEY (updated_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`
CREATE INDEX idx_finding_catalog_asset_overrides_asset
ON finding_catalog_asset_overrides (asset_id)
`);
await queryRunner.query(`
CREATE TABLE finding_catalog_proposals (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
finding_id uuid NOT NULL,
asset_id uuid NOT NULL,
asset_type_id uuid NOT NULL,
proposed_title varchar(500) NOT NULL,
proposed_legal_basis text,
proposed_severity smallint,
description text NOT NULL,
status varchar(20) NOT NULL DEFAULT 'PENDING',
resolved_catalog_item_id uuid,
office_notes text,
reviewed_by uuid,
reviewed_at timestamptz,
created_by uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_finding_catalog_proposals_finding UNIQUE (finding_id),
CONSTRAINT chk_finding_catalog_proposals_status CHECK (status IN ('PENDING', 'MATCHED', 'REJECTED')),
CONSTRAINT chk_finding_catalog_proposals_severity
CHECK (proposed_severity IS NULL OR proposed_severity BETWEEN 1 AND 10),
CONSTRAINT fk_finding_catalog_proposals_finding FOREIGN KEY (finding_id)
REFERENCES inspection_findings(id) ON DELETE RESTRICT,
CONSTRAINT fk_finding_catalog_proposals_asset FOREIGN KEY (asset_id)
REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_finding_catalog_proposals_asset_type FOREIGN KEY (asset_type_id)
REFERENCES asset_types(id) ON DELETE RESTRICT,
CONSTRAINT fk_finding_catalog_proposals_resolved_item FOREIGN KEY (resolved_catalog_item_id)
REFERENCES finding_catalog_items(id) ON DELETE RESTRICT,
CONSTRAINT fk_finding_catalog_proposals_reviewed_by FOREIGN KEY (reviewed_by)
REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_finding_catalog_proposals_created_by FOREIGN KEY (created_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`
CREATE INDEX idx_finding_catalog_proposals_status
ON finding_catalog_proposals (status, created_at DESC)
`);
await queryRunner.query(`
CREATE INDEX idx_finding_catalog_proposals_asset_type
ON finding_catalog_proposals (asset_type_id, status, created_at DESC)
`);
await queryRunner.query(`
INSERT INTO finding_catalog_proposals (
finding_id, asset_id, asset_type_id, proposed_title, proposed_legal_basis,
proposed_severity, description, status, created_by, created_at, updated_at
)
SELECT
finding.id,
finding.asset_id,
asset.asset_type_id,
finding.title,
finding.legal_basis,
finding.severity,
finding.description,
'PENDING',
finding.created_by,
finding.created_at,
finding.updated_at
FROM inspection_findings finding
INNER JOIN assets asset ON asset.id = finding.asset_id
WHERE finding.catalog_item_id IS NULL
ON CONFLICT (finding_id) DO NOTHING
`);
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, UPDATE ON TABLE finding_catalog_asset_type_profiles
TO ${applicationRole}
`);
await queryRunner.query(`
GRANT SELECT, INSERT, DELETE ON TABLE finding_catalog_item_asset_types
TO ${applicationRole}
`);
await queryRunner.query(`
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE finding_catalog_asset_overrides
TO ${applicationRole}
`);
await queryRunner.query(`
GRANT SELECT, INSERT, UPDATE ON TABLE finding_catalog_proposals
TO ${applicationRole}
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('DROP TABLE finding_catalog_proposals');
await queryRunner.query('DROP TABLE finding_catalog_asset_overrides');
await queryRunner.query('DROP TABLE finding_catalog_item_asset_types');
await queryRunner.query('DROP TABLE finding_catalog_asset_type_profiles');
await queryRunner.query(`
ALTER TABLE inspection_findings
DROP CONSTRAINT chk_inspection_findings_severity,
DROP CONSTRAINT chk_inspection_findings_suggested_severity,
DROP COLUMN severity,
DROP COLUMN suggested_severity
`);
await queryRunner.query(`
ALTER TABLE finding_catalog_items
DROP CONSTRAINT chk_finding_catalog_items_suggested_severity,
DROP COLUMN suggested_severity
`);
}
}