metal-orm 1.0.40 → 1.0.42

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 (45) hide show
  1. package/README.md +53 -14
  2. package/dist/index.cjs +1298 -126
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +676 -30
  5. package/dist/index.d.ts +676 -30
  6. package/dist/index.js +1293 -126
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/codegen/typescript.ts +6 -2
  10. package/src/core/ast/expression-builders.ts +25 -4
  11. package/src/core/ast/expression-nodes.ts +3 -1
  12. package/src/core/ast/expression.ts +2 -2
  13. package/src/core/ast/query.ts +24 -2
  14. package/src/core/dialect/abstract.ts +6 -2
  15. package/src/core/dialect/base/join-compiler.ts +9 -12
  16. package/src/core/dialect/base/sql-dialect.ts +98 -17
  17. package/src/core/dialect/mssql/index.ts +30 -62
  18. package/src/core/dialect/sqlite/index.ts +39 -34
  19. package/src/core/execution/db-executor.ts +46 -6
  20. package/src/core/execution/executors/mssql-executor.ts +39 -22
  21. package/src/core/execution/executors/mysql-executor.ts +23 -6
  22. package/src/core/execution/executors/sqlite-executor.ts +29 -3
  23. package/src/core/execution/pooling/pool-types.ts +30 -0
  24. package/src/core/execution/pooling/pool.ts +268 -0
  25. package/src/decorators/bootstrap.ts +7 -7
  26. package/src/index.ts +6 -0
  27. package/src/orm/domain-event-bus.ts +49 -0
  28. package/src/orm/entity-metadata.ts +9 -9
  29. package/src/orm/entity.ts +58 -0
  30. package/src/orm/orm-session.ts +465 -270
  31. package/src/orm/orm.ts +61 -11
  32. package/src/orm/pooled-executor-factory.ts +131 -0
  33. package/src/orm/query-logger.ts +6 -12
  34. package/src/orm/relation-change-processor.ts +75 -0
  35. package/src/orm/relations/many-to-many.ts +4 -2
  36. package/src/orm/save-graph.ts +303 -0
  37. package/src/orm/transaction-runner.ts +3 -3
  38. package/src/orm/unit-of-work.ts +128 -0
  39. package/src/query-builder/delete-query-state.ts +67 -38
  40. package/src/query-builder/delete.ts +37 -1
  41. package/src/query-builder/insert-query-state.ts +131 -61
  42. package/src/query-builder/insert.ts +27 -1
  43. package/src/query-builder/update-query-state.ts +114 -77
  44. package/src/query-builder/update.ts +38 -1
  45. package/src/schema/table.ts +210 -115
