Skip to content

Multi-Root Trees ​

In some scenarios, a single table needs to store multiple top-level nodes (multiple "parallel" trees), for example: multiple root directories in a file system, multiple topics in a message center, or multiple lists on a kanban board. MultiRootFlexTreeManager is the multi-root tree manager built for this purpose.

Multi-Root Tree vs Multi-Tree Table ​

FlexTree provides two approaches for "multiple top-level nodes", each with its own use cases:

Multi-Root Tree MultiRootFlexTreeManager Multi-Tree Table FlexTreeManager + treeId
Table structure Ordinary single-tree table, no treeId field needed Requires a treeId field to distinguish trees
Root relationships Roots are siblings — navigable and reorderable Trees are fully independent and unaware of each other
Cross-"tree" operations Just ordinary move/copy operations, supported natively Cross-tree moves require explicitly specifying treeId
Number of managers One manager for all roots One manager instance per treeId
Use cases A group of peer top-level nodes (directory lists, topic lists) Isolated independent trees (one tree per user)

How to choose

If the "multiple top-level nodes" are a group of parallel data items in your business (like entries in the same list), use a multi-root tree. If each tree belongs to a different owner (different users or tenants, each with their own tree), use a multi-tree table.

How It Works ​

The multi-root tree is implemented with the Hidden Root pattern:

Physical layer (an ordinary single-root tree in the table)    User's view (multi-root tree)
┌──────────────────────────┐
│ __root__  L=1  R=12      │ ← Hidden root (auto-created, invisible externally)
│  ├─ A     L=2  R=5       │         ├─ A          (level 0)
│  ├─ B     L=6  R=11      │         ├─ B          (level 0)
│  │   └─ C L=7  R=10      │         │   └─ C      (level 1)
└──────────────────────────┘
  • Internally it holds an ordinary single-tree FlexTreeManager and automatically creates/maintains a hidden root node (level=0, leftValue=1, named __root__ by default).
  • The "multiple roots" seen by the user are simply the hidden root's children. Therefore sibling navigation between roots and cross-root move/copy are all ordinary single-tree operations — every capability of FlexTreeManager is preserved as-is.
  • The level in all external reads is normalized: user roots are reported as level=0, their children as level=1, and so on.

Creating a Multi-Root Tree Manager ​

Like FlexTreeManager, both normal and singleton modes are supported:

ts
import { MultiRootFlexTreeManager } from "flextree";
import sqliteAdapter from "flextree-sqlite-adapter";

// Normal mode
const tree = new MultiRootFlexTreeManager("filesys", {
    adapter: new sqliteAdapter(),
});

// Singleton mode (recommended)
const tree = MultiRootFlexTreeManager.getInstance("filesys", {
    adapter: new sqliteAdapter(),
});

Constructor parameters:

Parameter Type Default Description
tableName string none Required, the database table name
options.adapter IFlexTreeAdapter none Required, the database adapter
options.hiddenRootName string "__root__" Optional, the hidden root node name
options.fields object default field names Optional, custom key field names (same as FlexTreeManager)

Note

The multi-root tree is based on a single-tree table — do not pass treeId (passing it throws an error). The table structure is just an ordinary tree table; no treeId field is involved.

The generic parameters are identical to FlexTreeManager (Fields/KeyFields), supporting custom fields and custom key field names.

Initialization ​

The constructor performs no database access. Call load() after creation to initialize:

ts
const tree = MultiRootFlexTreeManager.getInstance("filesys", { adapter });

await tree.load(); // Check/create the hidden root, load the root node list

load() behavior:

  • Auto-creates the hidden root: on an empty table (or one wiped externally) the hidden root is created automatically.
  • Self-healing: if the hidden root is accidentally deleted by external SQL, the next load() recreates it.
  • Idempotent: repeated calls are safe and only refresh the root list cache.

nodes: The Root Node List ​

A multi-root tree has no single root; instead, the synchronous property nodes returns all user roots:

ts
tree.nodes  // TreeNode[], level already normalized (roots = 0)
  • Automatically refreshed after each write — no manual updates needed.
  • After adding/removing roots or moving nodes in/out of the root level, nodes reflects the latest state immediately.

Adding Roots and Nodes ​

Adding a root ​

When addNodes is called without the at parameter (or with null), the nodes are attached to the top level and become new root nodes:

ts
await tree.write(async () => {
    // Add root nodes
    await tree.addNodes([{ name: "Documents" }]);
    await tree.addNodes([{ name: "Pictures" }]);

    tree.nodes.length; // 2
});

Nested structures are also supported, creating a root with its subtree in one call:

ts
await tree.write(async () => {
    await tree.addNodes([
        {
            name: "Music",
            children: [
                { name: "Rock" },
                { name: "Jazz" },
            ],
        },
    ]);
});

