210 lines
9.1 KiB
TypeScript
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
|
|
`);
|
|
}
|
|
}
|