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,426 +1,426 @@
1
- import { ColumnNode, eq } from '../core/ast/expression.js';
2
- import type { ValueOperandInput } from '../core/ast/expression.js';
3
- import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
4
- import { InsertQueryBuilder } from '../query-builder/insert.js';
5
- import { UpdateQueryBuilder } from '../query-builder/update.js';
6
- import { DeleteQueryBuilder } from '../query-builder/delete.js';
7
- import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
- import type { TableDef, TableHooks } from '../schema/table.js';
9
- import { payloadResultSets } from '../core/execution/db-executor.js';
10
- import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
11
- import { IdentityMap } from './identity-map.js';
12
- import { EntityStatus } from './runtime-types.js';
13
- import type { TrackedEntity } from './runtime-types.js';
14
- import type { PrimaryKey } from './entity-context.js';
15
-
16
- /**
17
- * Unit of Work pattern implementation for tracking entity changes.
18
- */
19
- export class UnitOfWork {
20
- private readonly trackedEntities = new Map<object, TrackedEntity>();
21
-
22
- /**
23
- * Creates a new UnitOfWork instance.
24
- * @param dialect - The database dialect
25
- * @param executor - The database executor
26
- * @param identityMap - The identity map
27
- * @param hookContext - Function to get the hook context
28
- */
29
- constructor(
30
- private readonly dialect: Dialect,
31
- private readonly executor: DbExecutor,
32
- private readonly identityMap: IdentityMap,
33
- private readonly hookContext: () => unknown
34
- ) { }
35
-
36
- /**
37
- * Gets the identity buckets map.
38
- */
39
- get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
40
- return this.identityMap.bucketsMap;
41
- }
42
-
43
- /**
44
- * Gets all tracked entities.
45
- * @returns Array of tracked entities
46
- */
47
- getTracked(): TrackedEntity[] {
48
- return Array.from(this.trackedEntities.values());
49
- }
50
-
51
- /**
52
- * Gets an entity by table and primary key.
53
- * @param table - The table definition
54
- * @param pk - The primary key value
55
- * @returns The entity or undefined if not found
56
- */
57
- getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
58
- return this.identityMap.getEntity(table, pk);
59
- }
60
-
61
- /**
62
- * Gets all tracked entities for a specific table.
63
- * @param table - The table definition
64
- * @returns Array of tracked entities
65
- */
66
- getEntitiesForTable(table: TableDef): TrackedEntity[] {
67
- return this.identityMap.getEntitiesForTable(table);
68
- }
69
-
70
- /**
71
- * Finds a tracked entity.
72
- * @param entity - The entity to find
73
- * @returns The tracked entity or undefined if not found
74
- */
75
- findTracked(entity: object): TrackedEntity | undefined {
76
- return this.trackedEntities.get(entity);
77
- }
78
-
79
- /**
80
- * Sets an entity in the identity map.
81
- * @param table - The table definition
82
- * @param pk - The primary key value
83
- * @param entity - The entity instance
84
- */
85
- setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
86
- if (pk === null || pk === undefined) return;
87
- let tracked = this.trackedEntities.get(entity);
88
- if (!tracked) {
89
- tracked = {
90
- table,
91
- entity,
92
- pk,
93
- status: EntityStatus.Managed,
94
- original: this.createSnapshot(table, entity as Record<string, unknown>)
95
- };
96
- this.trackedEntities.set(entity, tracked);
97
- } else {
98
- tracked.pk = pk;
99
- }
100
-
101
- this.registerIdentity(tracked);
102
- }
103
-
104
- /**
105
- * Tracks a new entity.
106
- * @param table - The table definition
107
- * @param entity - The entity instance
108
- * @param pk - Optional primary key value
109
- */
110
- trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
111
- const tracked: TrackedEntity = {
112
- table,
113
- entity,
114
- pk: pk ?? null,
115
- status: EntityStatus.New,
116
- original: null
117
- };
118
- this.trackedEntities.set(entity, tracked);
119
- if (pk != null) {
120
- this.registerIdentity(tracked);
121
- }
122
- }
123
-
124
- /**
125
- * Tracks a managed entity.
126
- * @param table - The table definition
127
- * @param pk - The primary key value
128
- * @param entity - The entity instance
129
- */
130
- trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
131
- const tracked: TrackedEntity = {
132
- table,
133
- entity,
134
- pk,
135
- status: EntityStatus.Managed,
136
- original: this.createSnapshot(table, entity as Record<string, unknown>)
137
- };
138
- this.trackedEntities.set(entity, tracked);
139
- this.registerIdentity(tracked);
140
- }
141
-
142
- /**
143
- * Marks an entity as dirty (modified).
144
- * @param entity - The entity to mark as dirty
145
- */
146
- markDirty(entity: object): void {
147
- const tracked = this.trackedEntities.get(entity);
148
- if (!tracked) return;
149
- if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
150
- tracked.status = EntityStatus.Dirty;
151
- }
152
-
153
- /**
154
- * Marks an entity as removed.
155
- * @param entity - The entity to mark as removed
156
- */
157
- markRemoved(entity: object): void {
158
- const tracked = this.trackedEntities.get(entity);
159
- if (!tracked) return;
160
- tracked.status = EntityStatus.Removed;
161
- }
162
-
163
- /**
164
- * Flushes pending changes to the database.
165
- */
166
- async flush(): Promise<void> {
167
- const toFlush = Array.from(this.trackedEntities.values());
168
- for (const tracked of toFlush) {
169
- switch (tracked.status) {
170
- case EntityStatus.New:
171
- await this.flushInsert(tracked);
172
- break;
173
- case EntityStatus.Dirty:
174
- await this.flushUpdate(tracked);
175
- break;
176
- case EntityStatus.Removed:
177
- await this.flushDelete(tracked);
178
- break;
179
- default:
180
- break;
181
- }
182
- }
183
- }
184
-
185
- /**
186
- * Resets the unit of work by clearing all tracked entities and identity map.
187
- */
188
- reset(): void {
189
- this.trackedEntities.clear();
190
- this.identityMap.clear();
191
- }
192
-
193
- /**
194
- * Flushes an insert operation for a new entity.
195
- * @param tracked - The tracked entity to insert
196
- */
197
- private async flushInsert(tracked: TrackedEntity): Promise<void> {
198
- await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
199
-
200
- const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
201
- let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
202
- if (this.dialect.supportsDmlReturningClause()) {
203
- builder = builder.returning(...this.getReturningColumns(tracked.table));
204
- }
205
- const compiled = builder.compile(this.dialect);
206
- const results = await this.executeCompiled(compiled);
207
- this.applyReturningResults(tracked, results);
208
- this.applyInsertedIdIfAbsent(tracked, results);
209
-
210
- tracked.status = EntityStatus.Managed;
211
- tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
212
- tracked.pk = this.getPrimaryKeyValue(tracked);
213
- this.registerIdentity(tracked);
214
-
215
- await this.runHook(tracked.table.hooks?.afterInsert, tracked);
216
- }
217
-
218
- /**
219
- * Flushes an update operation for a modified entity.
220
- * @param tracked - The tracked entity to update
221
- */
222
- private async flushUpdate(tracked: TrackedEntity): Promise<void> {
223
- if (tracked.pk == null) return;
224
- const changes = this.computeChanges(tracked);
225
- if (!Object.keys(changes).length) {
226
- tracked.status = EntityStatus.Managed;
227
- return;
228
- }
229
-
230
- await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
231
-
232
- const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
233
- if (!pkColumn) return;
234
-
235
- let builder = new UpdateQueryBuilder(tracked.table)
236
- .set(changes)
237
- .where(eq(pkColumn, tracked.pk));
238
-
239
- if (this.dialect.supportsDmlReturningClause()) {
240
- builder = builder.returning(...this.getReturningColumns(tracked.table));
241
- }
242
-
243
- const compiled = builder.compile(this.dialect);
244
- const results = await this.executeCompiled(compiled);
245
- this.applyReturningResults(tracked, results);
246
-
247
- tracked.status = EntityStatus.Managed;
248
- tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
249
- this.registerIdentity(tracked);
250
-
251
- await this.runHook(tracked.table.hooks?.afterUpdate, tracked);
252
- }
253
-
254
- /**
255
- * Flushes a delete operation for a removed entity.
256
- * @param tracked - The tracked entity to delete
257
- */
258
- private async flushDelete(tracked: TrackedEntity): Promise<void> {
259
- if (tracked.pk == null) return;
260
- await this.runHook(tracked.table.hooks?.beforeDelete, tracked);
261
-
262
- const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
263
- if (!pkColumn) return;
264
-
265
- const builder = new DeleteQueryBuilder(tracked.table).where(eq(pkColumn, tracked.pk));
266
- const compiled = builder.compile(this.dialect);
267
- await this.executeCompiled(compiled);
268
-
269
- tracked.status = EntityStatus.Detached;
270
- this.trackedEntities.delete(tracked.entity);
271
- this.identityMap.remove(tracked);
272
-
273
- await this.runHook(tracked.table.hooks?.afterDelete, tracked);
274
- }
275
-
276
- /**
277
- * Runs a table hook if defined.
278
- * @param hook - The hook function
279
- * @param tracked - The tracked entity
280
- */
281
- private async runHook(
282
- hook: TableHooks[keyof TableHooks] | undefined,
283
- tracked: TrackedEntity
284
- ): Promise<void> {
285
- if (!hook) return;
286
- await hook(this.hookContext(), tracked.entity);
287
- }
288
-
289
- /**
290
- * Computes changes between current entity state and original snapshot.
291
- * @param tracked - The tracked entity
292
- * @returns Object with changed column values
293
- */
294
- private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
295
- const snapshot = tracked.original ?? {};
296
- const changes: Record<string, unknown> = {};
297
- for (const column of Object.keys(tracked.table.columns)) {
298
- const current = (tracked.entity as Record<string, unknown>)[column];
299
- if (snapshot[column] !== current) {
300
- changes[column] = current;
301
- }
302
- }
303
- return changes;
304
- }
305
-
306
- /**
307
- * Extracts column values from an entity.
308
- * @param table - The table definition
309
- * @param entity - The entity instance
310
- * @returns Object with column values
311
- */
312
- private extractColumns(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
313
- const payload: Record<string, unknown> = {};
314
- for (const column of Object.keys(table.columns)) {
315
- if (entity[column] === undefined) continue;
316
- payload[column] = entity[column];
317
- }
318
- return payload;
319
- }
320
-
321
- /**
322
- * Executes a compiled query.
323
- * @param compiled - The compiled query
324
- * @returns Query results
325
- */
326
- private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
327
- const payload = await this.executor.executeSql(compiled.sql, compiled.params);
328
- return payloadResultSets(payload);
329
- }
330
-
331
- /**
332
- * Gets columns for RETURNING clause.
333
- * @param table - The table definition
334
- * @returns Array of column nodes
335
- */
336
- private getReturningColumns(table: TableDef): ColumnNode[] {
337
- return Object.values(table.columns).map(column => ({
338
- type: 'Column',
339
- table: table.name,
340
- name: column.name,
341
- alias: column.name
342
- }));
343
- }
344
-
345
- /**
346
- * Applies RETURNING clause results to the tracked entity.
347
- * @param tracked - The tracked entity
348
- * @param results - Query results
349
- */
350
- private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
351
- const first = results[0];
352
- if (!first || first.columns.length === 0 || first.values.length === 0) return;
353
-
354
- const row = first.values[0];
355
- for (let i = 0; i < first.columns.length; i++) {
356
- const columnName = this.normalizeColumnName(first.columns[i]);
357
- if (!(columnName in tracked.table.columns)) continue;
358
- (tracked.entity as Record<string, unknown>)[columnName] = row[i];
359
- }
360
- }
361
-
362
- /**
363
- * Applies the driver-provided insertId when no RETURNING clause was used.
364
- * Only sets the PK if it is currently absent on the entity.
365
- * @param tracked - The tracked entity
366
- * @param results - Query results (may contain meta.insertId)
367
- */
368
- private applyInsertedIdIfAbsent(tracked: TrackedEntity, results: QueryResult[]): void {
369
- const pkName = findPrimaryKey(tracked.table);
370
- const current = (tracked.entity as Record<string, unknown>)[pkName];
371
- if (current != null) return;
372
-
373
- const first = results[0];
374
- const insertId = first?.meta?.insertId;
375
- if (insertId == null) return;
376
-
377
- (tracked.entity as Record<string, unknown>)[pkName] = insertId;
378
- }
379
-
380
- /**
381
- * Normalizes a column name by removing quotes and table prefixes.
382
- * @param column - The column name to normalize
383
- * @returns Normalized column name
384
- */
385
- private normalizeColumnName(column: string): string {
386
- const parts = column.split('.');
387
- const candidate = parts[parts.length - 1];
388
- return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
389
- }
390
-
391
- /**
392
- * Registers an entity in the identity map.
393
- * @param tracked - The tracked entity to register
394
- */
395
- private registerIdentity(tracked: TrackedEntity): void {
396
- if (tracked.pk == null) return;
397
- this.identityMap.register(tracked);
398
- }
399
-
400
- /**
401
- * Creates a snapshot of an entity's current state.
402
- * @param table - The table definition
403
- * @param entity - The entity instance
404
- * @returns Object with entity state
405
- */
406
- private createSnapshot(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
407
- const snapshot: Record<string, unknown> = {};
408
- for (const column of Object.keys(table.columns)) {
409
- snapshot[column] = entity[column];
410
- }
411
- return snapshot;
412
- }
413
-
414
- /**
415
- * Gets the primary key value from a tracked entity.
416
- * @param tracked - The tracked entity
417
- * @returns Primary key value or null
418
- */
419
- private getPrimaryKeyValue(tracked: TrackedEntity): PrimaryKey | null {
420
- const key = findPrimaryKey(tracked.table);
421
- const val = (tracked.entity as Record<string, unknown>)[key];
422
- if (val === undefined || val === null) return null;
423
- if (typeof val !== 'string' && typeof val !== 'number') return null;
424
- return val as PrimaryKey;
425
- }
426
- }
1
+ import { ColumnNode, eq } from '../core/ast/expression.js';
2
+ import type { ValueOperandInput } from '../core/ast/expression.js';
3
+ import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
4
+ import { InsertQueryBuilder } from '../query-builder/insert.js';
5
+ import { UpdateQueryBuilder } from '../query-builder/update.js';
6
+ import { DeleteQueryBuilder } from '../query-builder/delete.js';
7
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
8
+ import type { TableDef, TableHooks } from '../schema/table.js';
9
+ import { payloadResultSets } from '../core/execution/db-executor.js';
10
+ import type { DbExecutor, QueryResult } from '../core/execution/db-executor.js';
11
+ import { IdentityMap } from './identity-map.js';
12
+ import { EntityStatus } from './runtime-types.js';
13
+ import type { TrackedEntity } from './runtime-types.js';
14
+ import type { PrimaryKey } from './entity-context.js';
15
+
16
+ /**
17
+ * Unit of Work pattern implementation for tracking entity changes.
18
+ */
19
+ export class UnitOfWork {
20
+ private readonly trackedEntities = new Map<object, TrackedEntity>();
21
+
22
+ /**
23
+ * Creates a new UnitOfWork instance.
24
+ * @param dialect - The database dialect
25
+ * @param executor - The database executor
26
+ * @param identityMap - The identity map
27
+ * @param hookContext - Function to get the hook context
28
+ */
29
+ constructor(
30
+ private readonly dialect: Dialect,
31
+ private readonly executor: DbExecutor,
32
+ private readonly identityMap: IdentityMap,
33
+ private readonly hookContext: () => unknown
34
+ ) { }
35
+
36
+ /**
37
+ * Gets the identity buckets map.
38
+ */
39
+ get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
40
+ return this.identityMap.bucketsMap;
41
+ }
42
+
43
+ /**
44
+ * Gets all tracked entities.
45
+ * @returns Array of tracked entities
46
+ */
47
+ getTracked(): TrackedEntity[] {
48
+ return Array.from(this.trackedEntities.values());
49
+ }
50
+
51
+ /**
52
+ * Gets an entity by table and primary key.
53
+ * @param table - The table definition
54
+ * @param pk - The primary key value
55
+ * @returns The entity or undefined if not found
56
+ */
57
+ getEntity(table: TableDef, pk: PrimaryKey): object | undefined {
58
+ return this.identityMap.getEntity(table, pk);
59
+ }
60
+
61
+ /**
62
+ * Gets all tracked entities for a specific table.
63
+ * @param table - The table definition
64
+ * @returns Array of tracked entities
65
+ */
66
+ getEntitiesForTable(table: TableDef): TrackedEntity[] {
67
+ return this.identityMap.getEntitiesForTable(table);
68
+ }
69
+
70
+ /**
71
+ * Finds a tracked entity.
72
+ * @param entity - The entity to find
73
+ * @returns The tracked entity or undefined if not found
74
+ */
75
+ findTracked(entity: object): TrackedEntity | undefined {
76
+ return this.trackedEntities.get(entity);
77
+ }
78
+
79
+ /**
80
+ * Sets an entity in the identity map.
81
+ * @param table - The table definition
82
+ * @param pk - The primary key value
83
+ * @param entity - The entity instance
84
+ */
85
+ setEntity(table: TableDef, pk: PrimaryKey, entity: object): void {
86
+ if (pk === null || pk === undefined) return;
87
+ let tracked = this.trackedEntities.get(entity);
88
+ if (!tracked) {
89
+ tracked = {
90
+ table,
91
+ entity,
92
+ pk,
93
+ status: EntityStatus.Managed,
94
+ original: this.createSnapshot(table, entity as Record<string, unknown>)
95
+ };
96
+ this.trackedEntities.set(entity, tracked);
97
+ } else {
98
+ tracked.pk = pk;
99
+ }
100
+
101
+ this.registerIdentity(tracked);
102
+ }
103
+
104
+ /**
105
+ * Tracks a new entity.
106
+ * @param table - The table definition
107
+ * @param entity - The entity instance
108
+ * @param pk - Optional primary key value
109
+ */
110
+ trackNew(table: TableDef, entity: object, pk?: PrimaryKey): void {
111
+ const tracked: TrackedEntity = {
112
+ table,
113
+ entity,
114
+ pk: pk ?? null,
115
+ status: EntityStatus.New,
116
+ original: null
117
+ };
118
+ this.trackedEntities.set(entity, tracked);
119
+ if (pk != null) {
120
+ this.registerIdentity(tracked);
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Tracks a managed entity.
126
+ * @param table - The table definition
127
+ * @param pk - The primary key value
128
+ * @param entity - The entity instance
129
+ */
130
+ trackManaged(table: TableDef, pk: PrimaryKey, entity: object): void {
131
+ const tracked: TrackedEntity = {
132
+ table,
133
+ entity,
134
+ pk,
135
+ status: EntityStatus.Managed,
136
+ original: this.createSnapshot(table, entity as Record<string, unknown>)
137
+ };
138
+ this.trackedEntities.set(entity, tracked);
139
+ this.registerIdentity(tracked);
140
+ }
141
+
142
+ /**
143
+ * Marks an entity as dirty (modified).
144
+ * @param entity - The entity to mark as dirty
145
+ */
146
+ markDirty(entity: object): void {
147
+ const tracked = this.trackedEntities.get(entity);
148
+ if (!tracked) return;
149
+ if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
150
+ tracked.status = EntityStatus.Dirty;
151
+ }
152
+
153
+ /**
154
+ * Marks an entity as removed.
155
+ * @param entity - The entity to mark as removed
156
+ */
157
+ markRemoved(entity: object): void {
158
+ const tracked = this.trackedEntities.get(entity);
159
+ if (!tracked) return;
160
+ tracked.status = EntityStatus.Removed;
161
+ }
162
+
163
+ /**
164
+ * Flushes pending changes to the database.
165
+ */
166
+ async flush(): Promise<void> {
167
+ const toFlush = Array.from(this.trackedEntities.values());
168
+ for (const tracked of toFlush) {
169
+ switch (tracked.status) {
170
+ case EntityStatus.New:
171
+ await this.flushInsert(tracked);
172
+ break;
173
+ case EntityStatus.Dirty:
174
+ await this.flushUpdate(tracked);
175
+ break;
176
+ case EntityStatus.Removed:
177
+ await this.flushDelete(tracked);
178
+ break;
179
+ default:
180
+ break;
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Resets the unit of work by clearing all tracked entities and identity map.
187
+ */
188
+ reset(): void {
189
+ this.trackedEntities.clear();
190
+ this.identityMap.clear();
191
+ }
192
+
193
+ /**
194
+ * Flushes an insert operation for a new entity.
195
+ * @param tracked - The tracked entity to insert
196
+ */
197
+ private async flushInsert(tracked: TrackedEntity): Promise<void> {
198
+ await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
199
+
200
+ const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
201
+ let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
202
+ if (this.dialect.supportsDmlReturningClause()) {
203
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
204
+ }
205
+ const compiled = builder.compile(this.dialect);
206
+ const results = await this.executeCompiled(compiled);
207
+ this.applyReturningResults(tracked, results);
208
+ this.applyInsertedIdIfAbsent(tracked, results);
209
+
210
+ tracked.status = EntityStatus.Managed;
211
+ tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
212
+ tracked.pk = this.getPrimaryKeyValue(tracked);
213
+ this.registerIdentity(tracked);
214
+
215
+ await this.runHook(tracked.table.hooks?.afterInsert, tracked);
216
+ }
217
+
218
+ /**
219
+ * Flushes an update operation for a modified entity.
220
+ * @param tracked - The tracked entity to update
221
+ */
222
+ private async flushUpdate(tracked: TrackedEntity): Promise<void> {
223
+ if (tracked.pk == null) return;
224
+ const changes = this.computeChanges(tracked);
225
+ if (!Object.keys(changes).length) {
226
+ tracked.status = EntityStatus.Managed;
227
+ return;
228
+ }
229
+
230
+ await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
231
+
232
+ const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
233
+ if (!pkColumn) return;
234
+
235
+ let builder = new UpdateQueryBuilder(tracked.table)
236
+ .set(changes)
237
+ .where(eq(pkColumn, tracked.pk));
238
+
239
+ if (this.dialect.supportsDmlReturningClause()) {
240
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
241
+ }
242
+
243
+ const compiled = builder.compile(this.dialect);
244
+ const results = await this.executeCompiled(compiled);
245
+ this.applyReturningResults(tracked, results);
246
+
247
+ tracked.status = EntityStatus.Managed;
248
+ tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
249
+ this.registerIdentity(tracked);
250
+
251
+ await this.runHook(tracked.table.hooks?.afterUpdate, tracked);
252
+ }
253
+
254
+ /**
255
+ * Flushes a delete operation for a removed entity.
256
+ * @param tracked - The tracked entity to delete
257
+ */
258
+ private async flushDelete(tracked: TrackedEntity): Promise<void> {
259
+ if (tracked.pk == null) return;
260
+ await this.runHook(tracked.table.hooks?.beforeDelete, tracked);
261
+
262
+ const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
263
+ if (!pkColumn) return;
264
+
265
+ const builder = new DeleteQueryBuilder(tracked.table).where(eq(pkColumn, tracked.pk));
266
+ const compiled = builder.compile(this.dialect);
267
+ await this.executeCompiled(compiled);
268
+
269
+ tracked.status = EntityStatus.Detached;
270
+ this.trackedEntities.delete(tracked.entity);
271
+ this.identityMap.remove(tracked);
272
+
273
+ await this.runHook(tracked.table.hooks?.afterDelete, tracked);
274
+ }
275
+
276
+ /**
277
+ * Runs a table hook if defined.
278
+ * @param hook - The hook function
279
+ * @param tracked - The tracked entity
280
+ */
281
+ private async runHook(
282
+ hook: TableHooks[keyof TableHooks] | undefined,
283
+ tracked: TrackedEntity
284
+ ): Promise<void> {
285
+ if (!hook) return;
286
+ await hook(this.hookContext(), tracked.entity);
287
+ }
288
+
289
+ /**
290
+ * Computes changes between current entity state and original snapshot.
291
+ * @param tracked - The tracked entity
292
+ * @returns Object with changed column values
293
+ */
294
+ private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
295
+ const snapshot = tracked.original ?? {};
296
+ const changes: Record<string, unknown> = {};
297
+ for (const column of Object.keys(tracked.table.columns)) {
298
+ const current = (tracked.entity as Record<string, unknown>)[column];
299
+ if (snapshot[column] !== current) {
300
+ changes[column] = current;
301
+ }
302
+ }
303
+ return changes;
304
+ }
305
+
306
+ /**
307
+ * Extracts column values from an entity.
308
+ * @param table - The table definition
309
+ * @param entity - The entity instance
310
+ * @returns Object with column values
311
+ */
312
+ private extractColumns(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
313
+ const payload: Record<string, unknown> = {};
314
+ for (const column of Object.keys(table.columns)) {
315
+ if (entity[column] === undefined) continue;
316
+ payload[column] = entity[column];
317
+ }
318
+ return payload;
319
+ }
320
+
321
+ /**
322
+ * Executes a compiled query.
323
+ * @param compiled - The compiled query
324
+ * @returns Query results
325
+ */
326
+ private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
327
+ const payload = await this.executor.executeSql(compiled.sql, compiled.params);
328
+ return payloadResultSets(payload);
329
+ }
330
+
331
+ /**
332
+ * Gets columns for RETURNING clause.
333
+ * @param table - The table definition
334
+ * @returns Array of column nodes
335
+ */
336
+ private getReturningColumns(table: TableDef): ColumnNode[] {
337
+ return Object.values(table.columns).map(column => ({
338
+ type: 'Column',
339
+ table: table.name,
340
+ name: column.name,
341
+ alias: column.name
342
+ }));
343
+ }
344
+
345
+ /**
346
+ * Applies RETURNING clause results to the tracked entity.
347
+ * @param tracked - The tracked entity
348
+ * @param results - Query results
349
+ */
350
+ private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
351
+ const first = results[0];
352
+ if (!first || first.columns.length === 0 || first.values.length === 0) return;
353
+
354
+ const row = first.values[0];
355
+ for (let i = 0; i < first.columns.length; i++) {
356
+ const columnName = this.normalizeColumnName(first.columns[i]);
357
+ if (!(columnName in tracked.table.columns)) continue;
358
+ (tracked.entity as Record<string, unknown>)[columnName] = row[i];
359
+ }
360
+ }
361
+
362
+ /**
363
+ * Applies the driver-provided insertId when no RETURNING clause was used.
364
+ * Only sets the PK if it is currently absent on the entity.
365
+ * @param tracked - The tracked entity
366
+ * @param results - Query results (may contain meta.insertId)
367
+ */
368
+ private applyInsertedIdIfAbsent(tracked: TrackedEntity, results: QueryResult[]): void {
369
+ const pkName = findPrimaryKey(tracked.table);
370
+ const current = (tracked.entity as Record<string, unknown>)[pkName];
371
+ if (current != null) return;
372
+
373
+ const first = results[0];
374
+ const insertId = first?.meta?.insertId;
375
+ if (insertId == null) return;
376
+
377
+ (tracked.entity as Record<string, unknown>)[pkName] = insertId;
378
+ }
379
+
380
+ /**
381
+ * Normalizes a column name by removing quotes and table prefixes.
382
+ * @param column - The column name to normalize
383
+ * @returns Normalized column name
384
+ */
385
+ private normalizeColumnName(column: string): string {
386
+ const parts = column.split('.');
387
+ const candidate = parts[parts.length - 1];
388
+ return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
389
+ }
390
+
391
+ /**
392
+ * Registers an entity in the identity map.
393
+ * @param tracked - The tracked entity to register
394
+ */
395
+ private registerIdentity(tracked: TrackedEntity): void {
396
+ if (tracked.pk == null) return;
397
+ this.identityMap.register(tracked);
398
+ }
399
+
400
+ /**
401
+ * Creates a snapshot of an entity's current state.
402
+ * @param table - The table definition
403
+ * @param entity - The entity instance
404
+ * @returns Object with entity state
405
+ */
406
+ private createSnapshot(table: TableDef, entity: Record<string, unknown>): Record<string, unknown> {
407
+ const snapshot: Record<string, unknown> = {};
408
+ for (const column of Object.keys(table.columns)) {
409
+ snapshot[column] = entity[column];
410
+ }
411
+ return snapshot;
412
+ }
413
+
414
+ /**
415
+ * Gets the primary key value from a tracked entity.
416
+ * @param tracked - The tracked entity
417
+ * @returns Primary key value or null
418
+ */
419
+ private getPrimaryKeyValue(tracked: TrackedEntity): PrimaryKey | null {
420
+ const key = findPrimaryKey(tracked.table);
421
+ const val = (tracked.entity as Record<string, unknown>)[key];
422
+ if (val === undefined || val === null) return null;
423
+ if (typeof val !== 'string' && typeof val !== 'number') return null;
424
+ return val as PrimaryKey;
425
+ }
426
+ }