172 lines
6.4 KiB
TypeScript
172 lines
6.4 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
const newPermissions = ['assets.read_media', 'assets.manage_media'] as const;
|
|
|
|
const rolePermissionValues = `
|
|
('admin', 'assets.read_media'),
|
|
('admin', 'assets.manage_media'),
|
|
('director', 'assets.read_media'),
|
|
('supervisor', 'assets.read_media'),
|
|
('supervisor', 'assets.manage_media'),
|
|
('inspector', 'assets.read_media'),
|
|
('inspector', 'assets.manage_media'),
|
|
('auditor', 'assets.read_media')
|
|
`;
|
|
|
|
function quoteIdentifier(identifier: string): string {
|
|
return `"${identifier.replaceAll('"', '""')}"`;
|
|
}
|
|
|
|
export class PhaseB4AssetMedia1786723202000 implements MigrationInterface {
|
|
name = 'PhaseB4AssetMedia1786723202000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE asset_media (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
asset_id uuid NOT NULL,
|
|
kind varchar(20) 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),
|
|
source varchar(20) NOT NULL,
|
|
uploaded_by uuid,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
deleted_at timestamptz,
|
|
deleted_by uuid,
|
|
CONSTRAINT uq_asset_media_stored_name UNIQUE (stored_name),
|
|
CONSTRAINT chk_asset_media_kind CHECK (kind IN ('PHOTO', 'DOCUMENT')),
|
|
CONSTRAINT chk_asset_media_source CHECK (source IN ('WEB', 'ANDROID', 'IMPORT')),
|
|
CONSTRAINT chk_asset_media_mime CHECK (mime_type IN (
|
|
'image/jpeg', 'image/png', 'image/webp', 'application/pdf'
|
|
)),
|
|
CONSTRAINT chk_asset_media_size CHECK (
|
|
size_bytes > 0 AND size_bytes <= 15728640
|
|
),
|
|
CONSTRAINT chk_asset_media_sha256 CHECK (sha256 ~ '^[0-9a-f]{64}$'),
|
|
CONSTRAINT chk_asset_media_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_asset_media_accuracy CHECK (
|
|
accuracy_m IS NULL OR accuracy_m >= 0
|
|
),
|
|
CONSTRAINT fk_asset_media_asset FOREIGN KEY (asset_id)
|
|
REFERENCES assets(id) ON DELETE RESTRICT,
|
|
CONSTRAINT fk_asset_media_uploaded_by FOREIGN KEY (uploaded_by)
|
|
REFERENCES users(id) ON DELETE SET NULL,
|
|
CONSTRAINT fk_asset_media_deleted_by FOREIGN KEY (deleted_by)
|
|
REFERENCES users(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE INDEX idx_asset_media_asset_created
|
|
ON asset_media (asset_id, created_at DESC)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE INDEX idx_asset_media_sha256
|
|
ON asset_media (sha256)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE INDEX idx_asset_media_active
|
|
ON asset_media (asset_id, deleted_at)
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
ALTER TABLE asset_versions
|
|
DROP CONSTRAINT chk_asset_versions_change_type
|
|
`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE asset_versions
|
|
ADD CONSTRAINT chk_asset_versions_change_type CHECK (change_type IN (
|
|
'BASELINE', 'CREATED', 'UPDATED', 'STATUS_CHANGED',
|
|
'GEOMETRY_UPDATED', 'GEOMETRY_REMOVED',
|
|
'MEDIA_UPLOADED', 'MEDIA_UPDATED', 'MEDIA_REMOVED'
|
|
))
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
INSERT INTO permissions (code, description)
|
|
VALUES
|
|
('assets.read_media', 'Consultar fotografías y documentos de activos'),
|
|
('assets.manage_media', 'Cargar, editar o retirar archivos de activos')
|
|
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, UPDATE ON TABLE asset_media TO ${applicationRole}
|
|
`);
|
|
await queryRunner.query(`
|
|
REVOKE DELETE ON TABLE asset_media 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(`
|
|
UPDATE asset_versions
|
|
SET change_type = 'UPDATED'
|
|
WHERE change_type IN ('MEDIA_UPLOADED', 'MEDIA_UPDATED', 'MEDIA_REMOVED')
|
|
`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE asset_versions
|
|
DROP CONSTRAINT chk_asset_versions_change_type
|
|
`);
|
|
await queryRunner.query(`
|
|
ALTER TABLE asset_versions
|
|
ADD CONSTRAINT chk_asset_versions_change_type CHECK (change_type IN (
|
|
'BASELINE', 'CREATED', 'UPDATED', 'STATUS_CHANGED',
|
|
'GEOMETRY_UPDATED', 'GEOMETRY_REMOVED'
|
|
))
|
|
`);
|
|
await queryRunner.query('DROP TABLE asset_media');
|
|
}
|
|
}
|