126 lines
7.5 KiB
TypeScript
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`);
|
|
}
|
|
}
|