@@ -0,0 +1,303 @@
1
+ import type { EntityInstance } from '../schema/types.js';
2
+ import {
3
+ RelationKinds,
4
+ type BelongsToManyRelation,
5
+ type BelongsToRelation,
6
+ type HasManyRelation,
7
+ type HasOneRelation,
8
+ type RelationDef
9
+ } from '../schema/relation.js';
10
+ import type { TableDef } from '../schema/table.js';
11
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
12
+ import { createEntityFromRow } from './entity.js';
13
+ import type { EntityConstructor } from './entity-metadata.js';
14
+ import { getTableDefFromEntity } from '../decorators/bootstrap.js';
15
+ import type { OrmSession } from './orm-session.js';
16
+
17
+ export interface SaveGraphOptions {
18
+ /** Remove existing collection members that are not present in the payload */
19
+ pruneMissing?: boolean;
20
+ }
21
+
22
+ type AnyEntity = Record<string, any>;
23
+
24
+ const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
25
+
26
+ const pickColumns = (table: TableDef, payload: AnyEntity): Record<string, any> => {
27
+ const columns: Record<string, any> = {};
28
+ for (const key of Object.keys(table.columns)) {
29
+ if (payload[key] !== undefined) {
30
+ columns[key] = payload[key];
31
+ }
32
+ }
33
+ return columns;
34
+ };
35
+
36
+ const ensureEntity = <TTable extends TableDef>(
37
+ session: OrmSession,
38
+ table: TTable,
39
+ payload: AnyEntity
40
+ ): EntityInstance<TTable> => {
41
+ const pk = findPrimaryKey(table);
42
+ const row = pickColumns(table, payload);
43
+ const pkValue = payload[pk];
44
+
45
+ if (pkValue !== undefined && pkValue !== null) {
46
+ const tracked = session.getEntity(table, pkValue);
47
+ if (tracked) {
48
+ return tracked as EntityInstance<TTable>;
49
+ }
50
+ // Seed the stub with PK to track a managed entity when updating.
51
+ if (row[pk] === undefined) {
52
+ row[pk] = pkValue;
53
+ }
54
+ }
55
+
56
+ return createEntityFromRow(session, table, row) as EntityInstance<TTable>;
57
+ };
58
+
59
+ const assignColumns = (table: TableDef, entity: AnyEntity, payload: AnyEntity): void => {
60
+ for (const key of Object.keys(table.columns)) {
61
+ if (payload[key] !== undefined) {
62
+ entity[key] = payload[key];
63
+ }
64
+ }
65
+ };
66
+
67
+ const isEntityInCollection = (items: AnyEntity[], pkName: string, entity: AnyEntity): boolean => {
68
+ if (items.includes(entity)) return true;
69
+ const entityPk = entity[pkName];
70
+ if (entityPk === undefined || entityPk === null) return false;
71
+ return items.some(item => toKey(item[pkName]) === toKey(entityPk));
72
+ };
73
+
74
+ const findInCollectionByPk = (items: AnyEntity[], pkName: string, pkValue: any): AnyEntity | undefined => {
75
+ if (pkValue === undefined || pkValue === null) return undefined;
76
+ return items.find(item => toKey(item[pkName]) === toKey(pkValue));
77
+ };
78
+
79
+ const handleHasMany = async (
80
+ session: OrmSession,
81
+ root: AnyEntity,
82
+ relationName: string,
83
+ relation: HasManyRelation,
84
+ payload: unknown,
85
+ options: SaveGraphOptions
86
+ ): Promise<void> => {
87
+ if (!Array.isArray(payload)) return;
88
+ const collection = root[relationName];
89
+ await collection.load();
90
+
91
+ const targetTable = relation.target;
92
+ const targetPk = findPrimaryKey(targetTable);
93
+ const existing = collection.getItems();
94
+ const seen = new Set<string>();
95
+
96
+ for (const item of payload) {
97
+ if (item === null || item === undefined) continue;
98
+ const asObj = typeof item === 'object' ? (item as AnyEntity) : { [targetPk]: item };
99
+ const pkValue = asObj[targetPk];
100
+
101
+ const current =
102
+ findInCollectionByPk(existing, targetPk, pkValue) ??
103
+ (pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue) : undefined);
104
+
105
+ const entity = current ?? ensureEntity(session, targetTable, asObj);
106
+ assignColumns(targetTable, entity, asObj);
107
+ await applyGraphToEntity(session, targetTable, entity, asObj, options);
108
+
109
+ if (!isEntityInCollection(collection.getItems(), targetPk, entity)) {
110
+ collection.attach(entity);
111
+ }
112
+
113
+ if (pkValue !== undefined && pkValue !== null) {
114
+ seen.add(toKey(pkValue));
115
+ }
116
+ }
117
+
118
+ if (options.pruneMissing) {
119
+ for (const item of [...collection.getItems()]) {
120
+ const pkValue = item[targetPk];
121
+ if (pkValue !== undefined && pkValue !== null && !seen.has(toKey(pkValue))) {
122
+ collection.remove(item);
123
+ }
124
+ }
125
+ }
126
+ };
127
+
128
+ const handleHasOne = async (
129
+ session: OrmSession,
130
+ root: AnyEntity,
131
+ relationName: string,
132
+ relation: HasOneRelation,
133
+ payload: unknown,
134
+ options: SaveGraphOptions
135
+ ): Promise<void> => {
136
+ const ref = root[relationName];
137
+ if (payload === undefined) return;
138
+ if (payload === null) {
139
+ ref.set(null);
140
+ return;
141
+ }
142
+ const pk = findPrimaryKey(relation.target);
143
+ if (typeof payload === 'number' || typeof payload === 'string') {
144
+ const entity = ref.set({ [pk]: payload });
145
+ if (entity) {
146
+ await applyGraphToEntity(session, relation.target, entity, { [pk]: payload }, options);
147
+ }
148
+ return;
149
+ }
150
+ const attached = ref.set(payload as AnyEntity);
151
+ if (attached) {
152
+ await applyGraphToEntity(session, relation.target, attached, payload as AnyEntity, options);
153
+ }
154
+ };
155
+
156
+ const handleBelongsTo = async (
157
+ session: OrmSession,
158
+ root: AnyEntity,
159
+ relationName: string,
160
+ relation: BelongsToRelation,
161
+ payload: unknown,
162
+ options: SaveGraphOptions
163
+ ): Promise<void> => {
164
+ const ref = root[relationName];
165
+ if (payload === undefined) return;
166
+ if (payload === null) {
167
+ ref.set(null);
168
+ return;
169
+ }
170
+ const pk = relation.localKey || findPrimaryKey(relation.target);
171
+ if (typeof payload === 'number' || typeof payload === 'string') {
172
+ const entity = ref.set({ [pk]: payload });
173
+ if (entity) {
174
+ await applyGraphToEntity(session, relation.target, entity, { [pk]: payload }, options);
175
+ }
176
+ return;
177
+ }
178
+ const attached = ref.set(payload as AnyEntity);
179
+ if (attached) {
180
+ await applyGraphToEntity(session, relation.target, attached, payload as AnyEntity, options);
181
+ }
182
+ };
183
+
184
+ const handleBelongsToMany = async (
185
+ session: OrmSession,
186
+ root: AnyEntity,
187
+ relationName: string,
188
+ relation: BelongsToManyRelation,
189
+ payload: unknown,
190
+ options: SaveGraphOptions
191
+ ): Promise<void> => {
192
+ if (!Array.isArray(payload)) return;
193
+ const collection = root[relationName];
194
+ await collection.load();
195
+
196
+ const targetTable = relation.target;
197
+ const targetPk = relation.targetKey || findPrimaryKey(targetTable);
198
+ const seen = new Set<string>();
199
+
200
+ for (const item of payload) {
201
+ if (item === null || item === undefined) continue;
202
+ if (typeof item === 'number' || typeof item === 'string') {
203
+ const id = item;
204
+ collection.attach(id);
205
+ seen.add(toKey(id));
206
+ continue;
207
+ }
208
+
209
+ const asObj = item as AnyEntity;
210
+ const pkValue = asObj[targetPk];
211
+ const entity = pkValue !== undefined && pkValue !== null
212
+ ? session.getEntity(targetTable, pkValue) ?? ensureEntity(session, targetTable, asObj)
213
+ : ensureEntity(session, targetTable, asObj);
214
+
215
+ assignColumns(targetTable, entity, asObj);
216
+ await applyGraphToEntity(session, targetTable, entity, asObj, options);
217
+
218
+ if (!isEntityInCollection(collection.getItems(), targetPk, entity)) {
219
+ collection.attach(entity);
220
+ }
221
+
222
+ if (pkValue !== undefined && pkValue !== null) {
223
+ seen.add(toKey(pkValue));
224
+ }
225
+ }
226
+
227
+ if (options.pruneMissing) {
228
+ for (const item of [...collection.getItems()]) {
229
+ const pkValue = item[targetPk];
230
+ if (pkValue !== undefined && pkValue !== null && !seen.has(toKey(pkValue))) {
231
+ collection.detach(item);
232
+ }
233
+ }
234
+ }
235
+ };
236
+
237
+ const applyRelation = async (
238
+ session: OrmSession,
239
+ table: TableDef,
240
+ entity: AnyEntity,
241
+ relationName: string,
242
+ relation: RelationDef,
243
+ payload: unknown,
244
+ options: SaveGraphOptions
245
+ ): Promise<void> => {
246
+ switch (relation.type) {
247
+ case RelationKinds.HasMany:
248
+ return handleHasMany(session, entity, relationName, relation, payload, options);
249
+ case RelationKinds.HasOne:
250
+ return handleHasOne(session, entity, relationName, relation, payload, options);
251
+ case RelationKinds.BelongsTo:
252
+ return handleBelongsTo(session, entity, relationName, relation, payload, options);
253
+ case RelationKinds.BelongsToMany:
254
+ return handleBelongsToMany(session, entity, relationName, relation, payload, options);
255
+ }
256
+ };
257
+
258
+ const applyGraphToEntity = async (
259
+ session: OrmSession,
260
+ table: TableDef,
261
+ entity: AnyEntity,
262
+ payload: AnyEntity,
263
+ options: SaveGraphOptions
264
+ ): Promise<void> => {
265
+ assignColumns(table, entity, payload);
266
+
267
+ for (const [relationName, relation] of Object.entries(table.relations)) {
268
+ if (!(relationName in payload)) continue;
269
+ await applyRelation(session, table, entity, relationName, relation as RelationDef, payload[relationName], options);
270
+ }
271
+ };
272
+
273
+ export const saveGraph = async <TTable extends TableDef>(
274
+ session: OrmSession,
275
+ entityClass: EntityConstructor<any>,
276
+ payload: AnyEntity,
277
+ options: SaveGraphOptions = {}
278
+ ): Promise<EntityInstance<TTable>> => {
279
+ const table = getTableDefFromEntity(entityClass);
280
+ if (!table) {
281
+ throw new Error('Entity metadata has not been bootstrapped');
282
+ }
283
+
284
+ const root = ensureEntity<TTable>(session, table as TTable, payload);
285
+ await applyGraphToEntity(session, table, root, payload, options);
286
+ return root;
287
+ };
288
+
289
+ export const saveGraphInternal = async <TCtor extends EntityConstructor<any>>(
290
+ session: OrmSession,
291
+ entityClass: TCtor,
292
+ payload: AnyEntity,
293
+ options: SaveGraphOptions = {}
294
+ ): Promise<InstanceType<TCtor>> => {
295
+ const table = getTableDefFromEntity(entityClass);
296
+ if (!table) {
297
+ throw new Error('Entity metadata has not been bootstrapped');
298
+ }
299
+
300
+ const root = ensureEntity(session, table, payload);
301
+ await applyGraphToEntity(session, table, root, payload, options);
302
+ return root as unknown as InstanceType<TCtor>;
303
+ };
@@ -8,7 +8,7 @@ import type { DbExecutor } from '../core/execution/db-executor.js';
8
8
  * @throws Re-throws any errors that occur during the transaction (after rolling back)
