@mikro-orm/core 7.0.0-dev.6 → 7.0.0-dev.60

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 (122) hide show
  1. package/EntityManager.d.ts +85 -32
  2. package/EntityManager.js +281 -178
  3. package/MikroORM.d.ts +8 -8
  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 +5 -4
  8. package/connections/Connection.d.ts +11 -7
  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 +3 -1
  18. package/decorators/Transactional.js +6 -3
  19. package/decorators/index.d.ts +1 -1
  20. package/drivers/DatabaseDriver.d.ts +11 -5
  21. package/drivers/DatabaseDriver.js +13 -4
  22. package/drivers/IDatabaseDriver.d.ts +29 -5
  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 +34 -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 +40 -22
  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 +2 -2
  38. package/entity/Reference.d.ts +9 -7
  39. package/entity/Reference.js +32 -5
  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 +18 -5
  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 +40 -20
  61. package/metadata/MetadataDiscovery.d.ts +5 -7
  62. package/metadata/MetadataDiscovery.js +150 -155
  63. package/metadata/MetadataStorage.js +1 -1
  64. package/metadata/MetadataValidator.js +4 -3
  65. package/metadata/discover-entities.d.ts +5 -0
  66. package/metadata/discover-entities.js +39 -0
  67. package/naming-strategy/AbstractNamingStrategy.d.ts +5 -1
  68. package/naming-strategy/AbstractNamingStrategy.js +7 -1
  69. package/naming-strategy/NamingStrategy.d.ts +11 -1
  70. package/package.json +14 -7
  71. package/platforms/Platform.d.ts +5 -8
  72. package/platforms/Platform.js +4 -17
  73. package/serialization/EntitySerializer.d.ts +2 -0
  74. package/serialization/EntitySerializer.js +29 -11
  75. package/serialization/EntityTransformer.js +22 -12
  76. package/serialization/SerializationContext.js +14 -11
  77. package/types/BigIntType.d.ts +9 -6
  78. package/types/BigIntType.js +3 -0
  79. package/types/BlobType.d.ts +0 -1
  80. package/types/BlobType.js +0 -3
  81. package/types/BooleanType.d.ts +2 -1
  82. package/types/BooleanType.js +3 -0
  83. package/types/DecimalType.d.ts +6 -4
  84. package/types/DecimalType.js +1 -1
  85. package/types/DoubleType.js +1 -1
  86. package/types/JsonType.d.ts +1 -1
  87. package/types/JsonType.js +7 -2
  88. package/types/Type.d.ts +2 -1
  89. package/types/Type.js +1 -1
  90. package/types/Uint8ArrayType.d.ts +0 -1
  91. package/types/Uint8ArrayType.js +0 -3
  92. package/types/index.d.ts +1 -1
  93. package/typings.d.ts +94 -50
  94. package/typings.js +31 -31
  95. package/unit-of-work/ChangeSetComputer.js +8 -3
  96. package/unit-of-work/ChangeSetPersister.d.ts +4 -2
  97. package/unit-of-work/ChangeSetPersister.js +37 -16
  98. package/unit-of-work/UnitOfWork.d.ts +8 -1
  99. package/unit-of-work/UnitOfWork.js +110 -53
  100. package/utils/AbstractSchemaGenerator.js +3 -1
  101. package/utils/Configuration.d.ts +201 -184
  102. package/utils/Configuration.js +143 -151
  103. package/utils/ConfigurationLoader.d.ts +9 -22
  104. package/utils/ConfigurationLoader.js +53 -76
  105. package/utils/Cursor.d.ts +3 -3
  106. package/utils/Cursor.js +3 -0
  107. package/utils/DataloaderUtils.d.ts +15 -5
  108. package/utils/DataloaderUtils.js +53 -7
  109. package/utils/EntityComparator.d.ts +8 -4
  110. package/utils/EntityComparator.js +105 -58
  111. package/utils/QueryHelper.d.ts +9 -1
  112. package/utils/QueryHelper.js +66 -5
  113. package/utils/RawQueryFragment.d.ts +36 -4
  114. package/utils/RawQueryFragment.js +34 -13
  115. package/utils/TransactionManager.d.ts +65 -0
  116. package/utils/TransactionManager.js +223 -0
  117. package/utils/Utils.d.ts +13 -12
  118. package/utils/Utils.js +106 -66
  119. package/utils/index.d.ts +1 -0
  120. package/utils/index.js +1 -0
  121. package/utils/upsert-utils.d.ts +7 -2
  122. package/utils/upsert-utils.js +52 -1
