@mikro-orm/core 7.1.0-dev.5 → 7.1.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 (66) hide show
  1. package/EntityManager.d.ts +87 -12
  2. package/EntityManager.js +252 -40
  3. package/README.md +2 -1
  4. package/connections/Connection.d.ts +67 -1
  5. package/connections/Connection.js +64 -0
  6. package/drivers/IDatabaseDriver.d.ts +45 -7
  7. package/entity/BaseEntity.d.ts +68 -1
  8. package/entity/BaseEntity.js +18 -0
  9. package/entity/Collection.d.ts +6 -3
  10. package/entity/Collection.js +15 -4
  11. package/entity/EntityAssigner.js +8 -0
  12. package/entity/EntityFactory.js +5 -3
  13. package/entity/EntityLoader.d.ts +8 -1
  14. package/entity/EntityLoader.js +103 -30
  15. package/entity/EntityRepository.d.ts +27 -9
  16. package/entity/EntityRepository.js +12 -0
  17. package/entity/Reference.d.ts +42 -1
  18. package/entity/Reference.js +9 -0
  19. package/entity/defineEntity.d.ts +101 -21
  20. package/entity/defineEntity.js +17 -6
  21. package/entity/utils.js +4 -5
  22. package/enums.d.ts +8 -1
  23. package/errors.d.ts +2 -0
  24. package/errors.js +4 -0
  25. package/index.d.ts +2 -2
  26. package/index.js +1 -1
  27. package/metadata/EntitySchema.js +18 -0
  28. package/metadata/MetadataDiscovery.d.ts +12 -0
  29. package/metadata/MetadataDiscovery.js +173 -22
  30. package/metadata/MetadataValidator.d.ts +26 -0
  31. package/metadata/MetadataValidator.js +237 -1
  32. package/metadata/Routine.d.ts +74 -0
  33. package/metadata/Routine.js +140 -0
  34. package/metadata/index.d.ts +1 -0
  35. package/metadata/index.js +1 -0
  36. package/metadata/types.d.ts +78 -9
  37. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  38. package/naming-strategy/NamingStrategy.d.ts +1 -1
  39. package/package.json +1 -1
  40. package/platforms/Platform.d.ts +18 -3
  41. package/platforms/Platform.js +58 -6
  42. package/serialization/EntitySerializer.js +4 -3
  43. package/typings.d.ts +399 -22
  44. package/typings.js +43 -12
  45. package/unit-of-work/UnitOfWork.js +43 -5
  46. package/utils/AbstractMigrator.d.ts +20 -5
  47. package/utils/AbstractMigrator.js +263 -28
  48. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  49. package/utils/AbstractSchemaGenerator.js +4 -1
  50. package/utils/Configuration.d.ts +30 -0
  51. package/utils/Configuration.js +45 -0
  52. package/utils/DataloaderUtils.d.ts +10 -1
  53. package/utils/DataloaderUtils.js +78 -0
  54. package/utils/EntityComparator.js +1 -1
  55. package/utils/QueryHelper.d.ts +16 -0
  56. package/utils/QueryHelper.js +15 -0
  57. package/utils/TransactionManager.js +2 -0
  58. package/utils/Utils.js +1 -1
  59. package/utils/fs-utils.d.ts +2 -0
  60. package/utils/fs-utils.js +7 -1
  61. package/utils/index.d.ts +1 -0
  62. package/utils/index.js +1 -0
  63. package/utils/partition-utils.d.ts +17 -0
  64. package/utils/partition-utils.js +79 -0
  65. package/utils/upsert-utils.d.ts +2 -0
  66. package/utils/upsert-utils.js +26 -1
package/EntityManager.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getOnConflictReturningFields, getWhereCondition } from './utils/upsert-utils.js';
1
+ import { getOnConflictReturningFields, getWhereCondition, resetUntouchedCollections } from './utils/upsert-utils.js';
2
2
  import { Utils } from './utils/Utils.js';
3
3
  import { Cursor } from './utils/Cursor.js';
4
4
  import { QueryHelper } from './utils/QueryHelper.js';