9
9
  */
10
10
  export const runInTransaction = async (executor: DbExecutor, action: () => Promise<void>): Promise<void> => {
11
- if (!executor.beginTransaction) {
11
+ if (!executor.capabilities.transactions) {
12
12
  await action();
13
13
  return;
14
14
  }
@@ -16,9 +16,9 @@ export const runInTransaction = async (executor: DbExecutor, action: () => Promi
16
16
  await executor.beginTransaction();
17
17
  try {
18
18
  await action();
19
- await executor.commitTransaction?.();
19
+ await executor.commitTransaction();
20
20
  } catch (error) {
21
- await executor.rollbackTransaction?.();
21
+ await executor.rollbackTransaction();
22
22
  throw error;
23
23
  }
24
24
  };
@@ -10,9 +10,19 @@ import { IdentityMap } from './identity-map.js';
10
10
  import { EntityStatus } from './runtime-types.js';
11
11
  import type { TrackedEntity } from './runtime-types.js';
12
12
 
13
+ /**
14
+ * Unit of Work pattern implementation for tracking entity changes.
15
+ */
13
16
  export class UnitOfWork {
14
17
  private readonly trackedEntities = new Map<any, TrackedEntity>();
15
18
 
19
+ /**
20
+ * Creates a new UnitOfWork instance.
21
+ * @param dialect - The database dialect
22
+ * @param executor - The database executor
23
+ * @param identityMap - The identity map
24
+ * @param hookContext - Function to get the hook context
25
+ */
16
26
  constructor(
17
27
  private readonly dialect: Dialect,
18
28
  private readonly executor: DbExecutor,
@@ -20,26 +30,55 @@ export class UnitOfWork {
20
30
  private readonly hookContext: () => unknown
21
31
  ) { }
22
32
 
33
+ /**
34
+ * Gets the identity buckets map.
35
+ */
23
36
  get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
24
37
  return this.identityMap.bucketsMap;
25
38
  }
26
39
 
40
+ /**
41
+ * Gets all tracked entities.
42
+ * @returns Array of tracked entities
43
+ */
27
44
  getTracked(): TrackedEntity[] {
28
45
  return Array.from(this.trackedEntities.values());
29
46
  }
30
47
 
48
+ /**
49
+ * Gets an entity by table and primary key.
50
+ * @param table - The table definition
51
+ * @param pk - The primary key value
52
+ * @returns The entity or undefined if not found
53
+ */
31
54
  getEntity(table: TableDef, pk: string | number): any | undefined {
32
55
  return this.identityMap.getEntity(table, pk);
33
56
  }
34
57
 
58
+ /**
59
+ * Gets all tracked entities for a specific table.
60
+ * @param table - The table definition
61
+ * @returns Array of tracked entities
62
+ */
35
63
  getEntitiesForTable(table: TableDef): TrackedEntity[] {
36
64
  return this.identityMap.getEntitiesForTable(table);
37
65
  }
38
66
 
67
+ /**
68
+ * Finds a tracked entity.
69
+ * @param entity - The entity to find
70
+ * @returns The tracked entity or undefined if not found
71
+ */
39
72
  findTracked(entity: any): TrackedEntity | undefined {
40
73
  return this.trackedEntities.get(entity);
41
74
  }
42
75
 
76
+ /**
77
+ * Sets an entity in the identity map.
78
+ * @param table - The table definition
79
+ * @param pk - The primary key value
80
+ * @param entity - The entity instance
81
+ */
43
82
  setEntity(table: TableDef, pk: string | number, entity: any): void {
44
83
  if (pk === null || pk === undefined) return;
45
84
  let tracked = this.trackedEntities.get(entity);
@@ -59,6 +98,12 @@ export class UnitOfWork {
59
98
  this.registerIdentity(tracked);
60
99
  }
61
100
 
101
+ /**
102
+ * Tracks a new entity.
103
+ * @param table - The table definition
104
+ * @param entity - The entity instance
105
+ * @param pk - Optional primary key value
106
+ */
62
107
  trackNew(table: TableDef, entity: any, pk?: string | number): void {
63
108
  const tracked: TrackedEntity = {
64
109
  table,
@@ -73,6 +118,12 @@ export class UnitOfWork {
73
118
  }
74
119
  }
75
120
 
121
+ /**
122
+ * Tracks a managed entity.
123
+ * @param table - The table definition
124
+ * @param pk - The primary key value
125
+ * @param entity - The entity instance
126
+ */
76
127
  trackManaged(table: TableDef, pk: string | number, entity: any): void {
77
128
  const tracked: TrackedEntity = {
78
129
  table,
@@ -85,6 +136,10 @@ export class UnitOfWork {
85
136
  this.registerIdentity(tracked);
86
137
  }
87
138
 
139
+ /**
140
+ * Marks an entity as dirty (modified).
141
+ * @param entity - The entity to mark as dirty
142
+ */
88
143
  markDirty(entity: any): void {
89
144
  const tracked = this.trackedEntities.get(entity);
90
145
  if (!tracked) return;
@@ -92,12 +147,19 @@ export class UnitOfWork {
92
147
  tracked.status = EntityStatus.Dirty;
93
148
  }
94
149
 
150
+ /**
151
+ * Marks an entity as removed.
152
+ * @param entity - The entity to mark as removed
153
+ */
95
154
  markRemoved(entity: any): void {
96
155
  const tracked = this.trackedEntities.get(entity);
97
156
  if (!tracked) return;
98
157
  tracked.status = EntityStatus.Removed;
99
158
  }
100
159
 
160
+ /**
161
+ * Flushes pending changes to the database.
162
+ */
101
163
  async flush(): Promise<void> {
102
164
  const toFlush = Array.from(this.trackedEntities.values());
103
165
  for (const tracked of toFlush) {
@@ -117,11 +179,18 @@ export class UnitOfWork {
117
179
  }
118
180
  }
119
181
 
182
+ /**
183
+ * Resets the unit of work by clearing all tracked entities and identity map.
184
+ */
120
185
  reset(): void {
121
186
  this.trackedEntities.clear();
122
187
  this.identityMap.clear();
123
188
  }
124
189
 
190
+ /**
191
+ * Flushes an insert operation for a new entity.
192
+ * @param tracked - The tracked entity to insert
193
+ */
125
194
  private async flushInsert(tracked: TrackedEntity): Promise<void> {
126
195
  await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
127
196
 
@@ -142,6 +211,10 @@ export class UnitOfWork {
142
211
  await this.runHook(tracked.table.hooks?.afterInsert, tracked);
143
212
  }
144
213
 
214
+ /**
215
+ * Flushes an update operation for a modified entity.
216
+ * @param tracked - The tracked entity to update
217
+ */
145
218
  private async flushUpdate(tracked: TrackedEntity): Promise<void> {
146
219
  if (tracked.pk == null) return;
147
220
  const changes = this.computeChanges(tracked);
@@ -174,6 +247,10 @@ export class UnitOfWork {
174
247
  await this.runHook(tracked.table.hooks?.afterUpdate, tracked);
175
248
  }
176
249
 
250
+ /**
251
+ * Flushes a delete operation for a removed entity.
252
+ * @param tracked - The tracked entity to delete
253
+ */
177
254
  private async flushDelete(tracked: TrackedEntity): Promise<void> {
178
255
  if (tracked.pk == null) return;
179
256
  await this.runHook(tracked.table.hooks?.beforeDelete, tracked);
@@ -192,6 +269,11 @@ export class UnitOfWork {
192
269
  await this.runHook(tracked.table.hooks?.afterDelete, tracked);
193
270
  }
194
271
 
272
+ /**
273
+ * Runs a table hook if defined.
274
+ * @param hook - The hook function
275
+ * @param tracked - The tracked entity
276
+ */
195
277
  private async runHook(
196
278
  hook: TableHooks[keyof TableHooks] | undefined,
197
279
  tracked: TrackedEntity
@@ -200,6 +282,11 @@ export class UnitOfWork {
200
282
  await hook(this.hookContext() as any, tracked.entity);
201
283
  }
202
284
 
285
+ /**
286
+ * Computes changes between current entity state and original snapshot.
287
+ * @param tracked - The tracked entity
288
+ * @returns Object with changed column values
289
+ */
203
290
  private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
204
291
  const snapshot = tracked.original ?? {};
205
292
  const changes: Record<string, unknown> = {};
@@ -212,6 +299,12 @@ export class UnitOfWork {
212
299
  return changes;
213
300
  }
214
301
 
302
+ /**
303
+ * Extracts column values from an entity.
304
+ * @param table - The table definition
305
+ * @param entity - The entity instance
306
+ * @returns Object with column values
307
+ */
215
308
  private extractColumns(table: TableDef, entity: any): Record<string, unknown> {
216
309
  const payload: Record<string, unknown> = {};
217
310
  for (const column of Object.keys(table.columns)) {
@@ -221,10 +314,20 @@ export class UnitOfWork {
221
314
  return payload;
222
315
  }
223
316
 
317
+ /**
318
+ * Executes a compiled query.
319
+ * @param compiled - The compiled query
320
+ * @returns Query results
321
+ */
224
322
  private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
225
323
  return this.executor.executeSql(compiled.sql, compiled.params);
226
324
  }
227
325
 
326
+ /**
327
+ * Gets columns for RETURNING clause.
328
+ * @param table - The table definition
329
+ * @returns Array of column nodes
330
+ */
228
331
  private getReturningColumns(table: TableDef): ColumnNode[] {
229
332
  return Object.values(table.columns).map(column => ({
230
333
  type: 'Column',
@@ -234,6 +337,11 @@ export class UnitOfWork {
234
337
  }));
235
338
  }
236
339
 
340
+ /**
341
+ * Applies RETURNING clause results to the tracked entity.
342
+ * @param tracked - The tracked entity
343
+ * @param results - Query results
344
+ */
237
345
  private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
238
346
  if (!this.dialect.supportsReturning()) return;
239
347
  const first = results[0];
@@ -247,17 +355,32 @@ export class UnitOfWork {
247
355
  }
248
356
  }
249
357
 
358
+ /**
359
+ * Normalizes a column name by removing quotes and table prefixes.
360
+ * @param column - The column name to normalize
361
+ * @returns Normalized column name
362
+ */
250
363
  private normalizeColumnName(column: string): string {
251
364
  const parts = column.split('.');
252
365
  const candidate = parts[parts.length - 1];
253
366
  return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
254
367
  }
255
368
 
369
+ /**
370
+ * Registers an entity in the identity map.
371
+ * @param tracked - The tracked entity to register
372
+ */
256
373
  private registerIdentity(tracked: TrackedEntity): void {
257
374
  if (tracked.pk == null) return;
258
375
  this.identityMap.register(tracked);
259
376
  }
260
377
 
378
+ /**
379
+ * Creates a snapshot of an entity's current state.
380
+ * @param table - The table definition
381
+ * @param entity - The entity instance
382
+ * @returns Object with entity state
383
+ */
261
384
  private createSnapshot(table: TableDef, entity: any): Record<string, any> {
262
385
  const snapshot: Record<string, any> = {};
263
386
  for (const column of Object.keys(table.columns)) {
@@ -266,6 +389,11 @@ export class UnitOfWork {
266
389
  return snapshot;
267
390
  }
268
391
 
392
+ /**
393
+ * Gets the primary key value from a tracked entity.
394
+ * @param tracked - The tracked entity
395
+ * @returns Primary key value or null
396
+ */
269
397
  private getPrimaryKeyValue(tracked: TrackedEntity): string | number | null {
270
398
  const key = findPrimaryKey(tracked.table);
271
399
  const val = tracked.entity[key];