@mikro-orm/core 7.0.0-dev.3 → 7.0.0-dev.31

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 (102) hide show
  1. package/EntityManager.d.ts +50 -7
  2. package/EntityManager.js +141 -97
  3. package/MikroORM.js +0 -1
  4. package/README.md +1 -2
  5. package/cache/FileCacheAdapter.d.ts +2 -1
  6. package/cache/FileCacheAdapter.js +6 -4
  7. package/connections/Connection.d.ts +4 -2
  8. package/connections/Connection.js +2 -2
  9. package/decorators/Check.d.ts +2 -2
  10. package/decorators/Embeddable.d.ts +5 -5
  11. package/decorators/Embeddable.js +1 -1
  12. package/decorators/Embedded.d.ts +6 -12
  13. package/decorators/Entity.d.ts +20 -5
  14. package/decorators/Entity.js +0 -1
  15. package/decorators/Enum.d.ts +1 -1
  16. package/decorators/Formula.d.ts +1 -2
  17. package/decorators/Indexed.d.ts +10 -8
  18. package/decorators/Indexed.js +1 -1
  19. package/decorators/ManyToMany.d.ts +4 -2
  20. package/decorators/ManyToOne.d.ts +6 -2
  21. package/decorators/OneToMany.d.ts +4 -4
  22. package/decorators/OneToOne.d.ts +5 -1
  23. package/decorators/PrimaryKey.d.ts +2 -3
  24. package/decorators/Property.d.ts +1 -1
  25. package/decorators/Transactional.d.ts +1 -0
  26. package/decorators/Transactional.js +3 -3
  27. package/drivers/IDatabaseDriver.d.ts +8 -1
  28. package/entity/ArrayCollection.d.ts +4 -2
  29. package/entity/ArrayCollection.js +18 -6
  30. package/entity/Collection.d.ts +1 -2
  31. package/entity/Collection.js +16 -10
  32. package/entity/EntityAssigner.d.ts +1 -1
  33. package/entity/EntityAssigner.js +9 -1
  34. package/entity/EntityFactory.d.ts +6 -0
  35. package/entity/EntityFactory.js +21 -7
  36. package/entity/EntityHelper.js +8 -1
  37. package/entity/EntityLoader.d.ts +3 -2
  38. package/entity/EntityLoader.js +54 -35
  39. package/entity/EntityRepository.d.ts +1 -1
  40. package/entity/EntityValidator.js +1 -1
  41. package/entity/Reference.d.ts +8 -7
  42. package/entity/Reference.js +22 -1
  43. package/entity/WrappedEntity.js +1 -1
  44. package/entity/defineEntity.d.ts +537 -0
  45. package/entity/defineEntity.js +693 -0
  46. package/entity/index.d.ts +2 -0
  47. package/entity/index.js +2 -0
  48. package/entity/utils.d.ts +7 -0
  49. package/entity/utils.js +15 -3
  50. package/enums.d.ts +16 -3
  51. package/enums.js +13 -0
  52. package/errors.d.ts +6 -0
  53. package/errors.js +14 -0
  54. package/events/EventSubscriber.d.ts +3 -1
  55. package/hydration/ObjectHydrator.js +10 -2
  56. package/index.d.ts +1 -1
  57. package/metadata/EntitySchema.d.ts +6 -4
  58. package/metadata/EntitySchema.js +33 -19
  59. package/metadata/MetadataDiscovery.d.ts +0 -1
  60. package/metadata/MetadataDiscovery.js +51 -29
  61. package/metadata/MetadataStorage.js +1 -1
  62. package/metadata/MetadataValidator.js +4 -3
  63. package/package.json +5 -5
  64. package/platforms/Platform.d.ts +3 -1
  65. package/serialization/EntitySerializer.d.ts +2 -0
  66. package/serialization/EntitySerializer.js +1 -1
  67. package/serialization/SerializationContext.js +13 -10
  68. package/types/BigIntType.d.ts +9 -6
  69. package/types/BigIntType.js +3 -0
  70. package/types/BooleanType.d.ts +1 -1
  71. package/types/DecimalType.d.ts +6 -4
  72. package/types/DecimalType.js +1 -1
  73. package/types/DoubleType.js +1 -1
  74. package/typings.d.ts +72 -35
  75. package/typings.js +24 -4
  76. package/unit-of-work/ChangeSetComputer.js +3 -1
  77. package/unit-of-work/ChangeSetPersister.d.ts +4 -2
  78. package/unit-of-work/ChangeSetPersister.js +21 -11
  79. package/unit-of-work/UnitOfWork.d.ts +2 -1
  80. package/unit-of-work/UnitOfWork.js +71 -24
  81. package/utils/AbstractSchemaGenerator.js +5 -2
  82. package/utils/Configuration.d.ts +15 -5
  83. package/utils/Configuration.js +7 -7
  84. package/utils/ConfigurationLoader.d.ts +0 -2
  85. package/utils/ConfigurationLoader.js +2 -24
  86. package/utils/Cursor.d.ts +3 -3
  87. package/utils/Cursor.js +3 -0
  88. package/utils/DataloaderUtils.d.ts +7 -2
  89. package/utils/DataloaderUtils.js +38 -7
  90. package/utils/EntityComparator.d.ts +6 -2
  91. package/utils/EntityComparator.js +98 -59
  92. package/utils/QueryHelper.d.ts +6 -0
  93. package/utils/QueryHelper.js +48 -5
  94. package/utils/RawQueryFragment.d.ts +34 -0
  95. package/utils/RawQueryFragment.js +40 -1
  96. package/utils/TransactionManager.d.ts +65 -0
  97. package/utils/TransactionManager.js +220 -0
  98. package/utils/Utils.d.ts +11 -7
  99. package/utils/Utils.js +67 -33
  100. package/utils/index.d.ts +1 -0
  101. package/utils/index.js +1 -0
  102. package/utils/upsert-utils.js +9 -1
