@mikro-orm/core 7.0.0-dev.4 → 7.0.0-dev.40

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 (117) hide show
  1. package/EntityManager.d.ts +84 -18
  2. package/EntityManager.js +265 -172
  3. package/MikroORM.d.ts +7 -5
  4. package/MikroORM.js +0 -1
  5. package/README.md +3 -2
  6. package/cache/FileCacheAdapter.d.ts +2 -1
  7. package/cache/FileCacheAdapter.js +6 -4
  8. package/connections/Connection.d.ts +4 -2
  9. package/connections/Connection.js +2 -2
  10. package/decorators/Check.d.ts +2 -2
  11. package/decorators/Embeddable.d.ts +5 -5
  12. package/decorators/Embeddable.js +1 -1
  13. package/decorators/Embedded.d.ts +6 -12
  14. package/decorators/Entity.d.ts +18 -3
  15. package/decorators/Enum.d.ts +1 -1
  16. package/decorators/Formula.d.ts +1 -2
  17. package/decorators/Indexed.d.ts +2 -2
  18. package/decorators/ManyToMany.d.ts +4 -2
  19. package/decorators/ManyToOne.d.ts +6 -2
  20. package/decorators/OneToMany.d.ts +4 -4
  21. package/decorators/OneToOne.d.ts +5 -1
  22. package/decorators/PrimaryKey.d.ts +2 -3
  23. package/decorators/Property.d.ts +54 -4
  24. package/decorators/Transactional.d.ts +1 -0
  25. package/decorators/Transactional.js +3 -3
  26. package/decorators/index.d.ts +1 -1
  27. package/drivers/DatabaseDriver.d.ts +4 -3
  28. package/drivers/IDatabaseDriver.d.ts +22 -2
  29. package/entity/ArrayCollection.d.ts +4 -2
  30. package/entity/ArrayCollection.js +18 -6
  31. package/entity/Collection.d.ts +1 -2
  32. package/entity/Collection.js +19 -10
  33. package/entity/EntityAssigner.d.ts +1 -1
  34. package/entity/EntityAssigner.js +9 -1
  35. package/entity/EntityFactory.d.ts +7 -0
  36. package/entity/EntityFactory.js +29 -9
  37. package/entity/EntityHelper.js +25 -3
  38. package/entity/EntityLoader.d.ts +5 -4
  39. package/entity/EntityLoader.js +74 -37
  40. package/entity/EntityRepository.d.ts +1 -1
  41. package/entity/EntityValidator.js +1 -1
  42. package/entity/Reference.d.ts +9 -7
  43. package/entity/Reference.js +30 -3
  44. package/entity/WrappedEntity.js +1 -1
  45. package/entity/defineEntity.d.ts +561 -0
  46. package/entity/defineEntity.js +537 -0
  47. package/entity/index.d.ts +2 -0
  48. package/entity/index.js +2 -0
  49. package/entity/utils.d.ts +7 -0
  50. package/entity/utils.js +15 -3
  51. package/enums.d.ts +16 -3
  52. package/enums.js +13 -0
  53. package/errors.d.ts +6 -0
  54. package/errors.js +14 -0
  55. package/events/EventSubscriber.d.ts +3 -1
  56. package/hydration/ObjectHydrator.d.ts +4 -4
  57. package/hydration/ObjectHydrator.js +35 -24
  58. package/index.d.ts +2 -1
  59. package/index.js +1 -1
  60. package/logging/DefaultLogger.d.ts +1 -1
  61. package/logging/SimpleLogger.d.ts +1 -1
  62. package/metadata/EntitySchema.d.ts +8 -4
  63. package/metadata/EntitySchema.js +39 -19
  64. package/metadata/MetadataDiscovery.d.ts +1 -1
  65. package/metadata/MetadataDiscovery.js +88 -32
  66. package/metadata/MetadataStorage.js +1 -1
  67. package/metadata/MetadataValidator.js +4 -3
  68. package/naming-strategy/AbstractNamingStrategy.d.ts +5 -1
  69. package/naming-strategy/AbstractNamingStrategy.js +7 -1
  70. package/naming-strategy/NamingStrategy.d.ts +11 -1
  71. package/package.json +5 -5
  72. package/platforms/Platform.d.ts +5 -3
  73. package/platforms/Platform.js +4 -8
  74. package/serialization/EntitySerializer.d.ts +2 -0
  75. package/serialization/EntitySerializer.js +2 -2
  76. package/serialization/EntityTransformer.js +1 -1
  77. package/serialization/SerializationContext.js +14 -11
  78. package/types/BigIntType.d.ts +9 -6
  79. package/types/BigIntType.js +3 -0
  80. package/types/BooleanType.d.ts +1 -1
  81. package/types/DecimalType.d.ts +6 -4
  82. package/types/DecimalType.js +1 -1
  83. package/types/DoubleType.js +1 -1
  84. package/types/JsonType.d.ts +1 -1
  85. package/types/JsonType.js +7 -2
  86. package/types/Type.d.ts +2 -1
  87. package/types/Type.js +1 -1
  88. package/types/index.d.ts +1 -1
  89. package/typings.d.ts +88 -39
  90. package/typings.js +24 -4
  91. package/unit-of-work/ChangeSetComputer.js +3 -1
  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 +109 -41
  96. package/utils/Configuration.d.ts +23 -5
  97. package/utils/Configuration.js +17 -3
  98. package/utils/ConfigurationLoader.d.ts +0 -2
  99. package/utils/ConfigurationLoader.js +2 -24
  100. package/utils/Cursor.d.ts +3 -3
  101. package/utils/Cursor.js +3 -0
  102. package/utils/DataloaderUtils.d.ts +7 -2
  103. package/utils/DataloaderUtils.js +38 -7
  104. package/utils/EntityComparator.d.ts +6 -2
  105. package/utils/EntityComparator.js +104 -58
  106. package/utils/QueryHelper.d.ts +9 -1
  107. package/utils/QueryHelper.js +66 -5
  108. package/utils/RawQueryFragment.d.ts +36 -2
  109. package/utils/RawQueryFragment.js +35 -1
  110. package/utils/TransactionManager.d.ts +65 -0
  111. package/utils/TransactionManager.js +218 -0
  112. package/utils/Utils.d.ts +11 -5
  113. package/utils/Utils.js +76 -33
  114. package/utils/index.d.ts +1 -0
  115. package/utils/index.js +1 -0
  116. package/utils/upsert-utils.d.ts +7 -2
  117. package/utils/upsert-utils.js +52 -1
