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 aGroupTreeNode[]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)— returnstruewhen the move is safe,falsewhen it would close a cycle.countTreeContacts(node)— recursive sum ofcontactCountacross the subtree.findInTree(roots, groupId)— DFS lookup returning the node ornull.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.