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
package/src/orm/orm-session.ts
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
this.
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
):
|
|
128
|
-
this.
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
});
|