@@ -6,7 +6,7 @@ import { EntityFactory } from './entity/EntityFactory.js';
6
6
  import { type AssignOptions } from './entity/EntityAssigner.js';
7
7
  import { EntityValidator } from './entity/EntityValidator.js';
8
8
  import { type EntityRepository } from './entity/EntityRepository.js';
9
- import { type EntityLoaderOptions } from './entity/EntityLoader.js';
9
+ import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js';
10
10
  import { Reference } from './entity/Reference.js';
11
11
  import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
12
12
  import type { CountOptions, DeleteOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
@@ -33,6 +33,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
33
33
  readonly name: string;
34
34
  protected readonly refLoader: DataLoader<[Reference<any>, (Omit<import("./entity/Reference.js").LoadReferenceOptions<any, any, "*", never>, "dataloader"> | undefined)?], any, [Reference<any>, (Omit<import("./entity/Reference.js").LoadReferenceOptions<any, any, "*", never>, "dataloader"> | undefined)?]>;
35
35
  protected readonly colLoader: DataLoader<[import("./index.js").Collection<any, object>, (Omit<import("./index.js").InitCollectionOptions<any, any, "*", never>, "dataloader"> | undefined)?], any, [import("./index.js").Collection<any, object>, (Omit<import("./index.js").InitCollectionOptions<any, any, "*", never>, "dataloader"> | undefined)?]>;
36
+ protected readonly colLoaderMtoN: DataLoader<[import("./index.js").Collection<any, object>, (Omit<import("./index.js").InitCollectionOptions<any, any, "*", never>, "dataloader"> | undefined)?], any, [import("./index.js").Collection<any, object>, (Omit<import("./index.js").InitCollectionOptions<any, any, "*", never>, "dataloader"> | undefined)?]>;
36
37
  private readonly validator;
37
38
  private readonly repositoryMap;
38
39
  private readonly entityLoader;
@@ -117,7 +118,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
117
118
  /**
118
119
  * Gets logger context for this entity manager.
119
120
  */
120
- getLoggerContext<T extends Dictionary = Dictionary>(): T;
121
+ getLoggerContext<T extends Dictionary = Dictionary>(options?: {
122
+ disableContextResolution?: boolean;
123
+ }): T;
121
124
  setFlushMode(flushMode?: FlushMode): void;
122
125
  protected processWhere<Entity extends object, Hint extends string = never, Fields extends string = '*', Excludes extends string = never>(entityName: string, where: FilterQuery<Entity>, options: FindOptions<Entity, Hint, Fields, Excludes> | FindOneOptions<Entity, Hint, Fields, Excludes>, type: 'read' | 'update' | 'delete'): Promise<FilterQuery<Entity>>;
123
126
  protected applyDiscriminatorCondition<Entity extends object>(entityName: string, where: FilterQuery<Entity>): FilterQuery<Entity>;
@@ -171,6 +174,10 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
171
174
  * });
