@mikro-orm/core 7.0.0-dev.5 → 7.0.0-dev.50

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 (115) hide show
  1. package/EntityManager.d.ts +81 -27
  2. package/EntityManager.js +287 -175
  3. package/MikroORM.d.ts +6 -6
  4. package/MikroORM.js +31 -74
  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 +9 -5
  9. package/connections/Connection.js +16 -13
  10. package/decorators/Embedded.d.ts +5 -11
  11. package/decorators/Entity.d.ts +18 -3
  12. package/decorators/Indexed.d.ts +2 -2
  13. package/decorators/ManyToMany.d.ts +2 -0
  14. package/decorators/ManyToOne.d.ts +4 -0
  15. package/decorators/OneToOne.d.ts +4 -0
  16. package/decorators/Property.d.ts +53 -9
  17. package/decorators/Transactional.d.ts +1 -0
  18. package/decorators/Transactional.js +3 -3
  19. package/decorators/index.d.ts +1 -1
  20. package/drivers/DatabaseDriver.d.ts +10 -5
  21. package/drivers/DatabaseDriver.js +4 -4
  22. package/drivers/IDatabaseDriver.d.ts +28 -4
  23. package/entity/ArrayCollection.d.ts +6 -4
  24. package/entity/ArrayCollection.js +26 -9
  25. package/entity/BaseEntity.d.ts +0 -1
  26. package/entity/BaseEntity.js +0 -3
  27. package/entity/Collection.d.ts +3 -4
  28. package/entity/Collection.js +37 -17
  29. package/entity/EntityAssigner.d.ts +1 -1
  30. package/entity/EntityAssigner.js +9 -1
  31. package/entity/EntityFactory.d.ts +7 -0
  32. package/entity/EntityFactory.js +29 -11
  33. package/entity/EntityHelper.js +25 -8
  34. package/entity/EntityLoader.d.ts +5 -4
  35. package/entity/EntityLoader.js +69 -36
  36. package/entity/EntityRepository.d.ts +1 -1
  37. package/entity/EntityValidator.js +1 -1
  38. package/entity/Reference.d.ts +9 -7
  39. package/entity/Reference.js +30 -3
  40. package/entity/WrappedEntity.d.ts +0 -2
  41. package/entity/WrappedEntity.js +1 -5
  42. package/entity/defineEntity.d.ts +555 -0
  43. package/entity/defineEntity.js +529 -0
  44. package/entity/index.d.ts +2 -0
  45. package/entity/index.js +2 -0
  46. package/entity/utils.d.ts +7 -0
  47. package/entity/utils.js +15 -3
  48. package/enums.d.ts +16 -3
  49. package/enums.js +13 -0
  50. package/errors.d.ts +6 -1
  51. package/errors.js +14 -4
  52. package/events/EventSubscriber.d.ts +3 -1
  53. package/hydration/ObjectHydrator.d.ts +4 -4
  54. package/hydration/ObjectHydrator.js +35 -24
  55. package/index.d.ts +2 -1
  56. package/index.js +1 -1
  57. package/logging/DefaultLogger.d.ts +1 -1
  58. package/logging/SimpleLogger.d.ts +1 -1
  59. package/metadata/EntitySchema.d.ts +8 -4
  60. package/metadata/EntitySchema.js +39 -19
  61. package/metadata/MetadataDiscovery.d.ts +4 -4
  62. package/metadata/MetadataDiscovery.js +139 -122
  63. package/metadata/MetadataStorage.js +1 -1
  64. package/metadata/MetadataValidator.js +4 -3
  65. package/naming-strategy/AbstractNamingStrategy.d.ts +5 -1
  66. package/naming-strategy/AbstractNamingStrategy.js +7 -1
  67. package/naming-strategy/NamingStrategy.d.ts +11 -1
  68. package/package.json +5 -5
  69. package/platforms/Platform.d.ts +5 -3
  70. package/platforms/Platform.js +4 -8
  71. package/serialization/EntitySerializer.d.ts +2 -0
  72. package/serialization/EntitySerializer.js +23 -5
  73. package/serialization/EntityTransformer.js +16 -6
  74. package/serialization/SerializationContext.js +14 -11
  75. package/types/BigIntType.d.ts +9 -6
  76. package/types/BigIntType.js +3 -0
  77. package/types/BooleanType.d.ts +1 -1
  78. package/types/DecimalType.d.ts +6 -4
  79. package/types/DecimalType.js +1 -1
  80. package/types/DoubleType.js +1 -1
  81. package/types/JsonType.d.ts +1 -1
  82. package/types/JsonType.js +7 -2
  83. package/types/Type.d.ts +2 -1
  84. package/types/Type.js +1 -1
  85. package/types/index.d.ts +1 -1
  86. package/typings.d.ts +89 -49
  87. package/typings.js +31 -31
  88. package/unit-of-work/ChangeSetComputer.js +8 -3
  89. package/unit-of-work/ChangeSetPersister.d.ts +4 -2
  90. package/unit-of-work/ChangeSetPersister.js +37 -16
  91. package/unit-of-work/UnitOfWork.d.ts +8 -1
  92. package/unit-of-work/UnitOfWork.js +110 -53
  93. package/utils/AbstractSchemaGenerator.js +3 -1
  94. package/utils/Configuration.d.ts +29 -16
  95. package/utils/Configuration.js +17 -18
  96. package/utils/ConfigurationLoader.d.ts +9 -22
  97. package/utils/ConfigurationLoader.js +49 -72
  98. package/utils/Cursor.d.ts +3 -3
  99. package/utils/Cursor.js +3 -0
  100. package/utils/DataloaderUtils.d.ts +7 -2
  101. package/utils/DataloaderUtils.js +38 -7
  102. package/utils/EntityComparator.d.ts +6 -2
  103. package/utils/EntityComparator.js +104 -58
  104. package/utils/QueryHelper.d.ts +9 -1
  105. package/utils/QueryHelper.js +66 -5
  106. package/utils/RawQueryFragment.d.ts +36 -4
  107. package/utils/RawQueryFragment.js +34 -13
  108. package/utils/TransactionManager.d.ts +65 -0
  109. package/utils/TransactionManager.js +223 -0
  110. package/utils/Utils.d.ts +13 -11
  111. package/utils/Utils.js +82 -55
  112. package/utils/index.d.ts +1 -0
  113. package/utils/index.js +1 -0
  114. package/utils/upsert-utils.d.ts +7 -2
  115. 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
  */
