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,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
+ }