172
175
  * ```
173
176
  *
177
+ * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
178
+ * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
179
+ * of pages.
180
+ *
174
181
  * The `Cursor` object provides the following interface:
175
182
  *
176
183
  * ```ts
@@ -180,7 +187,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
180
187
  * User { ... },
181
188
  * User { ... },
182
189
  * ],
183
- * totalCount: 50,
190
+ * totalCount: 50, // not included if `includeCount: false`
184
191
  * startCursor: 'WzRd',
185
192
  * endCursor: 'WzZd',
186
193
  * hasPrevPage: true,
@@ -188,7 +195,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
188
195
  * }
189
196
  * ```
190
197
  */
191
- findByCursor<Entity extends object, Hint extends string = never, Fields extends string = '*', Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options: FindByCursorOptions<Entity, Hint, Fields, Excludes>): Promise<Cursor<Entity, Hint, Fields, Excludes>>;
198
+ findByCursor<Entity extends object, Hint extends string = never, Fields extends string = '*', Excludes extends string = never, IncludeCount extends boolean = true>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options: FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
192
199
  /**
193
200
  * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
194
201
  * persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer
@@ -263,6 +270,29 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
263
270
  upsertMany<Entity extends object, Fields extends string = any>(entityNameOrEntity: EntityName<Entity> | Entity[], data?: (EntityData<Entity> | NoInfer<Entity>)[], options?: UpsertManyOptions<Entity, Fields>): Promise<Entity[]>;
264
271
  /**
265
272
  * Runs your callback wrapped inside a database transaction.
273
+ *
274
+ * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
275
+ * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
276
+ * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
277
+ * method automatically creates an async context for the transaction.
278
+ *
279
+ * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
280
+ * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
281
+ * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
282
+ * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
283
+ *
284
+ * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
285
+ * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
286
+ * and then call this method on the fork.
287
+ *
288
+ * **Example:**
289
+ * ```ts
290
+ * await em.transactional(async (em) => {
291
+ * const author = new Author('Jon');
292
+ * em.persist(author);
293
+ * // flush is called automatically at the end of the callback
294
+ * });
295
+ * ```
266
296
  */
267
297
  transactional<T>(cb: (em: this) => T | Promise<T>, options?: TransactionOptions): Promise<T>;
268
298
  /**
@@ -419,7 +449,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
419
449
  /**
420
450
  * Loads specified relations in batch. This will execute one query for each relation, that will populate it on all the specified entities.
421
451
  */
422
- populate<Entity extends object, Naked extends FromEntityType<UnboxArray<Entity>> = FromEntityType<UnboxArray<Entity>>, Hint extends string = never, Fields extends string = '*', Excludes extends string = never>(entities: Entity, populate: AutoPath<Naked, Hint, PopulatePath.ALL>[] | false, options?: EntityLoaderOptions<Naked, Fields, Excludes>): Promise<Entity extends object[] ? MergeLoaded<ArrayElement<Entity>, Naked, Hint, Fields, Excludes>[] : MergeLoaded<Entity, Naked, Hint, Fields, Excludes>>;
452
+ populate<Entity extends object, Naked extends FromEntityType<UnboxArray<Entity>> = FromEntityType<UnboxArray<Entity>>, Hint extends string = never, Fields extends string = '*', Excludes extends string = never>(entities: Entity, populate: readonly AutoPath<Naked, Hint, PopulatePath.ALL>[] | false, options?: EntityLoaderOptions<Naked, Fields, Excludes>): Promise<Entity extends object[] ? MergeLoaded<ArrayElement<Entity>, Naked, Hint, Fields, Excludes>[] : MergeLoaded<Entity, Naked, Hint, Fields, Excludes>>;
423
453
  /**
424
454
  * Returns new EntityManager instance with its own identity map
425
455
  */
@@ -432,6 +462,10 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
432
462
  * Gets the EntityFactory used by the EntityManager.
433
463
  */
434
464
  getEntityFactory(): EntityFactory;
465
+ /**
466
+ * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
467
+ */
468
+ getEntityLoader(): EntityLoader;
435
469
  /**
436
470
  * Gets the Hydrator used by the EntityManager.
437
471
  */
@@ -479,7 +513,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
479
513
  * some additional lazy properties, if so, we reload and merge the data from database
480
514
  */
481
515
  protected shouldRefresh<T extends object, P extends string = never, F extends string = '*', E extends string = never>(meta: EntityMetadata<T>, entity: T, options: FindOneOptions<T, P, F, E>): boolean;
482
- protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any>): void;
516
+ protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any>): void;
483
517
  /**
484
518
  * @internal
485
519
  */
@@ -488,7 +522,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
488
522
  * @internal
489
523
  */
490
524
  tryCache<T extends object, R>(entityName: string, config: boolean | number | [string, number] | undefined, key: unknown, refresh?: boolean, merge?: boolean): Promise<{
491
- data?: R;
525
+ data?: R | null;
492
526
  key: string;
493
527
  } | undefined>;
494
528
  /**
@@ -539,11 +573,20 @@ export interface CreateOptions<Convert extends boolean> {
539
573
  partial?: boolean;
540
574
  /** convert raw database values based on mapped types (by default, already converted values are expected) */
541
575
  convertCustomTypes?: Convert;
576
+ /**
577
+ * Property `onCreate` hooks are normally executed during `flush` operation.
578
+ * With this option, they will be processed early inside `em.create()` method.
579
+ */
580
+ processOnCreateHooksEarly?: boolean;
542
581
  }
543
582
  export interface MergeOptions {
544
583
  refresh?: boolean;
545
584
  convertCustomTypes?: boolean;
546
585
  schema?: string;
586
+ disableContextResolution?: boolean;
587
+ keepIdentity?: boolean;
588
+ validate?: boolean;
589
+ cascade?: boolean; /** @default true */
547
590
  }
548
591
  export interface ForkOptions {
549
592
  /** do we want a clear identity map? defaults to true */
package/EntityManager.js CHANGED
@@ -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
  });
@@ -149,7 +151,7 @@ export class EntityManager {
149
151
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
150
152
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
151
153
  options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
152
- const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, ...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
  });
@@ -232,8 +233,8 @@ export class EntityManager {
232
233
  /**
233
234
  * Gets logger context for this entity manager.
234
235
  */
235
- getLoggerContext() {
236
- const em = this.getContext();
236
+ getLoggerContext(options) {
237
+ const em = options?.disableContextResolution ? this : this.getContext();
237
238
  em.loggerContext ??= {};
238
239
  return em.loggerContext;
239
240
  }
@@ -289,7 +290,8 @@ export class EntityManager {
289
290
  for (const hint of options.populate) {
290
291
  const field = hint.field.split(':')[0];
291
292
  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
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
294
+ const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
293
295
  if (!joined && !hint.filter) {
294
296
  continue;
295
297
  }
@@ -326,10 +328,19 @@ export class EntityManager {
326
328
  const cond = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', options);
327
329
  if (!Utils.isEmpty(cond)) {
328
330
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
331
+ let found = false;
329
332
  if (populated.length > 0) {
330
- populated.forEach(hint => hint.filter = true);
333
+ for (const hint of populated) {
334
+ if (!hint.all) {
335
+ hint.filter = true;
336
+ found = true;
337
+ }
338
+ else if (hint.field === `${prop.name}:ref`) {
339
+ found = true;
340
+ }
341
+ }
331
342
  }
332
- else {
343
+ if (!found) {
333
344
  ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true });
334
345
  }
335
346
  }
@@ -367,7 +378,7 @@ export class EntityManager {
367
378
  if (!args && filter.cond.length > 0 && filter.args !== false) {
368
379
  throw new Error(`No arguments provided for filter '${filter.name}'`);
369
380
  }
370
- cond = await filter.cond(args, type, this, findOptions);
381
+ cond = await filter.cond(args, type, this, findOptions, entityName);
371
382
  }
372
383
  else {
373
384
  cond = filter.cond;
@@ -433,6 +444,10 @@ export class EntityManager {
433
444
  * });
434
445
  * ```
