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