import { MigrationInterface, QueryRunner } from 'typeorm'; const newPermissions = [ 'inspection_acts.read', 'inspection_acts.create', 'inspection_acts.update', 'inspection_acts.cancel', ] as const; const rolePermissionValues = ` ('admin', 'inspection_acts.read'), ('admin', 'inspection_acts.create'), ('admin', 'inspection_acts.update'), ('admin', 'inspection_acts.cancel'), ('director', 'inspection_acts.read'), ('supervisor', 'inspection_acts.read'), ('supervisor', 'inspection_acts.create'), ('supervisor', 'inspection_acts.update'), ('supervisor', 'inspection_acts.cancel'), ('inspector', 'inspection_acts.read'), ('inspector', 'inspection_acts.create'), ('inspector', 'inspection_acts.update'), ('auditor', 'inspection_acts.read') `; function quoteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } export class PhaseD2InspectionActs1787068800000 implements MigrationInterface { name = 'PhaseD2InspectionActs1787068800000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TABLE document_annual_sequences ( document_type varchar(20) NOT NULL, year integer NOT NULL, last_number integer NOT NULL DEFAULT 0, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (document_type, year), CONSTRAINT chk_document_annual_sequences_type CHECK ( document_type IN ('ACT', 'REPORT') ), CONSTRAINT chk_document_annual_sequences_year CHECK ( year BETWEEN 2000 AND 9999 ), CONSTRAINT chk_document_annual_sequences_number CHECK ( last_number >= 0 ) ) `); await queryRunner.query(` CREATE TABLE inspection_acts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), visit_id uuid NOT NULL, act_year integer NOT NULL, act_number integer NOT NULL, code varchar(24) NOT NULL, status varchar(24) NOT NULL DEFAULT 'DRAFT', occurred_at timestamptz NOT NULL, title varchar(200) NOT NULL, summary text NOT NULL, observations text, current_version integer NOT NULL DEFAULT 0, cancellation_reason 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_acts_year_number UNIQUE (act_year, act_number), CONSTRAINT uq_inspection_acts_code UNIQUE (code), CONSTRAINT chk_inspection_acts_year CHECK (act_year BETWEEN 2000 AND 9999), CONSTRAINT chk_inspection_acts_number CHECK (act_number > 0), CONSTRAINT chk_inspection_acts_code CHECK ( code ~ '^ACTA-[0-9]{4}-[0-9]{6}$' ), CONSTRAINT chk_inspection_acts_status CHECK ( status IN ('DRAFT', 'READY', 'CLOSED', 'CANCELLED', 'RECTIFIED') ), CONSTRAINT chk_inspection_acts_current_version CHECK (current_version >= 0), CONSTRAINT chk_inspection_acts_cancellation CHECK ( status <> 'CANCELLED' OR LENGTH(TRIM(COALESCE(cancellation_reason, ''))) >= 10 ), CONSTRAINT fk_inspection_acts_visit FOREIGN KEY (visit_id) REFERENCES inspection_visits(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_acts_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_inspection_acts_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_inspection_acts_visit_status ON inspection_acts (visit_id, status)`); await queryRunner.query(`CREATE INDEX idx_inspection_acts_occurred_at ON inspection_acts (occurred_at)`); await queryRunner.query(`CREATE INDEX idx_inspection_acts_created_by ON inspection_acts (created_by)`); await queryRunner.query(` CREATE TABLE inspection_act_assets ( act_id uuid NOT NULL, asset_id uuid NOT NULL, included boolean NOT NULL DEFAULT true, added_by uuid, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (act_id, asset_id), CONSTRAINT fk_inspection_act_assets_act FOREIGN KEY (act_id) REFERENCES inspection_acts(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_act_assets_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_act_assets_added_by FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_inspection_act_assets_asset_id ON inspection_act_assets (asset_id)`); await queryRunner.query(`CREATE INDEX idx_inspection_act_assets_included ON inspection_act_assets (act_id, included)`); await queryRunner.query(` CREATE TABLE inspection_act_versions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), act_id uuid NOT NULL, version_number integer NOT NULL, event varchar(24) 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_act_versions_number UNIQUE (act_id, version_number), CONSTRAINT chk_inspection_act_versions_number CHECK (version_number > 0), CONSTRAINT chk_inspection_act_versions_event CHECK ( event IN ('CREATED', 'UPDATED', 'CANCELLED') ), CONSTRAINT fk_inspection_act_versions_act FOREIGN KEY (act_id) REFERENCES inspection_acts(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_act_versions_actor FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_inspection_act_versions_created_at ON inspection_act_versions (created_at)`); await queryRunner.query(` INSERT INTO permissions (code, description) VALUES ('inspection_acts.read', 'Consultar actas y sus versiones'), ('inspection_acts.create', 'Crear actas dentro de visitas asignadas'), ('inspection_acts.update', 'Actualizar actas en borrador'), ('inspection_acts.cancel', 'Cancelar actas en borrador') 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 document_annual_sequences, inspection_acts, inspection_act_assets TO ${applicationRole} `); await queryRunner.query(` GRANT SELECT, INSERT ON TABLE inspection_act_versions TO ${applicationRole} `); await queryRunner.query(` REVOKE DELETE ON TABLE document_annual_sequences, inspection_acts, inspection_act_assets, inspection_act_versions FROM ${applicationRole} `); await queryRunner.query(` REVOKE UPDATE ON TABLE inspection_act_versions FROM ${applicationRole} `); } public async down(queryRunner: QueryRunner): Promise { 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_act_versions`); await queryRunner.query(`DROP TABLE inspection_act_assets`); await queryRunner.query(`DROP TABLE inspection_acts`); await queryRunner.query(`DROP TABLE document_annual_sequences`); } }