@mikro-orm/core 7.0.0-dev.7 → 7.0.0-dev.71

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 (161) hide show
  1. package/EntityManager.d.ts +85 -42
  2. package/EntityManager.js +282 -194
  3. package/MikroORM.d.ts +11 -29
  4. package/MikroORM.js +33 -127
  5. package/README.md +3 -2
  6. package/cache/FileCacheAdapter.js +1 -2
  7. package/connections/Connection.d.ts +11 -7
  8. package/connections/Connection.js +16 -14
  9. package/drivers/DatabaseDriver.d.ts +11 -5
  10. package/drivers/DatabaseDriver.js +13 -4
  11. package/drivers/IDatabaseDriver.d.ts +27 -5
  12. package/entity/BaseEntity.d.ts +0 -1
  13. package/entity/BaseEntity.js +0 -3
  14. package/entity/Collection.d.ts +98 -30
  15. package/entity/Collection.js +432 -93
  16. package/entity/EntityAssigner.d.ts +1 -1
  17. package/entity/EntityAssigner.js +9 -1
  18. package/entity/EntityFactory.d.ts +7 -0
  19. package/entity/EntityFactory.js +63 -40
  20. package/entity/EntityHelper.js +26 -9
  21. package/entity/EntityLoader.d.ts +5 -4
  22. package/entity/EntityLoader.js +69 -36
  23. package/entity/EntityRepository.d.ts +1 -1
  24. package/entity/EntityValidator.js +4 -4
  25. package/entity/Reference.d.ts +9 -7
  26. package/entity/Reference.js +32 -5
  27. package/entity/WrappedEntity.d.ts +0 -2
  28. package/entity/WrappedEntity.js +1 -5
  29. package/entity/defineEntity.d.ts +549 -0
  30. package/entity/defineEntity.js +529 -0
  31. package/entity/index.d.ts +2 -1
  32. package/entity/index.js +2 -1
  33. package/entity/utils.d.ts +7 -0
  34. package/entity/utils.js +15 -3
  35. package/enums.d.ts +20 -5
  36. package/enums.js +13 -0
  37. package/errors.d.ts +6 -1
  38. package/errors.js +14 -4
  39. package/events/EventSubscriber.d.ts +3 -1
  40. package/hydration/ObjectHydrator.d.ts +4 -4
  41. package/hydration/ObjectHydrator.js +35 -24
  42. package/index.d.ts +2 -2
  43. package/index.js +1 -2
  44. package/logging/DefaultLogger.d.ts +1 -1
  45. package/logging/SimpleLogger.d.ts +1 -1
  46. package/metadata/EntitySchema.d.ts +9 -13
  47. package/metadata/EntitySchema.js +44 -26
  48. package/metadata/MetadataDiscovery.d.ts +6 -7
  49. package/metadata/MetadataDiscovery.js +161 -162
  50. package/metadata/MetadataProvider.d.ts +2 -2
  51. package/metadata/MetadataProvider.js +15 -0
  52. package/metadata/MetadataStorage.d.ts +0 -4
  53. package/metadata/MetadataStorage.js +6 -10
  54. package/metadata/MetadataValidator.d.ts +0 -7
  55. package/metadata/MetadataValidator.js +4 -13
  56. package/metadata/discover-entities.d.ts +5 -0
  57. package/metadata/discover-entities.js +39 -0
  58. package/metadata/index.d.ts +1 -1
  59. package/metadata/index.js +1 -1
  60. package/metadata/types.d.ts +480 -0
  61. package/metadata/types.js +1 -0
  62. package/naming-strategy/AbstractNamingStrategy.d.ts +5 -1
  63. package/naming-strategy/AbstractNamingStrategy.js +7 -1
  64. package/naming-strategy/NamingStrategy.d.ts +11 -1
  65. package/package.json +11 -10
  66. package/platforms/Platform.d.ts +6 -10
  67. package/platforms/Platform.js +6 -22
  68. package/serialization/EntitySerializer.d.ts +2 -0
  69. package/serialization/EntitySerializer.js +29 -11
  70. package/serialization/EntityTransformer.js +22 -12
  71. package/serialization/SerializationContext.js +14 -11
  72. package/types/ArrayType.d.ts +1 -1
  73. package/types/ArrayType.js +1 -2
  74. package/types/BigIntType.d.ts +8 -6
  75. package/types/BlobType.d.ts +0 -1
  76. package/types/BlobType.js +0 -3
  77. package/types/BooleanType.d.ts +2 -1
  78. package/types/BooleanType.js +3 -0
  79. package/types/DecimalType.d.ts +6 -4
  80. package/types/DecimalType.js +1 -1
  81. package/types/DoubleType.js +1 -1
  82. package/types/JsonType.d.ts +1 -1
  83. package/types/JsonType.js +7 -2
  84. package/types/Type.d.ts +2 -1
  85. package/types/Type.js +1 -1
  86. package/types/Uint8ArrayType.d.ts +0 -1
  87. package/types/Uint8ArrayType.js +0 -3
  88. package/types/index.d.ts +1 -1
  89. package/typings.d.ts +112 -77
  90. package/typings.js +32 -32
  91. package/unit-of-work/ChangeSetComputer.js +8 -3
  92. package/unit-of-work/ChangeSetPersister.d.ts +4 -2
  93. package/unit-of-work/ChangeSetPersister.js +37 -16
  94. package/unit-of-work/UnitOfWork.d.ts +8 -1
  95. package/unit-of-work/UnitOfWork.js +111 -54
  96. package/utils/AbstractSchemaGenerator.d.ts +5 -5
  97. package/utils/AbstractSchemaGenerator.js +10 -8
  98. package/utils/Configuration.d.ts +200 -191
  99. package/utils/Configuration.js +141 -152
  100. package/utils/ConfigurationLoader.d.ts +3 -44
  101. package/utils/ConfigurationLoader.js +26 -239
  102. package/utils/Cursor.d.ts +3 -3
  103. package/utils/Cursor.js +3 -0
  104. package/utils/DataloaderUtils.d.ts +15 -5
  105. package/utils/DataloaderUtils.js +53 -7
  106. package/utils/EntityComparator.d.ts +8 -4
  107. package/utils/EntityComparator.js +107 -60
  108. package/utils/QueryHelper.d.ts +9 -1
  109. package/utils/QueryHelper.js +69 -8
  110. package/utils/RawQueryFragment.d.ts +36 -4
  111. package/utils/RawQueryFragment.js +34 -13
  112. package/utils/TransactionManager.d.ts +65 -0
  113. package/utils/TransactionManager.js +223 -0
  114. package/utils/Utils.d.ts +17 -84
  115. package/utils/Utils.js +132 -252
  116. package/utils/index.d.ts +1 -0
  117. package/utils/index.js +1 -0
  118. package/utils/upsert-utils.d.ts +7 -2
  119. package/utils/upsert-utils.js +52 -1
  120. package/decorators/Check.d.ts +0 -3
  121. package/decorators/Check.js +0 -13
  122. package/decorators/CreateRequestContext.d.ts +0 -3
  123. package/decorators/CreateRequestContext.js +0 -32
  124. package/decorators/Embeddable.d.ts +0 -8
  125. package/decorators/Embeddable.js +0 -11
  126. package/decorators/Embedded.d.ts +0 -18
  127. package/decorators/Embedded.js +0 -18
  128. package/decorators/Entity.d.ts +0 -18
  129. package/decorators/Entity.js +0 -12
  130. package/decorators/Enum.d.ts +0 -9
  131. package/decorators/Enum.js +0 -16
  132. package/decorators/Filter.d.ts +0 -2
  133. package/decorators/Filter.js +0 -8
  134. package/decorators/Formula.d.ts +0 -4
  135. package/decorators/Formula.js +0 -15
  136. package/decorators/Indexed.d.ts +0 -19
  137. package/decorators/Indexed.js +0 -20
  138. package/decorators/ManyToMany.d.ts +0 -40
  139. package/decorators/ManyToMany.js +0 -14
  140. package/decorators/ManyToOne.d.ts +0 -30
  141. package/decorators/ManyToOne.js +0 -14
  142. package/decorators/OneToMany.d.ts +0 -28
  143. package/decorators/OneToMany.js +0 -17
  144. package/decorators/OneToOne.d.ts +0 -24
  145. package/decorators/OneToOne.js +0 -7
  146. package/decorators/PrimaryKey.d.ts +0 -8
  147. package/decorators/PrimaryKey.js +0 -20
  148. package/decorators/Property.d.ts +0 -250
  149. package/decorators/Property.js +0 -32
  150. package/decorators/Transactional.d.ts +0 -13
  151. package/decorators/Transactional.js +0 -28
  152. package/decorators/hooks.d.ts +0 -16
  153. package/decorators/hooks.js +0 -47
  154. package/decorators/index.d.ts +0 -17
  155. package/decorators/index.js +0 -17
  156. package/entity/ArrayCollection.d.ts +0 -116
  157. package/entity/ArrayCollection.js +0 -402
  158. package/metadata/ReflectMetadataProvider.d.ts +0 -8
  159. package/metadata/ReflectMetadataProvider.js +0 -44
  160. package/utils/resolveContextProvider.d.ts +0 -10
  161. package/utils/resolveContextProvider.js +0 -28