@@ -200,15 +256,34 @@ export class EntityManager {
200
256
  }
201
257
  return { where: options.populateWhere };
202
258
  }
259
+ // /**
260
+ // * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
261
+ // */
262
+ // addFilter<T1>(name: string, cond: FilterQuery<T1> | ((args: Dictionary) => MaybePromise<FilterQuery<T1>>), entityName?: EntityName<T1> | [EntityName<T1>], options?: boolean | Partial<FilterDef>): void;
263
+ //
264
+ // /**
265
+ // * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
266
+ // */
267
+ // addFilter<T1, T2>(name: string, cond: FilterQuery<T1 | T2> | ((args: Dictionary) => MaybePromise<FilterQuery<T1 | T2>>), entityName?: [EntityName<T1>, EntityName<T2>], options?: boolean | Partial<FilterDef>): void;
268
+ //
269
+ // /**
270
+ // * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
271
+ // */
272
+ // addFilter<T1, T2, T3>(name: string, cond: FilterQuery<T1 | T2 | T3> | ((args: Dictionary) => MaybePromise<FilterQuery<T1 | T2 | T3>>), entityName?: [EntityName<T1>, EntityName<T2>, EntityName<T3>], options?: boolean | Partial<FilterDef>): void;
273
+ //
274
+ // /**
275
+ // * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
276
+ // */
277
+ // addFilter(name: string, cond: Dictionary | ((args: Dictionary) => MaybePromise<FilterQuery<AnyEntity>>), entityName?: EntityName<AnyEntity> | EntityName<AnyEntity>[], options?: boolean | Partial<FilterDef>): void;
203
278
  /**
204
279
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
205
280
  */
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));
281
+ addFilter(options) {
282
+ if (options.entity) {
283
+ options.entity = Utils.asArray(options.entity).map(n => Utils.className(n));
210
284
  }
211
- this.getContext(false).filters[name] = options;
285
+ options.default ??= true;
286
+ this.getContext(false).filters[options.name] = options;
212
287
  }
