Files
dh-inspeccion-v2/api-v3/src/database/migrations/1786982400000-phase-d1-inspection-visits.ts
T

179 lines
7.9 KiB
TypeScript

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<void> {
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<void> {
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`);
}
}