Adding child nodes ​

With at specified, everything works exactly like FlexTreeManager:

ts
await tree.write(async () => {
    await tree.addNodes([{ name: "resume.doc" }], documentsId);
});

Querying Nodes ​

The query APIs are identical to FlexTreeManager, with only two differences: the hidden root is never returned, and level is normalized.

ts
// All user nodes (hidden root excluded)
const nodes = await tree.getNodes();

// level semantics follow the user's perspective: 1 = roots only, 2 = roots + children
const roots = await tree.getNodes({ level: 1 });

// Sibling navigation between roots (physically they really are siblings)
const next = await tree.getNextSibling(documentsId);   // Pictures
const siblings = await tree.getSiblings(documentsId);  // all other roots

// Parent of a root: user roots have no parent, throws FlexTreeNodeNotFoundError
const parent = await tree.getParent(documentsId);      // throws

// Descendants of a root
const files = await tree.getDescendants(documentsId);
  • getParent(root) throws FlexTreeNodeNotFoundError (roots have no parent).
  • getAncestors(root) returns an empty array; getAncestorsCount(root) returns 0.
  • Conditions like findNodes({ level: 0 }) also use the user-perspective level.
  • In getNodes({ where }), the where clause is raw SQL — the level inside it is the physical value (1 greater than the user perspective).

Moving and Copying ​

Cross-root move/copy is just an ordinary same-tree operation — no special parameters needed:

ts
await tree.write(async () => {
    // Move a subdirectory under Documents into Pictures (cross-root move)
    await tree.moveNode(resumeId, picturesId, FlexNodeRelPosition.LastChild);

    // Move a root under another root (that root becomes an ordinary child)
    await tree.moveNode(musicId, documentsId, FlexNodeRelPosition.LastChild);

    // Move a node next to a root (promoting it to a new root)
    await tree.moveNode(rockId, documentsId, FlexNodeRelPosition.NextSibling);

    // Copy an entire root (the copy becomes a new root)
    const copy = await tree.copyNode(documentsId, {
        to: picturesId,
        pos: FlexNodeRelPosition.NextSibling,
    });
});
  • moveUpNode/moveDownNode work on roots too: roots move up/down within their sibling sequence. Moving the first root up or the last root down throws FlexTreeNodeInvalidOperationError.

Deleting and Clearing ​

ts
await tree.write(async () => {
    // Delete a root (along with all its descendants); other roots' coordinates shrink automatically
    await tree.deleteNode(documentsId);

    // Clear all user nodes (the hidden root is rebuilt automatically; new roots can be added afterwards)
    await tree.clear();
});
  • The hidden root cannot be deleted: when deleteNode hits it, a FlexTreeNodeInvalidOperationError is thrown (in normal use you never touch the hidden root).
  • clear() only removes user nodes; the tree remains usable afterwards.

Verify and Repair ​

Identical to FlexTreeManager, passed straight through:

ts
await tree.verify(); // Verify tree structure integrity (the hidden root passes all checks)
await tree.repair(); // Repair a broken tree structure; nodes is refreshed afterwards

Exporting ​

toJson returns a multi-root nested array (instead of a single root object):

ts
const json = await tree.toJson();
// [
//   { id: 2, name: "Documents", children: [{ id: 4, name: "resume.doc" }] },
//   { id: 3, name: "Pictures", children: [...] },
// ]

const list = await tree.toList();
// [
//   { id: 2, name: "Documents", pid: 0 },
//   { id: 4, name: "resume.doc", pid: 2 },
//   ...
// ]
  • All toJson options (childrenField/level/fields/includeKeyFields/countField) are the same as FlexTreeManager; level values are normalized.
  • In toList, user roots have pid set to 0 — the hidden root's id never leaks.
  • countField is also supported: it attaches a descendant-count field to every node. Since the bin always hangs under the hidden root, each user root's count is never polluted by recycled content (see Export).

Events ​

The event mechanism is identical to FlexTreeManager; all node:* events fire and are subscribed to the same way:

ts
tree.on("node:added", ({ nodes }) => {
    console.log(`${nodes.length} nodes added`);
});

API Differences at a Glance ​

Compared with FlexTreeManager, the only differences of MultiRootFlexTreeManager are:

Difference Description
No getRoot / hasRoot / createRoot No unique root in multi-root semantics; new roots are created via addNodes (without at)
New nodes Synchronously returns the user root list, auto-refreshed after write
toJson returns an array A multi-root nested array instead of a single root object
getParent(root) throws User roots have no parent
level normalization All read levels follow the user's perspective (roots = 0)

All other methods (addNodes/deleteNode/moveNode/copyNode/update/findNode/getDescendants/forEach/verify/repair, etc.) have identical signatures and semantics — see the Tree Manager guide and related chapters.