From 121ab7e263c8e28dc73ad6b7c7adf674c8e69c7c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 12:49:03 -0400 Subject: [PATCH] fix(galaxy): include undeployed areas in browse + re-root orphaned objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hierarchy query returned deployed objects only (deployed_package_id <> 0), so areas whose containing area is undeployed were orphaned and hidden from /browse — on wonder, only the lone deployed root area surfaced. Include category-13 Area objects regardless of deployment, and in GalaxyHierarchyIndex re-root any object whose parent is absent from the set (e.g. a deleted container area) so nothing disappears under a phantom parent id. --- .../Galaxy/GalaxyHierarchyIndex.cs | 7 +++++ .../Galaxy/GalaxyRepository.cs | 7 ++++- .../Galaxy/GalaxyHierarchyIndexTests.cs | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyIndex.cs b/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyIndex.cs index f71e818..676244d 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyIndex.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyIndex.cs @@ -106,6 +106,13 @@ public sealed class GalaxyHierarchyIndex { parentKey = 0; } + // Re-root orphans whose parent object is absent from the set (e.g. a deleted or + // never-loaded container area). Otherwise they bucket under a phantom parent id + // that is never reached from the root, so they vanish from browse entirely. + else if (parentKey != 0 && !objectsById.ContainsKey(parentKey)) + { + parentKey = 0; + } if (!childrenByParent.TryGetValue(parentKey, out List? bucket)) { bucket = []; diff --git a/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs b/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs index d8922fd..aad3183 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs @@ -172,6 +172,11 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR AckCommentSubtag = string.Empty, }; + // Area objects (category 13) are returned even when undeployed (deployed_package_id = 0): + // they are organizational/model nodes that group deployed objects, so excluding them + // orphans every area whose containing area is not itself deployed. All non-area objects + // still require deployment. Orphans left by a missing/deleted parent area are re-rooted + // by GalaxyHierarchyIndex.Build so nothing disappears from browse. private const string HierarchySql = @" ;WITH template_chain AS ( SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id, @@ -218,7 +223,7 @@ INNER JOIN template_definition td ON g.template_definition_id = td.template_definition_id WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26) AND g.is_template = 0 - AND g.deployed_package_id <> 0 + AND (g.deployed_package_id <> 0 OR td.category_id = 13) ORDER BY parent_gobject_id, g.tag_name"; // Unlike HierarchySql, this query has diverged from the OtOpcUa original. It returns two diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyHierarchyIndexTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyHierarchyIndexTests.cs index 940b183..5da3dcc 100644 --- a/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyHierarchyIndexTests.cs +++ b/src/ZB.MOM.WW.MxGateway.Tests/Galaxy/GalaxyHierarchyIndexTests.cs @@ -75,6 +75,35 @@ public sealed class GalaxyHierarchyIndexTests } } + /// + /// Verifies an object whose parent is absent from the set (e.g. a deleted/undeployed + /// container area) is re-rooted under sentinel 0 rather than vanishing under a phantom + /// parent id that browse never reaches from the root. + /// + [Fact] + public void ChildrenByParent_OrphanWithMissingParent_AppearsAsRoot() + { + GalaxyObject realRoot = new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "RealRoot" }; + GalaxyObject orphanArea = new() { GobjectId = 2, ParentGobjectId = 5008, IsArea = true, ContainedName = "Orphan" }; + GalaxyObject orphanChild = new() { GobjectId = 3, ParentGobjectId = 2, ContainedName = "Child" }; + + GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([realRoot, orphanArea, orphanChild]); + + // Both the real root and the orphan (its parent 5008 is absent) surface under root. + Assert.True(index.ChildrenByParent.TryGetValue(0, out IReadOnlyList? roots)); + Assert.NotNull(roots); + Assert.Contains(roots!, view => view.Object.GobjectId == 1); + Assert.Contains(roots!, view => view.Object.GobjectId == 2); + + // The orphan keeps its own deployed children nested beneath it. + Assert.True(index.ChildrenByParent.TryGetValue(2, out IReadOnlyList? underOrphan)); + Assert.Single(underOrphan!); + Assert.Equal(3, underOrphan![0].Object.GobjectId); + + // Nothing buckets under the phantom parent id. + Assert.False(index.ChildrenByParent.ContainsKey(5008)); + } + /// Verifies is OrdinalIgnoreCase and supports O(1) lookups. [Fact] public void ObjectViewsByTagName_IsCaseInsensitive_AndLookupsAreO1()