Copy Node
Note
Copying a node is a write operation and must be performed inside the write method.
Use copyNode to copy a node (by default together with all its descendants) to a specified position. The new subtree is identical to the source subtree except for the id — position attributes such as level, leftValue, rightValue and treeId are recalculated based on the destination.
async copyNode(
nodeId: NodeId,
options?: {
includeDescendants?: boolean // whether to include descendants, default true
to?: NodeId // destination reference node, default the source node itself
pos?: FlexNodeRelPosition // relative position, default NextSibling
treeId?: TreeId // target tree id, provide for cross-tree copy
fields?: string[] // fields to copy, default all
transformField?: Record<string,string> // field transform map
}
): Promise<TreeNode>- Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
nodeId | NodeId | — | The id of the source node to copy |
options.includeDescendants | boolean | true | Whether to copy descendants along with the node; when false, only the node itself is copied (the copy becomes a leaf) |
options.to | NodeId | source node itself | The destination reference node; the copy is placed relative to it according to pos |
options.pos | FlexNodeRelPosition | NextSibling | The relative position of the copy to the destination reference node |
options.treeId | TreeId | current tree | Target tree id; when provided and different from the current tree's treeId, this is a cross-tree copy and to refers to a node id in the target tree |
options.fields | string[] | all fields | List of field names to copy; [] (empty array) means copying key fields only; key fields (id/treeId/name/level/leftValue/rightValue) are always included and not affected by this parameter |
options.transformField | Record<string,string> | — | Field transform map { field: SQL expression }, applied to all nodes in the subtree; tree structure fields (treeId/leftValue/rightValue/level) cannot be transformed |
- Return value: the copy root node (
TreeNode).
Examples
import { FlexTreeManager, FlexNodeRelPosition } from 'flextree';
const tree = new FlexTreeManager("tree",{...})
await tree.write(async ()=>{
// Copy node A (with all descendants) after node B
const copyRoot = await tree.copyNode(aId, {
to: bId,
pos: FlexNodeRelPosition.NextSibling
})
// Default: copy as the next sibling of the source node
const copyRoot2 = await tree.copyNode(aId)
// Copy only the node itself; the copy is a leaf
const leafCopy = await tree.copyNode(aId, { includeDescendants: false })
// Copy as the last child of the source node itself
const childCopy = await tree.copyNode(aId, { pos: FlexNodeRelPosition.LastChild })
// Cross-tree copy: copy into the tree with treeId=2;
// `to` refers to a node id in that tree
const crossCopy = await tree.copyNode(aId, {
treeId: 2,
to: bIdInTree2,
pos: FlexNodeRelPosition.LastChild
})
})Destination rules
- When
tois omitted, it defaults to the source node itself, andpostakes effect as-is. For example, withpos: LastChildand noto, the copy becomes the last child of the source node. - The destination cannot be a descendant of the source node (self-referential copy); otherwise an exception is thrown.
- When the destination is the root node,
poscannot beNextSibling/PreviousSibling(the root has no siblings); otherwise an exception is thrown. - Cross-tree copy is supported (multi-tree single-table scenario) in two ways:
- Call it on the destination tree's manager, passing a source node id from another tree (the source is located by id across the whole table);
- Call it on the source tree's manager, specifying the target tree via the
treeIdoption — thentorefers to a node id in the target tree. - In both cases the copy's
treeIdadopts thetreeIdof the destination tree.
Selective field copy
A table may contain unimportant fields (no point in copying) or fields with unique constraints (copying would cause conflicts). Use fields to copy only the specified fields; unspecified custom fields are skipped (left empty in the copy):
await tree.write(async ()=>{
// Copy only the title field; other custom fields (e.g. size, url, hashCode) are skipped
const copyRoot = await tree.copyNode(aId, { fields: ["title"] })
// Empty array: copy only the key fields (id/treeId/name/level/leftValue/rightValue)
const bareCopy = await tree.copyNode(aId, { fields: [] })
})Note
No matter what fields specifies, the tree's key fields (id/treeId/name/level/leftValue/rightValue) are always copied correctly — they are the foundation of the tree structure and are not affected by the filter.
Field transforms (transformField)
Provide a { field: SQL expression } map via transformField to transform the value of any field in the copy. Expressions can reference the original columns and should be written in your own database dialect. The map applies to all nodes in the subtree; fields without a transform are copied as-is.
await tree.write(async ()=>{
// Append a suffix to the copy's name (SQLite / PostgreSQL)
const copyRoot = await tree.copyNode(aId, {
transformField: { name: "name || '-copy'" }
})
// MySQL syntax
const copyRoot2 = await tree.copyNode(aId, {
transformField: { name: "CONCAT(name,'-copy')" }
})
// Transform multiple fields at once: id, name and the custom field size
const copyRoot3 = await tree.copyNode(aId, {
transformField: {
id: "hex(randomblob(16))",
name: "name || '-copy'",
size: "size * 2"
}
})
})Typical scenarios:
- Non-auto-increment primary keys: no parameter is needed when id is auto-increment; when it is not (e.g. uuid), provide a transform expression for id (see above)
- Distinguishing copies: append a suffix to name so same-name siblings are easier to tell apart
- Adjusting copy data: arithmetic on numeric fields (e.g. resetting a counter
count: "0"), generating fresh values for unique-constrained fields (e.g.slug: "slug || '-' || hex(randomblob(4))")
Warning
Expressions in transformField are concatenated as-is into the executed INSERT ... SELECT statement. Never pass untrusted external input to this parameter — injection safety is the caller's responsibility.
Note
Tree structure fields (treeId/leftValue/rightValue/level) are computed automatically by the algorithm based on the destination and cannot be transformed — entries for them in transformField are ignored.
Performance
copyNode completes the entire operation inside one transaction with a fixed number of set-based SQL statements. The number of database round-trips is independent of the number of descendants — even if the source node has tens of thousands of descendants, the descendant data is never loaded into the application layer:
- One
INSERT ... SELECTsnapshots the source subtree as a staging copy with negative left/right values - Two
UPDATEs make room at the destination - One
UPDATEmirrors the staging copy into its final position
Finally, the copy root is fetched by its computed new leftValue and returned.
Events
When the copy completes, the node:added event is emitted with nodes set to [copyRoot]. To obtain all new nodes, query them in the event callback via getDescendants(copyRoot.id).