import { MigrationInterface, QueryRunner } from 'typeorm'; const newPermissions = [ 'inspections.read', 'inspections.manage', 'inspections.assign', 'inspections.execute', ] as const; const rolePermissionValues = ` ('admin', 'inspections.read'), ('admin', 'inspections.manage'), ('admin', 'inspections.assign'), ('admin', 'inspections.execute'), ('director', 'inspections.read'), ('supervisor', 'inspections.read'), ('supervisor', 'inspections.manage'), ('supervisor', 'inspections.assign'), ('supervisor', 'inspections.execute'), ('inspector', 'inspections.read'), ('inspector', 'inspections.execute'), ('auditor', 'inspections.read') `; function quoteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } export class PhaseD1InspectionVisits1786982400000 implements MigrationInterface { name = 'PhaseD1InspectionVisits1786982400000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` CREATE TABLE inspection_visits ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), code varchar(80) NOT NULL, title varchar(200) NOT NULL, objective text, status varchar(24) NOT NULL DEFAULT 'DRAFT', scope_asset_id uuid, lead_inspector_user_id uuid, planned_start_at timestamptz, planned_end_at timestamptz, actual_started_at timestamptz, actual_closed_at timestamptz, instructions text, 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_visits_code UNIQUE (code), CONSTRAINT chk_inspection_visits_code CHECK ( code = UPPER(code) AND code ~ '^[A-Z0-9][A-Z0-9._/-]*$' ), CONSTRAINT chk_inspection_visits_status CHECK ( status IN ('DRAFT', 'PLANNED', 'IN_PROGRESS', 'CLOSED', 'CANCELLED') ), CONSTRAINT chk_inspection_visits_planned_dates CHECK ( planned_start_at IS NULL OR planned_end_at IS NULL OR planned_end_at >= planned_start_at ), CONSTRAINT chk_inspection_visits_actual_dates CHECK ( actual_started_at IS NULL OR actual_closed_at IS NULL OR actual_closed_at >= actual_started_at ), CONSTRAINT chk_inspection_visits_cancellation CHECK ( status <> 'CANCELLED' OR LENGTH(TRIM(COALESCE(cancellation_reason, ''))) >= 10 ), CONSTRAINT fk_inspection_visits_scope_asset FOREIGN KEY (scope_asset_id) REFERENCES assets(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_visits_lead_inspector FOREIGN KEY (lead_inspector_user_id) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_inspection_visits_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_inspection_visits_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_inspection_visits_status ON inspection_visits (status)`); await queryRunner.query(`CREATE INDEX idx_inspection_visits_scope_asset_id ON inspection_visits (scope_asset_id)`); await queryRunner.query(`CREATE INDEX idx_inspection_visits_lead_inspector_user_id ON inspection_visits (lead_inspector_user_id)`); await queryRunner.query(`CREATE INDEX idx_inspection_visits_planned_dates ON inspection_visits (planned_start_at, planned_end_at)`); await queryRunner.query(` CREATE TABLE inspection_visit_assets ( visit_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 (visit_id, asset_id), CONSTRAINT fk_inspection_visit_assets_visit FOREIGN KEY (visit_id) REFERENCES inspection_visits(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_visit_assets_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_visit_assets_added_by FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_inspection_visit_assets_asset_id ON inspection_visit_assets (asset_id)`); await queryRunner.query(`CREATE INDEX idx_inspection_visit_assets_included ON inspection_visit_assets (visit_id, included)`); await queryRunner.query(` CREATE TABLE inspection_visit_members ( visit_id uuid NOT NULL, user_id uuid NOT NULL, included boolean NOT NULL DEFAULT true, assigned_by uuid, created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (visit_id, user_id), CONSTRAINT fk_inspection_visit_members_visit FOREIGN KEY (visit_id) REFERENCES inspection_visits(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_visit_members_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE RESTRICT, CONSTRAINT fk_inspection_visit_members_assigned_by FOREIGN KEY (assigned_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_inspection_visit_members_user_id ON inspection_visit_members (user_id)`); await queryRunner.query(`CREATE INDEX idx_inspection_visit_members_included ON inspection_visit_members (visit_id, included)`); await queryRunner.query(` INSERT INTO permissions (code, description) VALUES ('inspections.read', 'Consultar visitas de inspección'), ('inspections.manage', 'Crear y planificar visitas de inspección'), ('inspections.assign', 'Asignar equipos de inspección'), ('inspections.execute', 'Iniciar y ejecutar visitas asignadas') 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_visits, inspection_visit_assets, inspection_visit_members TO ${applicationRole} `); await queryRunner.query(` REVOKE DELETE ON TABLE inspection_visits, inspection_visit_assets, inspection_visit_members 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_visit_members`); await queryRunner.query(`DROP TABLE inspection_visit_assets`); await queryRunner.query(`DROP TABLE inspection_visits`); } }