213
288
  /**
214
289
  * Sets filter parameter values globally inside context defined by this entity manager.
@@ -232,8 +307,8 @@ export class EntityManager {
232
307
  /**
233
308
  * Gets logger context for this entity manager.
234
309
  */
235
- getLoggerContext() {
236
- const em = this.getContext();
310
+ getLoggerContext(options) {
311
+ const em = options?.disableContextResolution ? this : this.getContext();
237
312
  em.loggerContext ??= {};
238
313
  return em.loggerContext;
239
314
  }
@@ -283,28 +358,39 @@ export class EntityManager {
283
358
  }
284
359
  return ret;
285
360
  }
286
- async getJoinedFilters(meta, cond, options) {
361
+ async getJoinedFilters(meta, options) {
362
+ if (!this.config.get('filtersOnRelations') || !options.populate) {
363
+ return undefined;
364
+ }
287
365
  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;
366
+ for (const hint of options.populate) {
367
+ const field = hint.field.split(':')[0];
368
+ const prop = meta.properties[field];
369
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
370
+ const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
371
+ if (!joined && !hint.filter) {
372
+ continue;
373
+ }
374
+ const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
375
+ const where = await this.applyFilters(prop.type, {}, filters, 'read', {
376
+ ...options,
377
+ populate: hint.children,
378
+ });
379
+ const where2 = await this.getJoinedFilters(prop.targetMeta, {
380
+ ...options,
381
+ filters,
382
+ populate: hint.children,
383
+ populateWhere: PopulateHint.ALL,
384
+ });
385
+ if (Utils.hasObjectKeys(where)) {
386
+ ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
387
+ }
388
+ if (where2 && Utils.hasObjectKeys(where2)) {
389
+ if (ret[field]) {
390
+ Utils.merge(ret[field], where2);
300
391
  }
301
- if (Utils.hasObjectKeys(where2)) {
302
- if (ret[field]) {
303
- Utils.merge(ret[field], where2);
304
- }
305
- else {
306
- ret[field] = where2;
307
- }
392
+ else {
393
+ ret[field] = where2;
308
394
  }
309
395
  }
310
396
  }
@@ -313,27 +399,45 @@ export class EntityManager {
313
399
  /**
314
400
  * 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
401
  */
316
- async autoJoinRefsForFilters(meta, options) {
317
- if (!meta || !this.config.get('autoJoinRefsForFilters')) {
402
+ async autoJoinRefsForFilters(meta, options, parent) {
403
+ if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) {
318
404
  return;
319
405
  }
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
406
  const ret = options.populate;
325
- for (const prop of props) {
326
- const cond = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', options);
407
+ for (const prop of meta.relations) {
408
+ if (prop.object
409
+ || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
410
+ || !((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
411
+ || (parent?.className === prop.targetMeta.root.className && parent.propName === prop.inversedBy)) {
412
+ continue;
413
+ }
414
+ options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
415
+ const cond = await this.applyFilters(prop.type, {}, options.filters, 'read', options);
327
416
  if (!Utils.isEmpty(cond)) {
328
417
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
329
- if (populated.length > 0) {
330
- populated.forEach(hint => hint.filter = true);
418
+ let found = false;
419
+ for (const hint of populated) {
420
+ if (!hint.all) {
421
+ hint.filter = true;
422
+ }
423
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
424
+ if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
425
+ found = true;
426
+ }
331
427
  }
332
- else {
428
+ if (!found) {
333
429
  ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true });
334
430
  }
335
431
  }
336
432
  }
433
+ for (const hint of ret) {
434
+ const [field, ref] = hint.field.split(':');
435
+ const prop = meta?.properties[field];
436
+ if (prop && !ref) {
437
+ hint.children ??= [];
438
+ await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { className: meta.root.className, propName: prop.name });
439
+ }
440
+ }
337
441
  }
338
442
  /**
339
443
  * @internal
@@ -363,7 +467,7 @@ export class EntityManager {
363
467
  let cond;
364
468
  if (filter.cond instanceof Function) {
365
469
  // @ts-ignore
366
- const args = Utils.isPlainObject(options[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
470
+ const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
367
471
  if (!args && filter.cond.length > 0 && filter.args !== false) {
368
472
  throw new Error(`No arguments provided for filter '${filter.name}'`);
369
473
  }
@@ -372,13 +476,17 @@ export class EntityManager {
372
476
  else {
373
477
  cond = filter.cond;
374
478
  }
375
- ret.push(QueryHelper.processWhere({
479
+ cond = QueryHelper.processWhere({
376
480
  where: cond,
377
481
  entityName,
378
482
  metadata: this.metadata,
379
483
  platform: this.driver.getPlatform(),
380
484
  aliased: type === 'read',
381
- }));
485
+ });
486
+ if (filter.strict) {
487
+ Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false });
488
+ }
489
+ ret.push(cond);
382
490
  }
383
491
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c));
384
492
  return conds.length > 1 ? { $and: conds } : conds[0];
@@ -433,6 +541,10 @@ export class EntityManager {
433
541
  * });
434
542
  * ```
435
543
  *
544
+ * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
545
+ * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
546
+ * of pages.
547
+ *
436
548
  * The `Cursor` object provides the following interface:
437
549
  *
438
550
  * ```ts
@@ -442,7 +554,7 @@ export class EntityManager {
442
554
  * User { ... },
443
555
  * User { ... },
444
556
  * ],
445
- * totalCount: 50,
557
+ * totalCount: 50, // not included if `includeCount: false`
446
558
  * startCursor: 'WzRd',
447
559
  * endCursor: 'WzZd',
448
560
  * hasPrevPage: true,
@@ -457,7 +569,9 @@ export class EntityManager {
457
569
  if (Utils.isEmpty(options.orderBy)) {
458
570
  throw new Error('Explicit `orderBy` option required');
459
571
  }
460
- const [entities, count] = await em.findAndCount(entityName, where, options);
572
+ const [entities, count] = options.includeCount !== false
573
+ ? await em.findAndCount(entityName, where, options)
574
+ : [await em.find(entityName, where, options)];
461
575
  return new Cursor(entities, count, options, this.metadata.get(entityName));
462
576
  }
463
577
  /**
@@ -483,18 +597,31 @@ export class EntityManager {
483
597
  async refresh(entity, options = {}) {
484
598
  const fork = this.fork({ keepTransactionContext: true });
485
599
  const entityName = entity.constructor.name;
600
+ const wrapped = helper(entity);
486
601
  const reloaded = await fork.findOne(entityName, entity, {
487
- schema: helper(entity).__schema,
602
+ schema: wrapped.__schema,
488
603
  ...options,
489
604
  flushMode: FlushMode.COMMIT,
490
605
  });
491
- if (reloaded) {
492
- this.config.getHydrator(this.metadata).hydrate(entity, helper(entity).__meta, helper(reloaded).toPOJO(), this.getEntityFactory(), 'full');
606
+ const em = this.getContext();
607
+ if (!reloaded) {
608
+ em.unitOfWork.unsetIdentity(entity);
609
+ return null;
493
610
  }
494
- else {
495
- this.getUnitOfWork().unsetIdentity(entity);
611
+ let found = false;
612
+ for (const e of fork.unitOfWork.getIdentityMap()) {
613
+ const ref = em.getReference(e.constructor.name, helper(e).getPrimaryKey());
614
+ const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true });
615
+ em.config.getHydrator(this.metadata).hydrate(ref, helper(ref).__meta, data, em.entityFactory, 'full', false, true);
616
+ Utils.merge(helper(ref).__originalEntityData, this.comparator.prepareEntity(e));
617
+ found ||= ref === entity;
618
+ }
619
+ if (!found) {
620
+ const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true });
621
+ em.config.getHydrator(this.metadata).hydrate(entity, wrapped.__meta, data, em.entityFactory, 'full', false, true);
622
+ Utils.merge(wrapped.__originalEntityData, this.comparator.prepareEntity(reloaded));
496
623
  }
497
- return reloaded ? entity : reloaded;
624
+ return entity;
498
625
  }
499
626
  /**
500
627
  * Finds first entity matching your `where` query.
@@ -530,23 +657,25 @@ export class EntityManager {
530
657
  options.populate = await em.preparePopulate(entityName, options);
531
658
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
532
659
  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
- });
660
+ if (cached?.data !== undefined) {
661
+ if (cached.data) {
662
+ await em.entityLoader.populate(entityName, [cached.data], options.populate, {
663
+ ...options,
664
+ ...em.getPopulateWhere(where, options),
665
+ ignoreLazyScalarProperties: true,
666
+ lookup: false,
667
+ });
668
+ }
541
669
  return cached.data;
542
670
  }
543
671
  options = { ...options };
544
672
  // save the original hint value so we know it was infer/all
545
673
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
546
674
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
547
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
675
+ options.populateFilter = await this.getJoinedFilters(meta, options);
548
676
  const data = await em.driver.findOne(entityName, where, {
549
677
  ctx: em.transactionContext,
678
+ em,
550
679
  ...options,
551
680
  });
552
681
  if (!data) {
@@ -654,24 +783,7 @@ export class EntityManager {
654
783
  }
655
784
  }
656
785
  }
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
- }
786
+ where = getWhereCondition(meta, options.onConflictFields, data, where).where;
675
787
  data = QueryHelper.processObjectParams(data);
676
788
  em.validator.validateParams(data, 'insert data');
677
789
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
@@ -716,6 +828,7 @@ export class EntityManager {
716
828
  ctx: em.transactionContext,
717
829
  convertCustomTypes: true,
718
830
  connectionType: 'write',
831
+ schema: options.schema,
719
832
  });
720
833
  em.getHydrator().hydrate(entity, meta, data2, em.entityFactory, 'full', false, true);
721
834
  }
@@ -813,31 +926,17 @@ export class EntityManager {
813
926
  }
814
927
  }
815
928
  }
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);
929
+ const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
930
+ propIndex = !isRaw(unique) && unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
931
+ const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
932
+ propIndex = tmp.propIndex;
835
933
  where = QueryHelper.processWhere({
836
- where,
934
+ where: tmp.where,
837
935
  entityName,
838
936
  metadata: this.metadata,
839
937
  platform: this.getPlatform(),
840
938
  });
939
+ row = QueryHelper.processObjectParams(row);
841
940
  em.validator.validateParams(row, 'insert data');
842
941
  allData.push(row);
843
942
  allWhere.push(where);
@@ -879,7 +978,7 @@ export class EntityManager {
879
978
  const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length);
880
979
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
881
980
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
882
- const add = new Set(propIndex >= 0 ? [unique[propIndex]] : []);
981
+ const add = new Set(propIndex !== false && propIndex >= 0 ? [unique[propIndex]] : []);
883
982
  for (const cond of loadPK.values()) {
884
983
  Utils.keys(cond).forEach(key => add.add(key));
885
984
  }
@@ -896,6 +995,7 @@ export class EntityManager {
896
995
  ctx: em.transactionContext,
897
996
  convertCustomTypes: true,
898
997
  connectionType: 'write',
998
+ schema: options.schema,
899
999
  });
900
1000
  for (const [entity, cond] of loadPK.entries()) {
901
1001
  const row = data2.find(row => {
@@ -952,45 +1052,37 @@ export class EntityManager {
952
1052
  }
953
1053
  /**
954
1054
  * Runs your callback wrapped inside a database transaction.
1055
+ *
1056
+ * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
1057
+ * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
1058
+ * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
1059
+ * method automatically creates an async context for the transaction.
1060
+ *
1061
+ * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
1062
+ * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
1063
+ * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
1064
+ * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
1065
+ *
1066
+ * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
1067
+ * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
1068
+ * and then call this method on the fork.
1069
+ *
1070
+ * **Example:**
1071
+ * ```ts
1072
+ * await em.transactional(async (em) => {
1073
+ * const author = new Author('Jon');
1074
+ * em.persist(author);
1075
+ * // flush is called automatically at the end of the callback
1076
+ * });
1077
+ * ```
955
1078
  */
956
1079
  async transactional(cb, options = {}) {
957
1080
  const em = this.getContext(false);
958
1081
  if (this.disableTransactions || em.disableTransactions) {
959
1082
  return cb(em);
960
1083
  }
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
- });
1084
+ const manager = new TransactionManager(this);
1085
+ return manager.handle(cb, options);
994
1086
  }
995
1087
  /**
996
1088
  * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions.
@@ -1170,22 +1262,35 @@ export class EntityManager {
1170
1262
  * via second parameter. By default, it will return already loaded entities without modifying them.
1171
1263
  */
1172
1264
  merge(entityName, data, options = {}) {
1173
- const em = this.getContext();
1174
1265
  if (Utils.isEntity(entityName)) {
1175
- return em.merge(entityName.constructor.name, entityName, data);
1266
+ return this.merge(entityName.constructor.name, entityName, data);
1176
1267
  }
1268
+ const em = options.disableContextResolution ? this : this.getContext();
1177
1269
  options.schema ??= em._schema;
1270
+ options.validate ??= true;
1271
+ options.cascade ??= true;
1178
1272
  entityName = Utils.className(entityName);
1179
- em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1273
+ if (options.validate) {
1274
+ em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1275
+ }
1180
1276
  let entity = em.unitOfWork.tryGetById(entityName, data, options.schema, false);
1181
1277
  if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) {
1182
1278
  return entity;
1183
1279
  }
1184
1280
  const meta = em.metadata.find(entityName);
1185
1281
  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);
1282
+ const dataIsEntity = Utils.isEntity(data);
1283
+ if (options.keepIdentity && entity && dataIsEntity && entity !== data) {
1284
+ helper(entity).__data = helper(data).__data;
1285
+ helper(entity).__originalEntityData = helper(data).__originalEntityData;
1286
+ return entity;
1287
+ }
1288
+ entity = dataIsEntity ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1289
+ if (options.validate) {
1290
+ em.validator.validate(entity, data, childMeta ?? meta);
1291
+ }
1292
+ const visited = options.cascade ? undefined : new Set([entity]);
1293
+ em.unitOfWork.merge(entity, visited);
1189
1294
  return entity;
1190
1295
  }
1191
1296
  /**
@@ -1210,6 +1315,7 @@ export class EntityManager {
1210
1315
  ...options,
1211
1316
  newEntity: !options.managed,
1212
1317
  merge: options.managed,
1318
+ normalizeAccessors: true,
1213
1319
  });
1214
1320
  options.persist ??= em.config.get('persistOnCreate');
1215
1321
  if (options.persist && !this.getMetadata(entityName).embeddable) {
@@ -1248,10 +1354,8 @@ export class EntityManager {
1248
1354
  async count(entityName, where = {}, options = {}) {
1249
1355
  const em = this.getContext(false);
1250
1356
  // Shallow copy options since the object will be modified when deleting orderBy
1251
- options = {
1252
- schema: em._schema,
1253
- ...options,
1254
- };
1357
+ options = { ...options };
1358
+ em.prepareOptions(options);
1255
1359
  entityName = Utils.className(entityName);
1256
1360
  await em.tryFlush(entityName, options);
1257
1361
  where = await em.processWhere(entityName, where, options, 'read');
@@ -1261,15 +1365,15 @@ export class EntityManager {
1261
1365
  const meta = em.metadata.find(entityName);
1262
1366
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
1263
1367
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
1264
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
1368
+ options.populateFilter = await this.getJoinedFilters(meta, options);
1265
1369
  em.validator.validateParams(where);
1266
1370
  delete options.orderBy;
1267
1371
  const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
1268
1372
  const cached = await em.tryCache(entityName, options.cache, cacheKey);
1269
- if (cached?.data) {
1373
+ if (cached?.data !== undefined) {
1270
1374
  return cached.data;
1271
1375
  }
1272
- const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, ...options });
1376
+ const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, em, ...options });
1273
1377
  await em.storeCache(options.cache, cached, () => +count);
1274
1378
  return +count;
1275
1379
  }
@@ -1395,7 +1499,7 @@ export class EntityManager {
1395
1499
  const em = this.getContext();
1396
1500
  em.prepareOptions(options);
1397
1501
  const entityName = arr[0].constructor.name;
1398
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate }, options.validate);
1502
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1399
1503
  await em.entityLoader.populate(entityName, arr, preparedPopulate, options);
1400
1504
  return entities;
1401
1505
  }
@@ -1431,6 +1535,9 @@ export class EntityManager {
1431
1535
  for (const entity of em.unitOfWork.getIdentityMap()) {
1432
1536
  fork.unitOfWork.register(entity);
1433
1537
  }
1538
+ for (const entity of em.unitOfWork.getPersistStack()) {
1539
+ fork.unitOfWork.persist(entity);
1540
+ }
1434
1541
  for (const entity of em.unitOfWork.getOrphanRemoveStack()) {
1435
1542
  fork.unitOfWork.getOrphanRemoveStack().add(entity);
1436
1543
  }
@@ -1452,6 +1559,12 @@ export class EntityManager {
1452
1559
  getEntityFactory() {
1453
1560
  return this.getContext().entityFactory;
1454
1561
  }
1562
+ /**
1563
+ * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
1564
+ */
1565
+ getEntityLoader() {
1566
+ return this.getContext().entityLoader;
1567
+ }
1455
1568
  /**
1456
1569
  * Gets the Hydrator used by the EntityManager.
1457
1570
  */
@@ -1548,7 +1661,6 @@ export class EntityManager {
1548
1661
  ...options,
1549
1662
  ...this.getPopulateWhere(where, options),
1550
1663
  orderBy: options.populateOrderBy ?? options.orderBy,
1551
- convertCustomTypes: false,
1552
1664
  ignoreLazyScalarProperties: true,
1553
1665
  lookup: false,
1554
1666
  });
@@ -1671,7 +1783,7 @@ export class EntityManager {
1671
1783
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1672
1784
  }
1673
1785
  options.schema ??= this._schema;
1674
- options.logging = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1786
+ options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1675
1787
  }
1676
1788
  /**
1677
1789
  * @internal
@@ -1695,31 +1807,31 @@ export class EntityManager {
1695
1807
  const em = this.getContext();
1696
1808
  const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
1697
1809
  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 };
1810
+ if (!cached) {
1811
+ return { key: cacheKey, data: cached };
1812
+ }
1813
+ let data;
1814
+ if (Array.isArray(cached) && merge) {
1815
+ data = cached.map(item => em.entityFactory.create(entityName, item, {
1816
+ merge: true,
1817
+ convertCustomTypes: true,
1818
+ refresh,
1819
+ recomputeSnapshot: true,
1820
+ }));
1721
1821
  }
1722
- return { key: cacheKey };
1822
+ else if (Utils.isObject(cached) && merge) {
1823
+ data = em.entityFactory.create(entityName, cached, {
1824
+ merge: true,
1825
+ convertCustomTypes: true,
1826
+ refresh,
1827
+ recomputeSnapshot: true,
1828
+ });
1829
+ }
1830
+ else {
1831
+ data = cached;
1832
+ }
1833
+ await em.unitOfWork.dispatchOnLoadEvent();
1834
+ return { key: cacheKey, data };
1723
1835
  }
1724
1836
  /**
1725
1837
  * @internal