Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38a508553d | ||
|
|
a2e838cd01 | ||
|
|
b47c929a56 | ||
|
|
2ff00a077d | ||
|
|
b6b2ad0a9b | ||
|
|
7ed834935d | ||
|
|
3c5845bf6b | ||
|
|
21790ca250 | ||
|
|
2c7ef3f7af | ||
|
|
c7f9f7fe4b | ||
|
|
c172724f28 | ||
|
|
f05cecd947 | ||
|
|
f1db43f207 | ||
|
|
143ae01b57 | ||
|
|
2cf3f041be |
@@ -1,240 +0,0 @@
|
|||||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
||||||
|
|
||||||
const TARGET_CODES = [
|
|
||||||
'departamento',
|
|
||||||
'area',
|
|
||||||
'yacimiento',
|
|
||||||
'instalacion',
|
|
||||||
'subinstalacion',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const DELETE_ORDER = [
|
|
||||||
'subinstalacion',
|
|
||||||
'instalacion',
|
|
||||||
'yacimiento',
|
|
||||||
'area',
|
|
||||||
'departamento',
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
type ProtectedSnapshot = {
|
|
||||||
users: string;
|
|
||||||
companies: string;
|
|
||||||
companyProfiles: string;
|
|
||||||
assetTypes: string;
|
|
||||||
assetAttributes: string;
|
|
||||||
inventoryFamilies: string;
|
|
||||||
familyAttributes: string;
|
|
||||||
findingCategories: string;
|
|
||||||
findingItems: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ResetOperationalHierarchyData1790099200000 implements MigrationInterface {
|
|
||||||
name = 'ResetOperationalHierarchyData1790099200000';
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
// This is a one-time live-data cleanup, not a new canonical empty seed.
|
|
||||||
// Fresh CI/bootstrap databases intentionally have no admin account while
|
|
||||||
// replaying the historical migration chain, so they must retain the F6.1
|
|
||||||
// presentation seed used by hierarchy/planning contract tests.
|
|
||||||
const adminRows = (await queryRunner.query(`
|
|
||||||
SELECT id
|
|
||||||
FROM users
|
|
||||||
WHERE lower(btrim(username))='admin'
|
|
||||||
ORDER BY id
|
|
||||||
`)) as Array<{ id: string }>;
|
|
||||||
if (adminRows.length === 0) {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('[hierarchy-reset] skipped: no live admin account on migration replay');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (adminRows.length !== 1) {
|
|
||||||
throw new Error(
|
|
||||||
`Hierarchy reset aborted: expected exactly one live admin account, found ${adminRows.length}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetTypes = (await queryRunner.query(
|
|
||||||
`
|
|
||||||
SELECT lower(code) AS code
|
|
||||||
FROM asset_types
|
|
||||||
WHERE lower(code)=ANY($1::text[])
|
|
||||||
ORDER BY lower(code)
|
|
||||||
`,
|
|
||||||
[[...TARGET_CODES]],
|
|
||||||
)) as Array<{ code: string }>;
|
|
||||||
|
|
||||||
const found = new Set(targetTypes.map((row) => row.code));
|
|
||||||
const missing = TARGET_CODES.filter((code) => !found.has(code));
|
|
||||||
if (missing.length > 0) {
|
|
||||||
throw new Error(`Hierarchy reset aborted: missing asset types ${missing.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const before = await this.protectedSnapshot(queryRunner);
|
|
||||||
|
|
||||||
await queryRunner.query(
|
|
||||||
`
|
|
||||||
CREATE TEMP TABLE reset_target_assets ON COMMIT DROP AS
|
|
||||||
SELECT asset.id
|
|
||||||
FROM assets asset
|
|
||||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
||||||
WHERE lower(type.code)=ANY($1::text[])
|
|
||||||
`,
|
|
||||||
[[...TARGET_CODES]],
|
|
||||||
);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX reset_target_assets_pk ON reset_target_assets(id)`);
|
|
||||||
|
|
||||||
const [targetCount] = (await queryRunner.query(
|
|
||||||
`SELECT COUNT(*)::integer AS total FROM reset_target_assets`,
|
|
||||||
)) as Array<{ total: number }>;
|
|
||||||
|
|
||||||
// An Inspection freezes Area/Yacimiento/Operadora from creation and several
|
|
||||||
// inspection tables hold RESTRICT references to the hierarchy. Keeping a
|
|
||||||
// transaction that points to deleted territory would be invalid, so the
|
|
||||||
// complete disposable inspection graph is cleared first.
|
|
||||||
await queryRunner.query('TRUNCATE TABLE inspection_visits CASCADE');
|
|
||||||
|
|
||||||
// Legacy administrative departments are also presentation/operational data.
|
|
||||||
// Current F6 Departments live in assets, but this prevents old rows from
|
|
||||||
// resurfacing through compatibility paths.
|
|
||||||
await queryRunner.query('TRUNCATE TABLE administrative_departments CASCADE');
|
|
||||||
|
|
||||||
// Legal-right participants depend on area_legal_rights rather than directly
|
|
||||||
// on assets. Remove them before the generic direct-FK cleanup below.
|
|
||||||
await queryRunner.query(`
|
|
||||||
DELETE FROM area_legal_right_organizations organization
|
|
||||||
USING area_legal_rights legal_right
|
|
||||||
WHERE organization.right_id=legal_right.id
|
|
||||||
AND legal_right.area_id IN (SELECT id FROM reset_target_assets)
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Clean every table that directly references one of the hierarchy assets.
|
|
||||||
// This deliberately discovers the current schema instead of maintaining a
|
|
||||||
// fragile hand-written list as new dossier/history tables are added.
|
|
||||||
await queryRunner.query(`
|
|
||||||
DO $$
|
|
||||||
DECLARE dependency record;
|
|
||||||
BEGIN
|
|
||||||
FOR dependency IN
|
|
||||||
SELECT
|
|
||||||
namespace.nspname AS schema_name,
|
|
||||||
relation.relname AS table_name,
|
|
||||||
attribute.attname AS column_name
|
|
||||||
FROM pg_constraint constraint_row
|
|
||||||
JOIN pg_class relation ON relation.oid=constraint_row.conrelid
|
|
||||||
JOIN pg_namespace namespace ON namespace.oid=relation.relnamespace
|
|
||||||
JOIN LATERAL unnest(constraint_row.conkey) WITH ORDINALITY local_key(attnum,ordinality)
|
|
||||||
ON true
|
|
||||||
JOIN LATERAL unnest(constraint_row.confkey) WITH ORDINALITY referenced_key(attnum,ordinality)
|
|
||||||
ON referenced_key.ordinality=local_key.ordinality
|
|
||||||
JOIN pg_attribute attribute
|
|
||||||
ON attribute.attrelid=constraint_row.conrelid
|
|
||||||
AND attribute.attnum=local_key.attnum
|
|
||||||
JOIN pg_attribute referenced_attribute
|
|
||||||
ON referenced_attribute.attrelid=constraint_row.confrelid
|
|
||||||
AND referenced_attribute.attnum=referenced_key.attnum
|
|
||||||
WHERE constraint_row.contype='f'
|
|
||||||
AND constraint_row.confrelid='assets'::regclass
|
|
||||||
AND constraint_row.conrelid<>'assets'::regclass
|
|
||||||
AND array_length(constraint_row.conkey,1)=1
|
|
||||||
AND referenced_attribute.attname='id'
|
|
||||||
ORDER BY namespace.nspname,relation.relname,attribute.attname
|
|
||||||
LOOP
|
|
||||||
EXECUTE format(
|
|
||||||
'DELETE FROM %I.%I WHERE %I IN (SELECT id FROM reset_target_assets)',
|
|
||||||
dependency.schema_name,
|
|
||||||
dependency.table_name,
|
|
||||||
dependency.column_name
|
|
||||||
);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
`);
|
|
||||||
|
|
||||||
// parent_id is RESTRICT, therefore physical hierarchy rows are deleted from
|
|
||||||
// the leaves upward. Company/Operator assets are intentionally not targets.
|
|
||||||
for (const code of DELETE_ORDER) {
|
|
||||||
await queryRunner.query(
|
|
||||||
`
|
|
||||||
DELETE FROM assets asset
|
|
||||||
USING asset_types type
|
|
||||||
WHERE asset.asset_type_id=type.id
|
|
||||||
AND lower(type.code)=$1
|
|
||||||
`,
|
|
||||||
[code],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const after = await this.protectedSnapshot(queryRunner);
|
|
||||||
for (const key of Object.keys(before) as Array<keyof ProtectedSnapshot>) {
|
|
||||||
if (before[key] !== after[key]) {
|
|
||||||
throw new Error(
|
|
||||||
`Hierarchy reset verification failed: protected ${key} changed (${before[key]} -> ${after[key]})`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [verification] = (await queryRunner.query(`
|
|
||||||
SELECT
|
|
||||||
(
|
|
||||||
SELECT COUNT(*)::integer
|
|
||||||
FROM assets asset
|
|
||||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
||||||
WHERE lower(type.code)=ANY($1::text[])
|
|
||||||
) AS hierarchy_assets,
|
|
||||||
(SELECT COUNT(*)::integer FROM administrative_departments) AS administrative_departments,
|
|
||||||
(SELECT COUNT(*)::integer FROM inspection_visits) AS inspection_visits
|
|
||||||
`, [[...TARGET_CODES]])) as Array<{
|
|
||||||
hierarchy_assets: number;
|
|
||||||
administrative_departments: number;
|
|
||||||
inspection_visits: number;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!verification
|
|
||||||
|| Number(verification.hierarchy_assets) !== 0
|
|
||||||
|| Number(verification.administrative_departments) !== 0
|
|
||||||
|| Number(verification.inspection_visits) !== 0
|
|
||||||
) {
|
|
||||||
throw new Error(`Hierarchy reset verification failed: ${JSON.stringify(verification ?? {})}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(
|
|
||||||
`[hierarchy-reset] removed ${Number(targetCount?.total ?? 0)} Departamento/Área/Yacimiento/Instalación/Subinstalación assets; inspections and legacy departments cleared; users=${after.users}; companies=${after.companies} preserved`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(): Promise<void> {
|
|
||||||
throw new Error(
|
|
||||||
'ResetOperationalHierarchyData is intentionally destructive; restore the automatic deploy PRE database backup instead.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async protectedSnapshot(queryRunner: QueryRunner): Promise<ProtectedSnapshot> {
|
|
||||||
const [snapshot] = (await queryRunner.query(`
|
|
||||||
SELECT
|
|
||||||
(SELECT COUNT(*)::text FROM users) AS "users",
|
|
||||||
(
|
|
||||||
SELECT COUNT(*)::text
|
|
||||||
FROM assets asset
|
|
||||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
||||||
WHERE type.operational_role='COMPANY'
|
|
||||||
) AS "companies",
|
|
||||||
(
|
|
||||||
SELECT COUNT(*)::text
|
|
||||||
FROM organization_profiles profile
|
|
||||||
JOIN assets asset ON asset.id=profile.asset_id
|
|
||||||
JOIN asset_types type ON type.id=asset.asset_type_id
|
|
||||||
WHERE type.operational_role='COMPANY'
|
|
||||||
) AS "companyProfiles",
|
|
||||||
(SELECT COUNT(*)::text FROM asset_types) AS "assetTypes",
|
|
||||||
(SELECT COUNT(*)::text FROM asset_attribute_definitions) AS "assetAttributes",
|
|
||||||
(SELECT COUNT(*)::text FROM inventory_families) AS "inventoryFamilies",
|
|
||||||
(SELECT COUNT(*)::text FROM inventory_family_attribute_definitions) AS "familyAttributes",
|
|
||||||
(SELECT COUNT(*)::text FROM finding_categories) AS "findingCategories",
|
|
||||||
(SELECT COUNT(*)::text FROM finding_catalog_items) AS "findingItems"
|
|
||||||
`)) as ProtectedSnapshot[];
|
|
||||||
|
|
||||||
if (!snapshot) throw new Error('Hierarchy reset aborted: could not snapshot protected masters');
|
|
||||||
return snapshot;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user