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.
Files changed (52) hide show
  1. package/dist/index.cjs +1466 -189
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +723 -51
  4. package/dist/index.d.ts +723 -51
  5. package/dist/index.js +1457 -189
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/codegen/typescript.ts +66 -5
  9. package/src/core/ast/aggregate-functions.ts +15 -15
  10. package/src/core/ast/expression-builders.ts +378 -316
  11. package/src/core/ast/expression-nodes.ts +210 -186
  12. package/src/core/ast/expression-visitor.ts +40 -30
  13. package/src/core/ast/query.ts +164 -132
  14. package/src/core/ast/window-functions.ts +86 -86
  15. package/src/core/dialect/abstract.ts +509 -479
  16. package/src/core/dialect/base/groupby-compiler.ts +6 -6
  17. package/src/core/dialect/base/join-compiler.ts +9 -12
  18. package/src/core/dialect/base/orderby-compiler.ts +20 -6
  19. package/src/core/dialect/base/sql-dialect.ts +237 -138
  20. package/src/core/dialect/mssql/index.ts +164 -185
  21. package/src/core/dialect/sqlite/index.ts +39 -34
  22. package/src/core/execution/db-executor.ts +46 -6
  23. package/src/core/execution/executors/mssql-executor.ts +39 -22
  24. package/src/core/execution/executors/mysql-executor.ts +23 -6
  25. package/src/core/execution/executors/sqlite-executor.ts +29 -3
  26. package/src/core/execution/pooling/pool-types.ts +30 -0
  27. package/src/core/execution/pooling/pool.ts +268 -0
  28. package/src/core/functions/standard-strategy.ts +46 -37
  29. package/src/decorators/bootstrap.ts +7 -7
  30. package/src/index.ts +6 -0
  31. package/src/orm/domain-event-bus.ts +49 -0
  32. package/src/orm/entity-metadata.ts +9 -9
  33. package/src/orm/entity.ts +58 -0
  34. package/src/orm/orm-session.ts +465 -270
  35. package/src/orm/orm.ts +61 -11
  36. package/src/orm/pooled-executor-factory.ts +131 -0
  37. package/src/orm/query-logger.ts +6 -12
  38. package/src/orm/relation-change-processor.ts +75 -0
  39. package/src/orm/relations/many-to-many.ts +4 -2
  40. package/src/orm/save-graph.ts +303 -0
  41. package/src/orm/transaction-runner.ts +3 -3
  42. package/src/orm/unit-of-work.ts +128 -0
  43. package/src/query-builder/delete-query-state.ts +67 -38
  44. package/src/query-builder/delete.ts +37 -1
  45. package/src/query-builder/hydration-manager.ts +93 -79
  46. package/src/query-builder/insert-query-state.ts +131 -61
  47. package/src/query-builder/insert.ts +27 -1
  48. package/src/query-builder/query-ast-service.ts +207 -170
  49. package/src/query-builder/select-query-state.ts +169 -162
  50. package/src/query-builder/select.ts +15 -23
  51. package/src/query-builder/update-query-state.ts +114 -77
  52. package/src/query-builder/update.ts +38 -1
