metal-orm 1.1.11 → 1.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +758 -769
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +100 -96
- package/scripts/generate-entities/schema.mjs +122 -122
- package/scripts/generate-entities/tree-detection.mjs +92 -92
- package/scripts/inflection/pt-br.mjs +468 -468
- package/scripts/run-eslint.mjs +33 -34
- package/scripts/show-sql-seed.sql +102 -0
- package/scripts/show-sql.mjs +321 -325
- package/src/codegen/naming-strategy.ts +54 -54
- package/src/codegen/typescript.ts +482 -482
- package/src/core/ast/adapters.ts +1 -1
- package/src/core/ast/builders.ts +123 -123
- package/src/core/ast/expression-builders.ts +745 -745
- package/src/core/ast/expression-nodes.ts +304 -304
- package/src/core/ast/expression-visitor.ts +212 -212
- package/src/core/ast/expression.ts +20 -20
- package/src/core/ast/helpers.ts +14 -14
- package/src/core/ast/join.ts +18 -18
- package/src/core/ast/query.ts +200 -200
- package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
- package/src/core/ddl/dialects/render-reference.test.ts +70 -70
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
- package/src/core/ddl/introspect/catalogs/index.ts +5 -5
- package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
- package/src/core/ddl/introspect/context.ts +15 -15
- package/src/core/ddl/introspect/functions/postgres.ts +35 -35
- package/src/core/ddl/introspect/registry.ts +40 -40
- package/src/core/ddl/introspect/run-select.ts +35 -35
- package/src/core/ddl/introspect/utils.ts +56 -56
- package/src/core/ddl/naming-strategy.ts +16 -16
- package/src/core/ddl/schema-dialect.ts +56 -56
- package/src/core/ddl/schema-diff.ts +234 -234
- package/src/core/ddl/schema-generator.ts +1 -1
- package/src/core/ddl/schema-introspect.ts +25 -25
- package/src/core/ddl/sql-writing.ts +171 -171
- package/src/core/dialect/abstract.ts +541 -541
- package/src/core/dialect/base/cte-compiler.ts +33 -33
- package/src/core/dialect/base/function-table-formatter.ts +103 -103
- package/src/core/dialect/base/groupby-compiler.ts +21 -21
- package/src/core/dialect/base/join-compiler.ts +25 -25
- package/src/core/dialect/base/orderby-compiler.ts +35 -35
- package/src/core/dialect/base/pagination-strategy.ts +32 -32
- package/src/core/dialect/base/returning-strategy.ts +57 -57
- package/src/core/dialect/base/sql-dialect.ts +294 -294
- package/src/core/dialect/dialect-factory.ts +91 -91
- package/src/core/dialect/mssql/functions.ts +120 -120
- package/src/core/dialect/mssql/index.ts +317 -317
- package/src/core/dialect/mysql/functions.ts +103 -103
- package/src/core/dialect/mysql/index.ts +105 -105
- package/src/core/dialect/postgres/functions.ts +108 -108
- package/src/core/dialect/postgres/index.ts +91 -91
- package/src/core/dialect/sqlite/functions.ts +129 -129
- package/src/core/dialect/sqlite/index.ts +67 -67
- package/src/core/driver/database-driver.ts +19 -19
- package/src/core/driver/mssql-driver.ts +23 -23
- package/src/core/driver/mysql-driver.ts +23 -23
- package/src/core/driver/postgres-driver.ts +23 -23
- package/src/core/driver/sqlite-driver.ts +23 -23
- package/src/core/execution/db-executor.ts +92 -92
- package/src/core/execution/executors/better-sqlite3-executor.ts +90 -90
- package/src/core/execution/executors/mssql-executor.ts +184 -184
- package/src/core/execution/executors/mysql-executor.ts +88 -88
- package/src/core/execution/executors/postgres-executor.ts +26 -26
- package/src/core/execution/executors/sqlite-executor.ts +42 -42
- package/src/core/execution/pooling/pool-types.ts +30 -30
- package/src/core/execution/pooling/pool.ts +275 -275
- package/src/core/functions/array.ts +35 -35
- package/src/core/functions/control-flow.ts +79 -79
- package/src/core/functions/datetime.ts +237 -237
- package/src/core/functions/json.ts +70 -70
- package/src/core/functions/numeric.ts +297 -297
- package/src/core/functions/text.ts +334 -334
- package/src/core/functions/types.ts +33 -33
- package/src/core/hydration/types.ts +51 -51
- package/src/core/sql/sql-operator-config.ts +39 -39
- package/src/core/sql/sql.ts +152 -152
- package/src/decorators/column-decorator.ts +87 -87
- package/src/decorators/decorator-metadata.ts +143 -143
- package/src/decorators/entity.ts +93 -93
- package/src/decorators/index.ts +39 -39
- package/src/decorators/relations.ts +231 -231
- package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
- package/src/decorators/transformers/transformer-decorators.ts +134 -134
- package/src/decorators/transformers/transformer-executor.ts +171 -171
- package/src/decorators/transformers/transformer-metadata.ts +73 -73
- package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
- package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
- package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
- package/src/decorators/validators/country-validator-registry.ts +64 -64
- package/src/decorators/validators/country-validators-decorators.ts +203 -203
- package/src/decorators/validators/country-validators.ts +105 -105
- package/src/decorators/validators/validator-metadata.ts +59 -59
- package/src/dto/dto-types.ts +226 -226
- package/src/dto/filter-types.ts +141 -141
- package/src/dto/index.ts +95 -95
- package/src/dto/openapi/generators/base.ts +29 -29
- package/src/dto/openapi/generators/column.ts +37 -37
- package/src/dto/openapi/generators/dto.ts +91 -91
- package/src/dto/openapi/generators/filter.ts +75 -75
- package/src/dto/openapi/generators/nested-dto.ts +618 -618
- package/src/dto/openapi/generators/pagination.ts +111 -111
- package/src/dto/openapi/generators/relation-filter.ts +228 -228
- package/src/dto/openapi/index.ts +12 -12
- package/src/dto/openapi/types.ts +101 -101
- package/src/dto/openapi/utilities.ts +95 -95
- package/src/dto/pagination-utils.ts +150 -150
- package/src/dto/transform.ts +197 -197
- package/src/index.ts +86 -86
- package/src/orm/als.ts +58 -58
- package/src/orm/domain-event-bus.ts +119 -119
- package/src/orm/entity-context.ts +86 -86
- package/src/orm/entity-materializer.ts +159 -159
- package/src/orm/entity-meta.ts +97 -97
- package/src/orm/entity-registry.ts +39 -39
- package/src/orm/entity-relation-cache.ts +39 -39
- package/src/orm/execute.ts +194 -194
- package/src/orm/execution-context.ts +18 -18
- package/src/orm/hydration-context.ts +26 -26
- package/src/orm/hydration.ts +112 -112
- package/src/orm/identity-map.ts +61 -61
- package/src/orm/interceptor-pipeline.ts +16 -16
- package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
- package/src/orm/lazy-batch/belongs-to.ts +108 -108
- package/src/orm/lazy-batch/has-many.ts +69 -69
- package/src/orm/lazy-batch/has-one.ts +68 -68
- package/src/orm/lazy-batch/shared.ts +125 -125
- package/src/orm/orm-session.ts +620 -620
- package/src/orm/orm.ts +34 -34
- package/src/orm/pooled-executor-factory.ts +121 -121
- package/src/orm/query-logger.ts +40 -40
- package/src/orm/relation-change-processor.ts +420 -420
- package/src/orm/relations/belongs-to.ts +143 -143
- package/src/orm/relations/has-many.ts +204 -204
- package/src/orm/relations/has-one.ts +174 -174
- package/src/orm/relations/many-to-many.ts +288 -288
- package/src/orm/relations/morph-many.ts +156 -156
- package/src/orm/relations/morph-one.ts +151 -151
- package/src/orm/relations/morph-to.ts +162 -162
- package/src/orm/runtime-types.ts +92 -92
- package/src/orm/save-graph.ts +561 -561
- package/src/orm/transaction-runner.ts +24 -24
- package/src/orm/unit-of-work.ts +426 -426
- package/src/query/index.ts +69 -69
- package/src/query/target.ts +46 -46
- package/src/query-builder/column-selector.ts +81 -81
- package/src/query-builder/delete.ts +141 -141
- package/src/query-builder/insert-query-state.ts +149 -149
- package/src/query-builder/insert.ts +117 -117
- package/src/query-builder/query-ast-service.ts +287 -287
- package/src/query-builder/query-resolution.ts +78 -78
- package/src/query-builder/raw-column-parser.ts +38 -38
- package/src/query-builder/relation-filter-utils.ts +153 -153
- package/src/query-builder/relation-join-planner.ts +10 -10
- package/src/query-builder/relation-manager.ts +87 -87
- package/src/query-builder/relation-projection-helper.ts +102 -102
- package/src/query-builder/relation-types.ts +9 -9
- package/src/query-builder/relation-utils.ts +15 -15
- package/src/query-builder/select/cte-facet.ts +40 -40
- package/src/query-builder/select/from-facet.ts +80 -80
- package/src/query-builder/select/join-facet.ts +62 -62
- package/src/query-builder/select/predicate-facet.ts +104 -104
- package/src/query-builder/select/projection-facet.ts +70 -70
- package/src/query-builder/select/relation-facet.ts +81 -81
- package/src/query-builder/select/setop-facet.ts +36 -36
- package/src/query-builder/select-helpers.ts +1 -1
- package/src/query-builder/select-query-builder-deps.ts +130 -130
- package/src/query-builder/select-query-state.ts +207 -207
- package/src/query-builder/update.ts +151 -151
- package/src/schema/column-types.ts +322 -322
- package/src/schema/table-guards.ts +37 -37
- package/src/schema/table.ts +281 -281
- package/src/tree/index.ts +13 -13
- package/src/tree/nested-set-strategy.ts +545 -545
- package/src/tree/tree-decorator.ts +397 -397
- package/src/tree/tree-manager.ts +754 -754
- package/src/tree/tree-query.ts +427 -427
- package/src/tree/tree-types.ts +299 -299
|
@@ -1,545 +1,545 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Nested Set (MPTT) Strategy
|
|
3
|
-
*
|
|
4
|
-
* Implements the Modified Preorder Tree Traversal algorithm for managing
|
|
5
|
-
* hierarchical data in relational databases.
|
|
6
|
-
*
|
|
7
|
-
* @see https://en.wikipedia.org/wiki/Nested_set_model
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type {
|
|
11
|
-
TreeNode,
|
|
12
|
-
NestedSetBounds,
|
|
13
|
-
TreeMoveData,
|
|
14
|
-
ThreadedNode,
|
|
15
|
-
TreeScope,
|
|
16
|
-
} from './tree-types.js';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Represents a row with nested set columns for internal calculations.
|
|
20
|
-
*/
|
|
21
|
-
export interface NestedSetRow {
|
|
22
|
-
lft: number;
|
|
23
|
-
rght: number;
|
|
24
|
-
parentId: unknown;
|
|
25
|
-
depth?: number;
|
|
26
|
-
[key: string]: unknown;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Represents a node with its primary key for tree operations.
|
|
31
|
-
*/
|
|
32
|
-
export interface NodeWithPk<TPk = unknown> {
|
|
33
|
-
pk: TPk;
|
|
34
|
-
lft: number;
|
|
35
|
-
rght: number;
|
|
36
|
-
parentId: unknown;
|
|
37
|
-
depth?: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Core nested set calculations and algorithms.
|
|
42
|
-
* All methods are pure functions that compute values without database access.
|
|
43
|
-
*/
|
|
44
|
-
export class NestedSetStrategy {
|
|
45
|
-
/**
|
|
46
|
-
* Calculates the number of descendants for a node.
|
|
47
|
-
* Formula: (rght - lft - 1) / 2
|
|
48
|
-
*/
|
|
49
|
-
static childCount(lft: number, rght: number): number {
|
|
50
|
-
return (rght - lft - 1) / 2;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Determines if a node is a leaf (has no children).
|
|
55
|
-
*/
|
|
56
|
-
static isLeaf(lft: number, rght: number): boolean {
|
|
57
|
-
return rght - lft === 1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Determines if a node is a root (has no parent).
|
|
62
|
-
*/
|
|
63
|
-
static isRoot(parentId: unknown): boolean {
|
|
64
|
-
return parentId === null || parentId === undefined;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Calculates the depth (level) of a node from its ancestors.
|
|
69
|
-
* @param ancestorCount - Number of ancestors (nodes where ancestor.lft < node.lft AND ancestor.rght > node.rght)
|
|
70
|
-
*/
|
|
71
|
-
static calculateDepth(ancestorCount: number): number {
|
|
72
|
-
return ancestorCount;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Checks if nodeA is an ancestor of nodeB.
|
|
77
|
-
*/
|
|
78
|
-
static isAncestorOf(nodeA: NestedSetBounds, nodeB: NestedSetBounds): boolean {
|
|
79
|
-
return nodeA.lft < nodeB.lft && nodeA.rght > nodeB.rght;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Checks if nodeA is a descendant of nodeB.
|
|
84
|
-
*/
|
|
85
|
-
static isDescendantOf(nodeA: NestedSetBounds, nodeB: NestedSetBounds): boolean {
|
|
86
|
-
return nodeA.lft > nodeB.lft && nodeA.rght < nodeB.rght;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Checks if two nodes are siblings (share the same parent).
|
|
91
|
-
* This requires parent information, so we check if they don't overlap.
|
|
92
|
-
*/
|
|
93
|
-
static areSiblings(nodeA: NestedSetBounds, nodeB: NestedSetBounds): boolean {
|
|
94
|
-
return (
|
|
95
|
-
(nodeA.rght < nodeB.lft || nodeB.rght < nodeA.lft) &&
|
|
96
|
-
!this.isAncestorOf(nodeA, nodeB) &&
|
|
97
|
-
!this.isDescendantOf(nodeA, nodeB)
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Calculates the width of a subtree (number of lft/rght values it occupies).
|
|
103
|
-
*/
|
|
104
|
-
static subtreeWidth(lft: number, rght: number): number {
|
|
105
|
-
return rght - lft + 1;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Creates a TreeNode wrapper from raw row data.
|
|
110
|
-
*/
|
|
111
|
-
static createTreeNode<T extends NestedSetRow>(row: T): TreeNode<T> {
|
|
112
|
-
return {
|
|
113
|
-
entity: row,
|
|
114
|
-
lft: row.lft,
|
|
115
|
-
rght: row.rght,
|
|
116
|
-
depth: row.depth,
|
|
117
|
-
isLeaf: this.isLeaf(row.lft, row.rght),
|
|
118
|
-
isRoot: this.isRoot(row.parentId),
|
|
119
|
-
childCount: this.childCount(row.lft, row.rght),
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Calculates insert position for a new node as a child of a parent.
|
|
125
|
-
* New nodes are inserted as the last child (rightmost position).
|
|
126
|
-
*
|
|
127
|
-
* @param parentRght - The rght value of the parent node (or 0 for root)
|
|
128
|
-
* @param parentDepth - The depth of the parent (or -1 for root)
|
|
129
|
-
* @returns The lft, rght, and depth for the new node
|
|
130
|
-
*/
|
|
131
|
-
static calculateInsertAsLastChild(
|
|
132
|
-
parentRght: number,
|
|
133
|
-
parentDepth: number
|
|
134
|
-
): { lft: number; rght: number; depth: number } {
|
|
135
|
-
return {
|
|
136
|
-
lft: parentRght,
|
|
137
|
-
rght: parentRght + 1,
|
|
138
|
-
depth: parentDepth + 1,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Calculates insert position for a new node as the first child of a parent.
|
|
144
|
-
*
|
|
145
|
-
* @param parentLft - The lft value of the parent node
|
|
146
|
-
* @param parentDepth - The depth of the parent (or -1 for root)
|
|
147
|
-
* @returns The lft, rght, and depth for the new node
|
|
148
|
-
*/
|
|
149
|
-
static calculateInsertAsFirstChild(
|
|
150
|
-
parentLft: number,
|
|
151
|
-
parentDepth: number
|
|
152
|
-
): { lft: number; rght: number; depth: number } {
|
|
153
|
-
return {
|
|
154
|
-
lft: parentLft + 1,
|
|
155
|
-
rght: parentLft + 2,
|
|
156
|
-
depth: parentDepth + 1,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Calculates insert position for a new root node.
|
|
162
|
-
*
|
|
163
|
-
* @param maxRght - Current maximum rght value in the tree (or 0 if empty)
|
|
164
|
-
* @returns The lft, rght, and depth for the new root
|
|
165
|
-
*/
|
|
166
|
-
static calculateInsertAsRoot(maxRght: number): { lft: number; rght: number; depth: number } {
|
|
167
|
-
return {
|
|
168
|
-
lft: maxRght + 1,
|
|
169
|
-
rght: maxRght + 2,
|
|
170
|
-
depth: 0,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Generates SQL conditions for updating lft values after an insert.
|
|
176
|
-
* All nodes with lft >= insertPoint need lft += width.
|
|
177
|
-
*
|
|
178
|
-
* @param insertPoint - The lft value where the new node is inserted
|
|
179
|
-
* @param width - The width to shift (usually 2 for a new leaf)
|
|
180
|
-
*/
|
|
181
|
-
static calculateLftShiftForInsert(
|
|
182
|
-
insertPoint: number,
|
|
183
|
-
width: number = 2
|
|
184
|
-
): { threshold: number; delta: number } {
|
|
185
|
-
return { threshold: insertPoint, delta: width };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Generates SQL conditions for updating rght values after an insert.
|
|
190
|
-
* All nodes with rght >= insertPoint need rght += width.
|
|
191
|
-
*/
|
|
192
|
-
static calculateRghtShiftForInsert(
|
|
193
|
-
insertPoint: number,
|
|
194
|
-
width: number = 2
|
|
195
|
-
): { threshold: number; delta: number } {
|
|
196
|
-
return { threshold: insertPoint, delta: width };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Calculates the gap left after deleting a subtree.
|
|
201
|
-
* This gap needs to be closed by shifting all nodes to the right.
|
|
202
|
-
*/
|
|
203
|
-
static calculateDeleteGap(lft: number, rght: number): { start: number; width: number } {
|
|
204
|
-
return {
|
|
205
|
-
start: lft,
|
|
206
|
-
width: rght - lft + 1,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Calculates shifts needed for all nodes after a delete.
|
|
212
|
-
* Nodes with lft > deletedRght need lft -= width.
|
|
213
|
-
* Nodes with rght > deletedRght need rght -= width.
|
|
214
|
-
*/
|
|
215
|
-
static calculateShiftForDelete(
|
|
216
|
-
deletedRght: number,
|
|
217
|
-
width: number
|
|
218
|
-
): { threshold: number; delta: number } {
|
|
219
|
-
return { threshold: deletedRght, delta: -width };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Calculates the move operation for a subtree.
|
|
224
|
-
* This is complex as it involves:
|
|
225
|
-
* 1. Creating a gap at the new position
|
|
226
|
-
* 2. Moving the subtree
|
|
227
|
-
* 3. Closing the old gap
|
|
228
|
-
*
|
|
229
|
-
* @param node - The node being moved
|
|
230
|
-
* @param newParent - The new parent (null for moving to root)
|
|
231
|
-
* @param position - Where to place relative to newParent
|
|
232
|
-
*/
|
|
233
|
-
static calculateMove(
|
|
234
|
-
node: NestedSetBounds & { depth?: number },
|
|
235
|
-
newParent: (NestedSetBounds & { depth?: number }) | null,
|
|
236
|
-
position: 'firstChild' | 'lastChild' = 'lastChild'
|
|
237
|
-
): TreeMoveData {
|
|
238
|
-
const width = this.subtreeWidth(node.lft, node.rght);
|
|
239
|
-
|
|
240
|
-
let targetLft: number;
|
|
241
|
-
let newDepth: number;
|
|
242
|
-
|
|
243
|
-
if (newParent === null) {
|
|
244
|
-
targetLft = 1;
|
|
245
|
-
newDepth = 0;
|
|
246
|
-
} else if (position === 'firstChild') {
|
|
247
|
-
targetLft = newParent.lft + 1;
|
|
248
|
-
newDepth = (newParent.depth ?? 0) + 1;
|
|
249
|
-
} else {
|
|
250
|
-
targetLft = newParent.rght;
|
|
251
|
-
newDepth = (newParent.depth ?? 0) + 1;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const delta = targetLft - node.lft;
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
newParentId: newParent ? null : null,
|
|
258
|
-
newLft: targetLft,
|
|
259
|
-
newRght: targetLft + width - 1,
|
|
260
|
-
newDepth,
|
|
261
|
-
delta,
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Calculates the shifts needed for moveUp operation.
|
|
267
|
-
* Moves a node one position up among its siblings.
|
|
268
|
-
*
|
|
269
|
-
* @returns null if already at top, otherwise the swap data
|
|
270
|
-
*/
|
|
271
|
-
static calculateMoveUp(
|
|
272
|
-
node: NestedSetBounds,
|
|
273
|
-
previousSibling: NestedSetBounds | null
|
|
274
|
-
): { nodeShift: number; siblingShift: number } | null {
|
|
275
|
-
if (!previousSibling) return null;
|
|
276
|
-
|
|
277
|
-
const nodeWidth = this.subtreeWidth(node.lft, node.rght);
|
|
278
|
-
const siblingWidth = this.subtreeWidth(previousSibling.lft, previousSibling.rght);
|
|
279
|
-
|
|
280
|
-
return {
|
|
281
|
-
nodeShift: -siblingWidth,
|
|
282
|
-
siblingShift: nodeWidth,
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Calculates the shifts needed for moveDown operation.
|
|
288
|
-
* Moves a node one position down among its siblings.
|
|
289
|
-
*
|
|
290
|
-
* @returns null if already at bottom, otherwise the swap data
|
|
291
|
-
*/
|
|
292
|
-
static calculateMoveDown(
|
|
293
|
-
node: NestedSetBounds,
|
|
294
|
-
nextSibling: NestedSetBounds | null
|
|
295
|
-
): { nodeShift: number; siblingShift: number } | null {
|
|
296
|
-
if (!nextSibling) return null;
|
|
297
|
-
|
|
298
|
-
const nodeWidth = this.subtreeWidth(node.lft, node.rght);
|
|
299
|
-
const siblingWidth = this.subtreeWidth(nextSibling.lft, nextSibling.rght);
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
nodeShift: siblingWidth,
|
|
303
|
-
siblingShift: -nodeWidth,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Rebuilds the tree structure from parent_id relationships.
|
|
309
|
-
* Returns an array of updates to apply.
|
|
310
|
-
*
|
|
311
|
-
* This is a recursive algorithm that assigns lft/rght values
|
|
312
|
-
* based on the parent_id hierarchy.
|
|
313
|
-
*
|
|
314
|
-
* @param nodes - All nodes with their pk and parentId
|
|
315
|
-
* @param orderFn - Optional function to sort siblings
|
|
316
|
-
* @returns Array of updates with pk, new lft, rght, and depth
|
|
317
|
-
*/
|
|
318
|
-
static recover<TPk>(
|
|
319
|
-
nodes: NodeWithPk<TPk>[],
|
|
320
|
-
orderFn?: (a: NodeWithPk<TPk>, b: NodeWithPk<TPk>) => number
|
|
321
|
-
): Array<{ pk: TPk; lft: number; rght: number; depth: number }> {
|
|
322
|
-
const updates: Array<{ pk: TPk; lft: number; rght: number; depth: number }> = [];
|
|
323
|
-
const childrenMap = new Map<unknown, NodeWithPk<TPk>[]>();
|
|
324
|
-
|
|
325
|
-
for (const node of nodes) {
|
|
326
|
-
const parentKey = node.parentId ?? null;
|
|
327
|
-
if (!childrenMap.has(parentKey)) {
|
|
328
|
-
childrenMap.set(parentKey, []);
|
|
329
|
-
}
|
|
330
|
-
childrenMap.get(parentKey)!.push(node);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (orderFn) {
|
|
334
|
-
for (const children of childrenMap.values()) {
|
|
335
|
-
children.sort(orderFn);
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
const traverse = (parentId: unknown, left: number, depth: number): number => {
|
|
340
|
-
const children = childrenMap.get(parentId) ?? [];
|
|
341
|
-
let currentLeft = left;
|
|
342
|
-
|
|
343
|
-
for (const child of children) {
|
|
344
|
-
const childLft = currentLeft;
|
|
345
|
-
currentLeft = traverse(child.pk, currentLeft + 1, depth + 1);
|
|
346
|
-
const childRght = currentLeft;
|
|
347
|
-
currentLeft++;
|
|
348
|
-
|
|
349
|
-
updates.push({
|
|
350
|
-
pk: child.pk,
|
|
351
|
-
lft: childLft,
|
|
352
|
-
rght: childRght,
|
|
353
|
-
depth,
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return currentLeft;
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
traverse(null, 1, 0);
|
|
361
|
-
|
|
362
|
-
return updates;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/**
|
|
366
|
-
* Converts a flat list of nodes (ordered by lft) into a threaded tree structure.
|
|
367
|
-
*
|
|
368
|
-
* @param nodes - Flat array of nodes ordered by lft
|
|
369
|
-
* @param getLft - Function to extract lft from a node
|
|
370
|
-
* @param getRght - Function to extract rght from a node
|
|
371
|
-
*/
|
|
372
|
-
static toThreaded<T>(
|
|
373
|
-
nodes: T[],
|
|
374
|
-
getLft: (node: T) => number,
|
|
375
|
-
getRght: (node: T) => number
|
|
376
|
-
): ThreadedNode<T>[] {
|
|
377
|
-
const result: ThreadedNode<T>[] = [];
|
|
378
|
-
const stack: ThreadedNode<T>[] = [];
|
|
379
|
-
|
|
380
|
-
for (const node of nodes) {
|
|
381
|
-
const threadedNode: ThreadedNode<T> = { node, children: [] };
|
|
382
|
-
const nodeLft = getLft(node);
|
|
383
|
-
const nodeRght = getRght(node);
|
|
384
|
-
|
|
385
|
-
while (stack.length > 0) {
|
|
386
|
-
const parent = stack[stack.length - 1];
|
|
387
|
-
const parentRght = getRght(parent.node);
|
|
388
|
-
if (parentRght > nodeRght) {
|
|
389
|
-
break;
|
|
390
|
-
}
|
|
391
|
-
stack.pop();
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
if (stack.length === 0) {
|
|
395
|
-
result.push(threadedNode);
|
|
396
|
-
} else {
|
|
397
|
-
stack[stack.length - 1].children.push(threadedNode);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (!this.isLeaf(nodeLft, nodeRght)) {
|
|
401
|
-
stack.push(threadedNode);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
return result;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Generates a flat tree list with depth prefixes.
|
|
410
|
-
* Useful for select/dropdown rendering.
|
|
411
|
-
*
|
|
412
|
-
* @param nodes - Flat array of nodes ordered by lft
|
|
413
|
-
* @param getKey - Function to extract key from a node
|
|
414
|
-
* @param getValue - Function to extract display value from a node
|
|
415
|
-
* @param getDepth - Function to extract or calculate depth
|
|
416
|
-
* @param spacer - Prefix string per depth level
|
|
417
|
-
*/
|
|
418
|
-
static toTreeList<T, K, V extends string>(
|
|
419
|
-
nodes: T[],
|
|
420
|
-
getKey: (node: T) => K,
|
|
421
|
-
getValue: (node: T) => V,
|
|
422
|
-
getDepth: (node: T, index: number) => number,
|
|
423
|
-
spacer: string = '__'
|
|
424
|
-
): Array<{ key: K; value: string; depth: number }> {
|
|
425
|
-
return nodes.map((node, index) => {
|
|
426
|
-
const depth = getDepth(node, index);
|
|
427
|
-
const prefix = spacer.repeat(depth);
|
|
428
|
-
return {
|
|
429
|
-
key: getKey(node),
|
|
430
|
-
value: prefix + getValue(node),
|
|
431
|
-
depth,
|
|
432
|
-
};
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Validates that a tree has no overlapping or orphaned nodes.
|
|
438
|
-
* Returns validation errors if any.
|
|
439
|
-
*/
|
|
440
|
-
static validateTree<T>(
|
|
441
|
-
nodes: T[],
|
|
442
|
-
getLft: (node: T) => number,
|
|
443
|
-
getRght: (node: T) => number,
|
|
444
|
-
getPk: (node: T) => unknown
|
|
445
|
-
): string[] {
|
|
446
|
-
const errors: string[] = [];
|
|
447
|
-
const sorted = [...nodes].sort((a, b) => getLft(a) - getLft(b));
|
|
448
|
-
|
|
449
|
-
for (let i = 0; i < sorted.length; i++) {
|
|
450
|
-
const node = sorted[i];
|
|
451
|
-
const lft = getLft(node);
|
|
452
|
-
const rght = getRght(node);
|
|
453
|
-
const pk = getPk(node);
|
|
454
|
-
|
|
455
|
-
if (lft >= rght) {
|
|
456
|
-
errors.push(`Node ${pk}: lft (${lft}) must be less than rght (${rght})`);
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
if (lft < 1) {
|
|
460
|
-
errors.push(`Node ${pk}: lft (${lft}) must be positive`);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
for (let j = i + 1; j < sorted.length; j++) {
|
|
464
|
-
const other = sorted[j];
|
|
465
|
-
const otherLft = getLft(other);
|
|
466
|
-
const otherRght = getRght(other);
|
|
467
|
-
const otherPk = getPk(other);
|
|
468
|
-
|
|
469
|
-
if (otherLft < rght && otherRght > rght) {
|
|
470
|
-
errors.push(
|
|
471
|
-
`Node ${pk} (${lft}-${rght}) overlaps with node ${otherPk} (${otherLft}-${otherRght})`
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return errors;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Calculates depth for each node based on ancestor count.
|
|
482
|
-
* Assumes nodes are ordered by lft.
|
|
483
|
-
*/
|
|
484
|
-
static calculateDepths<T>(
|
|
485
|
-
nodes: T[],
|
|
486
|
-
getLft: (node: T) => number,
|
|
487
|
-
getRght: (node: T) => number
|
|
488
|
-
): Map<T, number> {
|
|
489
|
-
const depths = new Map<T, number>();
|
|
490
|
-
const stack: { rght: number }[] = [];
|
|
491
|
-
|
|
492
|
-
for (const node of nodes) {
|
|
493
|
-
const lft = getLft(node);
|
|
494
|
-
const rght = getRght(node);
|
|
495
|
-
|
|
496
|
-
while (stack.length > 0 && stack[stack.length - 1].rght < lft) {
|
|
497
|
-
stack.pop();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
depths.set(node, stack.length);
|
|
501
|
-
|
|
502
|
-
if (!this.isLeaf(lft, rght)) {
|
|
503
|
-
stack.push({ rght });
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return depths;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/**
|
|
512
|
-
* Builds scope conditions for multi-tree tables.
|
|
513
|
-
*/
|
|
514
|
-
export function buildScopeConditions(
|
|
515
|
-
scope: string[] | undefined,
|
|
516
|
-
scopeValues: TreeScope
|
|
517
|
-
): Record<string, unknown> {
|
|
518
|
-
if (!scope || scope.length === 0) return {};
|
|
519
|
-
|
|
520
|
-
const conditions: Record<string, unknown> = {};
|
|
521
|
-
for (const key of scope) {
|
|
522
|
-
if (key in scopeValues) {
|
|
523
|
-
conditions[key] = scopeValues[key];
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
return conditions;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Extracts scope values from an entity.
|
|
531
|
-
*/
|
|
532
|
-
export function extractScopeValues(
|
|
533
|
-
entity: Record<string, unknown>,
|
|
534
|
-
scope: string[] | undefined
|
|
535
|
-
): TreeScope {
|
|
536
|
-
if (!scope || scope.length === 0) return {};
|
|
537
|
-
|
|
538
|
-
const values: TreeScope = {};
|
|
539
|
-
for (const key of scope) {
|
|
540
|
-
if (key in entity) {
|
|
541
|
-
values[key] = entity[key];
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return values;
|
|
545
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Nested Set (MPTT) Strategy
|
|
3
|
+
*
|
|
4
|
+
* Implements the Modified Preorder Tree Traversal algorithm for managing
|
|
5
|
+
* hierarchical data in relational databases.
|
|
6
|
+
*
|
|
7
|
+
* @see https://en.wikipedia.org/wiki/Nested_set_model
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type {
|
|
11
|
+
TreeNode,
|
|
12
|
+
NestedSetBounds,
|
|
13
|
+
TreeMoveData,
|
|
14
|
+
ThreadedNode,
|
|
15
|
+
TreeScope,
|
|
16
|
+
} from './tree-types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Represents a row with nested set columns for internal calculations.
|
|
20
|
+
*/
|
|
21
|
+
export interface NestedSetRow {
|
|
22
|
+
lft: number;
|
|
23
|
+
rght: number;
|
|
24
|
+
parentId: unknown;
|
|
25
|
+
depth?: number;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Represents a node with its primary key for tree operations.
|
|
31
|
+
*/
|
|
32
|
+
export interface NodeWithPk<TPk = unknown> {
|
|
33
|
+
pk: TPk;
|
|
34
|
+
lft: number;
|
|
35
|
+
rght: number;
|
|
36
|
+
parentId: unknown;
|
|
37
|
+
depth?: number;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Core nested set calculations and algorithms.
|
|
42
|
+
* All methods are pure functions that compute values without database access.
|
|
43
|
+
*/
|
|
44
|
+
export class NestedSetStrategy {
|
|
45
|
+
/**
|
|
46
|
+
* Calculates the number of descendants for a node.
|
|
47
|
+
* Formula: (rght - lft - 1) / 2
|
|
48
|
+
*/
|
|
49
|
+
static childCount(lft: number, rght: number): number {
|
|
50
|
+
return (rght - lft - 1) / 2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Determines if a node is a leaf (has no children).
|
|
55
|
+
*/
|
|
56
|
+
static isLeaf(lft: number, rght: number): boolean {
|
|
57
|
+
return rght - lft === 1;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Determines if a node is a root (has no parent).
|
|
62
|
+
*/
|
|
63
|
+
static isRoot(parentId: unknown): boolean {
|
|
64
|
+
return parentId === null || parentId === undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Calculates the depth (level) of a node from its ancestors.
|
|
69
|
+
* @param ancestorCount - Number of ancestors (nodes where ancestor.lft < node.lft AND ancestor.rght > node.rght)
|
|
70
|
+
*/
|
|
71
|
+
static calculateDepth(ancestorCount: number): number {
|
|
72
|
+
return ancestorCount;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks if nodeA is an ancestor of nodeB.
|
|
77
|
+
*/
|
|
78
|
+
static isAncestorOf(nodeA: NestedSetBounds, nodeB: NestedSetBounds): boolean {
|
|
79
|
+
return nodeA.lft < nodeB.lft && nodeA.rght > nodeB.rght;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Checks if nodeA is a descendant of nodeB.
|
|
84
|
+
*/
|
|
85
|
+
static isDescendantOf(nodeA: NestedSetBounds, nodeB: NestedSetBounds): boolean {
|
|
86
|
+
return nodeA.lft > nodeB.lft && nodeA.rght < nodeB.rght;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Checks if two nodes are siblings (share the same parent).
|
|
91
|
+
* This requires parent information, so we check if they don't overlap.
|
|
92
|
+
*/
|
|
93
|
+
static areSiblings(nodeA: NestedSetBounds, nodeB: NestedSetBounds): boolean {
|
|
94
|
+
return (
|
|
95
|
+
(nodeA.rght < nodeB.lft || nodeB.rght < nodeA.lft) &&
|
|
96
|
+
!this.isAncestorOf(nodeA, nodeB) &&
|
|
97
|
+
!this.isDescendantOf(nodeA, nodeB)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Calculates the width of a subtree (number of lft/rght values it occupies).
|
|
103
|
+
*/
|
|
104
|
+
static subtreeWidth(lft: number, rght: number): number {
|
|
105
|
+
return rght - lft + 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Creates a TreeNode wrapper from raw row data.
|
|
110
|
+
*/
|
|
111
|
+
static createTreeNode<T extends NestedSetRow>(row: T): TreeNode<T> {
|
|
112
|
+
return {
|
|
113
|
+
entity: row,
|
|
114
|
+
lft: row.lft,
|
|
115
|
+
rght: row.rght,
|
|
116
|
+
depth: row.depth,
|
|
117
|
+
isLeaf: this.isLeaf(row.lft, row.rght),
|
|
118
|
+
isRoot: this.isRoot(row.parentId),
|
|
119
|
+
childCount: this.childCount(row.lft, row.rght),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Calculates insert position for a new node as a child of a parent.
|
|
125
|
+
* New nodes are inserted as the last child (rightmost position).
|
|
126
|
+
*
|
|
127
|
+
* @param parentRght - The rght value of the parent node (or 0 for root)
|
|
128
|
+
* @param parentDepth - The depth of the parent (or -1 for root)
|
|
129
|
+
* @returns The lft, rght, and depth for the new node
|
|
130
|
+
*/
|
|
131
|
+
static calculateInsertAsLastChild(
|
|
132
|
+
parentRght: number,
|
|
133
|
+
parentDepth: number
|
|
134
|
+
): { lft: number; rght: number; depth: number } {
|
|
135
|
+
return {
|
|
136
|
+
lft: parentRght,
|
|
137
|
+
rght: parentRght + 1,
|
|
138
|
+
depth: parentDepth + 1,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Calculates insert position for a new node as the first child of a parent.
|
|
144
|
+
*
|
|
145
|
+
* @param parentLft - The lft value of the parent node
|
|
146
|
+
* @param parentDepth - The depth of the parent (or -1 for root)
|
|
147
|
+
* @returns The lft, rght, and depth for the new node
|
|
148
|
+
*/
|
|
149
|
+
static calculateInsertAsFirstChild(
|
|
150
|
+
parentLft: number,
|
|
151
|
+
parentDepth: number
|
|
152
|
+
): { lft: number; rght: number; depth: number } {
|
|
153
|
+
return {
|
|
154
|
+
lft: parentLft + 1,
|
|
155
|
+
rght: parentLft + 2,
|
|
156
|
+
depth: parentDepth + 1,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Calculates insert position for a new root node.
|
|
162
|
+
*
|
|
163
|
+
* @param maxRght - Current maximum rght value in the tree (or 0 if empty)
|
|
164
|
+
* @returns The lft, rght, and depth for the new root
|
|
165
|
+
*/
|
|
166
|
+
static calculateInsertAsRoot(maxRght: number): { lft: number; rght: number; depth: number } {
|
|
167
|
+
return {
|
|
168
|
+
lft: maxRght + 1,
|
|
169
|
+
rght: maxRght + 2,
|
|
170
|
+
depth: 0,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generates SQL conditions for updating lft values after an insert.
|
|
176
|
+
* All nodes with lft >= insertPoint need lft += width.
|
|
177
|
+
*
|
|
178
|
+
* @param insertPoint - The lft value where the new node is inserted
|
|
179
|
+
* @param width - The width to shift (usually 2 for a new leaf)
|
|
180
|
+
*/
|
|
181
|
+
static calculateLftShiftForInsert(
|
|
182
|
+
insertPoint: number,
|
|
183
|
+
width: number = 2
|
|
184
|
+
): { threshold: number; delta: number } {
|
|
185
|
+
return { threshold: insertPoint, delta: width };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Generates SQL conditions for updating rght values after an insert.
|
|
190
|
+
* All nodes with rght >= insertPoint need rght += width.
|
|
191
|
+
*/
|
|
192
|
+
static calculateRghtShiftForInsert(
|
|
193
|
+
insertPoint: number,
|
|
194
|
+
width: number = 2
|
|
195
|
+
): { threshold: number; delta: number } {
|
|
196
|
+
return { threshold: insertPoint, delta: width };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Calculates the gap left after deleting a subtree.
|
|
201
|
+
* This gap needs to be closed by shifting all nodes to the right.
|
|
202
|
+
*/
|
|
203
|
+
static calculateDeleteGap(lft: number, rght: number): { start: number; width: number } {
|
|
204
|
+
return {
|
|
205
|
+
start: lft,
|
|
206
|
+
width: rght - lft + 1,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Calculates shifts needed for all nodes after a delete.
|
|
212
|
+
* Nodes with lft > deletedRght need lft -= width.
|
|
213
|
+
* Nodes with rght > deletedRght need rght -= width.
|
|
214
|
+
*/
|
|
215
|
+
static calculateShiftForDelete(
|
|
216
|
+
deletedRght: number,
|
|
217
|
+
width: number
|
|
218
|
+
): { threshold: number; delta: number } {
|
|
219
|
+
return { threshold: deletedRght, delta: -width };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Calculates the move operation for a subtree.
|
|
224
|
+
* This is complex as it involves:
|
|
225
|
+
* 1. Creating a gap at the new position
|
|
226
|
+
* 2. Moving the subtree
|
|
227
|
+
* 3. Closing the old gap
|
|
228
|
+
*
|
|
229
|
+
* @param node - The node being moved
|
|
230
|
+
* @param newParent - The new parent (null for moving to root)
|
|
231
|
+
* @param position - Where to place relative to newParent
|
|
232
|
+
*/
|
|
233
|
+
static calculateMove(
|
|
234
|
+
node: NestedSetBounds & { depth?: number },
|
|
235
|
+
newParent: (NestedSetBounds & { depth?: number }) | null,
|
|
236
|
+
position: 'firstChild' | 'lastChild' = 'lastChild'
|
|
237
|
+
): TreeMoveData {
|
|
238
|
+
const width = this.subtreeWidth(node.lft, node.rght);
|
|
239
|
+
|
|
240
|
+
let targetLft: number;
|
|
241
|
+
let newDepth: number;
|
|
242
|
+
|
|
243
|
+
if (newParent === null) {
|
|
244
|
+
targetLft = 1;
|
|
245
|
+
newDepth = 0;
|
|
246
|
+
} else if (position === 'firstChild') {
|
|
247
|
+
targetLft = newParent.lft + 1;
|
|
248
|
+
newDepth = (newParent.depth ?? 0) + 1;
|
|
249
|
+
} else {
|
|
250
|
+
targetLft = newParent.rght;
|
|
251
|
+
newDepth = (newParent.depth ?? 0) + 1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const delta = targetLft - node.lft;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
newParentId: newParent ? null : null,
|
|
258
|
+
newLft: targetLft,
|
|
259
|
+
newRght: targetLft + width - 1,
|
|
260
|
+
newDepth,
|
|
261
|
+
delta,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Calculates the shifts needed for moveUp operation.
|
|
267
|
+
* Moves a node one position up among its siblings.
|
|
268
|
+
*
|
|
269
|
+
* @returns null if already at top, otherwise the swap data
|
|
270
|
+
*/
|
|
271
|
+
static calculateMoveUp(
|
|
272
|
+
node: NestedSetBounds,
|
|
273
|
+
previousSibling: NestedSetBounds | null
|
|
274
|
+
): { nodeShift: number; siblingShift: number } | null {
|
|
275
|
+
if (!previousSibling) return null;
|
|
276
|
+
|
|
277
|
+
const nodeWidth = this.subtreeWidth(node.lft, node.rght);
|
|
278
|
+
const siblingWidth = this.subtreeWidth(previousSibling.lft, previousSibling.rght);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
nodeShift: -siblingWidth,
|
|
282
|
+
siblingShift: nodeWidth,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Calculates the shifts needed for moveDown operation.
|
|
288
|
+
* Moves a node one position down among its siblings.
|
|
289
|
+
*
|
|
290
|
+
* @returns null if already at bottom, otherwise the swap data
|
|
291
|
+
*/
|
|
292
|
+
static calculateMoveDown(
|
|
293
|
+
node: NestedSetBounds,
|
|
294
|
+
nextSibling: NestedSetBounds | null
|
|
295
|
+
): { nodeShift: number; siblingShift: number } | null {
|
|
296
|
+
if (!nextSibling) return null;
|
|
297
|
+
|
|
298
|
+
const nodeWidth = this.subtreeWidth(node.lft, node.rght);
|
|
299
|
+
const siblingWidth = this.subtreeWidth(nextSibling.lft, nextSibling.rght);
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
nodeShift: siblingWidth,
|
|
303
|
+
siblingShift: -nodeWidth,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Rebuilds the tree structure from parent_id relationships.
|
|
309
|
+
* Returns an array of updates to apply.
|
|
310
|
+
*
|
|
311
|
+
* This is a recursive algorithm that assigns lft/rght values
|
|
312
|
+
* based on the parent_id hierarchy.
|
|
313
|
+
*
|
|
314
|
+
* @param nodes - All nodes with their pk and parentId
|
|
315
|
+
* @param orderFn - Optional function to sort siblings
|
|
316
|
+
* @returns Array of updates with pk, new lft, rght, and depth
|
|
317
|
+
*/
|
|
318
|
+
static recover<TPk>(
|
|
319
|
+
nodes: NodeWithPk<TPk>[],
|
|
320
|
+
orderFn?: (a: NodeWithPk<TPk>, b: NodeWithPk<TPk>) => number
|
|
321
|
+
): Array<{ pk: TPk; lft: number; rght: number; depth: number }> {
|
|
322
|
+
const updates: Array<{ pk: TPk; lft: number; rght: number; depth: number }> = [];
|
|
323
|
+
const childrenMap = new Map<unknown, NodeWithPk<TPk>[]>();
|
|
324
|
+
|
|
325
|
+
for (const node of nodes) {
|
|
326
|
+
const parentKey = node.parentId ?? null;
|
|
327
|
+
if (!childrenMap.has(parentKey)) {
|
|
328
|
+
childrenMap.set(parentKey, []);
|
|
329
|
+
}
|
|
330
|
+
childrenMap.get(parentKey)!.push(node);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (orderFn) {
|
|
334
|
+
for (const children of childrenMap.values()) {
|
|
335
|
+
children.sort(orderFn);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const traverse = (parentId: unknown, left: number, depth: number): number => {
|
|
340
|
+
const children = childrenMap.get(parentId) ?? [];
|
|
341
|
+
let currentLeft = left;
|
|
342
|
+
|
|
343
|
+
for (const child of children) {
|
|
344
|
+
const childLft = currentLeft;
|
|
345
|
+
currentLeft = traverse(child.pk, currentLeft + 1, depth + 1);
|
|
346
|
+
const childRght = currentLeft;
|
|
347
|
+
currentLeft++;
|
|
348
|
+
|
|
349
|
+
updates.push({
|
|
350
|
+
pk: child.pk,
|
|
351
|
+
lft: childLft,
|
|
352
|
+
rght: childRght,
|
|
353
|
+
depth,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return currentLeft;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
traverse(null, 1, 0);
|
|
361
|
+
|
|
362
|
+
return updates;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Converts a flat list of nodes (ordered by lft) into a threaded tree structure.
|
|
367
|
+
*
|
|
368
|
+
* @param nodes - Flat array of nodes ordered by lft
|
|
369
|
+
* @param getLft - Function to extract lft from a node
|
|
370
|
+
* @param getRght - Function to extract rght from a node
|
|
371
|
+
*/
|
|
372
|
+
static toThreaded<T>(
|
|
373
|
+
nodes: T[],
|
|
374
|
+
getLft: (node: T) => number,
|
|
375
|
+
getRght: (node: T) => number
|
|
376
|
+
): ThreadedNode<T>[] {
|
|
377
|
+
const result: ThreadedNode<T>[] = [];
|
|
378
|
+
const stack: ThreadedNode<T>[] = [];
|
|
379
|
+
|
|
380
|
+
for (const node of nodes) {
|
|
381
|
+
const threadedNode: ThreadedNode<T> = { node, children: [] };
|
|
382
|
+
const nodeLft = getLft(node);
|
|
383
|
+
const nodeRght = getRght(node);
|
|
384
|
+
|
|
385
|
+
while (stack.length > 0) {
|
|
386
|
+
const parent = stack[stack.length - 1];
|
|
387
|
+
const parentRght = getRght(parent.node);
|
|
388
|
+
if (parentRght > nodeRght) {
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
stack.pop();
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (stack.length === 0) {
|
|
395
|
+
result.push(threadedNode);
|
|
396
|
+
} else {
|
|
397
|
+
stack[stack.length - 1].children.push(threadedNode);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!this.isLeaf(nodeLft, nodeRght)) {
|
|
401
|
+
stack.push(threadedNode);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Generates a flat tree list with depth prefixes.
|
|
410
|
+
* Useful for select/dropdown rendering.
|
|
411
|
+
*
|
|
412
|
+
* @param nodes - Flat array of nodes ordered by lft
|
|
413
|
+
* @param getKey - Function to extract key from a node
|
|
414
|
+
* @param getValue - Function to extract display value from a node
|
|
415
|
+
* @param getDepth - Function to extract or calculate depth
|
|
416
|
+
* @param spacer - Prefix string per depth level
|
|
417
|
+
*/
|
|
418
|
+
static toTreeList<T, K, V extends string>(
|
|
419
|
+
nodes: T[],
|
|
420
|
+
getKey: (node: T) => K,
|
|
421
|
+
getValue: (node: T) => V,
|
|
422
|
+
getDepth: (node: T, index: number) => number,
|
|
423
|
+
spacer: string = '__'
|
|
424
|
+
): Array<{ key: K; value: string; depth: number }> {
|
|
425
|
+
return nodes.map((node, index) => {
|
|
426
|
+
const depth = getDepth(node, index);
|
|
427
|
+
const prefix = spacer.repeat(depth);
|
|
428
|
+
return {
|
|
429
|
+
key: getKey(node),
|
|
430
|
+
value: prefix + getValue(node),
|
|
431
|
+
depth,
|
|
432
|
+
};
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Validates that a tree has no overlapping or orphaned nodes.
|
|
438
|
+
* Returns validation errors if any.
|
|
439
|
+
*/
|
|
440
|
+
static validateTree<T>(
|
|
441
|
+
nodes: T[],
|
|
442
|
+
getLft: (node: T) => number,
|
|
443
|
+
getRght: (node: T) => number,
|
|
444
|
+
getPk: (node: T) => unknown
|
|
445
|
+
): string[] {
|
|
446
|
+
const errors: string[] = [];
|
|
447
|
+
const sorted = [...nodes].sort((a, b) => getLft(a) - getLft(b));
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
450
|
+
const node = sorted[i];
|
|
451
|
+
const lft = getLft(node);
|
|
452
|
+
const rght = getRght(node);
|
|
453
|
+
const pk = getPk(node);
|
|
454
|
+
|
|
455
|
+
if (lft >= rght) {
|
|
456
|
+
errors.push(`Node ${pk}: lft (${lft}) must be less than rght (${rght})`);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (lft < 1) {
|
|
460
|
+
errors.push(`Node ${pk}: lft (${lft}) must be positive`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
464
|
+
const other = sorted[j];
|
|
465
|
+
const otherLft = getLft(other);
|
|
466
|
+
const otherRght = getRght(other);
|
|
467
|
+
const otherPk = getPk(other);
|
|
468
|
+
|
|
469
|
+
if (otherLft < rght && otherRght > rght) {
|
|
470
|
+
errors.push(
|
|
471
|
+
`Node ${pk} (${lft}-${rght}) overlaps with node ${otherPk} (${otherLft}-${otherRght})`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return errors;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Calculates depth for each node based on ancestor count.
|
|
482
|
+
* Assumes nodes are ordered by lft.
|
|
483
|
+
*/
|
|
484
|
+
static calculateDepths<T>(
|
|
485
|
+
nodes: T[],
|
|
486
|
+
getLft: (node: T) => number,
|
|
487
|
+
getRght: (node: T) => number
|
|
488
|
+
): Map<T, number> {
|
|
489
|
+
const depths = new Map<T, number>();
|
|
490
|
+
const stack: { rght: number }[] = [];
|
|
491
|
+
|
|
492
|
+
for (const node of nodes) {
|
|
493
|
+
const lft = getLft(node);
|
|
494
|
+
const rght = getRght(node);
|
|
495
|
+
|
|
496
|
+
while (stack.length > 0 && stack[stack.length - 1].rght < lft) {
|
|
497
|
+
stack.pop();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
depths.set(node, stack.length);
|
|
501
|
+
|
|
502
|
+
if (!this.isLeaf(lft, rght)) {
|
|
503
|
+
stack.push({ rght });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return depths;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Builds scope conditions for multi-tree tables.
|
|
513
|
+
*/
|
|
514
|
+
export function buildScopeConditions(
|
|
515
|
+
scope: string[] | undefined,
|
|
516
|
+
scopeValues: TreeScope
|
|
517
|
+
): Record<string, unknown> {
|
|
518
|
+
if (!scope || scope.length === 0) return {};
|
|
519
|
+
|
|
520
|
+
const conditions: Record<string, unknown> = {};
|
|
521
|
+
for (const key of scope) {
|
|
522
|
+
if (key in scopeValues) {
|
|
523
|
+
conditions[key] = scopeValues[key];
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
return conditions;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Extracts scope values from an entity.
|
|
531
|
+
*/
|
|
532
|
+
export function extractScopeValues(
|
|
533
|
+
entity: Record<string, unknown>,
|
|
534
|
+
scope: string[] | undefined
|
|
535
|
+
): TreeScope {
|
|
536
|
+
if (!scope || scope.length === 0) return {};
|
|
537
|
+
|
|
538
|
+
const values: TreeScope = {};
|
|
539
|
+
for (const key of scope) {
|
|
540
|
+
if (key in entity) {
|
|
541
|
+
values[key] = entity[key];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return values;
|
|
545
|
+
}
|