chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
+234
@@ -0,0 +1,234 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
const newPermissions = [
|
||||
'inspection_evidence.read',
|
||||
'inspection_evidence.create',
|
||||
'inspection_communications.read',
|
||||
'inspection_communications.create',
|
||||
] as const;
|
||||
|
||||
const rolePermissionValues = `
|
||||
('admin', 'inspection_evidence.read'),
|
||||
('admin', 'inspection_evidence.create'),
|
||||
('admin', 'inspection_communications.read'),
|
||||
('admin', 'inspection_communications.create'),
|
||||
('director', 'inspection_evidence.read'),
|
||||
('director', 'inspection_evidence.create'),
|
||||
('director', 'inspection_communications.read'),
|
||||
('director', 'inspection_communications.create'),
|
||||
('supervisor', 'inspection_evidence.read'),
|
||||
('supervisor', 'inspection_evidence.create'),
|
||||
('supervisor', 'inspection_communications.read'),
|
||||
('supervisor', 'inspection_communications.create'),
|
||||
('inspector', 'inspection_evidence.read'),
|
||||
('inspector', 'inspection_evidence.create'),
|
||||
('inspector', 'inspection_communications.read'),
|
||||
('inspector', 'inspection_communications.create'),
|
||||
('auditor', 'inspection_evidence.read'),
|
||||
('auditor', 'inspection_communications.read')
|
||||
`;
|
||||
|
||||
function quoteIdentifier(identifier: string): string {
|
||||
return `"${identifier.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
export class PhaseD4FindingEvidenceCommunications1787241600000 implements MigrationInterface {
|
||||
name = 'PhaseD4FindingEvidenceCommunications1787241600000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE inspection_finding_communications (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
finding_id uuid NOT NULL,
|
||||
direction varchar(20) NOT NULL,
|
||||
channel varchar(20) NOT NULL,
|
||||
type varchar(32) NOT NULL,
|
||||
occurred_at timestamptz NOT NULL,
|
||||
subject varchar(250) NOT NULL,
|
||||
details text,
|
||||
contact_name varchar(200),
|
||||
contact_email varchar(320),
|
||||
created_by uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_inspection_finding_communications_id_finding
|
||||
UNIQUE (id, finding_id),
|
||||
CONSTRAINT chk_inspection_finding_communications_direction CHECK (
|
||||
direction IN ('INBOUND', 'OUTBOUND', 'INTERNAL')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_communications_channel CHECK (
|
||||
channel IN ('EMAIL', 'IN_PERSON', 'PHONE', 'LETTER', 'SYSTEM', 'OTHER')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_communications_type CHECK (
|
||||
type IN ('COMPANY_RESPONSE', 'AUTHORITY_NOTICE', 'FOLLOW_UP', 'OTHER')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_communications_subject CHECK (
|
||||
LENGTH(TRIM(subject)) > 0
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_company_response_direction CHECK (
|
||||
type <> 'COMPANY_RESPONSE' OR direction = 'INBOUND'
|
||||
),
|
||||
CONSTRAINT fk_inspection_finding_communications_finding FOREIGN KEY (finding_id)
|
||||
REFERENCES inspection_findings(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_finding_communications_created_by FOREIGN KEY (created_by)
|
||||
REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_finding_communications_timeline
|
||||
ON inspection_finding_communications (finding_id, occurred_at DESC)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE inspection_finding_evidence (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
finding_id uuid NOT NULL,
|
||||
communication_id uuid,
|
||||
kind varchar(20) NOT NULL,
|
||||
purpose varchar(40) NOT NULL,
|
||||
original_name varchar(255) NOT NULL,
|
||||
stored_name varchar(80) NOT NULL,
|
||||
mime_type varchar(100) NOT NULL,
|
||||
size_bytes bigint NOT NULL,
|
||||
sha256 char(64) NOT NULL,
|
||||
title varchar(200),
|
||||
description text,
|
||||
captured_at timestamptz,
|
||||
latitude numeric(9, 6),
|
||||
longitude numeric(9, 6),
|
||||
accuracy_m numeric(12, 3),
|
||||
device_label varchar(200),
|
||||
source varchar(20) NOT NULL,
|
||||
uploaded_by uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_inspection_finding_evidence_stored_name UNIQUE (stored_name),
|
||||
CONSTRAINT chk_inspection_finding_evidence_kind CHECK (
|
||||
kind IN ('PHOTO', 'DOCUMENT')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_purpose CHECK (
|
||||
purpose IN (
|
||||
'OBSERVATION', 'COMPANY_RESPONSE',
|
||||
'COMMUNICATION_ATTACHMENT', 'OTHER_DOCUMENT'
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_source CHECK (
|
||||
source IN ('WEB', 'ANDROID', 'IMPORT')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_mime CHECK (
|
||||
mime_type IN ('image/jpeg', 'image/png', 'image/webp', 'application/pdf')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_size CHECK (
|
||||
size_bytes > 0 AND size_bytes <= 15728640
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_sha256 CHECK (
|
||||
sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_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_inspection_finding_evidence_accuracy CHECK (
|
||||
accuracy_m IS NULL OR accuracy_m >= 0
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_evidence_kind_mime CHECK (
|
||||
(kind = 'PHOTO' AND mime_type IN ('image/jpeg', 'image/png', 'image/webp'))
|
||||
OR (kind = 'DOCUMENT' AND mime_type = 'application/pdf')
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_company_response_pdf CHECK (
|
||||
purpose <> 'COMPANY_RESPONSE'
|
||||
OR (
|
||||
kind = 'DOCUMENT'
|
||||
AND mime_type = 'application/pdf'
|
||||
AND communication_id IS NOT NULL
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_inspection_finding_attachment_link CHECK (
|
||||
purpose <> 'COMMUNICATION_ATTACHMENT' OR communication_id IS NOT NULL
|
||||
),
|
||||
CONSTRAINT fk_inspection_finding_evidence_finding FOREIGN KEY (finding_id)
|
||||
REFERENCES inspection_findings(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_finding_evidence_communication
|
||||
FOREIGN KEY (communication_id, finding_id)
|
||||
REFERENCES inspection_finding_communications(id, finding_id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_inspection_finding_evidence_uploaded_by FOREIGN KEY (uploaded_by)
|
||||
REFERENCES users(id) ON DELETE SET NULL
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_finding_evidence_finding_created
|
||||
ON inspection_finding_evidence (finding_id, created_at DESC)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_finding_evidence_sha256
|
||||
ON inspection_finding_evidence (sha256)
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX idx_inspection_finding_evidence_communication
|
||||
ON inspection_finding_evidence (communication_id)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO permissions (code, description)
|
||||
VALUES
|
||||
('inspection_evidence.read', 'Consultar evidencias y documentos de hallazgos'),
|
||||
('inspection_evidence.create', 'Incorporar evidencias y documentos de hallazgos'),
|
||||
('inspection_communications.read', 'Consultar comunicaciones de hallazgos'),
|
||||
('inspection_communications.create', 'Registrar comunicaciones de hallazgos')
|
||||
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 roleRows = (await queryRunner.query(
|
||||
'SELECT 1 FROM pg_roles WHERE rolname = $1',
|
||||
[appRole],
|
||||
)) as unknown[];
|
||||
if (roleRows.length !== 1) throw new Error('Configured DB_APP_USER does not exist');
|
||||
const applicationRole = quoteIdentifier(appRole);
|
||||
await queryRunner.query(`
|
||||
GRANT SELECT, INSERT ON TABLE
|
||||
inspection_finding_communications,
|
||||
inspection_finding_evidence
|
||||
TO ${applicationRole}
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
REVOKE UPDATE, DELETE ON TABLE
|
||||
inspection_finding_communications,
|
||||
inspection_finding_evidence
|
||||
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_finding_evidence');
|
||||
await queryRunner.query('DROP TABLE inspection_finding_communications');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user