Files
dh-inspeccion-v2/api-v3/src/database/migrations/1787068800000-phase-d2-inspection-acts.ts
T

206 lines
8.6 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
const newPermissions = [
'inspection_acts.read',
'inspection_acts.create',
'inspection_acts.update',
'inspection_acts.cancel',
] as const;
const rolePermissionValues = `
('admin', 'inspection_acts.read'),
('admin', 'inspection_acts.create'),
('admin', 'inspection_acts.update'),
('admin', 'inspection_acts.cancel'),
('director', 'inspection_acts.read'),
('supervisor', 'inspection_acts.read'),
('supervisor', 'inspection_acts.create'),
('supervisor', 'inspection_acts.update'),
('supervisor', 'inspection_acts.cancel'),
('inspector', 'inspection_acts.read'),
('inspector', 'inspection_acts.create'),
('inspector', 'inspection_acts.update'),
('auditor', 'inspection_acts.read')
`;
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD2InspectionActs1787068800000 implements MigrationInterface {
name = 'PhaseD2InspectionActs1787068800000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE document_annual_sequences (
document_type varchar(20) NOT NULL,
year integer NOT NULL,
last_number integer NOT NULL DEFAULT 0,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (document_type, year),
CONSTRAINT chk_document_annual_sequences_type CHECK (
document_type IN ('ACT', 'REPORT')
),
CONSTRAINT chk_document_annual_sequences_year CHECK (
year BETWEEN 2000 AND 9999
),
CONSTRAINT chk_document_annual_sequences_number CHECK (
last_number >= 0
)
)
`);
await queryRunner.query(`
CREATE TABLE inspection_acts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
visit_id uuid NOT NULL,
act_year integer NOT NULL,
act_number integer NOT NULL,
code varchar(24) NOT NULL,
status varchar(24) NOT NULL DEFAULT 'DRAFT',
occurred_at timestamptz NOT NULL,
title varchar(200) NOT NULL,
summary text NOT NULL,
observations text,
current_version integer NOT NULL DEFAULT 0,
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_acts_year_number UNIQUE (act_year, act_number),
CONSTRAINT uq_inspection_acts_code UNIQUE (code),
CONSTRAINT chk_inspection_acts_year CHECK (act_year BETWEEN 2000 AND 9999),
CONSTRAINT chk_inspection_acts_number CHECK (act_number > 0),
CONSTRAINT chk_inspection_acts_code CHECK (
code ~ '^ACTA-[0-9]{4}-[0-9]{6}$'
),
CONSTRAINT chk_inspection_acts_status CHECK (
status IN ('DRAFT', 'READY', 'CLOSED', 'CANCELLED', 'RECTIFIED')
),
CONSTRAINT chk_inspection_acts_current_version CHECK (current_version >= 0),
CONSTRAINT chk_inspection_acts_cancellation CHECK (
status <> 'CANCELLED' OR LENGTH(TRIM(COALESCE(cancellation_reason, ''))) >= 10
),
CONSTRAINT fk_inspection_acts_visit FOREIGN KEY (visit_id)
REFERENCES inspection_visits(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_acts_created_by FOREIGN KEY (created_by)
REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_inspection_acts_updated_by FOREIGN KEY (updated_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_inspection_acts_visit_status ON inspection_acts (visit_id, status)`);
await queryRunner.query(`CREATE INDEX idx_inspection_acts_occurred_at ON inspection_acts (occurred_at)`);
await queryRunner.query(`CREATE INDEX idx_inspection_acts_created_by ON inspection_acts (created_by)`);
await queryRunner.query(`
CREATE TABLE inspection_act_assets (
act_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 (act_id, asset_id),
CONSTRAINT fk_inspection_act_assets_act FOREIGN KEY (act_id)
REFERENCES inspection_acts(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_assets_asset FOREIGN KEY (asset_id)
REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_assets_added_by FOREIGN KEY (added_by)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_inspection_act_assets_asset_id ON inspection_act_assets (asset_id)`);
await queryRunner.query(`CREATE INDEX idx_inspection_act_assets_included ON inspection_act_assets (act_id, included)`);
await queryRunner.query(`
CREATE TABLE inspection_act_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
act_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_inspection_act_versions_number UNIQUE (act_id, version_number),
CONSTRAINT chk_inspection_act_versions_number CHECK (version_number > 0),
CONSTRAINT chk_inspection_act_versions_event CHECK (
event IN ('CREATED', 'UPDATED', 'CANCELLED')
),
CONSTRAINT fk_inspection_act_versions_act FOREIGN KEY (act_id)
REFERENCES inspection_acts(id) ON DELETE RESTRICT,
CONSTRAINT fk_inspection_act_versions_actor FOREIGN KEY (actor_user_id)
REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_inspection_act_versions_created_at ON inspection_act_versions (created_at)`);
await queryRunner.query(`
INSERT INTO permissions (code, description)
VALUES
('inspection_acts.read', 'Consultar actas y sus versiones'),
('inspection_acts.create', 'Crear actas dentro de visitas asignadas'),
('inspection_acts.update', 'Actualizar actas en borrador'),
('inspection_acts.cancel', 'Cancelar actas en borrador')
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
document_annual_sequences, inspection_acts, inspection_act_assets
TO ${applicationRole}
`);
await queryRunner.query(`
GRANT SELECT, INSERT ON TABLE inspection_act_versions
TO ${applicationRole}
`);
await queryRunner.query(`
REVOKE DELETE ON TABLE
document_annual_sequences, inspection_acts,
inspection_act_assets, inspection_act_versions
FROM ${applicationRole}
`);
await queryRunner.query(`
REVOKE UPDATE ON TABLE inspection_act_versions
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_act_versions`);
await queryRunner.query(`DROP TABLE inspection_act_assets`);
await queryRunner.query(`DROP TABLE inspection_acts`);
await queryRunner.query(`DROP TABLE document_annual_sequences`);
}
}