Files
skills/docs/commerce/contacts/contacts/src/tree/tree-utils.md
2026-05-29 08:20:56 +01:00

4.6 KiB

tree-utils

(no submodules)

Purpose

Pure tree utilities for building, flattening, traversing, and inspecting GroupTreeNode<TMeta> hierarchies. None of these functions perform IO, call Date.now(), or pull randomness — they're deterministic transformations over arrays. Consumers build a tree from a flat group list with buildGroupTree, render it with flattenGroupTree (which annotates every row with level, hasChildren, isLastChild for tree-prefix rendering), and inspect it with the named helpers.

Features

  • buildGroupTree(groups) — assembles a GroupTreeNode[] from a flat list, promoting orphans to roots when their parent is absent.
  • flattenGroupTree(roots) — depth-first flatten with rendering metadata.
  • getAncestorPaths(path) — returns every proper ancestor of a slash-separated path.
  • validateNoCircularRef(groupId, targetParentId, groups) — returns true when the move is safe, false when it would close a cycle.
  • countTreeContacts(node) — recursive sum of contactCount across the subtree.
  • findInTree(roots, groupId) — DFS lookup returning the node or null.
  • collectAllGroupIds(roots) — DFS pre-order enumeration of every group id.

Usage: Basic

import {
 buildGroupTree,
 flattenGroupTree,
} from "@suaveplan/contacts";

const flat = (await groups.list({ tenantId: "acme" })).groups;
const tree = buildGroupTree(flat);
const rows = flattenGroupTree(tree);
for (const row of rows) {
 console.log(`${" ".repeat(row.level * 2)}${row.group.name}`);
}

Usage: Advanced

import {
 collectAllGroupIds,
 countTreeContacts,
 findInTree,
 getAncestorPaths,
 validateNoCircularRef,
} from "@suaveplan/contacts";

// Render a breadcrumb from a group's path
const crumbs = getAncestorPaths("/all-customers/vip/asia");
// → ["/all-customers", "/all-customers/vip"]

// Guard against creating a cycle before issuing a move
if (!validateNoCircularRef(groupId, targetParentId, allGroups)) {
 throw new Error("would create a cycle");
}

// Aggregate counts across a subtree
const tree = buildGroupTree(allGroups);
const node = findInTree(tree, "g_vip");
if (node) console.log(`VIP subtree: ${countTreeContacts(node)} contacts`);

// Bulk-export every group id reachable from a subtree
const ids = collectAllGroupIds(tree);

API Reference

buildGroupTree<TMeta>(groups): GroupTreeNode<TMeta>[]

Builds an in-memory map by id, then iterates a second time to wire children to parents. Groups whose parentId is missing from the input are promoted to roots — this matches the typical "I've fetched a subtree by tenant" use case.

flattenGroupTree<TMeta>(roots, level = 0): FlattenedGroupNode<TMeta>[]

DFS flatten. The root array's index determines isLastChild for top-level entries; recursion uses the child array's index for nested entries.

getAncestorPaths(path): string[]

Splits on /, drops empties, and returns every proper prefix as a /-prefixed string. Returns [] for /, /a, or any single-segment path.

validateNoCircularRef(groupId, targetParentId, groups): boolean

Returns true when moving groupId under targetParentId would NOT create a cycle. Returns false when source and target are the same, when the source is missing from the input set, or when traversing target's ancestor chain reaches the source. Note the inverted semantics: true means "safe", false means "blocked".

countTreeContacts(node) / findInTree(roots, id) / collectAllGroupIds(roots)

Pure recursive helpers. countTreeContacts sums node.group.contactCount plus every descendant's contribution. findInTree returns the first matching node or null. collectAllGroupIds returns a DFS pre-order id list (root then children left-to-right).

Implementation Notes

buildGroupTree uses a MutableNode<TMeta> interface internally so children can be appended without violating the publicly-exposed readonly arrays on GroupTreeNode. The function returns the structurally-compatible GroupTreeNode[] shape and consumers see the immutable surface.

validateNoCircularRef uses ancestor walking (via parentId chain) for the target side because callers typically have the full group set in memory by this point — the alternative (descendant scanning via path) requires the source group's path, which means an extra lookup that the chain-walk avoids.

All functions are exported as named functions (not class methods) so consumers can tree-shake unused utilities away. Importing only getAncestorPaths from @suaveplan/contacts/tree/tree-utils pulls only that function's code into the bundle.