import { MigrationInterface, QueryRunner } from 'typeorm'; const newPermissions = [ 'surveys.read_reports', 'surveys.capture', 'surveys.review', ] as const; const rolePermissionValues = ` ('admin', 'surveys.read_reports'), ('admin', 'surveys.capture'), ('admin', 'surveys.review'), ('director', 'surveys.read_reports'), ('director', 'surveys.review'), ('supervisor', 'surveys.read_reports'), ('supervisor', 'surveys.capture'), ('supervisor', 'surveys.review'), ('inspector', 'surveys.read_reports'), ('inspector', 'surveys.capture'), ('auditor', 'surveys.read_reports') `; function quoteIdentifier(identifier: string): string { return `"${identifier.replaceAll('"', '""')}"`; } export class PhaseC2SurveyExecution1786896000000 implements MigrationInterface { name = 'PhaseC2SurveyExecution1786896000000'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(` ALTER TABLE survey_campaign_targets DROP CONSTRAINT chk_survey_campaign_targets_status `); await queryRunner.query(` ALTER TABLE survey_campaign_targets ADD CONSTRAINT chk_survey_campaign_targets_status CHECK (status IN ( 'PENDING', 'IN_PROGRESS', 'SUBMITTED', 'COMPLETED', 'SKIPPED' )) `); await queryRunner.query(` CREATE TABLE survey_target_reports ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), target_id uuid NOT NULL, outcome varchar(32), status varchar(24) NOT NULL DEFAULT 'DRAFT', observed_at timestamptz, latitude numeric(9, 6), longitude numeric(9, 6), accuracy_m numeric(12, 3), notes text, asset_version_at_submission integer, submitted_at timestamptz, submitted_by uuid, reviewed_at timestamptz, reviewed_by uuid, review_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_survey_target_reports_target_id UNIQUE (target_id), CONSTRAINT chk_survey_target_reports_outcome CHECK ( outcome IS NULL OR outcome IN ('CONFIRMED', 'CHANGES_RECORDED', 'NOT_LOCATED') ), CONSTRAINT chk_survey_target_reports_status CHECK ( status IN ('DRAFT', 'SUBMITTED', 'APPROVED', 'REJECTED') ), CONSTRAINT chk_survey_target_reports_coordinates CHECK ( (latitude IS NULL AND longitude IS NULL) OR ( latitude IS NOT NULL AND longitude IS NOT NULL AND latitude BETWEEN -90 AND 90 AND longitude BETWEEN -180 AND 180 ) ), CONSTRAINT chk_survey_target_reports_accuracy CHECK ( accuracy_m IS NULL OR ( accuracy_m >= 0 AND latitude IS NOT NULL AND longitude IS NOT NULL ) ), CONSTRAINT fk_survey_target_reports_target FOREIGN KEY (target_id) REFERENCES survey_campaign_targets(id) ON DELETE RESTRICT, CONSTRAINT fk_survey_target_reports_submitted_by FOREIGN KEY (submitted_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_survey_target_reports_reviewed_by FOREIGN KEY (reviewed_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_survey_target_reports_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL, CONSTRAINT fk_survey_target_reports_updated_by FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_survey_target_reports_status ON survey_target_reports (status)`); await queryRunner.query(`CREATE INDEX idx_survey_target_reports_submitted_by ON survey_target_reports (submitted_by)`); await queryRunner.query(`CREATE INDEX idx_survey_target_reports_reviewed_by ON survey_target_reports (reviewed_by)`); await queryRunner.query(` CREATE TABLE survey_target_report_media ( report_id uuid NOT NULL, media_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 (report_id, media_id), CONSTRAINT fk_survey_target_report_media_report FOREIGN KEY (report_id) REFERENCES survey_target_reports(id) ON DELETE RESTRICT, CONSTRAINT fk_survey_target_report_media_media FOREIGN KEY (media_id) REFERENCES asset_media(id) ON DELETE RESTRICT, CONSTRAINT fk_survey_target_report_media_added_by FOREIGN KEY (added_by) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_survey_target_report_media_media_id ON survey_target_report_media (media_id)`); await queryRunner.query(`CREATE INDEX idx_survey_target_report_media_included ON survey_target_report_media (report_id, included)`); await queryRunner.query(` CREATE TABLE survey_target_report_versions ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), report_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_survey_target_report_versions_number UNIQUE (report_id, version_number), CONSTRAINT chk_survey_target_report_versions_event CHECK ( event IN ('SUBMITTED', 'APPROVED', 'REJECTED') ), CONSTRAINT fk_survey_target_report_versions_report FOREIGN KEY (report_id) REFERENCES survey_target_reports(id) ON DELETE RESTRICT, CONSTRAINT fk_survey_target_report_versions_actor FOREIGN KEY (actor_user_id) REFERENCES users(id) ON DELETE SET NULL ) `); await queryRunner.query(`CREATE INDEX idx_survey_target_report_versions_created_at ON survey_target_report_versions (created_at)`); await queryRunner.query(` INSERT INTO permissions (code, description) VALUES ('surveys.read_reports', 'Consultar capturas y versiones de relevamientos'), ('surveys.capture', 'Capturar y enviar relevamientos de campo'), ('surveys.review', 'Aprobar o rechazar relevamientos enviados') 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 survey_target_reports, survey_target_report_media TO ${applicationRole} `); await queryRunner.query(` GRANT SELECT, INSERT ON TABLE survey_target_report_versions TO ${applicationRole} `); await queryRunner.query(` REVOKE DELETE ON TABLE survey_target_reports, survey_target_report_media, survey_target_report_versions FROM ${applicationRole} `); await queryRunner.query(` REVOKE UPDATE ON TABLE survey_target_report_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 survey_target_report_versions`); await queryRunner.query(`DROP TABLE survey_target_report_media`); await queryRunner.query(`DROP TABLE survey_target_reports`); await queryRunner.query(` UPDATE survey_campaign_targets SET status = 'IN_PROGRESS' WHERE status = 'SUBMITTED' `); await queryRunner.query(` ALTER TABLE survey_campaign_targets DROP CONSTRAINT chk_survey_campaign_targets_status `); await queryRunner.query(` ALTER TABLE survey_campaign_targets ADD CONSTRAINT chk_survey_campaign_targets_status CHECK (status IN ( 'PENDING', 'IN_PROGRESS', 'COMPLETED', 'SKIPPED' )) `); } }