diff --git a/internal/repository/taxonomy_repository.go b/internal/repository/taxonomy_repository.go index de4af7a..755e44a 100644 --- a/internal/repository/taxonomy_repository.go +++ b/internal/repository/taxonomy_repository.go @@ -1046,38 +1046,36 @@ 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 +1085,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..bdca772 --- /dev/null +++ b/internal/repository/taxonomy_repository_test.go @@ -0,0 +1,109 @@ +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] +}