@@ -49,6 +49,10 @@ export class EntityManager {
49
49
  #disableTransactions;
50
50
  #flushMode;
51
51
  #schema;
52
+ /** @internal */
53
+ signal;
54
+ /** @internal */
55
+ inflightQueryAbortStrategy;
52
56
  #useContext;
53
57
  /**
54
58
  * @internal
@@ -103,9 +107,6 @@ export class EntityManager {
103
107
  repo(entityName) {
104
108
  return this.getRepository(entityName);
105
109
  }
106
- /**
107
- * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
108
- */
109
110
  async find(entityName, where, options = {}) {
110
111
  if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
111
112
  const em = this.getContext(false);
@@ -116,10 +117,11 @@ export class EntityManager {
116
117
  }
117
118
  const em = this.getContext();
118
119
  em.prepareOptions(options);
120
+ const meta = this.metadata.get(entityName);
121
+ em.validateIndexUsage(meta, where, options);
119
122
  await em.tryFlush(entityName, options);
120
123
  where = await em.processWhere(entityName, where, options, 'read');
121
124
  validateParams(where);
122
- const meta = this.metadata.get(entityName);
123
125
  if (meta.orderBy) {
124
126
  options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
125
127
  }
@@ -203,6 +205,7 @@ export class EntityManager {
203
205
  options.orderBy = options.orderBy || {};
204
206
  options.populate = (await em.preparePopulate(entityName, options));
205
207
  const meta = this.metadata.get(entityName);
208
+ em.validateIndexUsage(meta, options.where ?? {}, options);
206
209
  options = { ...options };
207
210
  // save the original hint value so we know it was infer/all
208
211
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
@@ -319,13 +322,13 @@ export class EntityManager {
319
322
  }
320
323
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
321
324
  const children = [];
322
- const lookUpChildren = (ret, type) => {
323
- const children = types.filter(meta2 => meta2.extends === type);
324
- children.forEach(m => lookUpChildren(ret, m.class));
325
+ const lookUpChildren = (ret, parent) => {
326
+ const children = types.filter(meta2 => meta2.extends && this.metadata.find(meta2.extends) === parent);
327
+ children.forEach(m => lookUpChildren(ret, m));
325
328
  ret.push(...children.filter(c => c.discriminatorValue));
326
329
  return children;
327
330
  };
328
- lookUpChildren(children, meta.class);
331
+ lookUpChildren(children, meta);
329
332
  /* v8 ignore next */
330
333
  where[meta.root.discriminatorColumn] =
331
334
  children.length > 0
@@ -355,10 +358,18 @@ export class EntityManager {
355
358
  const field = hint.field.split(':')[0];
356
359
  const prop = meta.properties[field];
357
360
  const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
358
- const joined = strategy === LoadStrategy.JOINED && prop.kind !== ReferenceKind.SCALAR;
361
+ const joined = (strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) &&
362
+ prop.kind !== ReferenceKind.SCALAR;
359
363
  if (!joined && !hint.filter) {
360
364
  continue;
361
365
  }
366
+ // Polymorphic to-one relations are already filtered via per-target LEFT JOINs created
367
+ // for the `:ref filter` hint; emitting a populate-filter entry here would auto-join only
368
+ // the first target meta and either drop rows pointing to other targets or reference
369
+ // parent-table-only columns on the child sub-table alias for TPT children.
370
+ if (prop.polymorphic && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
371
+ continue;
372
+ }
362
373
  const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
363
374
  const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
364
375
  ...options,
@@ -424,11 +435,13 @@ export class EntityManager {
424
435
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
425
436
  let found = false;
426
437
  for (const hint of populated) {
427
- if (!hint.all) {
438
+ const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
439
+ // inverse 1:1 always produces a JOIN (forced by `joinedProps()`), regardless of strategy
440
+ const willJoin = strategy === LoadStrategy.JOINED || (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner);
441
+ if (!hint.all || willJoin) {
428
442
  hint.filter = true;
429
443
  }
430
- const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
431
- if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
444
+ if (hint.field === `${prop.name}:ref` || (hint.filter && willJoin)) {
432
445
  found = true;
433
446
  }
434
447
  }
@@ -442,7 +455,23 @@ export class EntityManager {
442
455
  const prop = meta?.properties[field];
443
456
  if (prop && !ref) {
444
457
  hint.children ??= [];
445
- await this.autoJoinRefsForFilters(prop.targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
458
+ const targets = prop.polymorphic && prop.polymorphTargets?.length ? prop.polymorphTargets : [prop.targetMeta];
459
+ for (const targetMeta of targets) {
460
+ const before = hint.children.length;
461
+ await this.autoJoinRefsForFilters(targetMeta, { ...options, populate: hint.children }, { class: meta.root.class, propName: prop.name });
462
+ // For polymorphic relations, `hint.children` is applied to every polymorph target during
463
+ // population, so drop any auto-added hint whose field does not exist on every target —
464
+ // otherwise the shared `:ref` would crash when applied to a target lacking it (GH #7722).
465
+ if (targets.length > 1 && hint.children.length > before) {
466
+ const added = hint.children.splice(before);
467
+ for (const h of added) {
468
+ const f = h.field.split(':')[0];
469
+ if (targets.every(t => f in t.properties)) {
470
+ hint.children.push(h);
471
+ }
472
+ }
473
+ }
474
+ }
446
475
  }
447
476
  }
448
477
  }
@@ -494,10 +523,6 @@ export class EntityManager {
494
523
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
495
524
  return conds.length > 1 ? { $and: conds } : conds[0];
496
525
  }
497
- /**
498
- * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
499
- * where the first element is the array of entities, and the second is the count.
500
- */
501
526
  async findAndCount(entityName, where, options = {}) {
502
527
  const em = this.getContext(false);
503
528
  await em.tryFlush(entityName, options);
@@ -631,9 +656,6 @@ export class EntityManager {
631
656
  }
632
657
  return entity;
633
658
  }
634
- /**
635
- * Finds first entity matching your `where` query.
636
- */
637
659
  async findOne(entityName, where, options = {}) {
638
660
  if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
639
661
  const em = this.getContext(false);
@@ -653,10 +675,11 @@ export class EntityManager {
653
675
  }
654
676
  await em.tryFlush(entityName, options);
655
677
  const meta = em.metadata.get(entityName);
678
+ em.validateIndexUsage(meta, where, options);
656
679
  where = await em.processWhere(entityName, where, options, 'read');
657
680
  validateEmptyWhere(where);
658
681
  em.checkLockRequirements(options.lockMode, meta);
659
- const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
682
+ const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.NONE || options.lockMode === LockMode.OPTIMISTIC;
660
683
  if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
661
684
  return em.lockAndPopulate(meta, entity, where, options);
662
685
  }
@@ -701,12 +724,6 @@ export class EntityManager {
701
724
  await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
702
725
  return entity;
703
726
  }
704
- /**
705
- * Finds first entity matching your `where` query. If nothing found, it will throw an error.
706
- * If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
707
- * You can override the factory for creating this method via `options.failHandler` locally
708
- * or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
709
- */
710
727
  async findOneOrFail(entityName, where, options = {}) {
711
728
  let entity;
712
729
  let isStrictViolation = false;
@@ -803,7 +820,10 @@ export class EntityManager {
803
820
  data = QueryHelper.processObjectParams(data);
804
821
  validateParams(data, 'insert data');
805
822
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
806
- await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: data, em, meta }, meta);
823
+ await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity: entity ?? data, em, meta }, meta);
824
+ if (entity) {
825
+ data = em.#comparator.prepareEntity(entity);
826
+ }
807
827
  }
808
828
  const ret = await em.driver.nativeUpdate(entityName, where, data, {
809
829
  ctx: em.#transactionContext,
@@ -852,6 +872,7 @@ export class EntityManager {
852
872
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
853
873
  }
854
874
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
875
+ resetUntouchedCollections(meta, entity);
855
876
  const snapshot = this.#comparator.prepareEntity(entity);
856
877
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
857
878
  if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
@@ -918,6 +939,7 @@ export class EntityManager {
918
939
  const allWhere = [];
919
940
  const entities = new Map();
920
941
  const entitiesByData = new Map();
942
+ const entitiesByAllDataIdx = new Map();
921
943
  for (let i = 0; i < data.length; i++) {
922
944
  let row = data[i];
923
945
  let where;
@@ -931,6 +953,7 @@ export class EntityManager {
931
953
  }
932
954
  where = helper(entity).getPrimaryKey();
933
955
  em.#entityFactory.assignDefaultValues(entity, meta);
956
+ entitiesByAllDataIdx.set(allData.length, entity);
934
957
  row = em.#comparator.prepareEntity(entity);
935
958
  }
936
959
  else {
@@ -978,6 +1001,9 @@ export class EntityManager {
978
1001
  const entity = entitiesByData.get(dto) ?? dto;
979
1002
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
980
1003
  }
1004
+ for (const [idx, entity] of entitiesByAllDataIdx) {
1005
+ allData[idx] = em.#comparator.prepareEntity(entity);
1006
+ }
981
1007
  }
982
1008
  const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
983
1009
  ctx: em.#transactionContext,
@@ -1078,6 +1104,7 @@ export class EntityManager {
1078
1104
  }
1079
1105
  for (const [entity] of entities) {
1080
1106
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
1107
+ resetUntouchedCollections(meta, entity);
1081
1108
  const snapshot = this.#comparator.prepareEntity(entity);
1082
1109
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
1083
1110
  }
@@ -1171,6 +1198,7 @@ export class EntityManager {
1171
1198
  */
1172
1199
  async lock(entity, lockMode, options = {}) {
1173
1200
  options = Utils.isPlainObject(options) ? options : { lockVersion: options };
1201
+ this.getContext(false).prepareOptions(options);
1174
1202
  await this.getUnitOfWork().lock(entity, { lockMode, ...options });
1175
1203
  }
1176
1204
  /**
@@ -1192,9 +1220,6 @@ export class EntityManager {
1192
1220
  helper(data).setSchema(options.schema);
1193
1221
  }
1194
1222
  if (!helper(data).__managed) {
1195
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1196
- em.#unitOfWork.getPersistStack().delete(data);
1197
- // it can be also in the identity map if it had a PK value already
1198
1223
  em.#unitOfWork.unsetIdentity(data);
1199
1224
  }
1200
1225
  const meta = helper(data).__meta;
@@ -1283,9 +1308,6 @@ export class EntityManager {
1283
1308
  helper(row).setSchema(options.schema);
1284
1309
  }
1285
1310
  if (!helper(row).__managed) {
1286
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1287
- em.#unitOfWork.getPersistStack().delete(row);
1288
- // it can be also in the identity map if it had a PK value already
1289
1311
  em.#unitOfWork.unsetIdentity(row);
1290
1312
  }
1291
1313
  const payload = em.#comparator.prepareEntity(row);
@@ -1323,6 +1345,37 @@ export class EntityManager {
1323
1345
  });
1324
1346
  return res.affectedRows;
1325
1347
  }
1348
+ /**
1349
+ * Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
1350
+ * types are inferred from the literal config; use {@link Routine.create} to refine when needed.
1351
+ *
1352
+ * Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
1353
+ * `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
1354
+ *
1355
+ * Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
1356
+ * function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
1357
+ * pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
1358
+ *
1359
+ * @example
1360
+ * ```ts
1361
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
1362
+ *
1363
+ * const hashRef = new ScalarReference<string>();
1364
+ * await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
1365
+ *
1366
+ * const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
1367
+ * const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
1368
+ * ```
1369
+ */
1370
+ async callRoutine(routine, args) {
1371
+ const em = this.getContext(false);
1372
+ // Identity by reference so the user-held import survives bundling and tree-shakes unused routines.
1373
+ if (!em.config.hasRoutine(routine)) {
1374
+ throw new Error(`Routine '${routine.name}' is not registered in the 'routines' config option.`);
1375
+ }
1376
+ const conn = em.driver.getConnection('write');
1377
+ return conn.callRoutine(routine, args, em.#transactionContext);
1378
+ }
1326
1379
  /**
1327
1380
  * Fires native delete query. Calling this has no side effects on the context (identity map).
1328
1381
  */
@@ -1471,6 +1524,29 @@ export class EntityManager {
1471
1524
  await em.storeCache(options.cache, cached, () => +count);
1472
1525
  return +count;
1473
1526
  }
1527
+ /**
1528
+ * Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
1529
+ * field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
1530
+ *
1531
+ * SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
1532
+ *
1533
+ * @example
1534
+ * ```ts
1535
+ * // Count books per author
1536
+ * const counts = await em.countBy(Book, 'author');
1537
+ * // { '1': 2, '2': 1, '3': 3 }
1538
+ *
1539
+ * // Count with a filter
1540
+ * const counts = await em.countBy(Book, 'author', { where: { active: true } });
1541
+ *
1542
+ * // Composite groupBy — keys joined with ~~~
1543
+ * const counts = await em.countBy(Order, ['status', 'country']);
1544
+ * // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
1545
+ * ```
1546
+ */
1547
+ async countBy(entityName, groupBy, options) {
1548
+ throw new Error(`${this.constructor.name}.countBy() is not supported by the current driver`);
1549
+ }
1474
1550
  /**
1475
1551
  * Tells the EntityManager to make an instance managed and persistent.
1476
1552
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -1576,7 +1652,7 @@ export class EntityManager {
1576
1652
  const em = this.getContext();
1577
1653
  em.prepareOptions(options);
1578
1654
  const entityName = arr[0].constructor;
1579
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1655
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
1580
1656
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
1581
1657
  return entities;
1582
1658
  }
@@ -1609,6 +1685,8 @@ export class EntityManager {
1609
1685
  fork.#filterParams = Utils.copy(em.#filterParams);
1610
1686
  fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
1611
1687
  fork.#schema = options.schema ?? em.#schema;
1688
+ fork.signal = options.signal ?? em.signal;
1689
+ fork.inflightQueryAbortStrategy = options.inflightQueryAbortStrategy ?? em.inflightQueryAbortStrategy;
1612
1690
  if (!options.clear) {
1613
1691
  for (const entity of em.#unitOfWork.getIdentityMap()) {
1614
1692
  fork.#unitOfWork.register(entity);
@@ -1684,6 +1762,22 @@ export class EntityManager {
1684
1762
  getTransactionContext() {
1685
1763
  return this.getContext(false).#transactionContext;
1686
1764
  }
1765
+ /**
1766
+ * Returns the cancellation defaults configured on this EntityManager (via `em.fork({ signal })`
1767
+ * or inherited from a transactional fork). Returns `undefined` when no signal is set.
1768
+ *
1769
+ * @internal — exposed for subclass drivers and `UnitOfWork`; not part of the public API.
1770
+ */
1771
+ getAbortOptions() {
1772
+ const em = this.getContext(false);
1773
+ if (em.signal == null && em.inflightQueryAbortStrategy == null) {
1774
+ return undefined;
1775
+ }
1776
+ return {
1777
+ signal: em.signal,
1778
+ inflightQueryAbortStrategy: em.inflightQueryAbortStrategy,
1779
+ };
1780
+ }
1687
1781
  /**
1688
1782
  * Sets the transaction context.
1689
1783
  */
@@ -1727,6 +1821,79 @@ export class EntityManager {
1727
1821
  throw ValidationError.transactionRequired();
1728
1822
  }
1729
1823
  }
1824
+ validateIndexUsage(meta, where, options) {
1825
+ if (!options.using) {
1826
+ return;
1827
+ }
1828
+ const indexNames = Utils.asArray(options.using);
1829
+ const allIndexes = [...meta.indexes, ...meta.uniques];
1830
+ const indexMap = new Map();
1831
+ for (const idx of allIndexes) {
1832
+ if (idx.name) {
1833
+ indexMap.set(idx.name, Utils.asArray(idx.properties ?? []));
1834
+ }
1835
+ }
1836
+ for (const prop of meta.props) {
1837
+ if (typeof prop.index === 'string') {
1838
+ indexMap.set(prop.index, [prop.name]);
1839
+ }
1840
+ if (typeof prop.unique === 'string') {
1841
+ indexMap.set(prop.unique, [prop.name]);
1842
+ }
1843
+ }
1844
+ const allowedProps = new Set();
1845
+ for (const name of indexNames) {
1846
+ const props = indexMap.get(name);
1847
+ if (!props) {
1848
+ const available = [...indexMap.keys()];
1849
+ throw new Error(`Index '${name}' not found on entity '${meta.className}'. ` +
1850
+ (available.length > 0 ? `Available indexes: ${available.join(', ')}` : 'No named indexes defined.'));
1851
+ }
1852
+ for (const prop of props) {
1853
+ allowedProps.add(prop);
1854
+ }
1855
+ }
1856
+ this.validateWhereKeysForIndex(where, allowedProps, indexNames);
1857
+ if (options.orderBy) {
1858
+ const orderMaps = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
1859
+ for (const orderMap of orderMaps) {
1860
+ for (const key of Object.keys(orderMap)) {
1861
+ if (!allowedProps.has(key)) {
1862
+ throw new Error(`Property '${key}' in orderBy is not covered by index '${indexNames.join("', '")}'. ` +
1863
+ `Allowed properties: ${[...allowedProps].join(', ')}`);
1864
+ }
1865
+ }
1866
+ }
1867
+ }
1868
+ }
1869
+ validateWhereKeysForIndex(where, allowedProps, indexNames) {
1870
+ if (typeof where !== 'object' || where === null) {
1871
+ return;
1872
+ }
1873
+ if (Array.isArray(where)) {
1874
+ for (const item of where) {
1875
+ this.validateWhereKeysForIndex(item, allowedProps, indexNames);
1876
+ }
1877
+ return;
1878
+ }
1879
+ for (const key of Object.keys(where)) {
1880
+ if (key === '$and' || key === '$or') {
1881
+ for (const item of Utils.asArray(where[key])) {
1882
+ this.validateWhereKeysForIndex(item, allowedProps, indexNames);
1883
+ }
1884
+ }
1885
+ else if (key === '$not') {
1886
+ this.validateWhereKeysForIndex(where[key], allowedProps, indexNames);
1887
+ }
1888
+ else if (key.startsWith('$')) {
1889
+ continue;
1890
+ }
1891
+ else if (!allowedProps.has(key)) {
1892
+ throw new Error(`Property '${key}' in where clause is not covered by index '${indexNames.join("', '")}'. ` +
1893
+ `Allowed properties: ${[...allowedProps].join(', ')}`);
1894
+ }
1895
+ }
1896
+ }
1730
1897
  async lockAndPopulate(meta, entity, where, options) {
1731
1898
  if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
1732
1899
  await this.lock(entity, options.lockMode, {
@@ -1761,8 +1928,17 @@ export class EntityManager {
1761
1928
  return [];
1762
1929
  }
1763
1930
  const meta = this.metadata.find(entityName);
1764
- // infer populate hint if only `fields` are available
1765
- if (!options.populate && options.fields) {
1931
+ // infer populate hints from `fields` when present; merge with explicit `populate` if both are set.
1932
+ // `populateArray` is only kept when the entries are user-provided strings — when this method runs a
1933
+ // second time (e.g. from `lockAndPopulate`) the populate is already normalized and merging would be a no-op.
1934
+ const fields = options.fields ? this.buildFields(options.fields) : undefined;
1935
+ const populateArray = Array.isArray(options.populate) && options.populate.every(p => typeof p === 'string')
1936
+ ? options.populate
1937
+ : undefined;
1938
+ const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1939
+ const shouldInfer = fields !== undefined && options.populate === undefined;
1940
+ const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
1941
+ if (shouldInfer || shouldMerge) {
1766
1942
  // we need to prune the `populate` hint from to-one relations, as partially loading them does not require their population, we want just the FK
1767
1943
  const pruneToOneRelations = (meta, fields) => {
1768
1944
  const ret = [];
@@ -1796,7 +1972,19 @@ export class EntityManager {
1796
1972
  }
1797
1973
  return Utils.unique(ret);
1798
1974
  };
1799
- options.populate = pruneToOneRelations(meta, this.buildFields(options.fields));
1975
+ const fromFields = pruneToOneRelations(meta, fields);
1976
+ if (shouldInfer) {
1977
+ options.populate = fromFields;
1978
+ }
1979
+ else {
1980
+ // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1981
+ // require a JOIN to access sub-fields, whereas bare names are already handled by the driver.
1982
+ // skip paths already in user's `populate` (regardless of `:ref` modifier) to avoid producing
1983
+ // both a `:ref` and a non-`:ref` entry for the same relation
1984
+ const existing = new Set(populateArray.map(p => p.split(':')[0]));
1985
+ const extras = fromFields.filter(f => f.includes('.') && !existing.has(f));
1986
+ options.populate = [...populateArray, ...extras];
1987
+ }
1800
1988
  }
1801
1989
  if (!options.populate) {
1802
1990
  const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
@@ -1836,9 +2024,21 @@ export class EntityManager {
1836
2024
  }
1837
2025
  if (options.populateHints) {
1838
2026
  applyPopulateHints(populate, options.populateHints);
2027
+ this.forceSelectInForLimitedPopulate(populate);
1839
2028
  }
1840
2029
  return populate;
1841
2030
  }
2031
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
2032
+ forceSelectInForLimitedPopulate(populate) {
2033
+ for (const entry of populate) {
2034
+ if (entry.limit != null) {
2035
+ entry.strategy = LoadStrategy.SELECT_IN;
2036
+ }
2037
+ if (entry.children?.length) {
2038
+ this.forceSelectInForLimitedPopulate(entry.children);
2039
+ }
2040
+ }
2041
+ }
1842
2042
  /**
1843
2043
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
1844
2044
  * some additional lazy properties, if so, we reload and merge the data from database
@@ -1870,6 +2070,8 @@ export class EntityManager {
1870
2070
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1871
2071
  }
1872
2072
  options.schema ??= this.#schema;
2073
+ options.signal ??= this.signal;
2074
+ options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
1873
2075
  options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1874
2076
  }
1875
2077
  /**
@@ -1878,7 +2080,15 @@ export class EntityManager {
1878
2080
  cacheKey(entityName, options, method, where) {
1879
2081
  const { ...opts } = options;
1880
2082
  // ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
1881
- for (const k of ['ctx', 'strategy', 'flushMode', 'logging', 'loggerContext']) {
2083
+ for (const k of [
2084
+ 'ctx',
2085
+ 'strategy',
2086
+ 'flushMode',
2087
+ 'logging',
2088
+ 'loggerContext',
2089
+ 'signal',
2090
+ 'inflightQueryAbortStrategy',
2091
+ ]) {
1882
2092
  delete opts[k];
1883
2093
  }
1884
2094
  return [Utils.className(entityName), method, opts, where];
@@ -1971,6 +2181,8 @@ export class EntityManager {
1971
2181
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
1972
2182
  case 'm:n':
1973
2183
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
2184
+ case 'count':
2185
+ return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
1974
2186
  }
1975
2187
  }
1976
2188
  /**
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
@@ -19,6 +19,7 @@ Install a driver package for your database:
19
19
 
20
20
  ```sh
21
21
  npm install @mikro-orm/postgresql # PostgreSQL
22
+ npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
22
23
  npm install @mikro-orm/mysql # MySQL
23
24
  npm install @mikro-orm/mariadb # MariaDB
24
25
  npm install @mikro-orm/sqlite # SQLite
@@ -1,8 +1,10 @@
1
1
  import { type Configuration, type ConnectionOptions } from '../utils/Configuration.js';
2
2
  import type { LogContext, Logger } from '../logging/Logger.js';
3
3
  import type { MetadataStorage } from '../metadata/MetadataStorage.js';
4
- import type { ConnectionType, Dictionary, MaybePromise, Primary } from '../typings.js';
4
+ import type { ConnectionType, Dictionary, MaybePromise, Primary, RoutineProperty } from '../typings.js';
5
+ import type { Routine } from '../metadata/Routine.js';
5
6
  import type { Platform } from '../platforms/Platform.js';
7
+ import type { Type } from '../types/Type.js';
6
8
  import type { TransactionEventBroadcaster } from '../events/TransactionEventBroadcaster.js';
7
9
  import type { IsolationLevel } from '../enums.js';
8
10
  /** Abstract base class for database connections, providing transaction and query execution support. */
@@ -72,6 +74,41 @@ export declare abstract class Connection {
72
74
  rollback(ctx: Transaction, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
73
75
  /** Executes a raw query and returns the result. */
74
76
  abstract execute<T>(query: string, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction): Promise<QueryResult<T> | any | any[]>;
77
+ /** @internal — public callers go through {@link EntityManager.callRoutine}. */
78
+ callRoutine<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
79
+ /**
80
+ * Unwraps a routine argument (resolving any `ScalarReference` wrapper) and, when the param
81
+ * declares a `customType`, marshals it through `convertToDatabaseValue`. `undefined` is
82
+ * normalised to `null` so every driver sees the same shape.
83
+ *
84
+ * @internal
85
+ */
86
+ protected convertRoutineInbound(value: unknown, param: RoutineProperty | undefined): unknown;
87
+ /**
88
+ * Converts a raw database value to its JS representation via the supplied `customType`, when
89
+ * one is declared. Used to marshal scalar function returns and OUT/INOUT values back to the
90
+ * caller before they land in a `ScalarReference` or `em.callRoutine`'s return value.
91
+ *
92
+ * @internal
93
+ */
94
+ protected convertRoutineOutbound<T>(value: unknown, customType: Type<unknown> | undefined): T;
95
+ /**
96
+ * Executes a scalar function routine as `select <qualified>(?, ?, ...) as value`, marshalling
97
+ * IN params on the way in and the return value on the way out. The qualified name is built
98
+ * from `routine.schema`, falling back to the platform's default schema (e.g. `dbo` on MSSQL),
99
+ * which gives MySQL/SQLite a bare name and MSSQL/Oracle the mandatory `schema.name` form.
100
+ *
101
+ * @internal
102
+ */
103
+ protected callRoutineFunction<T>(routine: Routine, args: Record<string, unknown>, ctx?: Transaction): Promise<T>;
104
+ /**
105
+ * Walks a result row produced by an OUT/INOUT-param SELECT and writes each value into the
106
+ * caller's `ScalarReference` slot. Non-reference args are ignored (the user opted out of
107
+ * receiving the OUT value).
108
+ *
109
+ * @internal
110
+ */
111
+ protected applyRoutineOutParams(row: Dictionary, outParams: RoutineProperty[], args: Record<string, unknown>): void;
75
112
  /** Parses and returns the resolved connection configuration (host, port, user, etc.). */
76
113
  getConnectionOptions(): ConnectionConfig;
77
114
  /** Sets the metadata storage on this connection. */
@@ -104,3 +141,32 @@ export interface ConnectionConfig {
104
141
  }
105
142
  /** Opaque transaction context type, wrapping the driver-specific transaction object. */
106
143
  export type Transaction<T = any> = T & {};
144
+ /**
145
+ * Strategy applied when an `AbortSignal` fires while a query is in flight.
146
+ *
147
+ * - `'ignore query'` — stop awaiting; the query keeps running on the server until it settles
148
+ * (the connection returns to the pool only when the database replies).
149
+ * - `'cancel query'` — ask the database to cancel the running query (e.g. `pg_cancel_backend`,
150
+ * `KILL QUERY`). Falls back to `'ignore query'` if the dialect cannot cancel.
151
+ * Most engines do not cancel writes; partial commits are possible.
152
+ * - `'kill session'` — terminate the database session/process the query runs in
153
+ * (`pg_terminate_backend` etc.). Falls back to `'cancel query'` if not supported.
154
+ *
155
+ * Default: `'ignore query'`.
156
+ *
157
+ * **Streaming queries (`em.stream()` / `qb.stream()`):** the strategy is silently treated as
158
+ * `'ignore query'` because the underlying driver only accepts a plain `AbortSignal` for
159
+ * streamed reads — there is no server-side cancel for an open cursor. The MongoDB driver also
160
+ * has no notion of strategies; only the signal is honored there.
161
+ */
162
+ export type InflightQueryAbortStrategy = 'ignore query' | 'cancel query' | 'kill session';
163
+ /** Per-query cancellation controls forwarded to the underlying driver. */
164
+ export interface AbortQueryOptions {
165
+ /** AbortSignal that cancels the query when fired. */
166
+ signal?: AbortSignal;
167
+ /**
168
+ * Strategy used when the signal fires while the query is in flight. See
169
+ * {@apilink InflightQueryAbortStrategy} for caveats around streams and MongoDB.
170
+ */
171
+ inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
172
+ }