Files
dh-inspeccion-v2/api-v3/src/database/migrations/1787680800000-phase-d5-3-7-import-planning-application.ts
T

126 lines
7.5 KiB
TypeScript

import { MigrationInterface, QueryRunner } from 'typeorm';
const newPermissions = ['asset_imports.apply'] as const;
const rolePermissionValues = `
('admin', 'asset_imports.apply'),
('director', 'asset_imports.apply')
`;
function quoteIdentifier(identifier: string): string {
return `"${identifier.replaceAll('"', '""')}"`;
}
export class PhaseD537ImportPlanningApplication1787680800000 implements MigrationInterface {
name = 'PhaseD537ImportPlanningApplication1787680800000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE asset_import_plans (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
batch_id uuid NOT NULL,
revision integer NOT NULL,
status varchar(24) NOT NULL,
external_id_namespace varchar(80),
operator_asset_id uuid,
summary jsonb NOT NULL DEFAULT '{}'::jsonb,
plan_hash char(64) NOT NULL,
generated_by uuid,
generated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
applied_by uuid,
applied_at timestamptz,
application_summary jsonb,
rolled_back_by uuid,
rolled_back_at timestamptz,
rollback_summary jsonb,
superseded_at timestamptz,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_asset_import_plan_revision CHECK (revision > 0),
CONSTRAINT chk_asset_import_plan_status CHECK (status IN ('REVIEW_REQUIRED','READY','APPLIED','ROLLED_BACK','SUPERSEDED','FAILED')),
CONSTRAINT chk_asset_import_plan_namespace CHECK (external_id_namespace IS NULL OR external_id_namespace ~ '^[A-Z0-9][A-Z0-9._/-]{1,79}$'),
CONSTRAINT chk_asset_import_plan_hash CHECK (plan_hash ~ '^[0-9a-f]{64}$'),
CONSTRAINT fk_asset_import_plan_batch FOREIGN KEY (batch_id) REFERENCES asset_import_batches(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_import_plan_operator FOREIGN KEY (operator_asset_id) REFERENCES assets(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_import_plan_generated_by FOREIGN KEY (generated_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_import_plan_applied_by FOREIGN KEY (applied_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_import_plan_rolled_back_by FOREIGN KEY (rolled_back_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT uq_asset_import_plan_revision UNIQUE (batch_id, revision)
)
`);
await queryRunner.query(`CREATE UNIQUE INDEX uq_asset_import_plan_active_batch ON asset_import_plans (batch_id) WHERE superseded_at IS NULL`);
await queryRunner.query(`CREATE INDEX idx_asset_import_plans_status ON asset_import_plans (status, generated_at DESC)`);
await queryRunner.query(`
CREATE TABLE asset_import_plan_items (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
plan_id uuid NOT NULL,
item_order integer NOT NULL,
entity_key varchar(320) NOT NULL,
entity_kind varchar(40) NOT NULL,
action varchar(16) NOT NULL,
status varchar(24) NOT NULL,
asset_type_code varchar(80),
display_name varchar(260) NOT NULL,
generated_code varchar(120),
parent_entity_key varchar(320),
matched_asset_id uuid,
applied_asset_id uuid,
applied_object_id varchar(255),
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
source_row_numbers jsonb NOT NULL DEFAULT '[]'::jsonb,
review_codes jsonb NOT NULL DEFAULT '[]'::jsonb,
resolution_note text,
resolved_by uuid,
resolved_at timestamptz,
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_asset_import_plan_item_order CHECK (item_order > 0),
CONSTRAINT chk_asset_import_plan_item_kind CHECK (entity_kind IN ('ORGANIZATION','AREA','FIELD','OPERATOR_RELATION','INSTALLATION','TECHNICAL_ASSET')),
CONSTRAINT chk_asset_import_plan_item_action CHECK (action IN ('CREATE','MATCH','REVIEW','IGNORE')),
CONSTRAINT chk_asset_import_plan_item_status CHECK (status IN ('PLANNED','MATCHED','REVIEW','IGNORED','APPLIED','ROLLED_BACK','FAILED')),
CONSTRAINT chk_asset_import_plan_item_rows CHECK (jsonb_typeof(source_row_numbers)='array'),
CONSTRAINT chk_asset_import_plan_item_reviews CHECK (jsonb_typeof(review_codes)='array'),
CONSTRAINT fk_asset_import_plan_item_plan FOREIGN KEY (plan_id) REFERENCES asset_import_plans(id) ON DELETE RESTRICT,
CONSTRAINT fk_asset_import_plan_item_match FOREIGN KEY (matched_asset_id) REFERENCES assets(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_import_plan_item_applied_asset FOREIGN KEY (applied_asset_id) REFERENCES assets(id) ON DELETE SET NULL,
CONSTRAINT fk_asset_import_plan_item_resolved_by FOREIGN KEY (resolved_by) REFERENCES users(id) ON DELETE SET NULL,
CONSTRAINT uq_asset_import_plan_item_key UNIQUE (plan_id, entity_key)
)
`);
await queryRunner.query(`CREATE INDEX idx_asset_import_plan_items_plan_order ON asset_import_plan_items (plan_id, item_order)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_plan_items_plan_action ON asset_import_plan_items (plan_id, action, entity_kind)`);
await queryRunner.query(`CREATE INDEX idx_asset_import_plan_items_match ON asset_import_plan_items (matched_asset_id) WHERE matched_asset_id IS NOT NULL`);
await queryRunner.query(`CREATE INDEX idx_asset_import_plan_items_applied ON asset_import_plan_items (applied_asset_id) WHERE applied_asset_id IS NOT NULL`);
await queryRunner.query(
`INSERT INTO permissions (code,description) VALUES ($1,$2) ON CONFLICT (code) DO UPDATE SET description=EXCLUDED.description`,
['asset_imports.apply', 'Aplicar o revertir planes de importación transaccional sobre el Maestro'],
);
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_plans, asset_import_plan_items TO ${applicationRole}`);
await queryRunner.query(`REVOKE DELETE ON TABLE asset_import_plans, asset_import_plan_items 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_plan_items`);
await queryRunner.query(`DROP TABLE asset_import_plans`);
}
}