138 lines
8.4 KiB
TypeScript
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`);
|
|
}
|
|
}
|