chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import {
|
||||
PHASE_D3_FINDING_CATALOG_ITEMS,
|
||||
PHASE_D3_FINDING_CATEGORIES,
|
||||
} from '../seeds/phase-d3-finding-catalog';
|
||||
|
||||
const newPermissions = [
|
||||
'finding_catalog.read',
|
||||
'finding_catalog.manage',
|
||||
'inspection_findings.read',
|
||||
'inspection_findings.create',
|
||||
'inspection_findings.update',
|
||||
'inspection_findings.follow_up',
|
||||
] as const;
|
||||
|
||||
const rolePermissionValues = `
|
||||
('admin', 'finding_catalog.read'),
|
||||
('admin', 'finding_catalog.manage'),
|
||||
('admin', 'inspection_findings.read'),
|
||||
('admin', 'inspection_findings.create'),
|
||||
('admin', 'inspection_findings.update'),
|
||||
('admin', 'inspection_findings.follow_up'),
|
||||
('director', 'finding_catalog.read'),
|
||||
('director', 'inspection_findings.read'),
|
||||
('director', 'inspection_findings.follow_up'),
|
||||
('supervisor', 'finding_catalog.read'),
|
||||
('supervisor', 'finding_catalog.manage'),
|
||||
('supervisor', 'inspection_findings.read'),
|
||||
('supervisor', 'inspection_findings.create'),
|
||||
('supervisor', 'inspection_findings.update'),
|
||||
('supervisor', 'inspection_findings.follow_up'),
|
||||
('inspector', 'finding_catalog.read'),
|
||||
('inspector', 'inspection_findings.read'),
|
||||
('inspector', 'inspection_findings.create'),
|
||||
('inspector', 'inspection_findings.update'),
|
||||
('inspector', 'inspection_findings.follow_up'),
|
||||
('auditor', 'finding_catalog.read'),
|
||||
('auditor', 'inspection_findings.read')
|
||||
`;
|
||||
|
||||
function quoteIdentifier(identifier: string): string {
|
||||
return `"${identifier.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
export class PhaseD3InspectionFindings1787155200000 implements MigrationInterface {
|
||||
name = 'PhaseD3InspectionFindings1787155200000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE finding_categories (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
code varchar(80) NOT NULL,
|
||||
name varchar(200) NOT NULL,
|
||||
sort_order integer NOT NULL DEFAULT 0,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_finding_categories_code UNIQUE (code),
|
||||
CONSTRAINT chk_finding_categories_sort_order CHECK (sort_order >= 0),
|
||||
CONSTRAINT chk_finding_categories_name CHECK (LENGTH(TRIM(name)) > 0)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_finding_categories_active_order
|
||||
ON finding_categories (is_active, sort_order)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE finding_catalog_items (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
category_id uuid NOT NULL,
|
||||
code varchar(120) NOT NULL,
|
||||
source_number integer NOT NULL,
|
||||
title varchar(500) NOT NULL,
|
||||
legal_basis text,
|
||||
glossary text,
|
||||
import_note text,
|
||||
revision integer NOT NULL DEFAULT 1,
|
||||
is_active boolean NOT NULL DEFAULT true,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_finding_catalog_items_code UNIQUE (code),
|
||||
CONSTRAINT uq_finding_catalog_items_source UNIQUE (category_id, source_number),
|
||||
CONSTRAINT chk_finding_catalog_items_source CHECK (source_number > 0),
|
||||
CONSTRAINT chk_finding_catalog_items_revision CHECK (revision > 0),
|
||||
CONSTRAINT chk_finding_catalog_items_title CHECK (LENGTH(TRIM(title)) > 0),
|
||||
CONSTRAINT fk_finding_catalog_items_category FOREIGN KEY (category_id)
|
||||
REFERENCES finding_categories(id) ON DELETE RESTRICT
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_finding_catalog_items_active
|
||||
ON finding_catalog_items (category_id, is_active)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE inspection_findings (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
act_id uuid NOT NULL,
|
||||
asset_id uuid NOT NULL,
|
||||
catalog_item_id uuid,
|
||||
finding_number integer NOT NULL,
|
||||
code varchar(40) NOT NULL,
|
||||
status varchar(20) NOT NULL DEFAULT 'OPEN',
|
||||
title varchar(500) NOT NULL,
|
||||
description text NOT NULL,
|
||||
legal_basis text,
|
||||
glossary text,
|
||||
catalog_revision integer,
|
||||
correction_due_on date,
|
||||
company_response text,
|
||||
company_response_received_on date,
|
||||
company_committed_correction_on date,
|
||||
next_control_on date,
|
||||
current_version integer NOT NULL DEFAULT 0,
|
||||
closed_at timestamptz,
|
||||
closed_by uuid,
|
||||
closure_notes text,
|
||||
created_by uuid,
|
||||
updated_by uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_inspection_findings_number UNIQUE (act_id, finding_number),
|
||||
CONSTRAINT uq_inspection_findings_code UNIQUE (code),
|
||||
CONSTRAINT chk_inspection_findings_number CHECK (
|
||||
finding_number BETWEEN 1 AND 999
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_code CHECK (
|
||||
code ~ '^ACTA-[0-9]{4}-[0-9]{6}-H[0-9]{3}$'
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_status CHECK (
|
||||
status IN ('OPEN', 'CLOSED', 'VOIDED')
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_content CHECK (
|
||||
LENGTH(TRIM(title)) > 0 AND LENGTH(TRIM(description)) > 0
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_version CHECK (current_version >= 0),
|
||||
CONSTRAINT chk_inspection_findings_catalog_revision CHECK (
|
||||
catalog_revision IS NULL OR catalog_revision > 0
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_response CHECK (
|
||||
(company_response IS NULL AND company_response_received_on IS NULL)
|
||||
OR (
|
||||
LENGTH(TRIM(COALESCE(company_response, ''))) > 0
|
||||
AND company_response_received_on IS NOT NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_commitment CHECK (
|
||||
company_committed_correction_on IS NULL
|
||||
OR company_response_received_on IS NOT NULL
|
||||
),
|
||||
CONSTRAINT chk_inspection_findings_closure CHECK (
|
||||
status <> 'CLOSED'
|
||||
OR (closed_at IS NOT NULL AND closed_by IS NOT NULL
|
||||
AND LENGTH(TRIM(COALESCE(closure_notes, ''))) >= 10)
|
||||
),
|
||||
CONSTRAINT fk_inspection_findings_act FOREIGN KEY (act_id)
|
||||
REFERENCES inspection_acts(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_findings_asset FOREIGN KEY (asset_id)
|
||||
REFERENCES assets(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_findings_catalog_item FOREIGN KEY (catalog_item_id)
|
||||
REFERENCES finding_catalog_items(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_findings_closed_by FOREIGN KEY (closed_by)
|
||||
REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_inspection_findings_created_by FOREIGN KEY (created_by)
|
||||
REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_inspection_findings_updated_by FOREIGN KEY (updated_by)
|
||||
REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_findings_status_control
|
||||
ON inspection_findings (status, next_control_on)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_findings_asset_status
|
||||
ON inspection_findings (asset_id, status)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_findings_catalog_item
|
||||
ON inspection_findings (catalog_item_id)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE inspection_finding_versions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
finding_id uuid NOT NULL,
|
||||
version_number integer NOT NULL,
|
||||
event varchar(32) NOT NULL,
|
||||
snapshot jsonb NOT NULL,
|
||||
actor_user_id uuid,
|
||||
actor_username varchar(80),
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_inspection_finding_versions_number
|
||||
UNIQUE (finding_id, version_number),
|
||||
CONSTRAINT chk_inspection_finding_versions_number CHECK (version_number > 0),
|
||||
CONSTRAINT chk_inspection_finding_versions_event CHECK (
|
||||
event IN ('CREATED', 'UPDATED', 'FOLLOW_UP_UPDATED')
|
||||
),
|
||||
CONSTRAINT fk_inspection_finding_versions_finding FOREIGN KEY (finding_id)
|
||||
REFERENCES inspection_findings(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_finding_versions_actor FOREIGN KEY (actor_user_id)
|
||||
REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_finding_versions_created_at
|
||||
ON inspection_finding_versions (created_at)
|
||||
`);
|
||||
|
||||
for (const category of PHASE_D3_FINDING_CATEGORIES) {
|
||||
await queryRunner.query(`
|
||||
INSERT INTO finding_categories (code, name, sort_order)
|
||||
VALUES ($1, $2, $3)
|
||||
`, [category.code, category.name, category.sortOrder]);
|
||||
}
|
||||
for (const item of PHASE_D3_FINDING_CATALOG_ITEMS) {
|
||||
await queryRunner.query(`
|
||||
INSERT INTO finding_catalog_items (
|
||||
category_id, code, source_number, title, legal_basis, glossary, import_note
|
||||
)
|
||||
SELECT id, $2, $3, $4, $5, $6, $7
|
||||
FROM finding_categories
|
||||
WHERE code = $1
|
||||
`, [
|
||||
item.categoryCode,
|
||||
item.code,
|
||||
item.sourceNumber,
|
||||
item.title,
|
||||
item.legalBasis,
|
||||
item.glossary,
|
||||
item.importNote,
|
||||
]);
|
||||
}
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO permissions (code, description)
|
||||
VALUES
|
||||
('finding_catalog.read', 'Consultar el catálogo de hallazgos'),
|
||||
('finding_catalog.manage', 'Administrar el catálogo de hallazgos'),
|
||||
('inspection_findings.read', 'Consultar hallazgos y seguimiento'),
|
||||
('inspection_findings.create', 'Crear hallazgos dentro de actas'),
|
||||
('inspection_findings.update', 'Actualizar hallazgos en borrador'),
|
||||
('inspection_findings.follow_up', 'Registrar respuestas y próximos controles')
|
||||
ON CONFLICT (code) DO UPDATE SET description = EXCLUDED.description
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
WITH mapping(role_code, permission_code) AS (VALUES ${rolePermissionValues})
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT role.id, permission.id
|
||||
FROM mapping
|
||||
INNER JOIN roles role ON role.code = mapping.role_code
|
||||
INNER JOIN permissions permission ON permission.code = mapping.permission_code
|
||||
ON CONFLICT (role_id, permission_id) DO NOTHING
|
||||
`);
|
||||
|
||||
const appRole = process.env.DB_APP_USER;
|
||||
if (!appRole) throw new Error('Missing required environment variable: DB_APP_USER');
|
||||
const applicationRole = quoteIdentifier(appRole);
|
||||
await queryRunner.query(`
|
||||
GRANT SELECT, INSERT, UPDATE ON TABLE
|
||||
finding_categories, finding_catalog_items, inspection_findings
|
||||
TO ${applicationRole}
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
GRANT SELECT, INSERT ON TABLE inspection_finding_versions
|
||||
TO ${applicationRole}
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
REVOKE DELETE ON TABLE
|
||||
finding_categories, finding_catalog_items,
|
||||
inspection_findings, inspection_finding_versions
|
||||
FROM ${applicationRole}
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
REVOKE UPDATE ON TABLE inspection_finding_versions
|
||||
FROM ${applicationRole}
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
WITH mapping(role_code, permission_code) AS (VALUES ${rolePermissionValues})
|
||||
DELETE FROM role_permissions role_permission
|
||||
USING roles role, permissions permission, mapping
|
||||
WHERE role_permission.role_id = role.id
|
||||
AND role_permission.permission_id = permission.id
|
||||
AND role.code = mapping.role_code
|
||||
AND permission.code = mapping.permission_code
|
||||
`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM permissions WHERE code = ANY($1::varchar[])
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM role_permissions WHERE permission_id = permissions.id
|
||||
)`,
|
||||
[newPermissions],
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE inspection_finding_versions`);
|
||||
await queryRunner.query(`DROP TABLE inspection_findings`);
|
||||
await queryRunner.query(`DROP TABLE finding_catalog_items`);
|
||||
await queryRunner.query(`DROP TABLE finding_categories`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user