435
446
  *
447
+ * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
448
+ * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
449
+ * of pages.
450
+ *
436
451
  * The `Cursor` object provides the following interface:
437
452
  *
438
453
  * ```ts
@@ -442,7 +457,7 @@ export class EntityManager {
442
457
  * User { ... },
443
458
  * User { ... },
444
459
  * ],
445
- * totalCount: 50,
460
+ * totalCount: 50, // not included if `includeCount: false`
446
461
  * startCursor: 'WzRd',
447
462
  * endCursor: 'WzZd',
448
463
  * hasPrevPage: true,
@@ -457,7 +472,9 @@ export class EntityManager {
457
472
  if (Utils.isEmpty(options.orderBy)) {
458
473
  throw new Error('Explicit `orderBy` option required');
459
474
  }
460
- const [entities, count] = await em.findAndCount(entityName, where, options);
475
+ const [entities, count] = options.includeCount !== false
476
+ ? await em.findAndCount(entityName, where, options)
477
+ : [await em.find(entityName, where, options)];
461
478
  return new Cursor(entities, count, options, this.metadata.get(entityName));
462
479
  }
463
480
  /**
@@ -488,13 +505,25 @@ export class EntityManager {
488
505
  ...options,
489
506
  flushMode: FlushMode.COMMIT,
490
507
  });
491
- if (reloaded) {
492
- this.config.getHydrator(this.metadata).hydrate(entity, helper(entity).__meta, helper(reloaded).toPOJO(), this.getEntityFactory(), 'full');
508
+ const em = this.getContext();
509
+ if (!reloaded) {
510
+ em.unitOfWork.unsetIdentity(entity);
511
+ return null;
493
512
  }
494
- else {
495
- this.getUnitOfWork().unsetIdentity(entity);
513
+ let found = false;
514
+ for (const e of fork.unitOfWork.getIdentityMap()) {
515
+ const ref = em.getReference(e.constructor.name, helper(e).getPrimaryKey());
516
+ const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true });
517
+ em.config.getHydrator(this.metadata).hydrate(ref, helper(ref).__meta, data, em.entityFactory, 'full', false, true);
518
+ helper(ref).__originalEntityData = this.comparator.prepareEntity(e);
519
+ found ||= ref === entity;
496
520
  }
497
- return reloaded ? entity : reloaded;
521
+ if (!found) {
522
+ const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true });
523
+ em.config.getHydrator(this.metadata).hydrate(entity, helper(entity).__meta, data, em.entityFactory, 'full', false, true);
524
+ helper(entity).__originalEntityData = this.comparator.prepareEntity(reloaded);
525
+ }
526
+ return entity;
498
527
  }
499
528
  /**
500
529
  * Finds first entity matching your `where` query.
@@ -530,14 +559,15 @@ export class EntityManager {
530
559
  options.populate = await em.preparePopulate(entityName, options);
531
560
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
532
561
  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
- });
562
+ if (cached?.data !== undefined) {
563
+ if (cached.data) {
564
+ await em.entityLoader.populate(entityName, [cached.data], options.populate, {
565
+ ...options,
566
+ ...em.getPopulateWhere(where, options),
567
+ ignoreLazyScalarProperties: true,
568
+ lookup: false,
569
+ });
570
+ }
541
571
  return cached.data;
542
572
  }
543
573
  options = { ...options };
@@ -547,6 +577,7 @@ export class EntityManager {
547
577
  options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
548
578
  const data = await em.driver.findOne(entityName, where, {
549
579
  ctx: em.transactionContext,
580
+ em,
550
581
  ...options,
551
582
  });
552
583
  if (!data) {
@@ -716,8 +747,9 @@ export class EntityManager {
716
747
  ctx: em.transactionContext,
717
748
  convertCustomTypes: true,
718
749
  connectionType: 'write',
750
+ schema: options.schema,
719
751
  });
720
- em.getHydrator().hydrate(entity, meta, data2, em.entityFactory, 'full');
752
+ em.getHydrator().hydrate(entity, meta, data2, em.entityFactory, 'full', false, true);
721
753
  }
722
754
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
723
755
  const snapshot = this.comparator.prepareEntity(entity);
@@ -896,6 +928,7 @@ export class EntityManager {
896
928
  ctx: em.transactionContext,
897
929
  convertCustomTypes: true,
898
930
  connectionType: 'write',
931
+ schema: options.schema,
899
932
  });
900
933
  for (const [entity, cond] of loadPK.entries()) {
901
934
  const row = data2.find(row => {
@@ -911,7 +944,7 @@ export class EntityManager {
911
944
  if (!row) {
912
945
  throw new Error(`Cannot find matching entity for condition ${JSON.stringify(cond)}`);
913
946
  }
914
- em.getHydrator().hydrate(entity, meta, row, em.entityFactory, 'full');
947
+ em.getHydrator().hydrate(entity, meta, row, em.entityFactory, 'full', false, true);
915
948
  }
916
949
  if (loadPK.size !== data2.length && Array.isArray(uniqueFields)) {
917
950
  for (let i = 0; i < allData.length; i++) {
@@ -952,45 +985,37 @@ export class EntityManager {
952
985
  }
953
986
  /**
954
987
  * Runs your callback wrapped inside a database transaction.
988
+ *
989
+ * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
990
+ * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
991
+ * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
992
+ * method automatically creates an async context for the transaction.
993
+ *
994
+ * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
995
+ * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
996
+ * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
997
+ * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
998
+ *
999
+ * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
1000
+ * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
1001
+ * and then call this method on the fork.
1002
+ *
1003
+ * **Example:**
1004
+ * ```ts
1005
+ * await em.transactional(async (em) => {
1006
+ * const author = new Author('Jon');
1007
+ * em.persist(author);
1008
+ * // flush is called automatically at the end of the callback
1009
+ * });
1010
+ * ```
955
1011
  */
956
1012
  async transactional(cb, options = {}) {
957
1013
  const em = this.getContext(false);
958
1014
  if (this.disableTransactions || em.disableTransactions) {
959
1015
  return cb(em);
960
1016
  }
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
- });
1017
+ const manager = new TransactionManager(this);
1018
+ return manager.handle(cb, options);
994
1019
  }
995
1020
  /**
996
1021
  * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions.
@@ -1170,22 +1195,35 @@ export class EntityManager {
1170
1195
  * via second parameter. By default, it will return already loaded entities without modifying them.
1171
1196
  */
1172
1197
  merge(entityName, data, options = {}) {
1173
- const em = this.getContext();
1174
1198
  if (Utils.isEntity(entityName)) {
1175
- return em.merge(entityName.constructor.name, entityName, data);
1199
+ return this.merge(entityName.constructor.name, entityName, data);
1176
1200
  }
1201
+ const em = options.disableContextResolution ? this : this.getContext();
1177
1202
  options.schema ??= em._schema;
1203
+ options.validate ??= true;
1204
+ options.cascade ??= true;
1178
1205
  entityName = Utils.className(entityName);
1179
- em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1206
+ if (options.validate) {
1207
+ em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1208
+ }
1180
1209
  let entity = em.unitOfWork.tryGetById(entityName, data, options.schema, false);
1181
1210
  if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) {
1182
1211
  return entity;
1183
1212
  }
1184
1213
  const meta = em.metadata.find(entityName);
1185
1214
  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);
1215
+ const dataIsEntity = Utils.isEntity(data);
1216
+ if (options.keepIdentity && entity && dataIsEntity && entity !== data) {
1217
+ helper(entity).__data = helper(data).__data;
1218
+ helper(entity).__originalEntityData = helper(data).__originalEntityData;
1219
+ return entity;
1220
+ }
1221
+ entity = dataIsEntity ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1222
+ if (options.validate) {
1223
+ em.validator.validate(entity, data, childMeta ?? meta);
1224
+ }
1225
+ const visited = options.cascade ? undefined : new Set([entity]);
1226
+ em.unitOfWork.merge(entity, visited);
1189
1227
  return entity;
1190
1228
  }
1191
1229
  /**
@@ -1248,10 +1286,8 @@ export class EntityManager {
1248
1286
  async count(entityName, where = {}, options = {}) {
1249
1287
  const em = this.getContext(false);
1250
1288
  // Shallow copy options since the object will be modified when deleting orderBy
1251
- options = {
1252
- schema: em._schema,
1253
- ...options,
1254
- };
1289
+ options = { ...options };
1290
+ em.prepareOptions(options);
1255
1291
  entityName = Utils.className(entityName);
1256
1292
  await em.tryFlush(entityName, options);
1257
1293
  where = await em.processWhere(entityName, where, options, 'read');
@@ -1266,10 +1302,10 @@ export class EntityManager {
1266
1302
  delete options.orderBy;
1267
1303
  const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
1268
1304
  const cached = await em.tryCache(entityName, options.cache, cacheKey);
1269
- if (cached?.data) {
1305
+ if (cached?.data !== undefined) {
1270
1306
  return cached.data;
1271
1307
  }
1272
- const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, ...options });
1308
+ const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, em, ...options });
1273
1309
  await em.storeCache(options.cache, cached, () => +count);
1274
1310
  return +count;
1275
1311
  }
@@ -1431,6 +1467,9 @@ export class EntityManager {
1431
1467
  for (const entity of em.unitOfWork.getIdentityMap()) {
1432
1468
  fork.unitOfWork.register(entity);
1433
1469
  }
1470
+ for (const entity of em.unitOfWork.getPersistStack()) {
1471
+ fork.unitOfWork.persist(entity);
1472
+ }
1434
1473
  for (const entity of em.unitOfWork.getOrphanRemoveStack()) {
1435
1474
  fork.unitOfWork.getOrphanRemoveStack().add(entity);
1436
1475
  }
@@ -1452,6 +1491,12 @@ export class EntityManager {
1452
1491
  getEntityFactory() {
1453
1492
  return this.getContext().entityFactory;
1454
1493
  }
1494
+ /**
1495
+ * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
1496
+ */
1497
+ getEntityLoader() {
1498
+ return this.getContext().entityLoader;
1499
+ }
1455
1500
  /**
1456
1501
  * Gets the Hydrator used by the EntityManager.
1457
1502
  */
@@ -1548,7 +1593,6 @@ export class EntityManager {
1548
1593
  ...options,
1549
1594
  ...this.getPopulateWhere(where, options),
1550
1595
  orderBy: options.populateOrderBy ?? options.orderBy,
1551
- convertCustomTypes: false,
1552
1596
  ignoreLazyScalarProperties: true,
1553
1597
  lookup: false,
1554
1598
  });
@@ -1671,7 +1715,7 @@ export class EntityManager {
1671
1715
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1672
1716
  }
1673
1717
  options.schema ??= this._schema;
1674
- options.logging = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1718
+ options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1675
1719
  }
1676
1720
  /**
1677
1721
  * @internal
@@ -1695,31 +1739,31 @@ export class EntityManager {
1695
1739
  const em = this.getContext();
1696
1740
  const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
1697
1741
  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 };
1742
+ if (!cached) {
1743
+ return { key: cacheKey, data: cached };
1744
+ }
1745
+ let data;
1746
+ if (Array.isArray(cached) && merge) {
1747
+ data = cached.map(item => em.entityFactory.create(entityName, item, {
1748
+ merge: true,
1749
+ convertCustomTypes: true,
1750
+ refresh,
1751
+ recomputeSnapshot: true,
1752
+ }));
1721
1753
  }
1722
- return { key: cacheKey };
1754
+ else if (Utils.isObject(cached) && merge) {
1755
+ data = em.entityFactory.create(entityName, cached, {
1756
+ merge: true,
1757
+ convertCustomTypes: true,
1758
+ refresh,
1759
+ recomputeSnapshot: true,
1760
+ });
1761
+ }
1762
+ else {
1763
+ data = cached;
1764
+ }
1765
+ await em.unitOfWork.dispatchOnLoadEvent();
1766
+ return { key: cacheKey, data };
1723
1767
  }
1724
1768
  /**
1725
1769
  * @internal
package/MikroORM.js CHANGED
@@ -35,7 +35,6 @@ export class MikroORM {
35
35
  }
36
36
  }
37
37
  options = Utils.mergeConfig(options, env);
38
- ConfigurationLoader.commonJSCompat(options);
39
38
  if ('DRIVER' in this && !options.driver) {
40
39
  options.driver = this.DRIVER;
41
40
  }
package/README.md CHANGED
@@ -11,7 +11,6 @@ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-or
11
11
  [![Chat on discord](https://img.shields.io/discord/1214904142443839538?label=discord&color=blue)](https://discord.gg/w8bjxFHS7X)
12
12
  [![Downloads](https://img.shields.io/npm/dm/@mikro-orm/core.svg)](https://www.npmjs.com/package/@mikro-orm/core)
13
13
  [![Coverage Status](https://img.shields.io/coveralls/mikro-orm/mikro-orm.svg)](https://coveralls.io/r/mikro-orm/mikro-orm?branch=master)
14
- [![Maintainability](https://api.codeclimate.com/v1/badges/27999651d3adc47cfa40/maintainability)](https://codeclimate.com/github/mikro-orm/mikro-orm/maintainability)
15
14
  [![Build Status](https://github.com/mikro-orm/mikro-orm/workflows/tests/badge.svg?branch=master)](https://github.com/mikro-orm/mikro-orm/actions?workflow=tests)
16
15
 
17
16
  ## 🤔 Unit of What?
@@ -141,7 +140,7 @@ There is also auto-generated [CHANGELOG.md](CHANGELOG.md) file based on commit m
141
140
  - [Composite and Foreign Keys as Primary Key](https://mikro-orm.io/docs/composite-keys)
142
141
  - [Filters](https://mikro-orm.io/docs/filters)
143
142
  - [Using `QueryBuilder`](https://mikro-orm.io/docs/query-builder)
144
- - [Preloading Deeply Nested Structures via populate](https://mikro-orm.io/docs/nested-populate)
143
+ - [Populating relations](https://mikro-orm.io/docs/populating-relations)
145
144
  - [Property Validation](https://mikro-orm.io/docs/property-validation)
146
145
  - [Lifecycle Hooks](https://mikro-orm.io/docs/events#hooks)
147
146
  - [Vanilla JS Support](https://mikro-orm.io/docs/usage-with-js)
@@ -3,12 +3,13 @@ export declare class FileCacheAdapter implements SyncCacheAdapter {
3
3
  private readonly options;
4
4
  private readonly baseDir;
5
5
  private readonly pretty;
6
+ private readonly hashAlgorithm;
6
7
  private readonly VERSION;
7
8
  private cache;
8
9
  constructor(options: {
9
10
  cacheDir: string;
10
11
  combined?: boolean | string;
11
- }, baseDir: string, pretty?: boolean);
12
+ }, baseDir: string, pretty?: boolean, hashAlgorithm?: 'md5' | 'sha256');
12
13
  /**
13
14
  * @inheritDoc
14
15
  */