Files
dh-inspeccion-v2/api-v3/src/database/migrations/1787594400000-phase-d5-3-5-import-center-scalability.ts
T

138 lines
8.4 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
const newPermissions = ['asset_imports.read', 'asset_imports.manage'] as const;
const rolePermissionValues = `
('admin', 'asset_imports.read'), ('admin', 'asset_imports.manage'),
('director', 'asset_imports.read'), ('director', 'asset_imports.manage'),
('supervisor', 'asset_imports.read'), ('supervisor', 'asset_imports.manage')
`;
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD535ImportCenterScalability1787594400000 implements MigrationInterface {
name = 'PhaseD535ImportCenterScalability1787594400000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_assets_code_trgm ON assets USING gin (code gin_trgm_ops)`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_assets_name_trgm ON assets USING gin (name gin_trgm_ops)`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_asset_external_identifiers_value_trgm ON asset_external_identifiers USING gin (value gin_trgm_ops)`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_asset_attribute_values_text_trgm ON asset_attribute_values USING gin ((value #>> '{}') gin_trgm_ops)`);
await queryRunner.query(`CREATE TYPE asset_import_batch_status AS ENUM ('ANALYZED','REVIEW_REQUIRED','CANCELLED','FAILED')`);
await queryRunner.query(`CREATE TYPE asset_import_row_status AS ENUM ('READY','WARNING','CONFLICT','IGNORED')`);
await queryRunner.query(`CREATE TYPE asset_import_suggested_action AS ENUM ('CREATE','MATCH','REVIEW','IGNORE')`);
await queryRunner.query(`
CREATE TABLE asset_import_batches (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
original_name varchar(255) NOT NULL,
stored_name varchar(255) NOT NULL,
mime_type varchar(120) NOT NULL,
size_bytes integer NOT NULL,
sha256 char(64) NOT NULL,
profile_code varchar(80) NOT NULL,
profile_confidence integer NOT NULL DEFAULT 0,
worksheet_name varchar(160),
header_row integer,
total_rows integer NOT NULL DEFAULT 0,
ready_rows integer NOT NULL DEFAULT 0,
warning_rows integer NOT NULL DEFAULT 0,
conflict_rows integer NOT NULL DEFAULT 0,
ignored_rows integer NOT NULL DEFAULT 0,
status asset_import_batch_status NOT NULL,
source_document_id uuid,
source_label varchar(240),
notes text,
analysis jsonb NOT NULL DEFAULT '{}'::jsonb,
uploaded_by uuid,
analyzed_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_asset_import_batch_size CHECK (size_bytes > 0 AND size_bytes <= 26214400),
CONSTRAINT chk_asset_import_profile_confidence CHECK (profile_confidence >= 0 AND profile_confidence <= 100),
CONSTRAINT chk_asset_import_batch_counts CHECK (total_rows >= 0 AND ready_rows >= 0 AND warning_rows >= 0 AND conflict_rows >= 0 AND ignored_rows >= 0),
CONSTRAINT fk_asset_import_batch_source_document FOREIGN KEY (source_document_id) REFERENCES source_documents(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_import_batch_uploaded_by FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
)
`);
await queryRunner.query(`CREATE INDEX idx_asset_import_batches_created_at ON asset_import_batches (created_at DESC)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_batches_status ON asset_import_batches (status)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_batches_sha256 ON asset_import_batches (sha256)`);
await queryRunner.query(`
CREATE TABLE asset_import_rows (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
batch_id uuid NOT NULL,
worksheet_name varchar(160) NOT NULL,
row_number integer NOT NULL,
status asset_import_row_status NOT NULL,
suggested_action asset_import_suggested_action NOT NULL,
raw_data jsonb NOT NULL,
normalized_data jsonb NOT NULL,
issue_codes jsonb NOT NULL DEFAULT '[]'::jsonb,
fingerprint char(64) NOT NULL,
matched_asset_id uuid,
imported_asset_id uuid,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_asset_import_row_number CHECK (row_number > 0),
CONSTRAINT chk_asset_import_row_issues_array CHECK (jsonb_typeof(issue_codes) = 'array'),
CONSTRAINT fk_asset_import_rows_batch FOREIGN KEY (batch_id) REFERENCES asset_import_batches(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_import_rows_matched_asset FOREIGN KEY (matched_asset_id) REFERENCES assets(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_import_rows_imported_asset FOREIGN KEY (imported_asset_id) REFERENCES assets(id) ON DELETE SET NULL,
CONSTRAINT uq_asset_import_row_source UNIQUE (batch_id, worksheet_name, row_number)
)
`);
await queryRunner.query(`CREATE INDEX idx_asset_import_rows_batch_status ON asset_import_rows (batch_id,status,row_number)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_rows_batch_action ON asset_import_rows (batch_id,suggested_action)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_rows_fingerprint ON asset_import_rows (fingerprint)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_rows_inventory_id ON asset_import_rows ((normalized_data->>'inventoryId')) WHERE normalized_data ? 'inventoryId'`);
const descriptions: Record<string, string> = {
'asset_imports.read': 'Consultar lotes, análisis y conflictos de importación del Maestro',
'asset_imports.manage': 'Analizar archivos XLSX/CSV y administrar lotes de importación del Maestro',
};
for (const permission of newPermissions) {
await queryRunner.query(
`INSERT INTO permissions (code,description) VALUES ($1,$2) ON CONFLICT (code) DO UPDATE SET description=EXCLUDED.description`,
[permission, descriptions[permission]],
);
}
await queryRunner.query(`
WITH mapping(role_code,permission_code) AS (VALUES ${rolePermissionValues})
INSERT INTO role_permissions (role_id,permission_id)
SELECT r.id,p.id FROM mapping m JOIN roles r ON r.code=m.role_code JOIN permissions p ON p.code=m.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_import_batches, asset_import_rows TO ${applicationRole}`);
await queryRunner.query(`REVOKE DELETE ON TABLE asset_import_batches, asset_import_rows 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 rp USING roles r,permissions p,mapping m
WHERE rp.role_id=r.id AND rp.permission_id=p.id AND r.code=m.role_code AND p.code=m.permission_code
`);
await queryRunner.query(`DELETE FROM permissions WHERE code=ANY($1::text[]) AND NOT EXISTS (SELECT 1 FROM role_permissions WHERE permission_id=permissions.id)`, [newPermissions]);
await queryRunner.query(`DROP TABLE asset_import_rows`);
await queryRunner.query(`DROP TABLE asset_import_batches`);
await queryRunner.query(`DROP TYPE asset_import_suggested_action`);
await queryRunner.query(`DROP TYPE asset_import_row_status`);
await queryRunner.query(`DROP TYPE asset_import_batch_status`);
await queryRunner.query(`DROP INDEX IF EXISTS idx_asset_attribute_values_text_trgm`);
await queryRunner.query(`DROP INDEX IF EXISTS idx_asset_external_identifiers_value_trgm`);
await queryRunner.query(`DROP INDEX IF EXISTS idx_assets_name_trgm`);
await queryRunner.query(`DROP INDEX IF EXISTS idx_assets_code_trgm`);
}
}