import { MigrationInterface, QueryRunner } from 'typeorm'; const newPermissions = [ 'inspection_reports.read', 'inspection_reports.generate', ] as const; const rolePermissionValues = ` ('admin', 'inspection_reports.read'), ('director', 'inspection_reports.read'), ('supervisor', 'inspection_reports.read'), ('inspector', 'inspection_reports.read'), ('inspector', 'inspection_reports.generate'), ('auditor', 'inspection_reports.read') `; function quoteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } export class PhaseD5315DocumentCenter1788026400000 implements MigrationInterface { name = 'PhaseD5315DocumentCenter1788026400000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TABLE inspection_reports ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), visit_id uuid NOT NULL, act_id uuid NOT NULL, report_year integer NOT NULL, report_number integer NOT NULL, code varchar(24) NOT NULL, status varchar(24) NOT NULL DEFAULT 'FROZEN', pdf_status varchar(24) NOT NULL DEFAULT 'PENDING', title varchar(220) NOT NULL, act_version integer NOT NULL, act_closure_sha256 char(64) NOT NULL, frozen_sha256 char(64) NOT NULL, frozen_snapshot jsonb NOT NULL, generated_at timestamptz NOT NULL, generated_by uuid NOT NULL, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT uq_inspection_reports_visit UNIQUE (visit_id), CONSTRAINT uq_inspection_reports_act UNIQUE (act_id), CONSTRAINT uq_inspection_reports_year_number UNIQUE (report_year, report_number), CONSTRAINT uq_inspection_reports_code UNIQUE (code), CONSTRAINT chk_inspection_reports_number CHECK (report_year >= 2000 AND report_number > 0), CONSTRAINT chk_inspection_reports_status CHECK (status IN ('FROZEN', 'CANCELLED')), CONSTRAINT chk_inspection_reports_pdf_status CHECK (pdf_status IN ('PENDING', 'READY', 'FAILED')), CONSTRAINT chk_inspection_reports_hashes CHECK ( act_closure_sha256 ~ '^[0-9a-f]{64}$' AND frozen_sha256 ~ '^[0-9a-f]{64}$' ), CONSTRAINT chk_inspection_reports_snapshot CHECK (jsonb_typeof(frozen_snapshot) = 'object'), CONSTRAINT fk_inspection_reports_visit FOREIGN KEY (visit_id) REFERENCES inspection_visits(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_reports_act FOREIGN KEY (act_id) REFERENCES inspection_acts(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_reports_generated_by FOREIGN KEY (generated_by) REFERENCES users(id) ON DELETE RESTRICT ) `); await queryRunner.query('CREATE INDEX idx_inspection_reports_generated_at ON inspection_reports (generated_at DESC)'); await queryRunner.query('CREATE INDEX idx_inspection_reports_status ON inspection_reports (status, pdf_status)'); await queryRunner.query(` CREATE FUNCTION dhv2_guard_inspection_report_frozen() RETURNS trigger AS $$ BEGIN IF NEW.visit_id IS DISTINCT FROM OLD.visit_id OR NEW.act_id IS DISTINCT FROM OLD.act_id OR NEW.report_year IS DISTINCT FROM OLD.report_year OR NEW.report_number IS DISTINCT FROM OLD.report_number OR NEW.code IS DISTINCT FROM OLD.code OR NEW.title IS DISTINCT FROM OLD.title OR NEW.act_version IS DISTINCT FROM OLD.act_version OR NEW.act_closure_sha256 IS DISTINCT FROM OLD.act_closure_sha256 OR NEW.frozen_sha256 IS DISTINCT FROM OLD.frozen_sha256 OR NEW.frozen_snapshot IS DISTINCT FROM OLD.frozen_snapshot OR NEW.generated_at IS DISTINCT FROM OLD.generated_at OR NEW.generated_by IS DISTINCT FROM OLD.generated_by OR NEW.created_at IS DISTINCT FROM OLD.created_at THEN RAISE EXCEPTION 'El contenido congelado del informe es inmutable'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql `); await queryRunner.query(` CREATE TRIGGER trg_inspection_reports_frozen BEFORE UPDATE ON inspection_reports FOR EACH ROW EXECUTE FUNCTION dhv2_guard_inspection_report_frozen() `); await queryRunner.query(` INSERT INTO permissions (code, description) VALUES ('inspection_reports.read', 'Consultar informes de inspección y documentos pendientes de emisión'), ('inspection_reports.generate', 'Solicitar la emisión y congelado de un informe desde una inspección cerrada') 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 inspection_reports TO ${applicationRole} `); await queryRunner.query(` REVOKE DELETE ON TABLE inspection_reports FROM ${applicationRole} `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query('DROP TRIGGER trg_inspection_reports_frozen ON inspection_reports'); await queryRunner.query('DROP FUNCTION dhv2_guard_inspection_report_frozen()'); 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_reports'); } }