From a66adbf3cf3b3afa06aacf5ff28aea6cd6b68a7c Mon Sep 17 00:00:00 2001 From: Bhagya Amarasinghe Date: Fri, 12 Jun 2026 19:32:47 +0530 Subject: [PATCH 1/2] fix: preserve deep taxonomy tree children --- internal/repository/taxonomy_repository.go | 27 ++--- .../repository/taxonomy_repository_test.go | 106 ++++++++++++++++++ 2 files changed, 118 insertions(+), 15 deletions(-) create mode 100644 internal/repository/taxonomy_repository_test.go diff --git a/internal/repository/taxonomy_repository.go b/internal/repository/taxonomy_repository.go index de4af7a..72659c5 100644 --- a/internal/repository/taxonomy_repository.go +++ b/internal/repository/taxonomy_repository.go @@ -1046,38 +1046,35 @@ func buildTaxonomyTree(nodes []models.TaxonomyNode) *models.TaxonomyNode { return nil } - byID := make(map[uuid.UUID]*models.TaxonomyNode, len(nodes)) + childrenByParent := make(map[uuid.UUID][]models.TaxonomyNode, len(nodes)) + var root *models.TaxonomyNode + for _, node := range nodes { copyNode := node copyNode.Children = nil - byID[node.ID] = ©Node - } - var root *models.TaxonomyNode - - for _, node := range byID { - if node.ParentID == nil { - root = node + if copyNode.ParentID == nil { + if root == nil { + root = ©Node + } continue } - parent := byID[*node.ParentID] - if parent != nil { - parent.Children = append(parent.Children, *node) - } + childrenByParent[*copyNode.ParentID] = append(childrenByParent[*copyNode.ParentID], copyNode) } - sortTaxonomyChildren(root) + attachTaxonomyChildren(root, childrenByParent) return root } -func sortTaxonomyChildren(node *models.TaxonomyNode) { +func attachTaxonomyChildren(node *models.TaxonomyNode, childrenByParent map[uuid.UUID][]models.TaxonomyNode) { if node == nil { return } + node.Children = childrenByParent[node.ID] sort.SliceStable(node.Children, func(i, j int) bool { if node.Children[i].SortOrder == node.Children[j].SortOrder { return node.Children[i].ID.String() < node.Children[j].ID.String() @@ -1087,7 +1084,7 @@ func sortTaxonomyChildren(node *models.TaxonomyNode) { }) for i := range node.Children { - sortTaxonomyChildren(&node.Children[i]) + attachTaxonomyChildren(&node.Children[i], childrenByParent) } } diff --git a/internal/repository/taxonomy_repository_test.go b/internal/repository/taxonomy_repository_test.go new file mode 100644 index 0000000..287cc37 --- /dev/null +++ b/internal/repository/taxonomy_repository_test.go @@ -0,0 +1,106 @@ +package repository + +import ( + "testing" + + "github.com/google/uuid" + + "github.com/formbricks/hub/internal/models" +) + +func TestBuildTaxonomyTreePreservesDeepChildren(t *testing.T) { + runID := uuid.New() + rootID := uuid.New() + level1ID := uuid.New() + level2ID := uuid.New() + level3ID := uuid.New() + firstLeafID := uuid.New() + secondLeafID := uuid.New() + + root := buildTaxonomyTree([]models.TaxonomyNode{ + { + ID: rootID, + RunID: runID, + NodeType: models.TaxonomyNodeTypeRoot, + Label: "Feedback taxonomy", + Level: 0, + SortOrder: 0, + }, + { + ID: level1ID, + RunID: runID, + ParentID: &rootID, + NodeType: models.TaxonomyNodeTypeBranch, + Label: "Customer Feedback", + Level: 1, + SortOrder: 0, + }, + { + ID: level2ID, + RunID: runID, + ParentID: &level1ID, + NodeType: models.TaxonomyNodeTypeBranch, + Label: "Product Signals", + Level: 2, + SortOrder: 0, + }, + { + ID: level3ID, + RunID: runID, + ParentID: &level2ID, + NodeType: models.TaxonomyNodeTypeBranch, + Label: "Generated Topics", + Level: 3, + SortOrder: 0, + }, + { + ID: secondLeafID, + RunID: runID, + ParentID: &level3ID, + NodeType: models.TaxonomyNodeTypeLeaf, + Label: "Second topic", + Level: 4, + SortOrder: 1, + }, + { + ID: firstLeafID, + RunID: runID, + ParentID: &level3ID, + NodeType: models.TaxonomyNodeTypeLeaf, + Label: "First topic", + Level: 4, + SortOrder: 0, + }, + }) + + if root == nil { + t.Fatal("buildTaxonomyTree() returned nil") + } + + level1 := onlyChild(t, root, "Feedback taxonomy") + level2 := onlyChild(t, level1, "Customer Feedback") + level3 := onlyChild(t, level2, "Product Signals") + + if got := len(level3.Children); got != 2 { + t.Fatalf("level 3 child count = %d, want 2", got) + } + if level3.Children[0].ID != firstLeafID { + t.Fatalf("first leaf ID = %s, want %s", level3.Children[0].ID, firstLeafID) + } + if level3.Children[1].ID != secondLeafID { + t.Fatalf("second leaf ID = %s, want %s", level3.Children[1].ID, secondLeafID) + } +} + +func onlyChild(t *testing.T, node *models.TaxonomyNode, label string) *models.TaxonomyNode { + t.Helper() + + if node.Label != label { + t.Fatalf("node label = %q, want %q", node.Label, label) + } + if got := len(node.Children); got != 1 { + t.Fatalf("%q child count = %d, want 1", label, got) + } + + return &node.Children[0] +} From 4b8138b9032cb86a6a36d068ece849ecfac36982 Mon Sep 17 00:00:00 2001 From: Bhagya Amarasinghe Date: Mon, 15 Jun 2026 16:15:16 +0530 Subject: [PATCH 2/2] style: satisfy taxonomy lint whitespace --- internal/repository/taxonomy_repository.go | 1 + internal/repository/taxonomy_repository_test.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/internal/repository/taxonomy_repository.go b/internal/repository/taxonomy_repository.go index 72659c5..755e44a 100644 --- a/internal/repository/taxonomy_repository.go +++ b/internal/repository/taxonomy_repository.go @@ -1047,6 +1047,7 @@ func buildTaxonomyTree(nodes []models.TaxonomyNode) *models.TaxonomyNode { } childrenByParent := make(map[uuid.UUID][]models.TaxonomyNode, len(nodes)) + var root *models.TaxonomyNode for _, node := range nodes { diff --git a/internal/repository/taxonomy_repository_test.go b/internal/repository/taxonomy_repository_test.go index 287cc37..bdca772 100644 --- a/internal/repository/taxonomy_repository_test.go +++ b/internal/repository/taxonomy_repository_test.go @@ -84,9 +84,11 @@ func TestBuildTaxonomyTreePreservesDeepChildren(t *testing.T) { if got := len(level3.Children); got != 2 { t.Fatalf("level 3 child count = %d, want 2", got) } + if level3.Children[0].ID != firstLeafID { t.Fatalf("first leaf ID = %s, want %s", level3.Children[0].ID, firstLeafID) } + if level3.Children[1].ID != secondLeafID { t.Fatalf("second leaf ID = %s, want %s", level3.Children[1].ID, secondLeafID) } @@ -98,6 +100,7 @@ func onlyChild(t *testing.T, node *models.TaxonomyNode, label string) *models.Ta if node.Label != label { t.Fatalf("node label = %q, want %q", node.Label, label) } + if got := len(node.Children); got != 1 { t.Fatalf("%q child count = %d, want 1", label, got) }