metal-orm 1.0.39 → 1.0.41
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.
- package/dist/index.cjs +1466 -189
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +723 -51
- package/dist/index.d.ts +723 -51
- package/dist/index.js +1457 -189
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen/typescript.ts +66 -5
- package/src/core/ast/aggregate-functions.ts +15 -15
- package/src/core/ast/expression-builders.ts +378 -316
- package/src/core/ast/expression-nodes.ts +210 -186
- package/src/core/ast/expression-visitor.ts +40 -30
- package/src/core/ast/query.ts +164 -132
- package/src/core/ast/window-functions.ts +86 -86
- package/src/core/dialect/abstract.ts +509 -479
- package/src/core/dialect/base/groupby-compiler.ts +6 -6
- package/src/core/dialect/base/join-compiler.ts +9 -12
- package/src/core/dialect/base/orderby-compiler.ts +20 -6
- package/src/core/dialect/base/sql-dialect.ts +237 -138
- package/src/core/dialect/mssql/index.ts +164 -185
- package/src/core/dialect/sqlite/index.ts +39 -34
- package/src/core/execution/db-executor.ts +46 -6
- package/src/core/execution/executors/mssql-executor.ts +39 -22
- package/src/core/execution/executors/mysql-executor.ts +23 -6
- package/src/core/execution/executors/sqlite-executor.ts +29 -3
- package/src/core/execution/pooling/pool-types.ts +30 -0
- package/src/core/execution/pooling/pool.ts +268 -0
- package/src/core/functions/standard-strategy.ts +46 -37
- package/src/decorators/bootstrap.ts +7 -7
- package/src/index.ts +6 -0
- package/src/orm/domain-event-bus.ts +49 -0
- package/src/orm/entity-metadata.ts +9 -9
- package/src/orm/entity.ts +58 -0
- package/src/orm/orm-session.ts +465 -270
- package/src/orm/orm.ts +61 -11
- package/src/orm/pooled-executor-factory.ts +131 -0
- package/src/orm/query-logger.ts +6 -12
- package/src/orm/relation-change-processor.ts +75 -0
- package/src/orm/relations/many-to-many.ts +4 -2
- package/src/orm/save-graph.ts +303 -0
- package/src/orm/transaction-runner.ts +3 -3
- package/src/orm/unit-of-work.ts +128 -0
- package/src/query-builder/delete-query-state.ts +67 -38
- package/src/query-builder/delete.ts +37 -1
- package/src/query-builder/hydration-manager.ts +93 -79
- package/src/query-builder/insert-query-state.ts +131 -61
- package/src/query-builder/insert.ts +27 -1
- package/src/query-builder/query-ast-service.ts +207 -170
- package/src/query-builder/select-query-state.ts +169 -162
- package/src/query-builder/select.ts +15 -23
- package/src/query-builder/update-query-state.ts +114 -77
- package/src/query-builder/update.ts +38 -1
|
@@ -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.
|
|
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
|
};
|
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -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];
|