package/EntityManager.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { inspect } from 'node:util';
2
- import DataLoader from 'dataloader';
3
- import { getOnConflictReturningFields } from './utils/upsert-utils.js';
2
+ import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
4
3
  import { Utils } from './utils/Utils.js';
5
4
  import { Cursor } from './utils/Cursor.js';
6
5
  import { DataloaderUtils } from './utils/DataloaderUtils.js';
@@ -19,6 +18,8 @@ import { EventType, FlushMode, LoadStrategy, LockMode, PopulateHint, PopulatePat
19
18
  import { EventManager } from './events/EventManager.js';
20
19
  import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaster.js';
21
20
  import { OptimisticLockError, ValidationError } from './errors.js';
21
+ import { getLoadingStrategy } from './entity/utils.js';
22
+ import { TransactionManager } from './utils/TransactionManager.js';
22
23
  /**
23
24
  * The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
24
25
  * such as UnitOfWork, Query Language, and Repository API.
@@ -34,8 +35,7 @@ export class EntityManager {
34
35
  _id = EntityManager.counter++;
35
36
  global = false;
36
37
  name;
37
- refLoader = new DataLoader(DataloaderUtils.getRefBatchLoadFn(this));
38
- colLoader = new DataLoader(DataloaderUtils.getColBatchLoadFn(this));
38
+ loaders = {};
39
39
  validator;
40
40
  repositoryMap = {};
41
41
  entityLoader;
@@ -137,7 +137,6 @@ export class EntityManager {
137
137
  await em.entityLoader.populate(entityName, cached.data, populate, {
138
138
  ...options,
139
139
  ...em.getPopulateWhere(where, options),
140
- convertCustomTypes: false,
141
140
  ignoreLazyScalarProperties: true,
142
141
  lookup: false,
143
142
  });
@@ -148,7 +147,7 @@ export class EntityManager {
148
147
  // save the original hint value so we know it was infer/all
149
148
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
150
149
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
151
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
150
+ options.populateFilter = await this.getJoinedFilters(meta, options);
152
151
  const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, em, ...options });
153
152
  if (results.length === 0) {
154
153
  await em.storeCache(options.cache, cached, []);
@@ -168,7 +167,6 @@ export class EntityManager {
168
167
  await em.entityLoader.populate(entityName, unique, populate, {
169
168
  ...options,
170
169
  ...em.getPopulateWhere(where, options),
171
- convertCustomTypes: false,
172
170
  ignoreLazyScalarProperties: true,
173
171
  lookup: false,
174
172
  });
@@ -181,6 +179,61 @@ export class EntityManager {
181
179
  }
182
180
  return unique;
183
181
  }
182
+ /**
183
+ * Finds all entities and returns an async iterable (async generator) that yields results one by one.
184
+ * The results are merged and mapped to entity instances, without adding them to the identity map.
185
+ * You can disable merging by passing the options `{ mergeResults: false }`.
186
+ * With `mergeResults` disabled, to-many collections will contain at most one item, and you will get duplicate
187
+ * root entities when there are multiple items in the populated collection.
188
+ * This is useful for processing large datasets without loading everything into memory at once.
189
+ *
190
+ * ```ts
191
+ * const stream = em.stream(Book, { populate: ['author'] });
192
+ *
193
+ * for await (const book of stream) {
194
+ * // book is an instance of Book entity
195
+ * console.log(book.title, book.author.name);
196
+ * }
197
+ * ```
198
+ */
199
+ async *stream(entityName, options = {}) {
200
+ const em = this.getContext();
201
+ em.prepareOptions(options);
202
+ options.strategy = 'joined';
203
+ await em.tryFlush(entityName, options);
204
+ entityName = Utils.className(entityName);
205
+ const where = await em.processWhere(entityName, options.where ?? {}, options, 'read');
206
+ em.validator.validateParams(where);
207
+ options.orderBy = options.orderBy || {};
208
+ options.populate = await em.preparePopulate(entityName, options);
209
+ const meta = this.metadata.get(entityName);
210
+ options = { ...options };
211
+ // save the original hint value so we know it was infer/all
212
+ options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
213
+ options.populateWhere = this.createPopulateWhere({ ...where }, options);
214
+ options.populateFilter = await this.getJoinedFilters(meta, options);
215
+ const stream = em.driver.stream(entityName, where, {
216
+ ctx: em.transactionContext,
217
+ mapResults: false,
218
+ ...options,
219
+ });
220
+ for await (const data of stream) {
221
+ const fork = em.fork();
222
+ const entity = fork.entityFactory.create(entityName, data, {
223
+ refresh: options.refresh,
224
+ schema: options.schema,
225
+ convertCustomTypes: true,
226
+ });
227
+ helper(entity).setSerializationContext({
228
+ populate: options.populate,
229
+ fields: options.fields,
230
+ exclude: options.exclude,
231
+ });
232
+ await fork.unitOfWork.dispatchOnLoadEvent();
233
+ fork.clear();
234
+ yield entity;
235
+ }
236
+ }
184
237
  /**
185
238
  * Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
186
239
  */
@@ -203,12 +256,12 @@ export class EntityManager {
203
256
  /**
204
257
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
205
258
  */
206
- addFilter(name, cond, entityName, enabled = true) {
207
- const options = { name, cond, default: enabled };
208
- if (entityName) {
209
- options.entity = Utils.asArray(entityName).map(n => Utils.className(n));
259
+ addFilter(options) {
260
+ if (options.entity) {
261
+ options.entity = Utils.asArray(options.entity).map(n => Utils.className(n));
210
262
  }
211
- this.getContext(false).filters[name] = options;
263
+ options.default ??= true;
264
+ this.getContext(false).filters[options.name] = options;
212
265
  }
213
266
  /**
214
267
  * Sets filter parameter values globally inside context defined by this entity manager.
@@ -232,8 +285,8 @@ export class EntityManager {
232
285
  /**
233
286
  * Gets logger context for this entity manager.
234
287
  */
235
- getLoggerContext() {
236
- const em = this.getContext();
288
+ getLoggerContext(options) {
289
+ const em = options?.disableContextResolution ? this : this.getContext();
237
290
  em.loggerContext ??= {};
238
291
  return em.loggerContext;
239
292
  }
@@ -283,28 +336,39 @@ export class EntityManager {
283
336
  }
284
337
  return ret;
285
338
  }
286
- async getJoinedFilters(meta, cond, options) {
339
+ async getJoinedFilters(meta, options) {
340
+ if (!this.config.get('filtersOnRelations') || !options.populate) {
341
+ return undefined;
342
+ }
287
343
  const ret = {};
288
- if (options.populate) {
289
- for (const hint of options.populate) {
290
- const field = hint.field.split(':')[0];
291
- const prop = meta.properties[field];
292
- const joined = (prop.strategy || options.strategy || hint.strategy || this.config.get('loadStrategy')) === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
293
- if (!joined && !hint.filter) {
294
- continue;
295
- }
296
- const where = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', { ...options, populate: hint.children });
297
- const where2 = await this.getJoinedFilters(prop.targetMeta, {}, { ...options, populate: hint.children, populateWhere: PopulateHint.ALL });
298
- if (Utils.hasObjectKeys(where)) {
299
- ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
344
+ for (const hint of options.populate) {
345
+ const field = hint.field.split(':')[0];
346
+ const prop = meta.properties[field];
347
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
348
+ const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
349
+ if (!joined && !hint.filter) {
350
+ continue;
351
+ }
352
+ const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
353
+ const where = await this.applyFilters(prop.type, {}, filters, 'read', {
354
+ ...options,
355
+ populate: hint.children,
356
+ });
357
+ const where2 = await this.getJoinedFilters(prop.targetMeta, {
358
+ ...options,
359
+ filters,
360
+ populate: hint.children,
361
+ populateWhere: PopulateHint.ALL,
362
+ });
363
+ if (Utils.hasObjectKeys(where)) {
364
+ ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
365
+ }
366
+ if (where2 && Utils.hasObjectKeys(where2)) {
367
+ if (ret[field]) {
368
+ Utils.merge(ret[field], where2);
300
369
  }
301
- if (Utils.hasObjectKeys(where2)) {
302
- if (ret[field]) {
303
- Utils.merge(ret[field], where2);
304
- }
305
- else {
306
- ret[field] = where2;
307
- }
370
+ else {
371
+ ret[field] = where2;
308
372
  }
309
373
  }
310
374
  }
@@ -313,27 +377,45 @@ export class EntityManager {
313
377
  /**
314
378
  * When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
315
379
  */
316
- async autoJoinRefsForFilters(meta, options) {
317
- if (!meta || !this.config.get('autoJoinRefsForFilters')) {
380
+ async autoJoinRefsForFilters(meta, options, parent) {
381
+ if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) {
318
382
  return;
319
383
  }
320
- const props = meta.relations.filter(prop => {
321
- return !prop.object && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
322
- && ((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)));
323
- });
324
384
  const ret = options.populate;
325
- for (const prop of props) {
326
- const cond = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', options);
385
+ for (const prop of meta.relations) {
386
+ if (prop.object
387
+ || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
388
+ || !((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
389
+ || (parent?.className === prop.targetMeta.root.className && parent.propName === prop.inversedBy)) {
390
+ continue;
391
+ }
392
+ options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
393
+ const cond = await this.applyFilters(prop.type, {}, options.filters, 'read', options);
327
394
  if (!Utils.isEmpty(cond)) {
328
395
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
329
- if (populated.length > 0) {
330
- populated.forEach(hint => hint.filter = true);
396
+ let found = false;
397
+ for (const hint of populated) {
398
+ if (!hint.all) {
399
+ hint.filter = true;
400
+ }
401
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
402
+ if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
403
+ found = true;
404
+ }
331
405
  }
332
- else {
406
+ if (!found) {
333
407
  ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true });
334
408
  }
335
409
  }
336
410
  }
411
+ for (const hint of ret) {
412
+ const [field, ref] = hint.field.split(':');
413
+ const prop = meta?.properties[field];
414
+ if (prop && !ref) {
415
+ hint.children ??= [];
416
+ await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { className: meta.root.className, propName: prop.name });
417
+ }
418
+ }
337
419
  }
338
420
  /**
339
421
  * @internal
@@ -363,7 +445,7 @@ export class EntityManager {
363
445
  let cond;
364
446
  if (filter.cond instanceof Function) {
365
447
  // @ts-ignore
366
- const args = Utils.isPlainObject(options[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
448
+ const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
367
449
  if (!args && filter.cond.length > 0 && filter.args !== false) {
368
450
  throw new Error(`No arguments provided for filter '${filter.name}'`);
369
451
  }
@@ -372,13 +454,17 @@ export class EntityManager {
372
454
  else {
373
455
  cond = filter.cond;
374
456
  }
375
- ret.push(QueryHelper.processWhere({
457
+ cond = QueryHelper.processWhere({
376
458
  where: cond,
377
459
  entityName,
378
460
  metadata: this.metadata,
379
461
  platform: this.driver.getPlatform(),
380
462
  aliased: type === 'read',
381
- }));
463
+ });
464
+ if (filter.strict) {
465
+ Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false });
466
+ }
467
+ ret.push(cond);
382
468
  }
383
469
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c));
384
470
  return conds.length > 1 ? { $and: conds } : conds[0];
@@ -433,6 +519,10 @@ export class EntityManager {
433
519
  * });
434
520
  * ```
435
521
  *
522
+ * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
523
+ * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
524
+ * of pages.
525
+ *
436
526
  * The `Cursor` object provides the following interface:
437
527
  *
438
528
  * ```ts
@@ -442,7 +532,7 @@ export class EntityManager {
442
532
  * User { ... },
443
533
  * User { ... },
444
534
  * ],
445
- * totalCount: 50,
535
+ * totalCount: 50, // not included if `includeCount: false`
446
536
  * startCursor: 'WzRd',
447
537
  * endCursor: 'WzZd',
448
538
  * hasPrevPage: true,
@@ -457,7 +547,9 @@ export class EntityManager {
457
547
  if (Utils.isEmpty(options.orderBy)) {
458
548
  throw new Error('Explicit `orderBy` option required');
459
549
  }
460
- const [entities, count] = await em.findAndCount(entityName, where, options);
550
+ const [entities, count] = options.includeCount !== false
551
+ ? await em.findAndCount(entityName, where, options)
552
+ : [await em.find(entityName, where, options)];
461
553
  return new Cursor(entities, count, options, this.metadata.get(entityName));
462
554
  }
463
555
  /**
@@ -483,18 +575,31 @@ export class EntityManager {
483
575
  async refresh(entity, options = {}) {
484
576
  const fork = this.fork({ keepTransactionContext: true });
485
577
  const entityName = entity.constructor.name;
578
+ const wrapped = helper(entity);
486
579
  const reloaded = await fork.findOne(entityName, entity, {
487
- schema: helper(entity).__schema,
580
+ schema: wrapped.__schema,
488
581
  ...options,
489
582
  flushMode: FlushMode.COMMIT,
490
583
  });
491
- if (reloaded) {
492
- this.config.getHydrator(this.metadata).hydrate(entity, helper(entity).__meta, helper(reloaded).toPOJO(), this.getEntityFactory(), 'full');
584
+ const em = this.getContext();
585
+ if (!reloaded) {
586
+ em.unitOfWork.unsetIdentity(entity);
587
+ return null;
493
588
  }
494
- else {
495
- this.getUnitOfWork().unsetIdentity(entity);
589
+ let found = false;
590
+ for (const e of fork.unitOfWork.getIdentityMap()) {
591
+ const ref = em.getReference(e.constructor.name, helper(e).getPrimaryKey());
592
+ const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true });
593
+ em.config.getHydrator(this.metadata).hydrate(ref, helper(ref).__meta, data, em.entityFactory, 'full', false, true);
594
+ Utils.merge(helper(ref).__originalEntityData, this.comparator.prepareEntity(e));
595
+ found ||= ref === entity;
496
596
  }
497
- return reloaded ? entity : reloaded;
597
+ if (!found) {
598
+ const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true });
599
+ em.config.getHydrator(this.metadata).hydrate(entity, wrapped.__meta, data, em.entityFactory, 'full', false, true);
600
+ Utils.merge(wrapped.__originalEntityData, this.comparator.prepareEntity(reloaded));
601
+ }
602
+ return entity;
498
603
  }
499
604
  /**
500
605
  * Finds first entity matching your `where` query.
@@ -522,7 +627,7 @@ export class EntityManager {
522
627
  where = await em.processWhere(entityName, where, options, 'read');
523
628
  em.validator.validateEmptyWhere(where);
524
629
  em.checkLockRequirements(options.lockMode, meta);
525
- const isOptimisticLocking = !Utils.isDefined(options.lockMode) || options.lockMode === LockMode.OPTIMISTIC;
630
+ const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
526
631
  if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
527
632
  return em.lockAndPopulate(meta, entity, where, options);
528
633
  }
@@ -530,21 +635,22 @@ export class EntityManager {
530
635
  options.populate = await em.preparePopulate(entityName, options);
531
636
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
532
637
  const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
533
- if (cached?.data) {
534
- await em.entityLoader.populate(entityName, [cached.data], options.populate, {
535
- ...options,
536
- ...em.getPopulateWhere(where, options),
537
- convertCustomTypes: false,
538
- ignoreLazyScalarProperties: true,
539
- lookup: false,
540
- });
638
+ if (cached?.data !== undefined) {
639
+ if (cached.data) {
640
+ await em.entityLoader.populate(entityName, [cached.data], options.populate, {
641
+ ...options,
642
+ ...em.getPopulateWhere(where, options),
643
+ ignoreLazyScalarProperties: true,
644
+ lookup: false,
645
+ });
646
+ }
541
647
  return cached.data;
542
648
  }
543
649
  options = { ...options };
544
650
  // save the original hint value so we know it was infer/all
545
651
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
546
652
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
547
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
653
+ options.populateFilter = await this.getJoinedFilters(meta, options);
548
654
  const data = await em.driver.findOne(entityName, where, {
549
655
  ctx: em.transactionContext,
550
656
  em,
@@ -655,24 +761,7 @@ export class EntityManager {
655
761
  }
656
762
  }
657
763
  }
658
- const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
659
- const propIndex = !isRaw(unique) && unique.findIndex(p => data[p] != null);
660
- if (options.onConflictFields || where == null) {
661
- if (propIndex !== false && propIndex >= 0) {
662
- where = { [unique[propIndex]]: data[unique[propIndex]] };
663
- }
664
- else if (meta.uniques.length > 0) {
665
- for (const u of meta.uniques) {
666
- if (Utils.asArray(u.properties).every(p => data[p] != null)) {
667
- where = Utils.asArray(u.properties).reduce((o, key) => {
668
- o[key] = data[key];
669
- return o;
670
- }, {});
671
- break;
672
- }
673
- }
674
- }
675
- }
764
+ where = getWhereCondition(meta, options.onConflictFields, data, where).where;
676
765
  data = QueryHelper.processObjectParams(data);
677
766
  em.validator.validateParams(data, 'insert data');
678
767
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
@@ -717,6 +806,7 @@ export class EntityManager {
717
806
  ctx: em.transactionContext,
718
807
  convertCustomTypes: true,
719
808
  connectionType: 'write',
809
+ schema: options.schema,
720
810
  });
721
811
  em.getHydrator().hydrate(entity, meta, data2, em.entityFactory, 'full', false, true);
722
812
  }
@@ -814,31 +904,17 @@ export class EntityManager {
814
904
  }
815
905
  }
816
906
  }
817
- const unique = meta.props.filter(p => p.unique).map(p => p.name);
818
- propIndex = unique.findIndex(p => row[p] != null);
819
- if (options.onConflictFields || where == null) {
820
- if (propIndex >= 0) {
821
- where = { [unique[propIndex]]: row[unique[propIndex]] };
822
- }
823
- else if (meta.uniques.length > 0) {
824
- for (const u of meta.uniques) {
825
- if (Utils.asArray(u.properties).every(p => row[p] != null)) {
826
- where = Utils.asArray(u.properties).reduce((o, key) => {
827
- o[key] = row[key];
828
- return o;
829
- }, {});
830
- break;
831
- }
832
- }
833
- }
834
- }
835
- row = QueryHelper.processObjectParams(row);
907
+ const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
908
+ propIndex = !isRaw(unique) && unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
909
+ const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
910
+ propIndex = tmp.propIndex;
836
911
  where = QueryHelper.processWhere({
837
- where,
912
+ where: tmp.where,
838
913
  entityName,
839
914
  metadata: this.metadata,
840
915
  platform: this.getPlatform(),
841
916
  });
917
+ row = QueryHelper.processObjectParams(row);
842
918
  em.validator.validateParams(row, 'insert data');
843
919
  allData.push(row);
844
920
  allWhere.push(where);
@@ -880,7 +956,7 @@ export class EntityManager {
880
956
  const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length);
881
957
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
882
958
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
883
- const add = new Set(propIndex >= 0 ? [unique[propIndex]] : []);
959
+ const add = new Set(propIndex !== false && propIndex >= 0 ? [unique[propIndex]] : []);
884
960
  for (const cond of loadPK.values()) {
885
961
  Utils.keys(cond).forEach(key => add.add(key));
886
962
  }
@@ -897,6 +973,7 @@ export class EntityManager {
897
973
  ctx: em.transactionContext,
898
974
  convertCustomTypes: true,
899
975
  connectionType: 'write',
976
+ schema: options.schema,
900
977
  });
901
978
  for (const [entity, cond] of loadPK.entries()) {
902
979
  const row = data2.find(row => {
@@ -953,45 +1030,37 @@ export class EntityManager {
953
1030
  }
954
1031
  /**
955
1032
  * Runs your callback wrapped inside a database transaction.
1033
+ *
1034
+ * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
1035
+ * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
1036
+ * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
1037
+ * method automatically creates an async context for the transaction.
1038
+ *
1039
+ * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
1040
+ * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
1041
+ * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
1042
+ * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
1043
+ *
1044
+ * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
1045
+ * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
1046
+ * and then call this method on the fork.
1047
+ *
1048
+ * **Example:**
1049
+ * ```ts
1050
+ * await em.transactional(async (em) => {
1051
+ * const author = new Author('Jon');
1052
+ * em.persist(author);
1053
+ * // flush is called automatically at the end of the callback
1054
+ * });
1055
+ * ```
956
1056
  */
957
1057
  async transactional(cb, options = {}) {
958
1058
  const em = this.getContext(false);
959
1059
  if (this.disableTransactions || em.disableTransactions) {
960
1060
  return cb(em);
961
1061
  }
962
- const fork = em.fork({
963
- clear: options.clear ?? false, // state will be merged once resolves
964
- flushMode: options.flushMode,
965
- cloneEventManager: true,
966
- disableTransactions: options.ignoreNestedTransactions,
967
- loggerContext: options.loggerContext,
968
- });
969
- options.ctx ??= em.transactionContext;
970
- const propagateToUpperContext = !em.global || this.config.get('allowGlobalContext');
971
- return TransactionContext.create(fork, async () => {
972
- return fork.getConnection().transactional(async (trx) => {
973
- fork.transactionContext = trx;
974
- if (propagateToUpperContext) {
975
- fork.eventManager.registerSubscriber({
976
- afterFlush(args) {
977
- args.uow.getChangeSets()
978
- .filter(cs => [ChangeSetType.DELETE, ChangeSetType.DELETE_EARLY].includes(cs.type))
979
- .forEach(cs => em.unitOfWork.unsetIdentity(cs.entity));
980
- },
981
- });
982
- }
983
- const ret = await cb(fork);
984
- await fork.flush();
985
- if (propagateToUpperContext) {
986
- // ensure all entities from inner context are merged to the upper one
987
- for (const entity of fork.unitOfWork.getIdentityMap()) {
988
- em.unitOfWork.register(entity);
989
- entity.__helper.__em = em;
990
- }
991
- }
992
- return ret;
993
- }, { ...options, eventBroadcaster: new TransactionEventBroadcaster(fork, { topLevelTransaction: !options.ctx }) });
994
- });
1062
+ const manager = new TransactionManager(this);
1063
+ return manager.handle(cb, options);
995
1064
  }
996
1065
  /**
997
1066
  * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions.
@@ -1171,22 +1240,35 @@ export class EntityManager {
1171
1240
  * via second parameter. By default, it will return already loaded entities without modifying them.
1172
1241
  */
1173
1242
  merge(entityName, data, options = {}) {
1174
- const em = this.getContext();
1175
1243
  if (Utils.isEntity(entityName)) {
1176
- return em.merge(entityName.constructor.name, entityName, data);
1244
+ return this.merge(entityName.constructor.name, entityName, data);
1177
1245
  }
1246
+ const em = options.disableContextResolution ? this : this.getContext();
1178
1247
  options.schema ??= em._schema;
1248
+ options.validate ??= true;
1249
+ options.cascade ??= true;
1179
1250
  entityName = Utils.className(entityName);
1180
- em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1251
+ if (options.validate) {
1252
+ em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1253
+ }
1181
1254
  let entity = em.unitOfWork.tryGetById(entityName, data, options.schema, false);
1182
1255
  if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) {
1183
1256
  return entity;
1184
1257
  }
1185
1258
  const meta = em.metadata.find(entityName);
1186
1259
  const childMeta = em.metadata.getByDiscriminatorColumn(meta, data);
1187
- entity = Utils.isEntity(data) ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1188
- em.validator.validate(entity, data, childMeta ?? meta);
1189
- em.unitOfWork.merge(entity);
1260
+ const dataIsEntity = Utils.isEntity(data);
1261
+ if (options.keepIdentity && entity && dataIsEntity && entity !== data) {
1262
+ helper(entity).__data = helper(data).__data;
1263
+ helper(entity).__originalEntityData = helper(data).__originalEntityData;
1264
+ return entity;
1265
+ }
1266
+ entity = dataIsEntity ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1267
+ if (options.validate) {
1268
+ em.validator.validate(entity, data, childMeta ?? meta);
1269
+ }
1270
+ const visited = options.cascade ? undefined : new Set([entity]);
1271
+ em.unitOfWork.merge(entity, visited);
1190
1272
  return entity;
1191
1273
  }
1192
1274
  /**
@@ -1211,6 +1293,7 @@ export class EntityManager {
1211
1293
  ...options,
1212
1294
  newEntity: !options.managed,
1213
1295
  merge: options.managed,
1296
+ normalizeAccessors: true,
1214
1297
  });
1215
1298
  options.persist ??= em.config.get('persistOnCreate');
1216
1299
  if (options.persist && !this.getMetadata(entityName).embeddable) {
@@ -1249,10 +1332,8 @@ export class EntityManager {
1249
1332
  async count(entityName, where = {}, options = {}) {
1250
1333
  const em = this.getContext(false);
1251
1334
  // Shallow copy options since the object will be modified when deleting orderBy
1252
- options = {
1253
- schema: em._schema,
1254
- ...options,
1255
- };
1335
+ options = { ...options };
1336
+ em.prepareOptions(options);
1256
1337
  entityName = Utils.className(entityName);
1257
1338
  await em.tryFlush(entityName, options);
1258
1339
  where = await em.processWhere(entityName, where, options, 'read');
@@ -1262,15 +1343,15 @@ export class EntityManager {
1262
1343
  const meta = em.metadata.find(entityName);
1263
1344
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
1264
1345
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
1265
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
1346
+ options.populateFilter = await this.getJoinedFilters(meta, options);
1266
1347
  em.validator.validateParams(where);
1267
1348
  delete options.orderBy;
1268
1349
  const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
1269
1350
  const cached = await em.tryCache(entityName, options.cache, cacheKey);
1270
- if (cached?.data) {
1351
+ if (cached?.data !== undefined) {
1271
1352
  return cached.data;
1272
1353
  }
1273
- const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, ...options });
1354
+ const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, em, ...options });
1274
1355
  await em.storeCache(options.cache, cached, () => +count);
1275
1356
  return +count;
1276
1357
  }
@@ -1297,13 +1378,6 @@ export class EntityManager {
1297
1378
  }
1298
1379
  return this;
1299
1380
  }
1300
- /**
1301
- * Persists your entity immediately, flushing all not yet persisted changes to the database too.
1302
- * Equivalent to `em.persist(e).flush()`.
1303
- */
1304
- async persistAndFlush(entity) {
1305
- await this.persist(entity).flush();
1306
- }
1307
1381
  /**
1308
1382
  * Marks entity for removal.
1309
1383
  * A removed entity will be removed from the database at or before transaction commit or as a result of the flush operation.
@@ -1327,13 +1401,6 @@ export class EntityManager {
1327
1401
  }
1328
1402
  return em;
1329
1403
  }
1330
- /**
1331
- * Removes an entity instance immediately, flushing all not yet persisted changes to the database too.
1332
- * Equivalent to `em.remove(e).flush()`
1333
- */
1334
- async removeAndFlush(entity) {
1335
- await this.remove(entity).flush();
1336
- }
1337
1404
  /**
1338
1405
  * Flushes all changes to objects that have been queued up to now to the database.
1339
1406
  * This effectively synchronizes the in-memory state of managed objects with the database.
@@ -1396,7 +1463,7 @@ export class EntityManager {
1396
1463
  const em = this.getContext();
1397
1464
  em.prepareOptions(options);
1398
1465
  const entityName = arr[0].constructor.name;
1399
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate }, options.validate);
1466
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1400
1467
  await em.entityLoader.populate(entityName, arr, preparedPopulate, options);
1401
1468
  return entities;
1402
1469
  }
@@ -1432,6 +1499,9 @@ export class EntityManager {
1432
1499
  for (const entity of em.unitOfWork.getIdentityMap()) {
1433
1500
  fork.unitOfWork.register(entity);
1434
1501
  }
1502
+ for (const entity of em.unitOfWork.getPersistStack()) {
1503
+ fork.unitOfWork.persist(entity);
1504
+ }
1435
1505
  for (const entity of em.unitOfWork.getOrphanRemoveStack()) {
1436
1506
  fork.unitOfWork.getOrphanRemoveStack().add(entity);
1437
1507
  }
@@ -1453,6 +1523,12 @@ export class EntityManager {
1453
1523
  getEntityFactory() {
1454
1524
  return this.getContext().entityFactory;
1455
1525
  }
1526
+ /**
1527
+ * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
1528
+ */
1529
+ getEntityLoader() {
1530
+ return this.getContext().entityLoader;
1531
+ }
1456
1532
  /**
1457
1533
  * Gets the Hydrator used by the EntityManager.
1458
1534
  */
@@ -1549,7 +1625,6 @@ export class EntityManager {
1549
1625
  ...options,
1550
1626
  ...this.getPopulateWhere(where, options),
1551
1627
  orderBy: options.populateOrderBy ?? options.orderBy,
1552
- convertCustomTypes: false,
1553
1628
  ignoreLazyScalarProperties: true,
1554
1629
  lookup: false,
1555
1630
  });
@@ -1622,7 +1697,7 @@ export class EntityManager {
1622
1697
  options.flags.push(QueryFlag.INFER_POPULATE);
1623
1698
  return [];
1624
1699
  }
1625
- if (Utils.isString(field)) {
1700
+ if (typeof field === 'string') {
1626
1701
  return [{ field, strategy: options.strategy }];
1627
1702
  }
1628
1703
  return [field];
@@ -1672,7 +1747,7 @@ export class EntityManager {
1672
1747
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1673
1748
  }
1674
1749
  options.schema ??= this._schema;
1675
- options.logging = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1750
+ options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1676
1751
  }
1677
1752
  /**
1678
1753
  * @internal
@@ -1696,31 +1771,31 @@ export class EntityManager {
1696
1771
  const em = this.getContext();
1697
1772
  const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
1698
1773
  const cached = await em.resultCache.get(cacheKey);
1699
- if (cached) {
1700
- let data;
1701
- if (Array.isArray(cached) && merge) {
1702
- data = cached.map(item => em.entityFactory.create(entityName, item, {
1703
- merge: true,
1704
- convertCustomTypes: true,
1705
- refresh,
1706
- recomputeSnapshot: true,
1707
- }));
1708
- }
1709
- else if (Utils.isObject(cached) && merge) {
1710
- data = em.entityFactory.create(entityName, cached, {
1711
- merge: true,
1712
- convertCustomTypes: true,
1713
- refresh,
1714
- recomputeSnapshot: true,
1715
- });
1716
- }
1717
- else {
1718
- data = cached;
1719
- }
1720
- await em.unitOfWork.dispatchOnLoadEvent();
1721
- return { key: cacheKey, data };
1774
+ if (!cached) {
1775
+ return { key: cacheKey, data: cached };
1722
1776
  }
1723
- return { key: cacheKey };
1777
+ let data;
1778
+ if (Array.isArray(cached) && merge) {
1779
+ data = cached.map(item => em.entityFactory.create(entityName, item, {
1780
+ merge: true,
1781
+ convertCustomTypes: true,
1782
+ refresh,
1783
+ recomputeSnapshot: true,
1784
+ }));
1785
+ }
1786
+ else if (Utils.isObject(cached) && merge) {
1787
+ data = em.entityFactory.create(entityName, cached, {
1788
+ merge: true,
1789
+ convertCustomTypes: true,
1790
+ refresh,
1791
+ recomputeSnapshot: true,
1792
+ });
1793
+ }
1794
+ else {
1795
+ data = cached;
1796
+ }
1797
+ await em.unitOfWork.dispatchOnLoadEvent();
1798
+ return { key: cacheKey, data };
1724
1799
  }
1725
1800
  /**
1726
1801
  * @internal
@@ -1729,7 +1804,7 @@ export class EntityManager {
1729
1804
  config ??= this.config.get('resultCache').global;
1730
1805
  if (config) {
1731
1806
  const em = this.getContext();
1732
- const expiration = Array.isArray(config) ? config[1] : (Utils.isNumber(config) ? config : undefined);
1807
+ const expiration = Array.isArray(config) ? config[1] : (typeof config === 'number' ? config : undefined);
1733
1808
  await em.resultCache.set(key.key, data instanceof Function ? data() : data, '', expiration);
1734
1809
  }
1735
1810
  }
@@ -1762,6 +1837,19 @@ export class EntityManager {
1762
1837
  set schema(schema) {
1763
1838
  this.getContext(false)._schema = schema ?? undefined;
1764
1839
  }
1840
+ /** @internal */
1841
+ async getDataLoader(type) {
1842
+ const em = this.getContext();
1843
+ if (em.loaders[type]) {
1844
+ return em.loaders[type];
1845
+ }
1846
+ const DataLoader = await DataloaderUtils.getDataLoader();
1847
+ switch (type) {
1848
+ case 'ref': return (em.loaders[type] ??= new DataLoader(DataloaderUtils.getRefBatchLoadFn(em)));
1849
+ case '1:m': return (em.loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
1850
+ case 'm:n': return (em.loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
1851
+ }
1852
+ }
1765
1853
  /**
1766
1854
  * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
1767
1855
  * if executed inside request context handler.