188 lines
9.3 KiB
TypeScript
188 lines
9.3 KiB
TypeScript
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`);
|
|
}
|
|
}
|