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,397 +1,397 @@
1
- /**
2
- * Tree Decorators
3
- *
4
- * Level 3 API: Decorator-based tree configuration for entity classes.
5
- * Integrates with MetalORM's decorator entity system.
6
- */
7
-
8
- import type { TreeConfig } from './tree-types.js';
9
- import { resolveTreeConfig } from './tree-types.js';
10
- import { RelationKinds, belongsTo, hasMany } from '../schema/relation.js';
11
- import { addRelationMetadata, getEntityMetadata, type EntityConstructor } from '../orm/entity-metadata.js';
12
- import {
13
- type DecoratorTreeMetadata,
14
- readMetadataBag,
15
- readMetadataBagFromConstructor,
16
- resolveFieldDecoratorInfo
17
- } from '../decorators/decorator-metadata.js';
18
-
19
- /**
20
- * Symbol key for storing tree metadata on entity classes.
21
- */
22
- const TREE_METADATA_KEY = Symbol('metal-orm:tree');
23
-
24
- /**
25
- * Stored tree metadata for an entity class.
26
- */
27
- export interface TreeMetadata {
28
- config: TreeConfig;
29
- parentProperty?: string;
30
- childrenProperty?: string;
31
- }
32
-
33
- /**
34
- * Options for the @Tree decorator.
35
- */
36
- export interface TreeDecoratorOptions {
37
- /** Column name for parent reference (default: 'parentId') */
38
- parentKey?: string;
39
- /** Column name for left boundary (default: 'lft') */
40
- leftKey?: string;
41
- /** Column name for right boundary (default: 'rght') */
42
- rightKey?: string;
43
- /** Optional column name for cached depth level */
44
- depthKey?: string;
45
- /** Columns used for scoping multiple trees in one table */
46
- scope?: string[];
47
- /** Whether to load and delete children individually to trigger hooks */
48
- cascadeCallbacks?: boolean;
49
- /** Custom ordering for tree recovery */
50
- recoverOrder?: Record<string, 'ASC' | 'DESC'>;
51
- }
52
-
53
- /**
54
- * Decorator to mark an entity class as a tree/hierarchical entity.
55
- *
56
- * @example
57
- * ```ts
58
- * @Entity()
59
- * @Tree({ parentKey: 'parentId', leftKey: 'lft', rightKey: 'rght' })
60
- * class Category {
61
- * @PrimaryKey(col.int())
62
- * id!: number;
63
- *
64
- * @Column(col.varchar(255))
65
- * name!: string;
66
- *
67
- * @Column(col.int().nullable())
68
- * parentId!: number | null;
69
- *
70
- * @Column(col.int())
71
- * lft!: number;
72
- *
73
- * @Column(col.int())
74
- * rght!: number;
75
- *
76
- * @TreeParent()
77
- * parent?: Category;
78
- *
79
- * @TreeChildren()
80
- * children?: Category[];
81
- * }
82
- * ```
83
- */
84
- export function Tree(options: TreeDecoratorOptions = {}) {
85
- return function <T extends abstract new (...args: unknown[]) => object>(
86
- target: T,
87
- context?: ClassDecoratorContext<T>
88
- ): T {
89
- const config = resolveTreeConfig(options);
90
- const metadataBag = context
91
- ? readMetadataBag(context) ?? readMetadataBagFromConstructor(target)
92
- : readMetadataBagFromConstructor(target);
93
- const metadata: TreeMetadata = {
94
- config,
95
- parentProperty: metadataBag?.tree?.parentProperty,
96
- childrenProperty: metadataBag?.tree?.childrenProperty
97
- };
98
-
99
- setTreeMetadataOnClass(target, metadata);
100
- registerTreeRelations(target as unknown as EntityConstructor, metadata);
101
-
102
- return target;
103
- };
104
- }
105
-
106
- /**
107
- * Decorator to mark a property as the parent relation in a tree entity.
108
- *
109
- * @example
110
- * ```ts
111
- * @TreeParent()
112
- * parent?: Category;
113
- * ```
114
- */
115
- export function TreeParent() {
116
- return function (targetOrValue: unknown, contextOrProperty: unknown): void {
117
- const { propertyName, bag } = resolveFieldDecoratorInfo(
118
- targetOrValue,
119
- contextOrProperty,
120
- 'TreeParent'
121
- );
122
- bag.tree = { ...bag.tree, parentProperty: propertyName };
123
- };
124
- }
125
-
126
- /**
127
- * Decorator to mark a property as the children relation in a tree entity.
128
- *
129
- * @example
130
- * ```ts
131
- * @TreeChildren()
132
- * children?: Category[];
133
- * ```
134
- */
135
- export function TreeChildren() {
136
- return function (targetOrValue: unknown, contextOrProperty: unknown): void {
137
- const { propertyName, bag } = resolveFieldDecoratorInfo(
138
- targetOrValue,
139
- contextOrProperty,
140
- 'TreeChildren'
141
- );
142
- bag.tree = { ...bag.tree, childrenProperty: propertyName };
143
- };
144
- }
145
-
146
- /**
147
- * Synchronizes tree metadata + self-relations for entities using @Tree decorators.
148
- * Safe to call multiple times (idempotent).
149
- */
150
- export function syncTreeEntityMetadata(
151
- ctor: EntityConstructor,
152
- treeMetadata?: DecoratorTreeMetadata
153
- ): void {
154
- const current = getTreeMetadata(ctor);
155
- if (!current) return;
156
- const metadataBag = readMetadataBagFromConstructor(ctor);
157
-
158
- const nextParentProperty =
159
- treeMetadata?.parentProperty ?? metadataBag?.tree?.parentProperty ?? current.parentProperty;
160
- const nextChildrenProperty =
161
- treeMetadata?.childrenProperty ?? metadataBag?.tree?.childrenProperty ?? current.childrenProperty;
162
-
163
- if (nextParentProperty !== current.parentProperty || nextChildrenProperty !== current.childrenProperty) {
164
- setTreeMetadata(ctor, {
165
- ...current,
166
- parentProperty: nextParentProperty,
167
- childrenProperty: nextChildrenProperty
168
- });
169
- }
170
-
171
- registerTreeRelations(ctor, {
172
- ...current,
173
- parentProperty: nextParentProperty,
174
- childrenProperty: nextChildrenProperty
175
- });
176
- }
177
-
178
- /**
179
- * Gets tree metadata from an entity class.
180
- */
181
- export function getTreeMetadata(
182
- target: new (...args: unknown[]) => object
183
- ): TreeMetadata | undefined {
184
- return (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY];
185
- }
186
-
187
- /**
188
- * Sets tree metadata on an entity class.
189
- */
190
- export function setTreeMetadata(
191
- target: new (...args: unknown[]) => object,
192
- metadata: TreeMetadata
193
- ): void {
194
- (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY] = metadata;
195
- }
196
-
197
- const registerTreeRelations = (
198
- target: EntityConstructor,
199
- metadata: TreeMetadata
200
- ): void => {
201
- const entityMeta = getEntityMetadata(target);
202
- if (!entityMeta) return;
203
-
204
- if (metadata.parentProperty && !entityMeta.relations[metadata.parentProperty]) {
205
- addRelationMetadata(target, metadata.parentProperty, {
206
- kind: RelationKinds.BelongsTo,
207
- propertyKey: metadata.parentProperty,
208
- target: () => target,
209
- foreignKey: metadata.config.parentKey
210
- });
211
- if (entityMeta.table && !entityMeta.table.relations[metadata.parentProperty]) {
212
- entityMeta.table.relations[metadata.parentProperty] = belongsTo(
213
- entityMeta.table,
214
- metadata.config.parentKey
215
- );
216
- }
217
- }
218
-
219
- if (metadata.childrenProperty && !entityMeta.relations[metadata.childrenProperty]) {
220
- addRelationMetadata(target, metadata.childrenProperty, {
221
- kind: RelationKinds.HasMany,
222
- propertyKey: metadata.childrenProperty,
223
- target: () => target,
224
- foreignKey: metadata.config.parentKey
225
- });
226
- if (entityMeta.table && !entityMeta.table.relations[metadata.childrenProperty]) {
227
- entityMeta.table.relations[metadata.childrenProperty] = hasMany(
228
- entityMeta.table,
229
- metadata.config.parentKey
230
- );
231
- }
232
- }
233
- };
234
-
235
- /**
236
- * Sets tree metadata on an entity class (internal, handles abstract classes).
237
- */
238
- function setTreeMetadataOnClass(
239
- target: abstract new (...args: unknown[]) => object,
240
- metadata: TreeMetadata
241
- ): void {
242
- (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY] = metadata;
243
- }
244
-
245
- /**
246
- * Checks if an entity class has tree behavior configured.
247
- */
248
- export function hasTreeBehavior(
249
- target: new (...args: unknown[]) => object
250
- ): boolean {
251
- return getTreeMetadata(target) !== undefined;
252
- }
253
-
254
- /**
255
- * Gets the tree configuration from an entity class.
256
- */
257
- export function getTreeConfig(
258
- target: new (...args: unknown[]) => object
259
- ): TreeConfig | undefined {
260
- return getTreeMetadata(target)?.config;
261
- }
262
-
263
- /**
264
- * Registry for tree-enabled entity classes.
265
- * Allows looking up tree configuration by table name.
266
- */
267
- class TreeEntityRegistry {
268
- private entities = new Map<string, new (...args: unknown[]) => object>();
269
- private tableNames = new Map<new (...args: unknown[]) => object, string>();
270
-
271
- /**
272
- * Registers an entity class with its table name.
273
- */
274
- register(
275
- entityClass: new (...args: unknown[]) => object,
276
- tableName: string
277
- ): void {
278
- this.entities.set(tableName, entityClass);
279
- this.tableNames.set(entityClass, tableName);
280
- }
281
-
282
- /**
283
- * Gets the entity class for a table name.
284
- */
285
- getByTableName(tableName: string): (new (...args: unknown[]) => object) | undefined {
286
- return this.entities.get(tableName);
287
- }
288
-
289
- /**
290
- * Gets the table name for an entity class.
291
- */
292
- getTableName(entityClass: new (...args: unknown[]) => object): string | undefined {
293
- return this.tableNames.get(entityClass);
294
- }
295
-
296
- /**
297
- * Checks if a table has tree behavior.
298
- */
299
- isTreeTable(tableName: string): boolean {
300
- const entity = this.entities.get(tableName);
301
- return entity ? hasTreeBehavior(entity) : false;
302
- }
303
-
304
- /**
305
- * Gets all registered tree entities.
306
- */
307
- getAll(): Array<{
308
- entityClass: new (...args: unknown[]) => object;
309
- tableName: string;
310
- metadata: TreeMetadata;
311
- }> {
312
- const result: Array<{
313
- entityClass: new (...args: unknown[]) => object;
314
- tableName: string;
315
- metadata: TreeMetadata;
316
- }> = [];
317
-
318
- for (const [tableName, entityClass] of this.entities) {
319
- const metadata = getTreeMetadata(entityClass);
320
- if (metadata) {
321
- result.push({ entityClass, tableName, metadata });
322
- }
323
- }
324
-
325
- return result;
326
- }
327
-
328
- /**
329
- * Clears the registry.
330
- */
331
- clear(): void {
332
- this.entities.clear();
333
- this.tableNames.clear();
334
- }
335
- }
336
-
337
- /**
338
- * Global registry for tree entities.
339
- */
340
- export const treeEntityRegistry = new TreeEntityRegistry();
341
-
342
- /**
343
- * Helper to get tree bounds from an entity instance.
344
- */
345
- export function getTreeBounds(
346
- entity: object,
347
- config: TreeConfig
348
- ): { lft: number; rght: number } | null {
349
- const data = entity as Record<string, unknown>;
350
- const lft = data[config.leftKey];
351
- const rght = data[config.rightKey];
352
-
353
- if (typeof lft !== 'number' || typeof rght !== 'number') {
354
- return null;
355
- }
356
-
357
- return { lft, rght };
358
- }
359
-
360
- /**
361
- * Helper to get parent ID from an entity instance.
362
- */
363
- export function getTreeParentId(
364
- entity: object,
365
- config: TreeConfig
366
- ): unknown {
367
- return (entity as Record<string, unknown>)[config.parentKey];
368
- }
369
-
370
- /**
371
- * Helper to set tree bounds on an entity instance.
372
- */
373
- export function setTreeBounds(
374
- entity: object,
375
- config: TreeConfig,
376
- lft: number,
377
- rght: number,
378
- depth?: number
379
- ): void {
380
- const data = entity as Record<string, unknown>;
381
- data[config.leftKey] = lft;
382
- data[config.rightKey] = rght;
383
- if (config.depthKey && depth !== undefined) {
384
- data[config.depthKey] = depth;
385
- }
386
- }
387
-
388
- /**
389
- * Helper to set parent ID on an entity instance.
390
- */
391
- export function setTreeParentId(
392
- entity: object,
393
- config: TreeConfig,
394
- parentId: unknown
395
- ): void {
396
- (entity as Record<string, unknown>)[config.parentKey] = parentId;
397
- }
1
+ /**
2
+ * Tree Decorators
3
+ *
4
+ * Level 3 API: Decorator-based tree configuration for entity classes.
5
+ * Integrates with MetalORM's decorator entity system.
6
+ */
7
+
8
+ import type { TreeConfig } from './tree-types.js';
9
+ import { resolveTreeConfig } from './tree-types.js';
10
+ import { RelationKinds, belongsTo, hasMany } from '../schema/relation.js';
11
+ import { addRelationMetadata, getEntityMetadata, type EntityConstructor } from '../orm/entity-metadata.js';
12
+ import {
13
+ type DecoratorTreeMetadata,
14
+ readMetadataBag,
15
+ readMetadataBagFromConstructor,
16
+ resolveFieldDecoratorInfo
17
+ } from '../decorators/decorator-metadata.js';
18
+
19
+ /**
20
+ * Symbol key for storing tree metadata on entity classes.
21
+ */
22
+ const TREE_METADATA_KEY = Symbol('metal-orm:tree');
23
+
24
+ /**
25
+ * Stored tree metadata for an entity class.
26
+ */
27
+ export interface TreeMetadata {
28
+ config: TreeConfig;
29
+ parentProperty?: string;
30
+ childrenProperty?: string;
31
+ }
32
+
33
+ /**
34
+ * Options for the @Tree decorator.
35
+ */
36
+ export interface TreeDecoratorOptions {
37
+ /** Column name for parent reference (default: 'parentId') */
38
+ parentKey?: string;
39
+ /** Column name for left boundary (default: 'lft') */
40
+ leftKey?: string;
41
+ /** Column name for right boundary (default: 'rght') */
42
+ rightKey?: string;
43
+ /** Optional column name for cached depth level */
44
+ depthKey?: string;
45
+ /** Columns used for scoping multiple trees in one table */
46
+ scope?: string[];
47
+ /** Whether to load and delete children individually to trigger hooks */
48
+ cascadeCallbacks?: boolean;
49
+ /** Custom ordering for tree recovery */
50
+ recoverOrder?: Record<string, 'ASC' | 'DESC'>;
51
+ }
52
+
53
+ /**
54
+ * Decorator to mark an entity class as a tree/hierarchical entity.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * @Entity()
59
+ * @Tree({ parentKey: 'parentId', leftKey: 'lft', rightKey: 'rght' })
60
+ * class Category {
61
+ * @PrimaryKey(col.int())
62
+ * id!: number;
63
+ *
64
+ * @Column(col.varchar(255))
65
+ * name!: string;
66
+ *
67
+ * @Column(col.int().nullable())
68
+ * parentId!: number | null;
69
+ *
70
+ * @Column(col.int())
71
+ * lft!: number;
72
+ *
73
+ * @Column(col.int())
74
+ * rght!: number;
75
+ *
76
+ * @TreeParent()
77
+ * parent?: Category;
78
+ *
79
+ * @TreeChildren()
80
+ * children?: Category[];
81
+ * }
82
+ * ```
83
+ */
84
+ export function Tree(options: TreeDecoratorOptions = {}) {
85
+ return function <T extends abstract new (...args: unknown[]) => object>(
86
+ target: T,
87
+ context?: ClassDecoratorContext<T>
88
+ ): T {
89
+ const config = resolveTreeConfig(options);
90
+ const metadataBag = context
91
+ ? readMetadataBag(context) ?? readMetadataBagFromConstructor(target)
92
+ : readMetadataBagFromConstructor(target);
93
+ const metadata: TreeMetadata = {
94
+ config,
95
+ parentProperty: metadataBag?.tree?.parentProperty,
96
+ childrenProperty: metadataBag?.tree?.childrenProperty
97
+ };
98
+
99
+ setTreeMetadataOnClass(target, metadata);
100
+ registerTreeRelations(target as unknown as EntityConstructor, metadata);
101
+
102
+ return target;
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Decorator to mark a property as the parent relation in a tree entity.
108
+ *
109
+ * @example
110
+ * ```ts
111
+ * @TreeParent()
112
+ * parent?: Category;
113
+ * ```
114
+ */
115
+ export function TreeParent() {
116
+ return function (targetOrValue: unknown, contextOrProperty: unknown): void {
117
+ const { propertyName, bag } = resolveFieldDecoratorInfo(
118
+ targetOrValue,
119
+ contextOrProperty,
120
+ 'TreeParent'
121
+ );
122
+ bag.tree = { ...bag.tree, parentProperty: propertyName };
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Decorator to mark a property as the children relation in a tree entity.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * @TreeChildren()
132
+ * children?: Category[];
133
+ * ```
134
+ */
135
+ export function TreeChildren() {
136
+ return function (targetOrValue: unknown, contextOrProperty: unknown): void {
137
+ const { propertyName, bag } = resolveFieldDecoratorInfo(
138
+ targetOrValue,
139
+ contextOrProperty,
140
+ 'TreeChildren'
141
+ );
142
+ bag.tree = { ...bag.tree, childrenProperty: propertyName };
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Synchronizes tree metadata + self-relations for entities using @Tree decorators.
148
+ * Safe to call multiple times (idempotent).
149
+ */
150
+ export function syncTreeEntityMetadata(
151
+ ctor: EntityConstructor,
152
+ treeMetadata?: DecoratorTreeMetadata
153
+ ): void {
154
+ const current = getTreeMetadata(ctor);
155
+ if (!current) return;
156
+ const metadataBag = readMetadataBagFromConstructor(ctor);
157
+
158
+ const nextParentProperty =
159
+ treeMetadata?.parentProperty ?? metadataBag?.tree?.parentProperty ?? current.parentProperty;
160
+ const nextChildrenProperty =
161
+ treeMetadata?.childrenProperty ?? metadataBag?.tree?.childrenProperty ?? current.childrenProperty;
162
+
163
+ if (nextParentProperty !== current.parentProperty || nextChildrenProperty !== current.childrenProperty) {
164
+ setTreeMetadata(ctor, {
165
+ ...current,
166
+ parentProperty: nextParentProperty,
167
+ childrenProperty: nextChildrenProperty
168
+ });
169
+ }
170
+
171
+ registerTreeRelations(ctor, {
172
+ ...current,
173
+ parentProperty: nextParentProperty,
174
+ childrenProperty: nextChildrenProperty
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Gets tree metadata from an entity class.
180
+ */
181
+ export function getTreeMetadata(
182
+ target: new (...args: unknown[]) => object
183
+ ): TreeMetadata | undefined {
184
+ return (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY];
185
+ }
186
+
187
+ /**
188
+ * Sets tree metadata on an entity class.
189
+ */
190
+ export function setTreeMetadata(
191
+ target: new (...args: unknown[]) => object,
192
+ metadata: TreeMetadata
193
+ ): void {
194
+ (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY] = metadata;
195
+ }
196
+
197
+ const registerTreeRelations = (
198
+ target: EntityConstructor,
199
+ metadata: TreeMetadata
200
+ ): void => {
201
+ const entityMeta = getEntityMetadata(target);
202
+ if (!entityMeta) return;
203
+
204
+ if (metadata.parentProperty && !entityMeta.relations[metadata.parentProperty]) {
205
+ addRelationMetadata(target, metadata.parentProperty, {
206
+ kind: RelationKinds.BelongsTo,
207
+ propertyKey: metadata.parentProperty,
208
+ target: () => target,
209
+ foreignKey: metadata.config.parentKey
210
+ });
211
+ if (entityMeta.table && !entityMeta.table.relations[metadata.parentProperty]) {
212
+ entityMeta.table.relations[metadata.parentProperty] = belongsTo(
213
+ entityMeta.table,
214
+ metadata.config.parentKey
215
+ );
216
+ }
217
+ }
218
+
219
+ if (metadata.childrenProperty && !entityMeta.relations[metadata.childrenProperty]) {
220
+ addRelationMetadata(target, metadata.childrenProperty, {
221
+ kind: RelationKinds.HasMany,
222
+ propertyKey: metadata.childrenProperty,
223
+ target: () => target,
224
+ foreignKey: metadata.config.parentKey
225
+ });
226
+ if (entityMeta.table && !entityMeta.table.relations[metadata.childrenProperty]) {
227
+ entityMeta.table.relations[metadata.childrenProperty] = hasMany(
228
+ entityMeta.table,
229
+ metadata.config.parentKey
230
+ );
231
+ }
232
+ }
233
+ };
234
+
235
+ /**
236
+ * Sets tree metadata on an entity class (internal, handles abstract classes).
237
+ */
238
+ function setTreeMetadataOnClass(
239
+ target: abstract new (...args: unknown[]) => object,
240
+ metadata: TreeMetadata
241
+ ): void {
242
+ (target as unknown as Record<symbol, TreeMetadata>)[TREE_METADATA_KEY] = metadata;
243
+ }
244
+
245
+ /**
246
+ * Checks if an entity class has tree behavior configured.
247
+ */
248
+ export function hasTreeBehavior(
249
+ target: new (...args: unknown[]) => object
250
+ ): boolean {
251
+ return getTreeMetadata(target) !== undefined;
252
+ }
253
+
254
+ /**
255
+ * Gets the tree configuration from an entity class.
256
+ */
257
+ export function getTreeConfig(
258
+ target: new (...args: unknown[]) => object
259
+ ): TreeConfig | undefined {
260
+ return getTreeMetadata(target)?.config;
261
+ }
262
+
263
+ /**
264
+ * Registry for tree-enabled entity classes.
265
+ * Allows looking up tree configuration by table name.
266
+ */
267
+ class TreeEntityRegistry {
268
+ private entities = new Map<string, new (...args: unknown[]) => object>();
269
+ private tableNames = new Map<new (...args: unknown[]) => object, string>();
270
+
271
+ /**
272
+ * Registers an entity class with its table name.
273
+ */
274
+ register(
275
+ entityClass: new (...args: unknown[]) => object,
276
+ tableName: string
277
+ ): void {
278
+ this.entities.set(tableName, entityClass);
279
+ this.tableNames.set(entityClass, tableName);
280
+ }
281
+
282
+ /**
283
+ * Gets the entity class for a table name.
284
+ */
285
+ getByTableName(tableName: string): (new (...args: unknown[]) => object) | undefined {
286
+ return this.entities.get(tableName);
287
+ }
288
+
289
+ /**
290
+ * Gets the table name for an entity class.
291
+ */
292
+ getTableName(entityClass: new (...args: unknown[]) => object): string | undefined {
293
+ return this.tableNames.get(entityClass);
294
+ }
295
+
296
+ /**
297
+ * Checks if a table has tree behavior.
298
+ */
299
+ isTreeTable(tableName: string): boolean {
300
+ const entity = this.entities.get(tableName);
301
+ return entity ? hasTreeBehavior(entity) : false;
302
+ }
303
+
304
+ /**
305
+ * Gets all registered tree entities.
306
+ */
307
+ getAll(): Array<{
308
+ entityClass: new (...args: unknown[]) => object;
309
+ tableName: string;
310
+ metadata: TreeMetadata;
311
+ }> {
312
+ const result: Array<{
313
+ entityClass: new (...args: unknown[]) => object;
314
+ tableName: string;
315
+ metadata: TreeMetadata;
316
+ }> = [];
317
+
318
+ for (const [tableName, entityClass] of this.entities) {
319
+ const metadata = getTreeMetadata(entityClass);
320
+ if (metadata) {
321
+ result.push({ entityClass, tableName, metadata });
322
+ }
323
+ }
324
+
325
+ return result;
326
+ }
327
+
328
+ /**
329
+ * Clears the registry.
330
+ */
331
+ clear(): void {
332
+ this.entities.clear();
333
+ this.tableNames.clear();
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Global registry for tree entities.
339
+ */
340
+ export const treeEntityRegistry = new TreeEntityRegistry();
341
+
342
+ /**
343
+ * Helper to get tree bounds from an entity instance.
344
+ */
345
+ export function getTreeBounds(
346
+ entity: object,
347
+ config: TreeConfig
348
+ ): { lft: number; rght: number } | null {
349
+ const data = entity as Record<string, unknown>;
350
+ const lft = data[config.leftKey];
351
+ const rght = data[config.rightKey];
352
+
353
+ if (typeof lft !== 'number' || typeof rght !== 'number') {
354
+ return null;
355
+ }
356
+
357
+ return { lft, rght };
358
+ }
359
+
360
+ /**
361
+ * Helper to get parent ID from an entity instance.
362
+ */
363
+ export function getTreeParentId(
364
+ entity: object,
365
+ config: TreeConfig
366
+ ): unknown {
367
+ return (entity as Record<string, unknown>)[config.parentKey];
368
+ }
369
+
370
+ /**
371
+ * Helper to set tree bounds on an entity instance.
372
+ */
373
+ export function setTreeBounds(
374
+ entity: object,
375
+ config: TreeConfig,
376
+ lft: number,
377
+ rght: number,
378
+ depth?: number
379
+ ): void {
380
+ const data = entity as Record<string, unknown>;
381
+ data[config.leftKey] = lft;
382
+ data[config.rightKey] = rght;
383
+ if (config.depthKey && depth !== undefined) {
384
+ data[config.depthKey] = depth;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Helper to set parent ID on an entity instance.
390
+ */
391
+ export function setTreeParentId(
392
+ entity: object,
393
+ config: TreeConfig,
394
+ parentId: unknown
395
+ ): void {
396
+ (entity as Record<string, unknown>)[config.parentKey] = parentId;
397
+ }