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.
Files changed (182) hide show
  1. package/README.md +758 -769
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js.map +1 -1
  4. package/package.json +100 -96
  5. package/scripts/generate-entities/schema.mjs +122 -122
  6. package/scripts/generate-entities/tree-detection.mjs +92 -92
  7. package/scripts/inflection/pt-br.mjs +468 -468
  8. package/scripts/run-eslint.mjs +33 -34
  9. package/scripts/show-sql-seed.sql +102 -0
  10. package/scripts/show-sql.mjs +321 -325
  11. package/src/codegen/naming-strategy.ts +54 -54
  12. package/src/codegen/typescript.ts +482 -482
  13. package/src/core/ast/adapters.ts +1 -1
  14. package/src/core/ast/builders.ts +123 -123
  15. package/src/core/ast/expression-builders.ts +745 -745
  16. package/src/core/ast/expression-nodes.ts +304 -304
  17. package/src/core/ast/expression-visitor.ts +212 -212
  18. package/src/core/ast/expression.ts +20 -20
  19. package/src/core/ast/helpers.ts +14 -14
  20. package/src/core/ast/join.ts +18 -18
  21. package/src/core/ast/query.ts +200 -200
  22. package/src/core/ddl/dialects/base-schema-dialect.ts +1 -1
  23. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -1
  24. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -1
  25. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -1
  26. package/src/core/ddl/dialects/render-reference.test.ts +70 -70
  27. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -1
  28. package/src/core/ddl/introspect/catalogs/index.ts +5 -5
  29. package/src/core/ddl/introspect/catalogs/postgres.ts +146 -146
  30. package/src/core/ddl/introspect/context.ts +15 -15
  31. package/src/core/ddl/introspect/functions/postgres.ts +35 -35
  32. package/src/core/ddl/introspect/registry.ts +40 -40
  33. package/src/core/ddl/introspect/run-select.ts +35 -35
  34. package/src/core/ddl/introspect/utils.ts +56 -56
  35. package/src/core/ddl/naming-strategy.ts +16 -16
  36. package/src/core/ddl/schema-dialect.ts +56 -56
  37. package/src/core/ddl/schema-diff.ts +234 -234
  38. package/src/core/ddl/schema-generator.ts +1 -1
  39. package/src/core/ddl/schema-introspect.ts +25 -25
  40. package/src/core/ddl/sql-writing.ts +171 -171
  41. package/src/core/dialect/abstract.ts +541 -541
  42. package/src/core/dialect/base/cte-compiler.ts +33 -33
  43. package/src/core/dialect/base/function-table-formatter.ts +103 -103
  44. package/src/core/dialect/base/groupby-compiler.ts +21 -21
  45. package/src/core/dialect/base/join-compiler.ts +25 -25
  46. package/src/core/dialect/base/orderby-compiler.ts +35 -35
  47. package/src/core/dialect/base/pagination-strategy.ts +32 -32
  48. package/src/core/dialect/base/returning-strategy.ts +57 -57
  49. package/src/core/dialect/base/sql-dialect.ts +294 -294
  50. package/src/core/dialect/dialect-factory.ts +91 -91
  51. package/src/core/dialect/mssql/functions.ts +120 -120
  52. package/src/core/dialect/mssql/index.ts +317 -317
  53. package/src/core/dialect/mysql/functions.ts +103 -103
  54. package/src/core/dialect/mysql/index.ts +105 -105
  55. package/src/core/dialect/postgres/functions.ts +108 -108
  56. package/src/core/dialect/postgres/index.ts +91 -91
  57. package/src/core/dialect/sqlite/functions.ts +129 -129
  58. package/src/core/dialect/sqlite/index.ts +67 -67
  59. package/src/core/driver/database-driver.ts +19 -19
  60. package/src/core/driver/mssql-driver.ts +23 -23
  61. package/src/core/driver/mysql-driver.ts +23 -23
  62. package/src/core/driver/postgres-driver.ts +23 -23
  63. package/src/core/driver/sqlite-driver.ts +23 -23
  64. package/src/core/execution/db-executor.ts +92 -92
  65. package/src/core/execution/executors/better-sqlite3-executor.ts +90 -90
  66. package/src/core/execution/executors/mssql-executor.ts +184 -184
  67. package/src/core/execution/executors/mysql-executor.ts +88 -88
  68. package/src/core/execution/executors/postgres-executor.ts +26 -26
  69. package/src/core/execution/executors/sqlite-executor.ts +42 -42
  70. package/src/core/execution/pooling/pool-types.ts +30 -30
  71. package/src/core/execution/pooling/pool.ts +275 -275
  72. package/src/core/functions/array.ts +35 -35
  73. package/src/core/functions/control-flow.ts +79 -79
  74. package/src/core/functions/datetime.ts +237 -237
  75. package/src/core/functions/json.ts +70 -70
  76. package/src/core/functions/numeric.ts +297 -297
  77. package/src/core/functions/text.ts +334 -334
  78. package/src/core/functions/types.ts +33 -33
  79. package/src/core/hydration/types.ts +51 -51
  80. package/src/core/sql/sql-operator-config.ts +39 -39
  81. package/src/core/sql/sql.ts +152 -152
  82. package/src/decorators/column-decorator.ts +87 -87
  83. package/src/decorators/decorator-metadata.ts +143 -143
  84. package/src/decorators/entity.ts +93 -93
  85. package/src/decorators/index.ts +39 -39
  86. package/src/decorators/relations.ts +231 -231
  87. package/src/decorators/transformers/built-in/string-transformers.ts +231 -231
  88. package/src/decorators/transformers/transformer-decorators.ts +134 -134
  89. package/src/decorators/transformers/transformer-executor.ts +171 -171
  90. package/src/decorators/transformers/transformer-metadata.ts +73 -73
  91. package/src/decorators/validators/built-in/br-cep-validator.ts +55 -55
  92. package/src/decorators/validators/built-in/br-cnpj-validator.ts +105 -105
  93. package/src/decorators/validators/built-in/br-cpf-validator.ts +103 -103
  94. package/src/decorators/validators/country-validator-registry.ts +64 -64
  95. package/src/decorators/validators/country-validators-decorators.ts +203 -203
  96. package/src/decorators/validators/country-validators.ts +105 -105
  97. package/src/decorators/validators/validator-metadata.ts +59 -59
  98. package/src/dto/dto-types.ts +226 -226
  99. package/src/dto/filter-types.ts +141 -141
  100. package/src/dto/index.ts +95 -95
  101. package/src/dto/openapi/generators/base.ts +29 -29
  102. package/src/dto/openapi/generators/column.ts +37 -37
  103. package/src/dto/openapi/generators/dto.ts +91 -91
  104. package/src/dto/openapi/generators/filter.ts +75 -75
  105. package/src/dto/openapi/generators/nested-dto.ts +618 -618
  106. package/src/dto/openapi/generators/pagination.ts +111 -111
  107. package/src/dto/openapi/generators/relation-filter.ts +228 -228
  108. package/src/dto/openapi/index.ts +12 -12
  109. package/src/dto/openapi/types.ts +101 -101
  110. package/src/dto/openapi/utilities.ts +95 -95
  111. package/src/dto/pagination-utils.ts +150 -150
  112. package/src/dto/transform.ts +197 -197
  113. package/src/index.ts +86 -86
  114. package/src/orm/als.ts +58 -58
  115. package/src/orm/domain-event-bus.ts +119 -119
  116. package/src/orm/entity-context.ts +86 -86
  117. package/src/orm/entity-materializer.ts +159 -159
  118. package/src/orm/entity-meta.ts +97 -97
  119. package/src/orm/entity-registry.ts +39 -39
  120. package/src/orm/entity-relation-cache.ts +39 -39
  121. package/src/orm/execute.ts +194 -194
  122. package/src/orm/execution-context.ts +18 -18
  123. package/src/orm/hydration-context.ts +26 -26
  124. package/src/orm/hydration.ts +112 -112
  125. package/src/orm/identity-map.ts +61 -61
  126. package/src/orm/interceptor-pipeline.ts +16 -16
  127. package/src/orm/lazy-batch/belongs-to-many.ts +119 -119
  128. package/src/orm/lazy-batch/belongs-to.ts +108 -108
  129. package/src/orm/lazy-batch/has-many.ts +69 -69
  130. package/src/orm/lazy-batch/has-one.ts +68 -68
  131. package/src/orm/lazy-batch/shared.ts +125 -125
  132. package/src/orm/orm-session.ts +620 -620
  133. package/src/orm/orm.ts +34 -34
  134. package/src/orm/pooled-executor-factory.ts +121 -121
  135. package/src/orm/query-logger.ts +40 -40
  136. package/src/orm/relation-change-processor.ts +420 -420
  137. package/src/orm/relations/belongs-to.ts +143 -143
  138. package/src/orm/relations/has-many.ts +204 -204
  139. package/src/orm/relations/has-one.ts +174 -174
  140. package/src/orm/relations/many-to-many.ts +288 -288
  141. package/src/orm/relations/morph-many.ts +156 -156
  142. package/src/orm/relations/morph-one.ts +151 -151
  143. package/src/orm/relations/morph-to.ts +162 -162
  144. package/src/orm/runtime-types.ts +92 -92
  145. package/src/orm/save-graph.ts +561 -561
  146. package/src/orm/transaction-runner.ts +24 -24
  147. package/src/orm/unit-of-work.ts +426 -426
  148. package/src/query/index.ts +69 -69
  149. package/src/query/target.ts +46 -46
  150. package/src/query-builder/column-selector.ts +81 -81
  151. package/src/query-builder/delete.ts +141 -141
  152. package/src/query-builder/insert-query-state.ts +149 -149
  153. package/src/query-builder/insert.ts +117 -117
  154. package/src/query-builder/query-ast-service.ts +287 -287
  155. package/src/query-builder/query-resolution.ts +78 -78
  156. package/src/query-builder/raw-column-parser.ts +38 -38
  157. package/src/query-builder/relation-filter-utils.ts +153 -153
  158. package/src/query-builder/relation-join-planner.ts +10 -10
  159. package/src/query-builder/relation-manager.ts +87 -87
  160. package/src/query-builder/relation-projection-helper.ts +102 -102
  161. package/src/query-builder/relation-types.ts +9 -9
  162. package/src/query-builder/relation-utils.ts +15 -15
  163. package/src/query-builder/select/cte-facet.ts +40 -40
  164. package/src/query-builder/select/from-facet.ts +80 -80
  165. package/src/query-builder/select/join-facet.ts +62 -62
  166. package/src/query-builder/select/predicate-facet.ts +104 -104
  167. package/src/query-builder/select/projection-facet.ts +70 -70
  168. package/src/query-builder/select/relation-facet.ts +81 -81
  169. package/src/query-builder/select/setop-facet.ts +36 -36
  170. package/src/query-builder/select-helpers.ts +1 -1
  171. package/src/query-builder/select-query-builder-deps.ts +130 -130
  172. package/src/query-builder/select-query-state.ts +207 -207
  173. package/src/query-builder/update.ts +151 -151
  174. package/src/schema/column-types.ts +322 -322
  175. package/src/schema/table-guards.ts +37 -37
  176. package/src/schema/table.ts +281 -281
  177. package/src/tree/index.ts +13 -13
  178. package/src/tree/nested-set-strategy.ts +545 -545
  179. package/src/tree/tree-decorator.ts +397 -397
  180. package/src/tree/tree-manager.ts +754 -754
  181. package/src/tree/tree-query.ts +427 -427
  182. 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
+ }