chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user