chore: import DH V2 D5.6.4 production baseline
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
function quoteIdentifier(identifier: string): string {
|
||||
return `"${identifier.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
export class PhaseD5324AssetContextHistory1788804000000 implements MigrationInterface {
|
||||
name = 'PhaseD5324AssetContextHistory1788804000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE asset_context_history (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
asset_id uuid NOT NULL,
|
||||
parent_id uuid,
|
||||
operational_area_id uuid,
|
||||
operator_company_id uuid,
|
||||
valid_from timestamptz NOT NULL,
|
||||
valid_until timestamptz,
|
||||
change_reason text NOT NULL,
|
||||
end_reason text,
|
||||
asset_version_number integer NOT NULL,
|
||||
source varchar(32) NOT NULL DEFAULT 'WEB',
|
||||
request_id varchar(128),
|
||||
created_by uuid,
|
||||
ended_by uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at timestamptz,
|
||||
CONSTRAINT fk_asset_context_history_asset FOREIGN KEY (asset_id) REFERENCES assets(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_asset_context_history_parent FOREIGN KEY (parent_id) REFERENCES assets(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_asset_context_history_area FOREIGN KEY (operational_area_id) REFERENCES assets(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_asset_context_history_company FOREIGN KEY (operator_company_id) REFERENCES assets(id) ON DELETE RESTRICT,
|
||||
CONSTRAINT fk_asset_context_history_created_by FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT fk_asset_context_history_ended_by FOREIGN KEY (ended_by) REFERENCES users(id) ON DELETE SET NULL,
|
||||
CONSTRAINT chk_asset_context_history_dates CHECK (valid_until IS NULL OR valid_until >= valid_from),
|
||||
CONSTRAINT chk_asset_context_history_context_pair CHECK (
|
||||
(operational_area_id IS NULL AND operator_company_id IS NULL)
|
||||
OR (operational_area_id IS NOT NULL AND operator_company_id IS NOT NULL)
|
||||
),
|
||||
CONSTRAINT chk_asset_context_history_reason CHECK (length(btrim(change_reason)) >= 5),
|
||||
CONSTRAINT chk_asset_context_history_version CHECK (asset_version_number >= 1)
|
||||
)
|
||||
`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX uq_asset_context_history_active ON asset_context_history (asset_id) WHERE valid_until IS NULL`);
|
||||
await queryRunner.query(`CREATE INDEX idx_asset_context_history_asset_period ON asset_context_history (asset_id, valid_from DESC, valid_until)`);
|
||||
await queryRunner.query(`CREATE INDEX idx_asset_context_history_area_period ON asset_context_history (operational_area_id, valid_from DESC) WHERE operational_area_id IS NOT NULL`);
|
||||
await queryRunner.query(`CREATE INDEX idx_asset_context_history_company_period ON asset_context_history (operator_company_id, valid_from DESC) WHERE operator_company_id IS NOT NULL`);
|
||||
|
||||
await queryRunner.query(`
|
||||
WITH ordered AS (
|
||||
SELECT
|
||||
version.asset_id,
|
||||
version.version_number,
|
||||
version.occurred_at,
|
||||
NULLIF(version.snapshot #>> '{parent,id}', '')::uuid AS parent_id,
|
||||
NULLIF(version.snapshot #>> '{operationalArea,id}', '')::uuid AS operational_area_id,
|
||||
NULLIF(version.snapshot #>> '{operatorCompany,id}', '')::uuid AS operator_company_id,
|
||||
version.actor_user_id,
|
||||
version.source,
|
||||
version.request_id,
|
||||
ROW_NUMBER() OVER (PARTITION BY version.asset_id ORDER BY version.occurred_at, version.version_number) AS row_number,
|
||||
LAG(NULLIF(version.snapshot #>> '{parent,id}', '')::uuid) OVER (PARTITION BY version.asset_id ORDER BY version.occurred_at, version.version_number) AS previous_parent_id,
|
||||
LAG(NULLIF(version.snapshot #>> '{operationalArea,id}', '')::uuid) OVER (PARTITION BY version.asset_id ORDER BY version.occurred_at, version.version_number) AS previous_operational_area_id,
|
||||
LAG(NULLIF(version.snapshot #>> '{operatorCompany,id}', '')::uuid) OVER (PARTITION BY version.asset_id ORDER BY version.occurred_at, version.version_number) AS previous_operator_company_id
|
||||
FROM asset_versions version
|
||||
), changes AS (
|
||||
SELECT *
|
||||
FROM ordered
|
||||
WHERE row_number = 1
|
||||
OR parent_id IS DISTINCT FROM previous_parent_id
|
||||
OR operational_area_id IS DISTINCT FROM previous_operational_area_id
|
||||
OR operator_company_id IS DISTINCT FROM previous_operator_company_id
|
||||
), bounded AS (
|
||||
SELECT
|
||||
changes.*,
|
||||
LEAD(changes.occurred_at) OVER (PARTITION BY changes.asset_id ORDER BY changes.occurred_at, changes.version_number) AS valid_until
|
||||
FROM changes
|
||||
)
|
||||
INSERT INTO asset_context_history (
|
||||
asset_id, parent_id, operational_area_id, operator_company_id,
|
||||
valid_from, valid_until, change_reason, asset_version_number,
|
||||
source, request_id, created_by
|
||||
)
|
||||
SELECT
|
||||
bounded.asset_id,
|
||||
bounded.parent_id,
|
||||
bounded.operational_area_id,
|
||||
bounded.operator_company_id,
|
||||
bounded.occurred_at,
|
||||
bounded.valid_until,
|
||||
CASE WHEN bounded.row_number = 1
|
||||
THEN 'Contexto inicial reconstruido desde el historial versionado'
|
||||
ELSE 'Cambio de contexto reconstruido desde el historial versionado'
|
||||
END,
|
||||
bounded.version_number,
|
||||
bounded.source,
|
||||
bounded.request_id,
|
||||
bounded.actor_user_id
|
||||
FROM bounded
|
||||
ORDER BY bounded.asset_id, bounded.occurred_at, bounded.version_number
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO asset_context_history (
|
||||
asset_id, parent_id, operational_area_id, operator_company_id,
|
||||
valid_from, change_reason, asset_version_number, source, created_by
|
||||
)
|
||||
SELECT
|
||||
asset.id, asset.parent_id, asset.operational_area_id, asset.operator_company_id,
|
||||
asset.created_at, 'Contexto inicial incorporado al historial', GREATEST(asset.current_version, 1),
|
||||
'SYSTEM', asset.created_by
|
||||
FROM assets asset
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM asset_context_history history WHERE history.asset_id = asset.id
|
||||
)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
WITH mismatched AS (
|
||||
SELECT
|
||||
history.id AS history_id,
|
||||
asset.id AS asset_id,
|
||||
asset.parent_id,
|
||||
asset.operational_area_id,
|
||||
asset.operator_company_id,
|
||||
GREATEST(asset.updated_at, history.valid_from) AS effective_from,
|
||||
GREATEST(asset.current_version, 1) AS asset_version_number,
|
||||
asset.updated_by
|
||||
FROM asset_context_history history
|
||||
JOIN assets asset ON asset.id=history.asset_id
|
||||
WHERE history.valid_until IS NULL
|
||||
AND (
|
||||
history.parent_id IS DISTINCT FROM asset.parent_id
|
||||
OR history.operational_area_id IS DISTINCT FROM asset.operational_area_id
|
||||
OR history.operator_company_id IS DISTINCT FROM asset.operator_company_id
|
||||
)
|
||||
), closed AS (
|
||||
UPDATE asset_context_history history
|
||||
SET valid_until=mismatched.effective_from,
|
||||
end_reason='Sincronización con contexto vigente al instalar D5.3.24',
|
||||
ended_at=CURRENT_TIMESTAMP
|
||||
FROM mismatched
|
||||
WHERE history.id=mismatched.history_id
|
||||
RETURNING mismatched.*
|
||||
)
|
||||
INSERT INTO asset_context_history (
|
||||
asset_id, parent_id, operational_area_id, operator_company_id,
|
||||
valid_from, change_reason, asset_version_number, source, created_by
|
||||
)
|
||||
SELECT
|
||||
closed.asset_id, closed.parent_id, closed.operational_area_id, closed.operator_company_id,
|
||||
closed.effective_from, 'Contexto vigente sincronizado al instalar D5.3.24',
|
||||
closed.asset_version_number, 'SYSTEM', closed.updated_by
|
||||
FROM closed
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO permissions (code, description)
|
||||
VALUES ('assets.manage_context', 'Cambiar jerarquía, Área u Operadora preservando el historial temporal')
|
||||
ON CONFLICT (code) DO UPDATE SET description = EXCLUDED.description
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
WITH mapping(role_code, permission_code) AS (VALUES
|
||||
('admin', 'assets.manage_context'),
|
||||
('supervisor', 'assets.manage_context')
|
||||
)
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT role.id, permission.id
|
||||
FROM mapping
|
||||
JOIN roles role ON role.code = mapping.role_code
|
||||
JOIN permissions permission ON permission.code = mapping.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 applicationRole = quoteIdentifier(appRole);
|
||||
await queryRunner.query(`GRANT SELECT, INSERT, UPDATE ON TABLE asset_context_history TO ${applicationRole}`);
|
||||
await queryRunner.query(`REVOKE DELETE ON TABLE asset_context_history FROM ${applicationRole}`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DELETE FROM role_permissions rp USING permissions p WHERE rp.permission_id = p.id AND p.code = 'assets.manage_context'`);
|
||||
await queryRunner.query(`DELETE FROM permissions WHERE code = 'assets.manage_context'`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS asset_context_history`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user