@@ -1,270 +1,465 @@
1
- import { Dialect } from '../core/dialect/abstract.js';
2
- import { eq } from '../core/ast/expression.js';
3
- import type { DbExecutor } from '../core/execution/db-executor.js';
4
- import { SelectQueryBuilder } from '../query-builder/select.js';
5
- import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
- import type { ColumnDef } from '../schema/column.js';
7
- import type { TableDef } from '../schema/table.js';
8
- import { EntityInstance } from '../schema/types.js';
9
- import { RelationDef } from '../schema/relation.js';
10
-
11
- import { selectFromEntity, getTableDefFromEntity } from '../decorators/bootstrap.js';
12
- import type { EntityConstructor } from './entity-metadata.js';
13
- import { Orm } from './orm.js';
14
- import { IdentityMap } from './identity-map.js';
15
- import { UnitOfWork } from './unit-of-work.js';
16
- import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
17
- import { RelationChangeProcessor } from './relation-change-processor.js';
18
- import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
19
- import { ExecutionContext } from './execution-context.js';
20
- import type { HydrationContext } from './hydration-context.js';
21
- import type { EntityContext } from './entity-context.js';
22
- import {
23
- DomainEvent,
24
- OrmDomainEvent,
25
- RelationChange,
26
- RelationChangeEntry,
27
- RelationKey,
28
- TrackedEntity
29
- } from './runtime-types.js';
30
- import { executeHydrated } from './execute.js';
31
- import { runInTransaction } from './transaction-runner.js';
32
-
33
- export interface OrmInterceptor {
34
- beforeFlush?(ctx: EntityContext): Promise<void> | void;
35
- afterFlush?(ctx: EntityContext): Promise<void> | void;
36
- }
37
-
38
- export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
39
- orm: Orm<E>;
40
- executor: DbExecutor;
41
- queryLogger?: QueryLogger;
42
- interceptors?: OrmInterceptor[];
43
- domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
44
- }
45
-
46
- export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
47
- readonly orm: Orm<E>;
48
- readonly executor: DbExecutor;
49
- readonly identityMap: IdentityMap;
50
- readonly unitOfWork: UnitOfWork;
51
- readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
52
- readonly relationChanges: RelationChangeProcessor;
53
-
54
- private readonly interceptors: OrmInterceptor[];
55
-
56
- constructor(opts: OrmSessionOptions<E>) {
57
- this.orm = opts.orm;
58
- this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
59
- this.interceptors = [...(opts.interceptors ?? [])];
60
-
61
- this.identityMap = new IdentityMap();
62
- this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
63
- this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
64
- this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
65
- }
66
-
67
- get dialect(): Dialect {
68
- return this.orm.dialect;
69
- }
70
-
71
- get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
72
- return this.unitOfWork.identityBuckets;
73
- }
74
-
75
- get tracked(): TrackedEntity[] {
76
- return this.unitOfWork.getTracked();
77
- }
78
-
79
- getEntity(table: TableDef, pk: any): any | undefined {
80
- return this.unitOfWork.getEntity(table, pk);
81
- }
82
-
83
- setEntity(table: TableDef, pk: any, entity: any): void {
84
- this.unitOfWork.setEntity(table, pk, entity);
85
- }
86
-
87
- trackNew(table: TableDef, entity: any, pk?: any): void {
88
- this.unitOfWork.trackNew(table, entity, pk);
89
- }
90
-
91
- trackManaged(table: TableDef, pk: any, entity: any): void {
92
- this.unitOfWork.trackManaged(table, pk, entity);
93
- }
94
-
95
- markDirty(entity: any): void {
96
- this.unitOfWork.markDirty(entity);
97
- }
98
-
99
- markRemoved(entity: any): void {
100
- this.unitOfWork.markRemoved(entity);
101
- }
102
-
103
- registerRelationChange = (
104
- root: any,
105
- relationKey: RelationKey,
106
- rootTable: TableDef,
107
- relationName: string,
108
- relation: RelationDef,
109
- change: RelationChange<any>
110
- ): void => {
111
- this.relationChanges.registerChange(
112
- buildRelationChangeEntry(root, relationKey, rootTable, relationName, relation, change)
113
- );
114
- };
115
-
116
- getEntitiesForTable(table: TableDef): TrackedEntity[] {
117
- return this.unitOfWork.getEntitiesForTable(table);
118
- }
119
-
120
- registerInterceptor(interceptor: OrmInterceptor): void {
121
- this.interceptors.push(interceptor);
122
- }
123
-
124
- registerDomainEventHandler<TType extends E['type']>(
125
- type: TType,
126
- handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
127
- ): void {
128
- this.domainEvents.on(type, handler);
129
- }
130
-
131
- async find<TTable extends TableDef>(entityClass: EntityConstructor, id: any): Promise<EntityInstance<TTable> | null> {
132
- const table = getTableDefFromEntity(entityClass);
133
- if (!table) {
134
- throw new Error('Entity metadata has not been bootstrapped');
135
- }
136
- const primaryKey = findPrimaryKey(table);
137
- const column = table.columns[primaryKey];
138
- if (!column) {
139
- throw new Error('Entity table does not expose a primary key');
140
- }
141
- const columnSelections = Object.values(table.columns).reduce<Record<string, ColumnDef>>((acc, col) => {
142
- acc[col.name] = col;
143
- return acc;
144
- }, {});
145
- const qb = selectFromEntity<TTable>(entityClass)
146
- .select(columnSelections)
147
- .where(eq(column, id))
148
- .limit(1);
149
- const rows = await executeHydrated(this, qb);
150
- return rows[0] ?? null;
151
- }
152
-
153
- async findOne<TTable extends TableDef>(qb: SelectQueryBuilder<any, TTable>): Promise<EntityInstance<TTable> | null> {
154
- const limited = qb.limit(1);
155
- const rows = await executeHydrated(this, limited);
156
- return rows[0] ?? null;
157
- }
158
-
159
- async findMany<TTable extends TableDef>(qb: SelectQueryBuilder<any, TTable>): Promise<EntityInstance<TTable>[]> {
160
- return executeHydrated(this, qb);
161
- }
162
-
163
- async persist(entity: object): Promise<void> {
164
- if (this.unitOfWork.findTracked(entity)) {
165
- return;
166
- }
167
- const table = getTableDefFromEntity((entity as any).constructor as EntityConstructor);
168
- if (!table) {
169
- throw new Error('Entity metadata has not been bootstrapped');
170
- }
171
- const primaryKey = findPrimaryKey(table);
172
- const pkValue = (entity as Record<string, any>)[primaryKey];
173
- if (pkValue !== undefined && pkValue !== null) {
174
- this.trackManaged(table, pkValue, entity);
175
- } else {
176
- this.trackNew(table, entity);
177
- }
178
- }
179
-
180
- async remove(entity: object): Promise<void> {
181
- this.markRemoved(entity);
182
- }
183
-
184
- async flush(): Promise<void> {
185
- await this.unitOfWork.flush();
186
- }
187
-
188
- private async flushWithHooks(): Promise<void> {
189
- for (const interceptor of this.interceptors) {
190
- await interceptor.beforeFlush?.(this);
191
- }
192
-
193
- await this.unitOfWork.flush();
194
- await this.relationChanges.process();
195
- await this.unitOfWork.flush();
196
-
197
- for (const interceptor of this.interceptors) {
198
- await interceptor.afterFlush?.(this);
199
- }
200
- }
201
-
202
- async commit(): Promise<void> {
203
- await runInTransaction(this.executor, async () => {
204
- await this.flushWithHooks();
205
- });
206
-
207
- await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
208
- }
209
-
210
- async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
211
- // If the executor can't do transactions, just run and commit once.
212
- if (!this.executor.beginTransaction) {
213
- const result = await fn(this);
214
- await this.commit();
215
- return result;
216
- }
217
-
218
- await this.executor.beginTransaction();
219
- try {
220
- const result = await fn(this);
221
- await this.flushWithHooks();
222
- await this.executor.commitTransaction?.();
223
- await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
224
- return result;
225
- } catch (err) {
226
- await this.rollback();
227
- throw err;
228
- }
229
- }
230
-
231
- async rollback(): Promise<void> {
232
- await this.executor.rollbackTransaction?.();
233
- this.unitOfWork.reset();
234
- this.relationChanges.reset();
235
- }
236
-
237
- getExecutionContext(): ExecutionContext {
238
- return {
239
- dialect: this.orm.dialect,
240
- executor: this.executor,
241
- interceptors: this.orm.interceptors
242
- };
243
- }
244
-
245
- getHydrationContext(): HydrationContext<E> {
246
- return {
247
- identityMap: this.identityMap,
248
- unitOfWork: this.unitOfWork,
249
- domainEvents: this.domainEvents,
250
- relationChanges: this.relationChanges,
251
- entityContext: this
252
- };
253
- }
254
- }
255
-
256
- const buildRelationChangeEntry = (
257
- root: any,
258
- relationKey: RelationKey,
259
- rootTable: TableDef,
260
- relationName: string,
261
- relation: RelationDef,
262
- change: RelationChange<any>
263
- ): RelationChangeEntry => ({
264
- root,
265
- relationKey,
266
- rootTable,
267
- relationName,
268
- relation,
269
- change
270
- });
1
+ import { Dialect } from '../core/dialect/abstract.js';
2
+ import { eq } from '../core/ast/expression.js';
3
+ import type { DbExecutor } from '../core/execution/db-executor.js';
4
+ import { SelectQueryBuilder } from '../query-builder/select.js';
5
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
6
+ import type { ColumnDef } from '../schema/column.js';
7
+ import type { TableDef } from '../schema/table.js';
8
+ import { EntityInstance } from '../schema/types.js';
9
+ import { RelationDef } from '../schema/relation.js';
10
+
11
+ import { selectFromEntity, getTableDefFromEntity } from '../decorators/bootstrap.js';
12
+ import type { EntityConstructor } from './entity-metadata.js';
13
+ import { Orm } from './orm.js';
14
+ import { IdentityMap } from './identity-map.js';
15
+ import { UnitOfWork } from './unit-of-work.js';
16
+ import { DomainEventBus, DomainEventHandler, InitialHandlers } from './domain-event-bus.js';
17
+ import { RelationChangeProcessor } from './relation-change-processor.js';
18
+ import { createQueryLoggingExecutor, QueryLogger } from './query-logger.js';
19
+ import { ExecutionContext } from './execution-context.js';
20
+ import type { HydrationContext } from './hydration-context.js';
21
+ import type { EntityContext } from './entity-context.js';
22
+ import {
23
+ DomainEvent,
24
+ OrmDomainEvent,
25
+ RelationChange,
26
+ RelationChangeEntry,
27
+ RelationKey,
28
+ TrackedEntity
29
+ } from './runtime-types.js';
30
+ import { executeHydrated } from './execute.js';
31
+ import { runInTransaction } from './transaction-runner.js';
32
+ import { saveGraphInternal, SaveGraphOptions } from './save-graph.js';
33
+
34
+ /**
35
+ * Interface for ORM interceptors that allow hooking into the flush lifecycle.
36
+ */
37
+ export interface OrmInterceptor {
38
+ /**
39
+ * Called before the flush operation begins.
40
+ * @param ctx - The entity context
41
+ */
42
+ beforeFlush?(ctx: EntityContext): Promise<void> | void;
43
+
44
+ /**
45
+ * Called after the flush operation completes.
46
+ * @param ctx - The entity context
47
+ */
48
+ afterFlush?(ctx: EntityContext): Promise<void> | void;
49
+ }
50
+
51
+ /**
52
+ * Options for creating an OrmSession instance.
53
+ * @template E - The domain event type
54
+ */
55
+ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
56
+ /** The ORM instance */
57
+ orm: Orm<E>;
58
+ /** The database executor */
59
+ executor: DbExecutor;
60
+ /** Optional query logger for debugging */
61
+ queryLogger?: QueryLogger;
62
+ /** Optional interceptors for flush lifecycle hooks */
63
+ interceptors?: OrmInterceptor[];
64
+ /** Optional domain event handlers */
65
+ domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
66
+ }
67
+
68
+ /**
69
+ * ORM Session that manages entity lifecycle, identity mapping, and database operations.
70
+ * @template E - The domain event type
71
+ */
72
+ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
73
+ /** The ORM instance */
74
+ readonly orm: Orm<E>;
75
+ /** The database executor */
76
+ readonly executor: DbExecutor;
77
+ /** The identity map for tracking entity instances */
78
+ readonly identityMap: IdentityMap;
79
+ /** The unit of work for tracking entity changes */
80
+ readonly unitOfWork: UnitOfWork;
81
+ /** The domain event bus */
82
+ readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
83
+ /** The relation change processor */
84
+ readonly relationChanges: RelationChangeProcessor;
85
+
86
+ private readonly interceptors: OrmInterceptor[];
87
+
88
+ /**
89
+ * Creates a new OrmSession instance.
90
+ * @param opts - Session options
91
+ */
92
+ constructor(opts: OrmSessionOptions<E>) {
93
+ this.orm = opts.orm;
94
+ this.executor = createQueryLoggingExecutor(opts.executor, opts.queryLogger);
95
+ this.interceptors = [...(opts.interceptors ?? [])];
96
+
97
+ this.identityMap = new IdentityMap();
98
+ this.unitOfWork = new UnitOfWork(this.orm.dialect, this.executor, this.identityMap, () => this);
99
+ this.relationChanges = new RelationChangeProcessor(this.unitOfWork, this.orm.dialect, this.executor);
100
+ this.domainEvents = new DomainEventBus<E, OrmSession<E>>(opts.domainEventHandlers);
101
+ }
102
+
103
+ /**
104
+ * Releases resources associated with this session (executor/pool leases) and resets tracking.
105
+ * Must be safe to call multiple times.
106
+ */
107
+ async dispose(): Promise<void> {
108
+ try {
109
+ await this.executor.dispose();
110
+ } finally {
111
+ // Always reset in-memory tracking.
112
+ this.unitOfWork.reset();
113
+ this.relationChanges.reset();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Gets the database dialect.
119
+ */
120
+ get dialect(): Dialect {
121
+ return this.orm.dialect;
122
+ }
123
+
124
+ /**
125
+ * Gets the identity buckets map.
126
+ */
127
+ get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
128
+ return this.unitOfWork.identityBuckets;
129
+ }
130
+
131
+ /**
132
+ * Gets all tracked entities.
133
+ */
134
+ get tracked(): TrackedEntity[] {
135
+ return this.unitOfWork.getTracked();
136
+ }
137
+
138
+ /**
139
+ * Gets an entity by table and primary key.
140
+ * @param table - The table definition
141
+ * @param pk - The primary key value
142
+ * @returns The entity or undefined if not found
143
+ */
144
+ getEntity(table: TableDef, pk: any): any | undefined {
145
+ return this.unitOfWork.getEntity(table, pk);
146
+ }
147
+
148
+ /**
149
+ * Sets an entity in the identity map.
150
+ * @param table - The table definition
151
+ * @param pk - The primary key value
152
+ * @param entity - The entity instance
153
+ */
154
+ setEntity(table: TableDef, pk: any, entity: any): void {
155
+ this.unitOfWork.setEntity(table, pk, entity);
156
+ }
157
+
158
+ /**
159
+ * Tracks a new entity.
160
+ * @param table - The table definition
161
+ * @param entity - The entity instance
162
+ * @param pk - Optional primary key value
163
+ */
164
+ trackNew(table: TableDef, entity: any, pk?: any): void {
165
+ this.unitOfWork.trackNew(table, entity, pk);
166
+ }
167
+
168
+ /**
169
+ * Tracks a managed entity.
170
+ * @param table - The table definition
171
+ * @param pk - The primary key value
172
+ * @param entity - The entity instance
173
+ */
174
+ trackManaged(table: TableDef, pk: any, entity: any): void {
175
+ this.unitOfWork.trackManaged(table, pk, entity);
176
+ }
177
+
178
+ /**
179
+ * Marks an entity as dirty (modified).
180
+ * @param entity - The entity to mark as dirty
181
+ */
182
+ markDirty(entity: any): void {
183
+ this.unitOfWork.markDirty(entity);
184
+ }
185
+
186
+ /**
187
+ * Marks an entity as removed.
188
+ * @param entity - The entity to mark as removed
189
+ */
190
+ markRemoved(entity: any): void {
191
+ this.unitOfWork.markRemoved(entity);
192
+ }
193
+
194
+ /**
195
+ * Registers a relation change.
196
+ * @param root - The root entity
197
+ * @param relationKey - The relation key
198
+ * @param rootTable - The root table definition
199
+ * @param relationName - The relation name
200
+ * @param relation - The relation definition
201
+ * @param change - The relation change
202
+ */
203
+ registerRelationChange = (
204
+ root: any,
205
+ relationKey: RelationKey,
206
+ rootTable: TableDef,
207
+ relationName: string,
208
+ relation: RelationDef,
209
+ change: RelationChange<any>
210
+ ): void => {
211
+ this.relationChanges.registerChange(
212
+ buildRelationChangeEntry(root, relationKey, rootTable, relationName, relation, change)
213
+ );
214
+ };
215
+
216
+ /**
217
+ * Gets all tracked entities for a specific table.
218
+ * @param table - The table definition
219
+ * @returns Array of tracked entities
220
+ */
221
+ getEntitiesForTable(table: TableDef): TrackedEntity[] {
222
+ return this.unitOfWork.getEntitiesForTable(table);
223
+ }
224
+
225
+ /**
226
+ * Registers an interceptor for flush lifecycle hooks.
227
+ * @param interceptor - The interceptor to register
228
+ */
229
+ registerInterceptor(interceptor: OrmInterceptor): void {
230
+ this.interceptors.push(interceptor);
231
+ }
232
+
233
+ /**
234
+ * Registers a domain event handler.
235
+ * @param type - The event type
236
+ * @param handler - The event handler
237
+ */
238
+ registerDomainEventHandler<TType extends E['type']>(
239
+ type: TType,
240
+ handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
241
+ ): void {
242
+ this.domainEvents.on(type, handler);
243
+ }
244
+
245
+ /**
246
+ * Finds an entity by its primary key.
247
+ * @template TCtor - The entity constructor type
248
+ * @param entityClass - The entity constructor
249
+ * @param id - The primary key value
250
+ * @returns The entity instance or null if not found
251
+ * @throws If entity metadata is not bootstrapped or table has no primary key
252
+ */
253
+ async find<TCtor extends EntityConstructor<any>>(
254
+ entityClass: TCtor,
255
+ id: any
256
+ ): Promise<InstanceType<TCtor> | null> {
257
+ const table = getTableDefFromEntity(entityClass);
258
+ if (!table) {
259
+ throw new Error('Entity metadata has not been bootstrapped');
260
+ }
261
+ const primaryKey = findPrimaryKey(table);
262
+ const column = table.columns[primaryKey];
263
+ if (!column) {
264
+ throw new Error('Entity table does not expose a primary key');
265
+ }
266
+ const columnSelections = Object.values(table.columns).reduce<Record<string, ColumnDef>>((acc, col) => {
267
+ acc[col.name] = col;
268
+ return acc;
269
+ }, {});
270
+ const qb = selectFromEntity(entityClass)
271
+ .select(columnSelections)
272
+ .where(eq(column, id))
273
+ .limit(1);
274
+ const rows = await executeHydrated(this, qb);
275
+ return (rows[0] ?? null) as InstanceType<TCtor> | null;
276
+ }
277
+
278
+ /**
279
+ * Finds a single entity using a query builder.
280
+ * @template TTable - The table type
281
+ * @param qb - The query builder
282
+ * @returns The first entity instance or null if not found
283
+ */
284
+ async findOne<TTable extends TableDef>(qb: SelectQueryBuilder<any, TTable>): Promise<EntityInstance<TTable> | null> {
285
+ const limited = qb.limit(1);
286
+ const rows = await executeHydrated(this, limited);
287
+ return rows[0] ?? null;
288
+ }
289
+
290
+ /**
291
+ * Finds multiple entities using a query builder.
292
+ * @template TTable - The table type
293
+ * @param qb - The query builder
294
+ * @returns Array of entity instances
295
+ */
296
+ async findMany<TTable extends TableDef>(qb: SelectQueryBuilder<any, TTable>): Promise<EntityInstance<TTable>[]> {
297
+ return executeHydrated(this, qb);
298
+ }
299
+
300
+ /**
301
+ * Saves an entity graph (root + nested relations) based on a DTO-like payload.
302
+ * @param entityClass - Root entity constructor
303
+ * @param payload - DTO payload containing column values and nested relations
304
+ * @param options - Graph save options
305
+ * @returns The root entity instance
306
+ */
307
+ async saveGraph<TCtor extends EntityConstructor<any>>(
308
+ entityClass: TCtor,
309
+ payload: Record<string, any>,
310
+ options?: SaveGraphOptions & { transactional?: boolean }
311
+ ): Promise<InstanceType<TCtor>> {
312
+ const { transactional = true, ...graphOptions } = options ?? {};
313
+ const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
314
+ if (!transactional) {
315
+ return execute();
316
+ }
317
+ return this.transaction(() => execute());
318
+ }
319
+
320
+ /**
321
+ * Persists an entity (either inserts or updates).
322
+ * @param entity - The entity to persist
323
+ * @throws If entity metadata is not bootstrapped
324
+ */
325
+ async persist(entity: object): Promise<void> {
326
+ if (this.unitOfWork.findTracked(entity)) {
327
+ return;
328
+ }
329
+ const table = getTableDefFromEntity((entity as any).constructor as EntityConstructor<any>);
330
+ if (!table) {
331
+ throw new Error('Entity metadata has not been bootstrapped');
332
+ }
333
+ const primaryKey = findPrimaryKey(table);
334
+ const pkValue = (entity as Record<string, any>)[primaryKey];
335
+ if (pkValue !== undefined && pkValue !== null) {
336
+ this.trackManaged(table, pkValue, entity);
337
+ } else {
338
+ this.trackNew(table, entity);
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Marks an entity for removal.
344
+ * @param entity - The entity to remove
345
+ */
346
+ async remove(entity: object): Promise<void> {
347
+ this.markRemoved(entity);
348
+ }
349
+
350
+ /**
351
+ * Flushes pending changes to the database.
352
+ */
353
+ async flush(): Promise<void> {
354
+ await this.unitOfWork.flush();
355
+ }
356
+
357
+ /**
358
+ * Flushes pending changes with interceptors and relation processing.
359
+ */
360
+ private async flushWithHooks(): Promise<void> {
361
+ for (const interceptor of this.interceptors) {
362
+ await interceptor.beforeFlush?.(this);
363
+ }
364
+
365
+ await this.unitOfWork.flush();
366
+ await this.relationChanges.process();
367
+ await this.unitOfWork.flush();
368
+
369
+ for (const interceptor of this.interceptors) {
370
+ await interceptor.afterFlush?.(this);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Commits the current transaction.
376
+ */
377
+ async commit(): Promise<void> {
378
+ await runInTransaction(this.executor, async () => {
379
+ await this.flushWithHooks();
380
+ });
381
+
382
+ await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
383
+ }
384
+
385
+ /**
386
+ * Executes a function within a transaction.
387
+ * @template T - The return type
388
+ * @param fn - The function to execute
389
+ * @returns The result of the function
390
+ * @throws If the transaction fails
391
+ */
392
+ async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
393
+ // If the executor can't do transactions, just run and commit once.
394
+ if (!this.executor.capabilities.transactions) {
395
+ const result = await fn(this);
396
+ await this.commit();
397
+ return result;
398
+ }
399
+
400
+ await this.executor.beginTransaction();
401
+ try {
402
+ const result = await fn(this);
403
+ await this.flushWithHooks();
404
+ await this.executor.commitTransaction();
405
+ await this.domainEvents.dispatch(this.unitOfWork.getTracked(), this);
406
+ return result;
407
+ } catch (err) {
408
+ await this.rollback();
409
+ throw err;
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Rolls back the current transaction.
415
+ */
416
+ async rollback(): Promise<void> {
417
+ if (this.executor.capabilities.transactions) {
418
+ await this.executor.rollbackTransaction();
419
+ }
420
+ this.unitOfWork.reset();
421
+ this.relationChanges.reset();
422
+ }
423
+
424
+ /**
425
+ * Gets the execution context.
426
+ * @returns The execution context
427
+ */
428
+ getExecutionContext(): ExecutionContext {
429
+ return {
430
+ dialect: this.orm.dialect,
431
+ executor: this.executor,
432
+ interceptors: this.orm.interceptors
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Gets the hydration context.
438
+ * @returns The hydration context
439
+ */
440
+ getHydrationContext(): HydrationContext<E> {
441
+ return {
442
+ identityMap: this.identityMap,
443
+ unitOfWork: this.unitOfWork,
444
+ domainEvents: this.domainEvents,
445
+ relationChanges: this.relationChanges,
446
+ entityContext: this
447
+ };
448
+ }
449
+ }
450
+
451
+ const buildRelationChangeEntry = (
452
+ root: any,
453
+ relationKey: RelationKey,
454
+ rootTable: TableDef,
455
+ relationName: string,
456
+ relation: RelationDef,
457
+ change: RelationChange<any>
458
+ ): RelationChangeEntry => ({
459
+ root,
460
+ relationKey,
461
+ rootTable,
462
+ relationName,
463
+ relation,
464
+ change
465
+ });