package/EntityManager.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { inspect } from 'node:util';
2
- import DataLoader from 'dataloader';
3
- import { getOnConflictReturningFields } from './utils/upsert-utils.js';
2
+ import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
4
3
  import { Utils } from './utils/Utils.js';
5
4
  import { Cursor } from './utils/Cursor.js';
6
5
  import { DataloaderUtils } from './utils/DataloaderUtils.js';
@@ -19,6 +18,8 @@ import { EventType, FlushMode, LoadStrategy, LockMode, PopulateHint, PopulatePat
19
18
  import { EventManager } from './events/EventManager.js';
20
19
  import { TransactionEventBroadcaster } from './events/TransactionEventBroadcaster.js';
21
20
  import { OptimisticLockError, ValidationError } from './errors.js';
21
+ import { getLoadingStrategy } from './entity/utils.js';
22
+ import { TransactionManager } from './utils/TransactionManager.js';
22
23
  /**
23
24
  * The EntityManager is the central access point to ORM functionality. It is a facade to all different ORM subsystems
24
25
  * such as UnitOfWork, Query Language, and Repository API.
@@ -34,8 +35,7 @@ export class EntityManager {
34
35
  _id = EntityManager.counter++;
35
36
  global = false;
36
37
  name;
37
- refLoader = new DataLoader(DataloaderUtils.getRefBatchLoadFn(this));
38
- colLoader = new DataLoader(DataloaderUtils.getColBatchLoadFn(this));
38
+ loaders = {};
39
39
  validator;
40
40
  repositoryMap = {};
41
41
  entityLoader;
@@ -137,7 +137,6 @@ export class EntityManager {
137
137
  await em.entityLoader.populate(entityName, cached.data, populate, {
138
138
  ...options,
139
139
  ...em.getPopulateWhere(where, options),
140
- convertCustomTypes: false,
141
140
  ignoreLazyScalarProperties: true,
142
141
  lookup: false,
143
142
  });
@@ -148,8 +147,8 @@ export class EntityManager {
148
147
  // save the original hint value so we know it was infer/all
149
148
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
150
149
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
151
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
152
- const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, ...options });
150
+ options.populateFilter = await this.getJoinedFilters(meta, options);
151
+ const results = await em.driver.find(entityName, where, { ctx: em.transactionContext, em, ...options });
153
152
  if (results.length === 0) {
154
153
  await em.storeCache(options.cache, cached, []);
155
154
  return [];
@@ -168,7 +167,6 @@ export class EntityManager {
168
167
  await em.entityLoader.populate(entityName, unique, populate, {
169
168
  ...options,
170
169
  ...em.getPopulateWhere(where, options),
171
- convertCustomTypes: false,
172
170
  ignoreLazyScalarProperties: true,
173
171
  lookup: false,
174
172
  });
@@ -181,6 +179,61 @@ export class EntityManager {
181
179
  }
182
180
  return unique;
183
181
  }
182
+ /**
183
+ * Finds all entities and returns an async iterable (async generator) that yields results one by one.
184
+ * The results are merged and mapped to entity instances, without adding them to the identity map.
185
+ * You can disable merging by passing the options `{ mergeResults: false }`.
186
+ * With `mergeResults` disabled, to-many collections will contain at most one item, and you will get duplicate
187
+ * root entities when there are multiple items in the populated collection.
188
+ * This is useful for processing large datasets without loading everything into memory at once.
189
+ *
190
+ * ```ts
191
+ * const stream = em.stream(Book, { populate: ['author'] });
192
+ *
193
+ * for await (const book of stream) {
194
+ * // book is an instance of Book entity
195
+ * console.log(book.title, book.author.name);
196
+ * }
197
+ * ```
198
+ */
199
+ async *stream(entityName, options = {}) {
200
+ const em = this.getContext();
201
+ em.prepareOptions(options);
202
+ options.strategy = 'joined';
203
+ await em.tryFlush(entityName, options);
204
+ entityName = Utils.className(entityName);
205
+ const where = await em.processWhere(entityName, options.where ?? {}, options, 'read');
206
+ em.validator.validateParams(where);
207
+ options.orderBy = options.orderBy || {};
208
+ options.populate = await em.preparePopulate(entityName, options);
209
+ const meta = this.metadata.get(entityName);
210
+ options = { ...options };
211
+ // save the original hint value so we know it was infer/all
212
+ options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
213
+ options.populateWhere = this.createPopulateWhere({ ...where }, options);
214
+ options.populateFilter = await this.getJoinedFilters(meta, options);
215
+ const stream = em.driver.stream(entityName, where, {
216
+ ctx: em.transactionContext,
217
+ mapResults: false,
218
+ ...options,
219
+ });
220
+ for await (const data of stream) {
221
+ const fork = em.fork();
222
+ const entity = fork.entityFactory.create(entityName, data, {
223
+ refresh: options.refresh,
224
+ schema: options.schema,
225
+ convertCustomTypes: true,
226
+ });
227
+ helper(entity).setSerializationContext({
228
+ populate: options.populate,
229
+ fields: options.fields,
230
+ exclude: options.exclude,
231
+ });
232
+ await fork.unitOfWork.dispatchOnLoadEvent();
233
+ fork.clear();
234
+ yield entity;
235
+ }
236
+ }
184
237
  /**
185
238
  * Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
186
239
  */
@@ -203,12 +256,12 @@ export class EntityManager {
203
256
  /**
204
257
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
205
258
  */
206
- addFilter(name, cond, entityName, enabled = true) {
207
- const options = { name, cond, default: enabled };
208
- if (entityName) {
209
- options.entity = Utils.asArray(entityName).map(n => Utils.className(n));
259
+ addFilter(options) {
260
+ if (options.entity) {
261
+ options.entity = Utils.asArray(options.entity).map(n => Utils.className(n));
210
262
  }
211
- this.getContext(false).filters[name] = options;
263
+ options.default ??= true;
264
+ this.getContext(false).filters[options.name] = options;
212
265
  }
213
266
  /**
214
267
  * Sets filter parameter values globally inside context defined by this entity manager.
@@ -232,8 +285,8 @@ export class EntityManager {
232
285
  /**
233
286
  * Gets logger context for this entity manager.
234
287
  */
235
- getLoggerContext() {
236
- const em = this.getContext();
288
+ getLoggerContext(options) {
289
+ const em = options?.disableContextResolution ? this : this.getContext();
237
290
  em.loggerContext ??= {};
238
291
  return em.loggerContext;
239
292
  }
@@ -283,28 +336,39 @@ export class EntityManager {
283
336
  }
284
337
  return ret;
285
338
  }
286
- async getJoinedFilters(meta, cond, options) {
339
+ async getJoinedFilters(meta, options) {
340
+ if (!this.config.get('filtersOnRelations') || !options.populate) {
341
+ return undefined;
342
+ }
287
343
  const ret = {};
288
- if (options.populate) {
289
- for (const hint of options.populate) {
290
- const field = hint.field.split(':')[0];
291
- const prop = meta.properties[field];
292
- const joined = (prop.strategy || options.strategy || hint.strategy || this.config.get('loadStrategy')) === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
293
- if (!joined && !hint.filter) {
294
- continue;
295
- }
296
- const where = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', { ...options, populate: hint.children });
297
- const where2 = await this.getJoinedFilters(prop.targetMeta, {}, { ...options, populate: hint.children, populateWhere: PopulateHint.ALL });
298
- if (Utils.hasObjectKeys(where)) {
299
- ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
344
+ for (const hint of options.populate) {
345
+ const field = hint.field.split(':')[0];
346
+ const prop = meta.properties[field];
347
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
348
+ const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
349
+ if (!joined && !hint.filter) {
350
+ continue;
351
+ }
352
+ const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
353
+ const where = await this.applyFilters(prop.type, {}, filters, 'read', {
354
+ ...options,
355
+ populate: hint.children,
356
+ });
357
+ const where2 = await this.getJoinedFilters(prop.targetMeta, {
358
+ ...options,
359
+ filters,
360
+ populate: hint.children,
361
+ populateWhere: PopulateHint.ALL,
362
+ });
363
+ if (Utils.hasObjectKeys(where)) {
364
+ ret[field] = ret[field] ? { $and: [where, ret[field]] } : where;
365
+ }
366
+ if (where2 && Utils.hasObjectKeys(where2)) {
367
+ if (ret[field]) {
368
+ Utils.merge(ret[field], where2);
300
369
  }
301
- if (Utils.hasObjectKeys(where2)) {
302
- if (ret[field]) {
303
- Utils.merge(ret[field], where2);
304
- }
305
- else {
306
- ret[field] = where2;
307
- }
370
+ else {
371
+ ret[field] = where2;
308
372
  }
309
373
  }
310
374
  }
@@ -313,27 +377,45 @@ export class EntityManager {
313
377
  /**
314
378
  * When filters are active on M:1 or 1:1 relations, we need to ref join them eagerly as they might affect the FK value.
315
379
  */
316
- async autoJoinRefsForFilters(meta, options) {
317
- if (!meta || !this.config.get('autoJoinRefsForFilters')) {
380
+ async autoJoinRefsForFilters(meta, options, parent) {
381
+ if (!meta || !this.config.get('autoJoinRefsForFilters') || options.filters === false) {
318
382
  return;
319
383
  }
320
- const props = meta.relations.filter(prop => {
321
- return !prop.object && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
322
- && ((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)));
323
- });
324
384
  const ret = options.populate;
325
- for (const prop of props) {
326
- const cond = await this.applyFilters(prop.type, {}, options.filters ?? {}, 'read', options);
385
+ for (const prop of meta.relations) {
386
+ if (prop.object
387
+ || ![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)
388
+ || !((options.fields?.length ?? 0) === 0 || options.fields?.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
389
+ || (parent?.className === prop.targetMeta.root.className && parent.propName === prop.inversedBy)) {
390
+ continue;
391
+ }
392
+ options = { ...options, filters: QueryHelper.mergePropertyFilters(prop.filters, options.filters) };
393
+ const cond = await this.applyFilters(prop.type, {}, options.filters, 'read', options);
327
394
  if (!Utils.isEmpty(cond)) {
328
395
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
329
- if (populated.length > 0) {
330
- populated.forEach(hint => hint.filter = true);
396
+ let found = false;
397
+ for (const hint of populated) {
398
+ if (!hint.all) {
399
+ hint.filter = true;
400
+ }
401
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
402
+ if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
403
+ found = true;
404
+ }
331
405
  }
332
- else {
406
+ if (!found) {
333
407
  ret.push({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED, filter: true });
334
408
  }
335
409
  }
336
410
  }
411
+ for (const hint of ret) {
412
+ const [field, ref] = hint.field.split(':');
413
+ const prop = meta?.properties[field];
414
+ if (prop && !ref) {
415
+ hint.children ??= [];
416
+ await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { className: meta.root.className, propName: prop.name });
417
+ }
418
+ }
337
419
  }
338
420
  /**
339
421
  * @internal
@@ -363,7 +445,7 @@ export class EntityManager {
363
445
  let cond;
364
446
  if (filter.cond instanceof Function) {
365
447
  // @ts-ignore
366
- const args = Utils.isPlainObject(options[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
448
+ const args = Utils.isPlainObject(options?.[filter.name]) ? options[filter.name] : this.getContext().filterParams[filter.name];
367
449
  if (!args && filter.cond.length > 0 && filter.args !== false) {
368
450
  throw new Error(`No arguments provided for filter '${filter.name}'`);
369
451
  }
@@ -372,13 +454,17 @@ export class EntityManager {
372
454
  else {
373
455
  cond = filter.cond;
374
456
  }
375
- ret.push(QueryHelper.processWhere({
457
+ cond = QueryHelper.processWhere({
376
458
  where: cond,
377
459
  entityName,
378
460
  metadata: this.metadata,
379
461
  platform: this.driver.getPlatform(),
380
462
  aliased: type === 'read',
381
- }));
463
+ });
464
+ if (filter.strict) {
465
+ Object.defineProperty(cond, '__strict', { value: filter.strict, enumerable: false });
466
+ }
467
+ ret.push(cond);
382
468
  }
383
469
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c));
384
470
  return conds.length > 1 ? { $and: conds } : conds[0];
@@ -433,6 +519,10 @@ export class EntityManager {
433
519
  * });
434
520
  * ```
435
521
  *
522
+ * The options also support an `includeCount` (true by default) option. If set to false, the `totalCount` is not
523
+ * returned as part of the cursor. This is useful for performance reason, when you don't care about the total number
524
+ * of pages.
525
+ *
436
526
  * The `Cursor` object provides the following interface:
437
527
  *
438
528
  * ```ts
@@ -442,7 +532,7 @@ export class EntityManager {
442
532
  * User { ... },
443
533
  * User { ... },
444
534
  * ],
445
- * totalCount: 50,
535
+ * totalCount: 50, // not included if `includeCount: false`
446
536
  * startCursor: 'WzRd',
447
537
  * endCursor: 'WzZd',
448
538
  * hasPrevPage: true,
@@ -457,7 +547,9 @@ export class EntityManager {
457
547
  if (Utils.isEmpty(options.orderBy)) {
458
548
  throw new Error('Explicit `orderBy` option required');
459
549
  }
460
- const [entities, count] = await em.findAndCount(entityName, where, options);
550
+ const [entities, count] = options.includeCount !== false
551
+ ? await em.findAndCount(entityName, where, options)
552
+ : [await em.find(entityName, where, options)];
461
553
  return new Cursor(entities, count, options, this.metadata.get(entityName));
462
554
  }
463
555
  /**
@@ -483,18 +575,31 @@ export class EntityManager {
483
575
  async refresh(entity, options = {}) {
484
576
  const fork = this.fork({ keepTransactionContext: true });
485
577
  const entityName = entity.constructor.name;
578
+ const wrapped = helper(entity);
486
579
  const reloaded = await fork.findOne(entityName, entity, {
487
- schema: helper(entity).__schema,
580
+ schema: wrapped.__schema,
488
581
  ...options,
489
582
  flushMode: FlushMode.COMMIT,
490
583
  });
491
- if (reloaded) {
492
- this.config.getHydrator(this.metadata).hydrate(entity, helper(entity).__meta, helper(reloaded).toPOJO(), this.getEntityFactory(), 'full');
584
+ const em = this.getContext();
585
+ if (!reloaded) {
586
+ em.unitOfWork.unsetIdentity(entity);
587
+ return null;
493
588
  }
494
- else {
495
- this.getUnitOfWork().unsetIdentity(entity);
589
+ let found = false;
590
+ for (const e of fork.unitOfWork.getIdentityMap()) {
591
+ const ref = em.getReference(e.constructor.name, helper(e).getPrimaryKey());
592
+ const data = helper(e).serialize({ ignoreSerializers: true, includeHidden: true });
593
+ em.config.getHydrator(this.metadata).hydrate(ref, helper(ref).__meta, data, em.entityFactory, 'full', false, true);
594
+ Utils.merge(helper(ref).__originalEntityData, this.comparator.prepareEntity(e));
595
+ found ||= ref === entity;
596
+ }
597
+ if (!found) {
598
+ const data = helper(reloaded).serialize({ ignoreSerializers: true, includeHidden: true });
599
+ em.config.getHydrator(this.metadata).hydrate(entity, wrapped.__meta, data, em.entityFactory, 'full', false, true);
600
+ Utils.merge(wrapped.__originalEntityData, this.comparator.prepareEntity(reloaded));
496
601
  }
497
- return reloaded ? entity : reloaded;
602
+ return entity;
498
603
  }
499
604
  /**
500
605
  * Finds first entity matching your `where` query.
@@ -530,23 +635,25 @@ export class EntityManager {
530
635
  options.populate = await em.preparePopulate(entityName, options);
531
636
  const cacheKey = em.cacheKey(entityName, options, 'em.findOne', where);
532
637
  const cached = await em.tryCache(entityName, options.cache, cacheKey, options.refresh, true);
533
- if (cached?.data) {
534
- await em.entityLoader.populate(entityName, [cached.data], options.populate, {
535
- ...options,
536
- ...em.getPopulateWhere(where, options),
537
- convertCustomTypes: false,
538
- ignoreLazyScalarProperties: true,
539
- lookup: false,
540
- });
638
+ if (cached?.data !== undefined) {
639
+ if (cached.data) {
640
+ await em.entityLoader.populate(entityName, [cached.data], options.populate, {
641
+ ...options,
642
+ ...em.getPopulateWhere(where, options),
643
+ ignoreLazyScalarProperties: true,
644
+ lookup: false,
645
+ });
646
+ }
541
647
  return cached.data;
542
648
  }
543
649
  options = { ...options };
544
650
  // save the original hint value so we know it was infer/all
545
651
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
546
652
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
547
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
653
+ options.populateFilter = await this.getJoinedFilters(meta, options);
548
654
  const data = await em.driver.findOne(entityName, where, {
549
655
  ctx: em.transactionContext,
656
+ em,
550
657
  ...options,
551
658
  });
552
659
  if (!data) {
@@ -654,24 +761,7 @@ export class EntityManager {
654
761
  }
655
762
  }
656
763
  }
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
- }
764
+ where = getWhereCondition(meta, options.onConflictFields, data, where).where;
675
765
  data = QueryHelper.processObjectParams(data);
676
766
  em.validator.validateParams(data, 'insert data');
677
767
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
@@ -716,6 +806,7 @@ export class EntityManager {
716
806
  ctx: em.transactionContext,
717
807
  convertCustomTypes: true,
718
808
  connectionType: 'write',
809
+ schema: options.schema,
719
810
  });
720
811
  em.getHydrator().hydrate(entity, meta, data2, em.entityFactory, 'full', false, true);
721
812
  }
@@ -813,31 +904,17 @@ export class EntityManager {
813
904
  }
814
905
  }
815
906
  }
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);
907
+ const unique = options.onConflictFields ?? meta.props.filter(p => p.unique).map(p => p.name);
908
+ propIndex = !isRaw(unique) && unique.findIndex(p => data[p] ?? data[p.substring(0, p.indexOf('.'))] != null);
909
+ const tmp = getWhereCondition(meta, options.onConflictFields, row, where);
910
+ propIndex = tmp.propIndex;
835
911
  where = QueryHelper.processWhere({
836
- where,
912
+ where: tmp.where,
837
913
  entityName,
838
914
  metadata: this.metadata,
839
915
  platform: this.getPlatform(),
840
916
  });
917
+ row = QueryHelper.processObjectParams(row);
841
918
  em.validator.validateParams(row, 'insert data');
842
919
  allData.push(row);
843
920
  allWhere.push(where);
@@ -879,7 +956,7 @@ export class EntityManager {
879
956
  const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length);
880
957
  if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
881
958
  const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
882
- const add = new Set(propIndex >= 0 ? [unique[propIndex]] : []);
959
+ const add = new Set(propIndex !== false && propIndex >= 0 ? [unique[propIndex]] : []);
883
960
  for (const cond of loadPK.values()) {
884
961
  Utils.keys(cond).forEach(key => add.add(key));
885
962
  }
@@ -896,6 +973,7 @@ export class EntityManager {
896
973
  ctx: em.transactionContext,
897
974
  convertCustomTypes: true,
898
975
  connectionType: 'write',
976
+ schema: options.schema,
899
977
  });
900
978
  for (const [entity, cond] of loadPK.entries()) {
901
979
  const row = data2.find(row => {
@@ -952,45 +1030,37 @@ export class EntityManager {
952
1030
  }
953
1031
  /**
954
1032
  * Runs your callback wrapped inside a database transaction.
1033
+ *
1034
+ * If a transaction is already active, a new savepoint (nested transaction) will be created by default. This behavior
1035
+ * can be controlled via the `propagation` option. Use the provided EntityManager instance for all operations that
1036
+ * should be part of the transaction. You can safely use a global EntityManager instance from a DI container, as this
1037
+ * method automatically creates an async context for the transaction.
1038
+ *
1039
+ * **Concurrency note:** When running multiple transactions concurrently (e.g. in parallel requests or jobs), use the
1040
+ * `clear: true` option. This ensures the callback runs in a clear fork of the EntityManager, providing full isolation
1041
+ * between concurrent transactional handlers. Using `clear: true` is an alternative to forking explicitly and calling
1042
+ * the method on the new fork – it already provides the necessary isolation for safe concurrent usage.
1043
+ *
1044
+ * **Propagation note:** Changes made within a transaction (whether top-level or nested) are always propagated to the
1045
+ * parent context, unless the parent context is a global one. If you want to avoid that, fork the EntityManager first
1046
+ * and then call this method on the fork.
1047
+ *
1048
+ * **Example:**
1049
+ * ```ts
1050
+ * await em.transactional(async (em) => {
1051
+ * const author = new Author('Jon');
1052
+ * em.persist(author);
1053
+ * // flush is called automatically at the end of the callback
1054
+ * });
1055
+ * ```
955
1056
  */
956
1057
  async transactional(cb, options = {}) {
957
1058
  const em = this.getContext(false);
958
1059
  if (this.disableTransactions || em.disableTransactions) {
959
1060
  return cb(em);
960
1061
  }
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
- });
1062
+ const manager = new TransactionManager(this);
1063
+ return manager.handle(cb, options);
994
1064
  }
995
1065
  /**
996
1066
  * Starts new transaction bound to this EntityManager. Use `ctx` parameter to provide the parent when nesting transactions.
@@ -1170,22 +1240,35 @@ export class EntityManager {
1170
1240
  * via second parameter. By default, it will return already loaded entities without modifying them.
1171
1241
  */
1172
1242
  merge(entityName, data, options = {}) {
1173
- const em = this.getContext();
1174
1243
  if (Utils.isEntity(entityName)) {
1175
- return em.merge(entityName.constructor.name, entityName, data);
1244
+ return this.merge(entityName.constructor.name, entityName, data);
1176
1245
  }
1246
+ const em = options.disableContextResolution ? this : this.getContext();
1177
1247
  options.schema ??= em._schema;
1248
+ options.validate ??= true;
1249
+ options.cascade ??= true;
1178
1250
  entityName = Utils.className(entityName);
1179
- em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1251
+ if (options.validate) {
1252
+ em.validator.validatePrimaryKey(data, em.metadata.get(entityName));
1253
+ }
1180
1254
  let entity = em.unitOfWork.tryGetById(entityName, data, options.schema, false);
1181
1255
  if (entity && helper(entity).__managed && helper(entity).__initialized && !options.refresh) {
1182
1256
  return entity;
1183
1257
  }
1184
1258
  const meta = em.metadata.find(entityName);
1185
1259
  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);
1260
+ const dataIsEntity = Utils.isEntity(data);
1261
+ if (options.keepIdentity && entity && dataIsEntity && entity !== data) {
1262
+ helper(entity).__data = helper(data).__data;
1263
+ helper(entity).__originalEntityData = helper(data).__originalEntityData;
1264
+ return entity;
1265
+ }
1266
+ entity = dataIsEntity ? data : em.entityFactory.create(entityName, data, { merge: true, ...options });
1267
+ if (options.validate) {
1268
+ em.validator.validate(entity, data, childMeta ?? meta);
1269
+ }
1270
+ const visited = options.cascade ? undefined : new Set([entity]);
1271
+ em.unitOfWork.merge(entity, visited);
1189
1272
  return entity;
1190
1273
  }
1191
1274
  /**
@@ -1210,6 +1293,7 @@ export class EntityManager {
1210
1293
  ...options,
1211
1294
  newEntity: !options.managed,
1212
1295
  merge: options.managed,
1296
+ normalizeAccessors: true,
1213
1297
  });
1214
1298
  options.persist ??= em.config.get('persistOnCreate');
1215
1299
  if (options.persist && !this.getMetadata(entityName).embeddable) {
@@ -1248,10 +1332,8 @@ export class EntityManager {
1248
1332
  async count(entityName, where = {}, options = {}) {
1249
1333
  const em = this.getContext(false);
1250
1334
  // Shallow copy options since the object will be modified when deleting orderBy
1251
- options = {
1252
- schema: em._schema,
1253
- ...options,
1254
- };
1335
+ options = { ...options };
1336
+ em.prepareOptions(options);
1255
1337
  entityName = Utils.className(entityName);
1256
1338
  await em.tryFlush(entityName, options);
1257
1339
  where = await em.processWhere(entityName, where, options, 'read');
@@ -1261,15 +1343,15 @@ export class EntityManager {
1261
1343
  const meta = em.metadata.find(entityName);
1262
1344
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
1263
1345
  options.populateWhere = this.createPopulateWhere({ ...where }, options);
1264
- options.populateFilter = await this.getJoinedFilters(meta, { ...where }, options);
1346
+ options.populateFilter = await this.getJoinedFilters(meta, options);
1265
1347
  em.validator.validateParams(where);
1266
1348
  delete options.orderBy;
1267
1349
  const cacheKey = em.cacheKey(entityName, options, 'em.count', where);
1268
1350
  const cached = await em.tryCache(entityName, options.cache, cacheKey);
1269
- if (cached?.data) {
1351
+ if (cached?.data !== undefined) {
1270
1352
  return cached.data;
1271
1353
  }
1272
- const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, ...options });
1354
+ const count = await em.driver.count(entityName, where, { ctx: em.transactionContext, em, ...options });
1273
1355
  await em.storeCache(options.cache, cached, () => +count);
1274
1356
  return +count;
1275
1357
  }
@@ -1395,7 +1477,7 @@ export class EntityManager {
1395
1477
  const em = this.getContext();
1396
1478
  em.prepareOptions(options);
1397
1479
  const entityName = arr[0].constructor.name;
1398
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate }, options.validate);
1480
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1399
1481
  await em.entityLoader.populate(entityName, arr, preparedPopulate, options);
1400
1482
  return entities;
1401
1483
  }
@@ -1431,6 +1513,9 @@ export class EntityManager {
1431
1513
  for (const entity of em.unitOfWork.getIdentityMap()) {
1432
1514
  fork.unitOfWork.register(entity);
1433
1515
  }
1516
+ for (const entity of em.unitOfWork.getPersistStack()) {
1517
+ fork.unitOfWork.persist(entity);
1518
+ }
1434
1519
  for (const entity of em.unitOfWork.getOrphanRemoveStack()) {
1435
1520
  fork.unitOfWork.getOrphanRemoveStack().add(entity);
1436
1521
  }
@@ -1452,6 +1537,12 @@ export class EntityManager {
1452
1537
  getEntityFactory() {
1453
1538
  return this.getContext().entityFactory;
1454
1539
  }
1540
+ /**
1541
+ * @internal use `em.populate()` as the user facing API, this is exposed only for internal usage
1542
+ */
1543
+ getEntityLoader() {
1544
+ return this.getContext().entityLoader;
1545
+ }
1455
1546
  /**
1456
1547
  * Gets the Hydrator used by the EntityManager.
1457
1548
  */
@@ -1548,7 +1639,6 @@ export class EntityManager {
1548
1639
  ...options,
1549
1640
  ...this.getPopulateWhere(where, options),
1550
1641
  orderBy: options.populateOrderBy ?? options.orderBy,
1551
- convertCustomTypes: false,
1552
1642
  ignoreLazyScalarProperties: true,
1553
1643
  lookup: false,
1554
1644
  });
@@ -1671,7 +1761,7 @@ export class EntityManager {
1671
1761
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1672
1762
  }
1673
1763
  options.schema ??= this._schema;
1674
- options.logging = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1764
+ options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1675
1765
  }
1676
1766
  /**
1677
1767
  * @internal
@@ -1695,31 +1785,31 @@ export class EntityManager {
1695
1785
  const em = this.getContext();
1696
1786
  const cacheKey = Array.isArray(config) ? config[0] : JSON.stringify(key);
1697
1787
  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 };
1788
+ if (!cached) {
1789
+ return { key: cacheKey, data: cached };
1790
+ }
1791
+ let data;
1792
+ if (Array.isArray(cached) && merge) {
1793
+ data = cached.map(item => em.entityFactory.create(entityName, item, {
1794
+ merge: true,
1795
+ convertCustomTypes: true,
1796
+ refresh,
1797
+ recomputeSnapshot: true,
1798
+ }));
1799
+ }
1800
+ else if (Utils.isObject(cached) && merge) {
1801
+ data = em.entityFactory.create(entityName, cached, {
1802
+ merge: true,
1803
+ convertCustomTypes: true,
1804
+ refresh,
1805
+ recomputeSnapshot: true,
1806
+ });
1807
+ }
1808
+ else {
1809
+ data = cached;
1721
1810
  }
1722
- return { key: cacheKey };
1811
+ await em.unitOfWork.dispatchOnLoadEvent();
1812
+ return { key: cacheKey, data };
1723
1813
  }
1724
1814
  /**
1725
1815
  * @internal
@@ -1761,6 +1851,19 @@ export class EntityManager {
1761
1851
  set schema(schema) {
1762
1852
  this.getContext(false)._schema = schema ?? undefined;
1763
1853
  }
1854
+ /** @internal */
1855
+ async getDataLoader(type) {
1856
+ const em = this.getContext();
1857
+ if (em.loaders[type]) {
1858
+ return em.loaders[type];
1859
+ }
1860
+ const DataLoader = await DataloaderUtils.getDataLoader();
1861
+ switch (type) {
1862
+ case 'ref': return (em.loaders[type] ??= new DataLoader(DataloaderUtils.getRefBatchLoadFn(em)));
1863
+ case '1:m': return (em.loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
1864
+ case 'm:n': return (em.loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
1865
+ }
1866
+ }
1764
1867
  /**
1765
1868
  * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
1766
1869
  * if executed inside request context handler.