From e3b24cba3df294bfcdf71b7abe3a38ae538ac43e Mon Sep 17 00:00:00 2001 From: enlineawork Date: Tue, 8 Sep 2026 06:44:06 -0300 Subject: [PATCH] F4: remove obsolete Survey subsystem schema and permissions --- ...790000300000-f4-remove-survey-subsystem.ts | 220 ++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 api-v3/src/database/migrations/1790000300000-f4-remove-survey-subsystem.ts diff --git a/api-v3/src/database/migrations/1790000300000-f4-remove-survey-subsystem.ts b/api-v3/src/database/migrations/1790000300000-f4-remove-survey-subsystem.ts new file mode 100644 index 0000000..dad8d8e --- /dev/null +++ b/api-v3/src/database/migrations/1790000300000-f4-remove-survey-subsystem.ts @@ -0,0 +1,220 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +const surveyPermissions = [ + 'surveys.read', + 'surveys.manage', + 'surveys.assign', + 'surveys.execute', + 'surveys.read_reports', + 'surveys.capture', + 'surveys.review', +] as const; + +const rolePermissionValues = ` + ('admin', 'surveys.read'), + ('admin', 'surveys.manage'), + ('admin', 'surveys.assign'), + ('admin', 'surveys.execute'), + ('admin', 'surveys.read_reports'), + ('admin', 'surveys.capture'), + ('admin', 'surveys.review'), + ('director', 'surveys.read'), + ('director', 'surveys.read_reports'), + ('director', 'surveys.review'), + ('supervisor', 'surveys.read'), + ('supervisor', 'surveys.manage'), + ('supervisor', 'surveys.assign'), + ('supervisor', 'surveys.execute'), + ('supervisor', 'surveys.read_reports'), + ('supervisor', 'surveys.capture'), + ('supervisor', 'surveys.review'), + ('inspector', 'surveys.read'), + ('inspector', 'surveys.execute'), + ('inspector', 'surveys.read_reports'), + ('inspector', 'surveys.capture'), + ('auditor', 'surveys.read'), + ('auditor', 'surveys.read_reports') +`; + +function quoteIdentifier(identifier: string): string { + return `"${identifier.replaceAll('"', '""')}"`; +} + +export class F4RemoveSurveySubsystem1790000300000 implements MigrationInterface { + name = 'F4RemoveSurveySubsystem1790000300000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DELETE FROM role_permissions + WHERE permission_id IN ( + SELECT id FROM permissions WHERE code = ANY($1::varchar[]) + ) + `, [surveyPermissions]); + await queryRunner.query( + `DELETE FROM permissions WHERE code = ANY($1::varchar[])`, + [surveyPermissions], + ); + + // Deliberadamente sin CASCADE: una dependencia externa debe detener la migración. + await queryRunner.query(`DROP TABLE IF EXISTS survey_target_report_versions`); + await queryRunner.query(`DROP TABLE IF EXISTS survey_target_report_media`); + await queryRunner.query(`DROP TABLE IF EXISTS survey_target_reports`); + await queryRunner.query(`DROP TABLE IF EXISTS survey_campaign_targets`); + await queryRunner.query(`DROP TABLE IF EXISTS survey_campaigns`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS survey_campaigns ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + code varchar(80) NOT NULL UNIQUE, + name varchar(200) NOT NULL, + description text, + status varchar(24) NOT NULL DEFAULT 'DRAFT', + planned_start_at timestamptz, + planned_end_at timestamptz, + scope_asset_id uuid REFERENCES assets(id) ON DELETE RESTRICT, + coordinator_user_id uuid REFERENCES users(id) ON DELETE SET NULL, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + updated_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT chk_survey_campaigns_status CHECK ( + status IN ('DRAFT','PLANNED','IN_PROGRESS','COMPLETED','CANCELLED') + ), + CONSTRAINT chk_survey_campaigns_dates CHECK ( + planned_start_at IS NULL OR planned_end_at IS NULL OR planned_end_at >= planned_start_at + ) + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaigns_status ON survey_campaigns(status)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaigns_scope_asset_id ON survey_campaigns(scope_asset_id)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaigns_coordinator_user_id ON survey_campaigns(coordinator_user_id)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaigns_planned_dates ON survey_campaigns(planned_start_at,planned_end_at)`); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS survey_campaign_targets ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + campaign_id uuid NOT NULL REFERENCES survey_campaigns(id) ON DELETE RESTRICT, + asset_id uuid NOT NULL REFERENCES assets(id) ON DELETE RESTRICT, + assigned_user_id uuid REFERENCES users(id) ON DELETE SET NULL, + status varchar(24) NOT NULL DEFAULT 'PENDING', + due_at timestamptz, + instructions text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + updated_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_survey_campaign_targets_campaign_asset UNIQUE(campaign_id,asset_id), + CONSTRAINT chk_survey_campaign_targets_status CHECK ( + status IN ('PENDING','IN_PROGRESS','SUBMITTED','COMPLETED','SKIPPED') + ) + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaign_targets_campaign_status ON survey_campaign_targets(campaign_id,status)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaign_targets_asset_id ON survey_campaign_targets(asset_id)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_campaign_targets_assigned_user_id ON survey_campaign_targets(assigned_user_id)`); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS survey_target_reports ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + target_id uuid NOT NULL UNIQUE REFERENCES survey_campaign_targets(id) ON DELETE RESTRICT, + 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 REFERENCES users(id) ON DELETE SET NULL, + reviewed_at timestamptz, + reviewed_by uuid REFERENCES users(id) ON DELETE SET NULL, + review_notes text, + created_by uuid REFERENCES users(id) ON DELETE SET NULL, + updated_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + 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) + ) + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_target_reports_status ON survey_target_reports(status)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_target_reports_submitted_by ON survey_target_reports(submitted_by)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_target_reports_reviewed_by ON survey_target_reports(reviewed_by)`); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS survey_target_report_media ( + report_id uuid NOT NULL REFERENCES survey_target_reports(id) ON DELETE RESTRICT, + media_id uuid NOT NULL REFERENCES asset_media(id) ON DELETE RESTRICT, + included boolean NOT NULL DEFAULT true, + added_by uuid REFERENCES users(id) ON DELETE SET NULL, + created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(report_id,media_id) + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_target_report_media_media_id ON survey_target_report_media(media_id)`); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_survey_target_report_media_included ON survey_target_report_media(report_id,included)`); + + await queryRunner.query(` + CREATE TABLE IF NOT EXISTS survey_target_report_versions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + report_id uuid NOT NULL REFERENCES survey_target_reports(id) ON DELETE RESTRICT, + version_number integer NOT NULL, + event varchar(24) NOT NULL, + snapshot jsonb NOT NULL, + actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL, + 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') + ) + ) + `); + await queryRunner.query(`CREATE INDEX IF NOT EXISTS 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','Consultar planificación de relevamientos'), + ('surveys.manage','Crear y actualizar campañas de relevamiento'), + ('surveys.assign','Asignar objetivos de relevamiento'), + ('surveys.execute','Actualizar el avance de objetivos asignados'), + ('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 + JOIN roles role ON role.code=mapping.role_code + 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_campaigns,survey_campaign_targets,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_campaigns,survey_campaign_targets,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}`); + } +}