179 lines
7.9 KiB
TypeScript
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`);
|
|
}
|
|
}
|