package/EntityManager.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { inspect } from 'node:util';
2
2
  import DataLoader from 'dataloader';
3
- import { getOnConflictReturningFields } from './utils/upsert-utils.js';
3
+ import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
4
4
  import { Utils } from './utils/Utils.js';
5
5
  import { Cursor } from './utils/Cursor.js';
6
6
  import { DataloaderUtils } from './utils/DataloaderUtils.js';
@@ -19,6 +19,8 @@ import { EventType, FlushMode, LoadStrategy, LockMode, PopulateHint, PopulatePat
19
19
  import { EventManager } from './events/EventManager.js';
20
20
  import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaster.js';
21
21
  import { OptimisticLockError, ValidationError } from './errors.js';
22
+ import { getLoadingStrategy } from './entity/utils.js';
23
+ import { TransactionManager } from './utils/TransactionManager.js';
22
24
  /**
23
25
  * The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
24
26
  * such as UnitOfWork, Query Language, and Repository API.
@@ -36,6 +38,7 @@ export class EntityManager {
36
38
  name;
37
39
  refLoader = new DataLoader(DataloaderUtils.getRefBatchLoadFn(this));
38
40
  colLoader = new DataLoader(DataloaderUtils.getColBatchLoadFn(this));
41
+ colLoaderMtoN = new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(this));
39
42
  validator;
40
43
  repositoryMap = {};
41
44
  entityLoader;
@@ -137,7 +140,6 @@ export class EntityManager {
137
140
  await em.entityLoader.populate(entityName, cached.data, populate, {
138
141
  ...options,
139
142
  ...em.getPopulateWhere(where, options),
140
- convertCustomTypes: false,
141
143
  ignoreLazyScalarProperties: true,
142
144
  lookup: false,
143
145
  });
@@ -148,8 +150,8 @@ export class EntityManager {
148
150
  // save the original hint value so we know it was infer/all
149
151
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
150
152
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
151
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
152
- const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, ...options });
153
+ options.populateFilter = await this.getJoinedFilters(meta, options);
154
+ const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, em, ...options });
153
155
  if (results.length === 0) {
154
156
  await em.storeCache(options.cache, cached, []);
155
157
  return [];
@@ -168,7 +170,6 @@ export class EntityManager {
168
170
  await em.entityLoader.populate(entityName, unique, populate, {
169
171
  ...options,
170
172
  ...em.getPopulateWhere(where, options),
171
- convertCustomTypes: false,
172
173
  ignoreLazyScalarProperties: true,
173
174
  lookup: false,
174
175
  });
@@ -181,6 +182,61 @@ export class EntityManager {
181
182
  }
182
183
  return unique;
183
184
  }
185
+ /**
186
+ * Finds all entities and returns an async iterable (async generator) that yields results one by one.
187
+ * The results are merged and mapped to entity instances, without adding them to the identity map.
188
+ * You can disable merging by passing the options `{ mergeResults: false }`.
189
+ * With `mergeResults` disabled, to-many collections will contain at most one item, and you will get duplicate
190
+ * root entities when there are multiple items in the populated collection.
191
+ * This is useful for processing large datasets without loading everything into memory at once.
192
+ *
193
+ * ```ts
194
+ * const stream = em.stream(Book, { populate: ['author'] });
195
+ *
196
+ * for await (const book of stream) {
197
+ * // book is an instance of Book entity
198
+ * console.log(book.title, book.author.name);
199
+ * }
200
+ * ```
201
+ */
202
+ async *stream(entityName, options = {}) {
203
+ const em = this.getContext();
204
+ em.prepareOptions(options);
205
+ options.strategy = 'joined';
206
+ await em.tryFlush(entityName, options);
207
+ entityName = Utils.className(entityName);
208
+ const where = await em.processWhere(entityName, options.where ?? {}, options, 'read');
209
+ em.validator.validateParams(where);
210
+ options.orderBy = options.orderBy || {};
211
+ options.populate = await em.preparePopulate(entityName, options);
212
+ const meta = this.metadata.get(entityName);
213
+ options = { ...options };
214
+ // save the original hint value so we know it was infer/all
215
+ options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
216
+ options.populateWhere = this.createPopulateWhere({ ...where }, options);
217
+ options.populateFilter = await this.getJoinedFilters(meta, options);
218
+ const stream = em.driver.stream(entityName, where, {
219
+ ctx: em.transactionContext,
220
+ mapResults: false,
221
+ ...options,
222
+ });
223
+ for await (const data of stream) {
224
+ const fork = em.fork();
225
+ const entity = fork.entityFactory.create(entityName, data, {
226
+ refresh: options.refresh,
227
+ schema: options.schema,
228
+ convertCustomTypes: true,
229
+ });
230
+ helper(entity).setSerializationContext({
231
+ populate: options.populate,
232
+ fields: options.fields,
233
+ exclude: options.exclude,
234
+ });
235
+ await fork.unitOfWork.dispatchOnLoadEvent();
236
+ fork.clear();
237
+ yield entity;
238
+ }
239
+ }
184
240
  /**
185
241
  * Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
186
242
  */
@@ -203,8 +259,8 @@ export class EntityManager {
203
259
  /**
204
260
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
205
261
  */
206
- addFilter(name, cond, entityName, enabled = true) {
207
- const options = { name, cond, default: enabled };
262
+ addFilter(name, cond, entityName, options = true) {
263
+ options = typeof options === 'object' ? { name, cond, default: true, ...options } : { name, cond, default: options };
208
264
  if (entityName) {
209
265
  options.entity = Utils.asArray(entityName).map(n => Utils.className(n));
210
266
  }
@@ -232,8 +288,8 @@ export class EntityManager {
232
288
  /**
233
289
  * Gets logger context for this entity manager.
234
290
  */
235
- getLoggerContext() {
236
- const em = this.getContext();
291
+ getLoggerContext(options) {
292
+ const em = options?.disableContextResolution ? this : this.getContext();
237
293
  em.loggerContext ??= {};
238
294
  return em.loggerContext;
239
295
  }
@@ -283,28 +339,39 @@ export class EntityManager {
283
339
  }
284
340
  return ret;
285
341
  }
286
- async getJoinedFilters(meta, cond, options) {
342
+ async getJoinedFilters(meta, options) {
343
+ if (!this.config.get('filtersOnRelations') || !options.populate) {
344
+ return undefined;
345
+ }
287
346
  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;
347
+ for (const hint of options.populate) {
348
+ const field = hint.field.split(':')[0];
349
+ const prop = meta.properties[field];
350
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
351
+ const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
352
+ if (!joined && !hint.filter) {
353
+ continue;
354
+ }
355
+ const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
356
+ const where = await this.applyFilters(prop.type, {}, filters, 'read', {
357
+ ...options,
358
+ populate: hint.children,
359
+ });
360
+ const where2 = await this.getJoinedFilters(prop.targetMeta, {
361
+ ...options,
362
+ filters,
363
+ populate: hint.children,
364
+ populateWhere: PopulateHint.ALL,
365
+ });
366
+ if (Utils.hasObjectKeys(where)) {
367
+ ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
368
+ }
369
+ if (where2 && Utils.hasObjectKeys(where2)) {
370
+ if (ret[field]) {
371
+ Utils.merge(ret[field], where2);
300
372
  }
301
- if (Utils.hasObjectKeys(where2)) {
302
- if (ret[field]) {
303
- Utils.merge(ret[field], where2);
304
- }
305
- else {
306
- ret[field] = where2;
307
- }
373
+ else {
374
+ ret[field] = where2;
308
375
  }
309
376
  }
310
377
  }
@@ -313,27 +380,45 @@ export class EntityManager {
313
380
  /**
314
381
  * 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
382
  */
316
- async autoJoinRefsForFilters(meta, options) {
317
- if (!meta || !this.config.get('autoJoinRefsForFilters')) {
383
+ async autoJoinRefsForFilters(meta, options, parent) {
384
+ if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) {
318
385
  return;
319
386
  }
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
387
  const ret = options.populate;
325
- for (const prop of props) {
326
- const cond = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', options);
388
+ for (const prop of meta.relations) {
389
+ if (prop.object
390
+ || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
391
+ || !((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
392
+ || (parent?.className === prop.targetMeta.root.className && parent.propName === prop.inversedBy)) {
393
+ continue;
394
+ }
395
+ options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
396
+ const cond = await this.applyFilters(prop.type, {}, options.filters, 'read', options);
327
397
  if (!Utils.isEmpty(cond)) {
328
398
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
329
- if (populated.length > 0) {
330
- populated.forEach(hint => hint.filter = true);
399
+ let found = false;
400
+ for (const hint of populated) {
401
+ if (!hint.all) {
402
+ hint.filter = true;
403
+ }
404
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
405
+ if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
406
+ found = true;
407
+ }
331
408
  }
332
- else {
409
+ if (!found) {
333
410
  ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true });
334
411
  }
335
412
  }
336
413
  }
414
+ for (const hint of ret) {
415
+ const [field, ref] = hint.field.split(':');
416
+ const prop = meta?.properties[field];
417
+ if (prop && !ref) {
418
+ hint.children ??= [];
419
+ await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { className: meta.root.className, propName: prop.name });
420
+ }
421
+ }
337
422
  }
338
423
  /**
339
424
  * @internal
@@ -363,7 +448,7 @@ export class EntityManager {
363
448
  let cond;
364
449
  if (filter.cond instanceof Function) {
365
450
  // @ts-ignore
366
- const args = Utils.isPlainObject(options[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
451
+ const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
367
452
  if (!args && filter.cond.length > 0 && filter.args !== false) {
368
453
  throw new Error(`No arguments provided for filter '${filter.name}'`);
369
454
  }
@@ -372,13 +457,17 @@ export class EntityManager {
372
457
  else {
373
458
  cond = filter.cond;
374
459
  }
375
- ret.push(QueryHelper.processWhere({
460
+ cond = QueryHelper.processWhere({
376
461
  where: cond,
377
462
  entityName,
378
463
  metadata: this.metadata,
379
464
  platform: this.driver.getPlatform(),
380
465
  aliased: type === 'read',
381
- }));
466
+ });
467
+ if (filter.strict) {
468
+ Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false });
469
+ }
470
+ ret.push(cond);
382
471
  }
383
472
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c));
384
473
  return conds.length > 1 ? { $and: conds } : conds[0];
@@ -433,6 +522,10 @@ export class EntityManager {
433
522
  * });
434
523
  * ```
435
524
  *
525
+ * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
526
+ * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
527
+ * of pages.
528
+ *
436
529
  * The `Cursor` object provides the following interface:
437
530
  *
438
531
  * ```ts
@@ -442,7 +535,7 @@ export class EntityManager {
442
535
  * User { ... },
443
536
  * User { ... },
444
537
  * ],
445
- * totalCount: 50,
538
+ * totalCount: 50, // not included if `includeCount: false`
446
539
  * startCursor: 'WzRd',
447
540
  * endCursor: 'WzZd',
448
541
  * hasPrevPage: true,
@@ -457,7 +550,9 @@ export class EntityManager {
457
550
  if (Utils.isEmpty(options.orderBy)) {
458
551
  throw new Error('Explicit `orderBy` option required');
459
552
  }
460
- const [entities, count] = await em.findAndCount(entityName, where, options);
553
+ const [entities, count] = options.includeCount !== false
554
+ ? await em.findAndCount(entityName, where, options)
555
+ : [await em.find(entityName, where, options)];
461
556
  return new Cursor(entities, count, options, this.metadata.get(entityName));
462
557
  }
463
558
  /**
@@ -483,18 +578,31 @@ export class EntityManager {
483
578
  async refresh(entity, options = {}) {
484
579
  const fork = this.fork({ keepTransactionContext: true });
485
580
  const entityName = entity.constructor.name;
581
+ const wrapped = helper(entity);
486
582
  const reloaded = await fork.findOne(entityName, entity, {
487
- schema: helper(entity).__schema,
583
+ schema: wrapped.__schema,
488
584
  ...options,
489
585
  flushMode: FlushMode.COMMIT,
490
586
  });
491
- if (reloaded) {
492
- this.config.getHydrator(this.metadata).hydrate(entity, helper(entity).__meta, helper(reloaded).toPOJO(), this.getEntityFactory(), 'full');
587
+ const em = this.getContext();
588
+ if (!reloaded) {
589
+ em.unitOfWork.unsetIdentity(entity);
590
+ return null;
493
591
  }
494
- else {
495
- this.getUnitOfWork().unsetIdentity(entity);
592
+ let found = false;
593
+ for (const e of fork.unitOfWork.getIdentityMap()) {
594
+ const ref = em.getReference(e.constructor.name, helper(e).getPrimaryKey());
595
+ const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true });
596
+ em.config.getHydrator(this.metadata).hydrate(ref, helper(ref).__meta, data, em.entityFactory, 'full', false, true);
597
+ Utils.merge(helper(ref).__originalEntityData, this.comparator.prepareEntity(e));
598
+ found ||= ref === entity;
496
599
  }
497
- return reloaded ? entity : reloaded;
600
+ if (!found) {
601
+ const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true });
602
+ em.config.getHydrator(this.metadata).hydrate(entity, wrapped.__meta, data, em.entityFactory, 'full', false, true);
603
+ Utils.merge(wrapped.__originalEntityData, this.comparator.prepareEntity(reloaded));
604
+ }
605
+ return entity;
498
606
  }
499
607
  /**
500
608
  * Finds first entity matching your `where` query.
@@ -530,23 +638,25 @@ export class EntityManager {
530
638
  options.populate = await em.preparePopulate(entityName, options);
531
639
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
532
640
  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
- });
641
+ if (cached?.data !== undefined) {
642
+ if (cached.data) {
643
+ await em.entityLoader.populate(entityName, [cached.data], options.populate, {
644
+ ...options,
645
+ ...em.getPopulateWhere(where, options),
646
+ ignoreLazyScalarProperties: true,
647
+ lookup: false,
648
+ });
649
+ }
541
650
  return cached.data;
542
651
  }
543
652
  options = { ...options };
544
653
  // save the original hint value so we know it was infer/all
545
654
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
546
655
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
547
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
656
+ options.populateFilter = await this.getJoinedFilters(meta, options);
548
657
  const data = await em.driver.findOne(entityName, where, {
549
658
  ctx: em.transactionContext,
659
+ em,
550
660
  ...options,
551
661
  });
552
662
  if (!data) {
@@ -654,24 +764,7 @@ export class EntityManager {
654
764
  }
655
765
  }
656
766
  }
657
- const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
658
- const propIndex = !isRaw(unique) && unique.findIndex(p => data[p] != null);
659
- if (options.onConflictFields || where == null) {
660
- if (propIndex !== false && propIndex >= 0) {
661
- where = { [unique[propIndex]]: data[unique[propIndex]] };
662
- }
663
- else if (meta.uniques.length > 0) {
664
- for (const u of meta.uniques) {
665
- if (Utils.asArray(u.properties).every(p => data[p] != null)) {
666
- where = Utils.asArray(u.properties).reduce((o, key) => {
667
- o[key] = data[key];
668
- return o;
669
- }, {});
670
- break;
671
- }
672
- }
673
- }
674
- }
767
+ where = getWhereCondition(meta, options.onConflictFields, data, where).where;
675
768
  data = QueryHelper.processObjectParams(data);
676
769
  em.validator.validateParams(data, 'insert data');
677
770
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
@@ -716,6 +809,7 @@ export class EntityManager {
716
809
  ctx: em.transactionContext,
717
810
  convertCustomTypes: true,
718
811
  connectionType: 'write',
812
+ schema: options.schema,
719
813
  });
720
814
  em.getHydrator().hydrate(entity, meta, data2, em.entityFactory, 'full', false, true);
721
815
  }
@@ -813,31 +907,17 @@ export class EntityManager {
813
907
  }
814
908
  }
815
909
  }
816
- const unique = meta.props.filter(p => p.unique).map(p => p.name);
817
- propIndex = unique.findIndex(p => row[p] != null);
818
- if (options.onConflictFields || where == null) {
819
- if (propIndex >= 0) {
820
- where = { [unique[propIndex]]: row[unique[propIndex]] };
821
- }
822
- else if (meta.uniques.length > 0) {
823
- for (const u of meta.uniques) {
824
- if (Utils.asArray(u.properties).every(p => row[p] != null)) {
825
- where = Utils.asArray(u.properties).reduce((o, key) => {
826
- o[key] = row[key];
827
- return o;
828
- }, {});
829
- break;
830
- }
831
- }
832
- }
833
- }
834
- row = QueryHelper.processObjectParams(row);
910
+ const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
911
+ propIndex = !isRaw(unique) && unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
912
+ const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
913
+ propIndex = tmp.propIndex;
835
914
  where = QueryHelper.processWhere({
836
- where,
915
+ where: tmp.where,
837
916
  entityName,
838
917
  metadata: this.metadata,
839
918
  platform: this.getPlatform(),
840
919
  });
920
+ row = QueryHelper.processObjectParams(row);
841
921
  em.validator.validateParams(row, 'insert data');
842
922
  allData.push(row);
843
923
  allWhere.push(where);
@@ -879,7 +959,7 @@ export class EntityManager {
879
959
  const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length);
880
960
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
881
961
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
882
- const add = new Set(propIndex >= 0 ? [unique[propIndex]] : []);
962
+ const add = new Set(propIndex !== false && propIndex >= 0 ? [unique[propIndex]] : []);
883
963
  for (const cond of loadPK.values()) {
884
964
  Utils.keys(cond).forEach(key => add.add(key));
885
965
  }
@@ -896,6 +976,7 @@ export class EntityManager {
896
976
  ctx: em.transactionContext,
897
977
  convertCustomTypes: true,
898
978
  connectionType: 'write',
979
+ schema: options.schema,
899
980
  });
900
981
  for (const [entity, cond] of loadPK.entries()) {
901
982
  const row = data2.find(row => {
@@ -952,45 +1033,37 @@ export class EntityManager {
952
1033
  }
953
1034
  /**
954
1035
  * Runs your callback wrapped inside a database transaction.
1036
+ *
1037
+ * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
1038
+ * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
1039
+ * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
1040
+ * method automatically creates an async context for the transaction.
1041
+ *
1042
+ * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
1043
+ * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
1044
+ * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
1045
+ * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
1046
+ *
1047
+ * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
1048
+ * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
1049
+ * and then call this method on the fork.
1050
+ *
1051
+ * **Example:**
1052
+ * ```ts
1053
+ * await em.transactional(async (em) => {
1054
+ * const author = new Author('Jon');
1055
+ * em.persist(author);
1056
+ * // flush is called automatically at the end of the callback
1057
+ * });
1058
+ * ```
955
1059
  */
956
1060
  async transactional(cb, options = {}) {
957
1061
  const em = this.getContext(false);
958
1062
  if (this.disableTransactions || em.disableTransactions) {
959
1063
  return cb(em);
960
1064
  }
961
- const fork = em.fork({
962
- clear: options.clear ?? false, // state will be merged once resolves
963
- flushMode: options.flushMode,
964
- cloneEventManager: true,
965
- disableTransactions: options.ignoreNestedTransactions,
966
- loggerContext: options.loggerContext,
967
- });
968
- options.ctx ??= em.transactionContext;
969
- const propagateToUpperContext = !em.global || this.config.get('allowGlobalContext');
970
- return TransactionContext.create(fork, async () => {
971
- return fork.getConnection().transactional(async (trx) => {
972
- fork.transactionContext = trx;
973
- if (propagateToUpperContext) {
974
- fork.eventManager.registerSubscriber({
975
- afterFlush(args) {
976
- args.uow.getChangeSets()
977
- .filter(cs => [ChangeSetType.DELETE, ChangeSetType.DELETE_EARLY].includes(cs.type))
978
- .forEach(cs => em.unitOfWork.unsetIdentity(cs.entity));
979
- },
980
- });
981
- }
982
- const ret = await cb(fork);
983
- await fork.flush();
984
- if (propagateToUpperContext) {
985
- // ensure all entities from inner context are merged to the upper one
986
- for (const entity of fork.unitOfWork.getIdentityMap()) {
987
- em.unitOfWork.register(entity);
988
- entity.__helper.__em = em;
989
- }
990
- }
991
- return ret;
992
- }, { ...options, eventBroadcaster: new TransactionEventBroadcaster(fork, { topLevelTransaction: !options.ctx }) });
993
- });
1065
+ const manager = new TransactionManager(this);
1066
+ return manager.handle(cb, options);
994
1067
  }
995
1068
  /**
996
1069
  * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions.
@@ -1170,22 +1243,35 @@ export class EntityManager {
1170
1243
  * via second parameter. By default, it will return already loaded entities without modifying them.
1171
1244
  */
1172
1245
  merge(entityName, data, options = {}) {
1173
- const em = this.getContext();
1174
1246
  if (Utils.isEntity(entityName)) {
1175
- return em.merge(entityName.constructor.name, entityName, data);
1247
+ return this.merge(entityName.constructor.name, entityName, data);
1176
1248
  }
1249
+ const em = options.disableContextResolution ? this : this.getContext();
1177
1250
  options.schema ??= em._schema;
1251
+ options.validate ??= true;
1252
+ options.cascade ??= true;
1178
1253
  entityName = Utils.className(entityName);
1179
- em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1254
+ if (options.validate) {
1255
+ em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1256
+ }
1180
1257
  let entity = em.unitOfWork.tryGetById(entityName, data, options.schema, false);
1181
1258
  if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) {
1182
1259
  return entity;
1183
1260
  }
1184
1261
  const meta = em.metadata.find(entityName);
1185
1262
  const childMeta = em.metadata.getByDiscriminatorColumn(meta, data);
1186
- entity = Utils.isEntity(data) ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1187
- em.validator.validate(entity, data, childMeta ?? meta);
1188
- em.unitOfWork.merge(entity);
1263
+ const dataIsEntity = Utils.isEntity(data);
1264
+ if (options.keepIdentity && entity && dataIsEntity && entity !== data) {
1265
+ helper(entity).__data = helper(data).__data;
1266
+ helper(entity).__originalEntityData = helper(data).__originalEntityData;
1267
+ return entity;
1268
+ }
1269
+ entity = dataIsEntity ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1270
+ if (options.validate) {
1271
+ em.validator.validate(entity, data, childMeta ?? meta);
1272
+ }
1273
+ const visited = options.cascade ? undefined : new Set([entity]);
1274
+ em.unitOfWork.merge(entity, visited);
1189
1275
  return entity;
1190
1276
  }
1191
1277
  /**
@@ -1210,6 +1296,7 @@ export class EntityManager {
1210
1296
  ...options,
1211
1297
  newEntity: !options.managed,
1212
1298
  merge: options.managed,
1299
+ normalizeAccessors: true,
1213
1300
  });
1214
1301
  options.persist ??= em.config.get('persistOnCreate');
1215
1302
  if (options.persist && !this.getMetadata(entityName).embeddable) {
@@ -1248,10 +1335,8 @@ export class EntityManager {
1248
1335
  async count(entityName, where = {}, options = {}) {
1249
1336
  const em = this.getContext(false);
1250
1337
  // Shallow copy options since the object will be modified when deleting orderBy
1251
- options = {
1252
- schema: em._schema,
1253
- ...options,
1254
- };
1338
+ options = { ...options };
1339
+ em.prepareOptions(options);
1255
1340
  entityName = Utils.className(entityName);
1256
1341
  await em.tryFlush(entityName, options);
1257
1342
  where = await em.processWhere(entityName, where, options, 'read');
@@ -1261,15 +1346,15 @@ export class EntityManager {
1261
1346
  const meta = em.metadata.find(entityName);
1262
1347
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
1263
1348
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
1264
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
1349
+ options.populateFilter = await this.getJoinedFilters(meta, options);
1265
1350
  em.validator.validateParams(where);
1266
1351
  delete options.orderBy;
1267
1352
  const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
1268
1353
  const cached = await em.tryCache(entityName, options.cache, cacheKey);
1269
- if (cached?.data) {
1354
+ if (cached?.data !== undefined) {
1270
1355
  return cached.data;
1271
1356
  }
1272
- const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, ...options });
1357
+ const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, em, ...options });
1273
1358
  await em.storeCache(options.cache, cached, () => +count);
1274
1359
  return +count;
1275
1360
  }
@@ -1395,7 +1480,7 @@ export class EntityManager {
1395
1480
  const em = this.getContext();
1396
1481
  em.prepareOptions(options);
1397
1482
  const entityName = arr[0].constructor.name;
1398
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate }, options.validate);
1483
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1399
1484
  await em.entityLoader.populate(entityName, arr, preparedPopulate, options);
1400
1485
  return entities;
1401
1486
  }
@@ -1431,6 +1516,9 @@ export class EntityManager {
1431
1516
  for (const entity of em.unitOfWork.getIdentityMap()) {
1432
1517
  fork.unitOfWork.register(entity);
1433
1518
  }
1519
+ for (const entity of em.unitOfWork.getPersistStack()) {
1520
+ fork.unitOfWork.persist(entity);
1521
+ }
1434
1522
  for (const entity of em.unitOfWork.getOrphanRemoveStack()) {
1435
1523
  fork.unitOfWork.getOrphanRemoveStack().add(entity);
1436
1524
  }
@@ -1452,6 +1540,12 @@ export class EntityManager {
1452
1540
  getEntityFactory() {
1453
1541
  return this.getContext().entityFactory;
1454
1542
  }
1543
+ /**
1544
+ * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
1545
+ */
1546
+ getEntityLoader() {
1547
+ return this.getContext().entityLoader;
1548
+ }
1455
1549
  /**
1456
1550
  * Gets the Hydrator used by the EntityManager.
1457
1551
  */
@@ -1548,7 +1642,6 @@ export class EntityManager {
1548
1642
  ...options,
1549
1643
  ...this.getPopulateWhere(where, options),
1550
1644
  orderBy: options.populateOrderBy ?? options.orderBy,
1551
- convertCustomTypes: false,
1552
1645
  ignoreLazyScalarProperties: true,
1553
1646
  lookup: false,
1554
1647
  });
@@ -1671,7 +1764,7 @@ export class EntityManager {
1671
1764
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1672
1765
  }
1673
1766
  options.schema ??= this._schema;
1674
- options.logging = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1767
+ options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1675
1768
  }
1676
1769
  /**
1677
1770
  * @internal
@@ -1695,31 +1788,31 @@ export class EntityManager {
1695
1788
  const em = this.getContext();
1696
1789
  const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
1697
1790
  const cached = await em.resultCache.get(cacheKey);
1698
- if (cached) {
1699
- let data;
1700
- if (Array.isArray(cached) && merge) {
1701
- data = cached.map(item => em.entityFactory.create(entityName, item, {
1702
- merge: true,
1703
- convertCustomTypes: true,
1704
- refresh,
1705
- recomputeSnapshot: true,
1706
- }));
1707
- }
1708
- else if (Utils.isObject(cached) && merge) {
1709
- data = em.entityFactory.create(entityName, cached, {
1710
- merge: true,
1711
- convertCustomTypes: true,
1712
- refresh,
1713
- recomputeSnapshot: true,
1714
- });
1715
- }
1716
- else {
1717
- data = cached;
1718
- }
1719
- await em.unitOfWork.dispatchOnLoadEvent();
1720
- return { key: cacheKey, data };
1791
+ if (!cached) {
1792
+ return { key: cacheKey, data: cached };
1793
+ }
1794
+ let data;
1795
+ if (Array.isArray(cached) && merge) {
1796
+ data = cached.map(item => em.entityFactory.create(entityName, item, {
1797
+ merge: true,
1798
+ convertCustomTypes: true,
1799
+ refresh,
1800
+ recomputeSnapshot: true,
1801
+ }));
1721
1802
  }
1722
- return { key: cacheKey };
1803
+ else if (Utils.isObject(cached) && merge) {
1804
+ data = em.entityFactory.create(entityName, cached, {
1805
+ merge: true,
1806
+ convertCustomTypes: true,
1807
+ refresh,
1808
+ recomputeSnapshot: true,
1809
+ });
1810
+ }
1811
+ else {
1812
+ data = cached;
1813
+ }
1814
+ await em.unitOfWork.dispatchOnLoadEvent();
1815
+ return { key: cacheKey, data };
1723
1816
  }
1724
1817
  /**
1725
1818
  * @internal