* fix(web): simplify inventory administration menu * fix(web): remove legacy imports and function catalog routes * fix(web): remove redundant inspections lifecycle legend * feat(inventory): distinguish physical instances from structural records * fix(dashboard): align inventory and act follow-up metrics * fix(web): align dashboard summary contract * fix(web): clarify dashboard act and report concepts * feat(inventory): mark field-created records as real instances * feat(inventory): map physical instance flag on asset entity * fix(inventory): keep field yacimientos structural * feat(inventory): classify future concrete instances at database level * fix(inventory): count only installation and subinstallation instances * feat(inventory): add authoritative F5 source snapshot * feat(inventory): preload authoritative territory model * feat(inventory): preload authoritative technical catalog * fix(findings): use only authoritative F5 family catalog * fix(inventory): preserve non-hierarchical operator snapshot compatibility * feat(inventory): add inventory-only asset filter * feat(inventory): add inventory-only tree filter * feat(inventory): add inventory browser query contract * feat(inventory): add area-owned inventory browser * feat(inventory): expose area-owned inventory browser * refactor(inventory): remove function catalog and add inventory browser * fix(inventory): make operator relation temporal and non-owning * feat(web): add inventory browser API client * feat(inventory): extend inventory browser filters * feat(inventory): add real inventory list endpoint logic * feat(inventory): expose real inventory list * feat(web): add real inventory list client * refactor(web): make inventory hierarchy area-owned * fix(web): show only real inventory instances * fix(web): style act follow-up tabs and F5 inventory context * fix(web): load F5 flow styles * fix(inventory): apply area-owned operational guard on F5 up * fix(inventory): treat company on asset as non-owning creation snapshot * fix(inventory): resolve field inventory by area hierarchy, not company ownership * fix(inventory): preserve custom catalog and apply authoritative universal findings * fix(inventory): harden authoritative catalog migration checks * fix(inventory): make authoritative territory preload safely reversible * feat(inventory): allow independent company master creation * fix(inventory): make guided creation area-owned and support companies * feat(web): expose independent company master in inventory setup * feat(web): create companies independently from physical inventory hierarchy * fix(inventory): merge by physical area and preserve sealed documents * test(inventory): lock F5 authoritative model and merge invariants * feat(inventory): add family administration DTOs * feat(inventory): administer installation and subinstallation classifications * feat(inventory): expose family classification administration * feat(web): add inventory classification administration API * fix(web): configure finding applicability by inventory classification * fix(web): redefine inventory configuration around hierarchy classifications and columns * chore(release): identify F5 inventory model * chore(release): bump API for F5 inventory model * test(release): expect F5 health metadata * chore(release): align WEB package with F5 inventory cut * chore(release): expose F5 WEB phase * test(dashboard): expect inspector activity and act follow-up metrics * test(dashboard): route F5 summary query mocks explicitly * ci: rehearse all migrations on clean PostGIS before merge * ci: prove F5 migrations revert and reapply cleanly * test(f5): align operational navigation contract * test(f5): align operator lifecycle with area-owned inventory * test(f5): make merge compatibility area-based * test(f5): distinguish literal and normalized yacimiento counts * test(f5): model normalized yacimiento collision explicitly * ci(f5): bootstrap historical admin prerequisite in clean migration rehearsal * ci(f5): bypass irreversible historical reset in clean rehearsal * fix(f5): make territory SQL parameter types explicit * fix(f5): guarantee canonical inventory hierarchy before territory preload * ci(f5): include canonical hierarchy migration in rollback gate * fix(f5): type relation backup markers explicitly * fix(f5): make catalog SQL text parameter types explicit
193 lines
7.1 KiB
TypeScript
193 lines
7.1 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
type TypeRow = {
|
|
id: string;
|
|
code: string;
|
|
role: string;
|
|
active: boolean;
|
|
canBeRoot: boolean;
|
|
};
|
|
|
|
type RuleRow = {
|
|
childTypeId: string;
|
|
parentTypeId: string;
|
|
};
|
|
|
|
type CountRow = { total: number };
|
|
|
|
const CREATED_TYPES_TABLE = 'f5_canonical_hierarchy_created_types';
|
|
const CREATED_RULES_TABLE = 'f5_canonical_hierarchy_created_rules';
|
|
|
|
const CANONICAL_TYPES = [
|
|
{
|
|
code: 'yacimiento',
|
|
name: 'Yacimiento',
|
|
description: 'Yacimiento perteneciente a un Área.',
|
|
},
|
|
{
|
|
code: 'instalacion',
|
|
name: 'Instalación',
|
|
description: 'Instancia física de una Instalación dentro de un Yacimiento.',
|
|
},
|
|
{
|
|
code: 'subinstalacion',
|
|
name: 'Subinstalación',
|
|
description: 'Instancia física subordinada a una Instalación.',
|
|
},
|
|
] as const;
|
|
|
|
const CANONICAL_RULES = [
|
|
['yacimiento', 'area'],
|
|
['instalacion', 'yacimiento'],
|
|
['subinstalacion', 'instalacion'],
|
|
] as const;
|
|
|
|
export class F5CanonicalInventoryHierarchy1790087150000 implements MigrationInterface {
|
|
name = 'F5CanonicalInventoryHierarchy1790087150000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE ${CREATED_TYPES_TABLE} (
|
|
type_id uuid PRIMARY KEY,
|
|
code varchar(80) NOT NULL UNIQUE,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
CONSTRAINT fk_f5_canonical_created_type FOREIGN KEY (type_id)
|
|
REFERENCES asset_types(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
await queryRunner.query(`
|
|
CREATE TABLE ${CREATED_RULES_TABLE} (
|
|
child_type_id uuid NOT NULL,
|
|
parent_type_id uuid NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
PRIMARY KEY (child_type_id,parent_type_id),
|
|
CONSTRAINT fk_f5_canonical_created_rule_child FOREIGN KEY (child_type_id)
|
|
REFERENCES asset_types(id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_f5_canonical_created_rule_parent FOREIGN KEY (parent_type_id)
|
|
REFERENCES asset_types(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
|
|
const area = await this.requireType(queryRunner, 'area');
|
|
if (area.role !== 'AREA' || !area.active || !area.canBeRoot) {
|
|
throw new Error('F5 requires canonical active root type area with AREA operational role');
|
|
}
|
|
|
|
for (const definition of CANONICAL_TYPES) {
|
|
const inserted = (await queryRunner.query(`
|
|
INSERT INTO asset_types(code,name,description,can_be_root,is_active,operational_role)
|
|
SELECT $1::varchar,$2::varchar,$3::text,false,true,'GENERIC'
|
|
WHERE NOT EXISTS (
|
|
SELECT 1 FROM asset_types WHERE lower(code)=lower($1::varchar)
|
|
)
|
|
RETURNING id,code,operational_role AS role,is_active AS active,can_be_root AS "canBeRoot"
|
|
`, [definition.code, definition.name, definition.description])) as TypeRow[];
|
|
|
|
if (inserted[0]?.id) {
|
|
await queryRunner.query(`
|
|
INSERT INTO ${CREATED_TYPES_TABLE}(type_id,code)
|
|
VALUES ($1::uuid,$2::varchar)
|
|
`, [inserted[0].id, definition.code]);
|
|
}
|
|
|
|
const type = await this.requireType(queryRunner, definition.code);
|
|
if (type.role !== 'GENERIC' || !type.active || type.canBeRoot) {
|
|
throw new Error(`F5 incompatible canonical type configuration: ${definition.code}`);
|
|
}
|
|
}
|
|
|
|
for (const [childCode, parentCode] of CANONICAL_RULES) {
|
|
const inserted = (await queryRunner.query(`
|
|
INSERT INTO asset_type_parent_rules(child_type_id,parent_type_id)
|
|
SELECT child.id,parent.id
|
|
FROM asset_types child CROSS JOIN asset_types parent
|
|
WHERE lower(child.code)=lower($1::varchar)
|
|
AND lower(parent.code)=lower($2::varchar)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM asset_type_parent_rules existing
|
|
WHERE existing.child_type_id=child.id AND existing.parent_type_id=parent.id
|
|
)
|
|
RETURNING child_type_id AS "childTypeId",parent_type_id AS "parentTypeId"
|
|
`, [childCode, parentCode])) as RuleRow[];
|
|
|
|
if (inserted[0]?.childTypeId && inserted[0]?.parentTypeId) {
|
|
await queryRunner.query(`
|
|
INSERT INTO ${CREATED_RULES_TABLE}(child_type_id,parent_type_id)
|
|
VALUES ($1::uuid,$2::uuid)
|
|
`, [inserted[0].childTypeId, inserted[0].parentTypeId]);
|
|
}
|
|
}
|
|
|
|
const [verified] = (await queryRunner.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM asset_type_parent_rules rule
|
|
JOIN asset_types child ON child.id=rule.child_type_id
|
|
JOIN asset_types parent ON parent.id=rule.parent_type_id
|
|
WHERE (lower(child.code)='yacimiento' AND lower(parent.code)='area')
|
|
OR (lower(child.code)='instalacion' AND lower(parent.code)='yacimiento')
|
|
OR (lower(child.code)='subinstalacion' AND lower(parent.code)='instalacion')
|
|
`)) as CountRow[];
|
|
if (Number(verified?.total ?? 0) !== 3) {
|
|
throw new Error(`F5 canonical hierarchy verification failed: rules=${Number(verified?.total ?? 0)}`);
|
|
}
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
const [usedCreatedTypes] = (await queryRunner.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM assets asset
|
|
JOIN ${CREATED_TYPES_TABLE} owned ON owned.type_id=asset.asset_type_id
|
|
`)) as CountRow[];
|
|
if (Number(usedCreatedTypes?.total ?? 0) !== 0) {
|
|
throw new Error('Cannot safely rollback F5 canonical hierarchy: an F5-created type is already used by Inventory');
|
|
}
|
|
|
|
const [foreignRules] = (await queryRunner.query(`
|
|
SELECT COUNT(*)::integer AS total
|
|
FROM asset_type_parent_rules rule
|
|
WHERE (
|
|
rule.child_type_id IN (SELECT type_id FROM ${CREATED_TYPES_TABLE})
|
|
OR rule.parent_type_id IN (SELECT type_id FROM ${CREATED_TYPES_TABLE})
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM ${CREATED_RULES_TABLE} owned
|
|
WHERE owned.child_type_id=rule.child_type_id
|
|
AND owned.parent_type_id=rule.parent_type_id
|
|
)
|
|
`)) as CountRow[];
|
|
if (Number(foreignRules?.total ?? 0) !== 0) {
|
|
throw new Error('Cannot safely rollback F5 canonical hierarchy: an F5-created type gained external parent rules');
|
|
}
|
|
|
|
await queryRunner.query(`
|
|
DELETE FROM asset_type_parent_rules rule
|
|
USING ${CREATED_RULES_TABLE} owned
|
|
WHERE rule.child_type_id=owned.child_type_id
|
|
AND rule.parent_type_id=owned.parent_type_id
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
DELETE FROM asset_types type
|
|
USING ${CREATED_TYPES_TABLE} owned
|
|
WHERE type.id=owned.type_id
|
|
`);
|
|
|
|
await queryRunner.query(`DROP TABLE ${CREATED_RULES_TABLE}`);
|
|
await queryRunner.query(`DROP TABLE ${CREATED_TYPES_TABLE}`);
|
|
}
|
|
|
|
private async requireType(queryRunner: QueryRunner, code: string): Promise<TypeRow> {
|
|
const rows = (await queryRunner.query(`
|
|
SELECT id,code,operational_role AS role,is_active AS active,can_be_root AS "canBeRoot"
|
|
FROM asset_types
|
|
WHERE lower(code)=lower($1::varchar)
|
|
ORDER BY created_at
|
|
`, [code])) as TypeRow[];
|
|
|
|
if (rows.length !== 1) {
|
|
throw new Error(`F5 requires exactly one canonical asset type ${code}; found ${rows.length}`);
|
|
}
|
|
return rows[0];
|
|
}
|
|
}
|