@mikro-orm/core 7.1.0-dev.8 → 7.1.0

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 (57) hide show
  1. package/EntityManager.d.ts +70 -5
  2. package/EntityManager.js +175 -23
  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 +30 -6
  7. package/entity/BaseEntity.d.ts +68 -1
  8. package/entity/BaseEntity.js +18 -0
  9. package/entity/Collection.d.ts +4 -1
  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 +97 -29
  15. package/entity/EntityRepository.d.ts +12 -2
  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 +69 -16
  20. package/entity/defineEntity.js +17 -0
  21. package/entity/utils.js +4 -5
  22. package/enums.d.ts +8 -1
  23. package/index.d.ts +1 -1
  24. package/metadata/EntitySchema.js +18 -0
  25. package/metadata/MetadataDiscovery.d.ts +1 -0
  26. package/metadata/MetadataDiscovery.js +79 -15
  27. package/metadata/MetadataValidator.d.ts +26 -0
  28. package/metadata/MetadataValidator.js +228 -1
  29. package/metadata/Routine.d.ts +74 -0
  30. package/metadata/Routine.js +140 -0
  31. package/metadata/index.d.ts +1 -0
  32. package/metadata/index.js +1 -0
  33. package/metadata/types.d.ts +75 -8
  34. package/package.json +1 -1
  35. package/platforms/Platform.d.ts +13 -3
  36. package/platforms/Platform.js +51 -6
  37. package/serialization/EntitySerializer.js +4 -3
  38. package/typings.d.ts +323 -21
  39. package/typings.js +32 -12
  40. package/unit-of-work/UnitOfWork.js +43 -5
  41. package/utils/AbstractMigrator.d.ts +9 -4
  42. package/utils/AbstractMigrator.js +60 -28
  43. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  44. package/utils/AbstractSchemaGenerator.js +4 -1
  45. package/utils/Configuration.d.ts +30 -0
  46. package/utils/Configuration.js +45 -0
  47. package/utils/DataloaderUtils.d.ts +10 -1
  48. package/utils/DataloaderUtils.js +78 -0
  49. package/utils/EntityComparator.js +1 -1
  50. package/utils/TransactionManager.js +2 -0
  51. package/utils/Utils.js +1 -1
  52. package/utils/index.d.ts +1 -0
  53. package/utils/index.js +1 -0
  54. package/utils/partition-utils.d.ts +17 -0
  55. package/utils/partition-utils.js +79 -0
  56. package/utils/upsert-utils.d.ts +2 -0
  57. package/utils/upsert-utils.js +26 -1
@@ -6,11 +6,12 @@ import { type EntityRepository } from './entity/EntityRepository.js';
6
6
  import { EntityLoader, type EntityLoaderOptions } from './entity/EntityLoader.js';
7
7
  import { Reference } from './entity/Reference.js';
8
8
  import { UnitOfWork } from './unit-of-work/UnitOfWork.js';
9
- import type { CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
10
- import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
9
+ import type { CountByOptions, CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
10
+ import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, RoutineArgs, RoutineReturn, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
11
+ import type { Routine } from './metadata/Routine.js';
11
12
  import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
12
13
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
13
- import type { Transaction } from './connections/Connection.js';
14
+ import type { AbortQueryOptions, InflightQueryAbortStrategy, Transaction } from './connections/Connection.js';
14
15
  import { EventManager } from './events/EventManager.js';
15
16
  import type { EntityComparator } from './utils/EntityComparator.js';
16
17
  /**
@@ -33,6 +34,10 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
33
34
  /** The context name of this EntityManager, derived from the ORM configuration. */
34
35
  readonly name: string;
35
36
  protected loggerContext?: Dictionary;
37
+ /** @internal */
38
+ protected signal?: AbortSignal;
39
+ /** @internal */
40
+ protected inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
36
41
  /**
37
42
  * @internal
38
43
  */
@@ -342,6 +347,29 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
342
347
  * Fires native update query. Calling this has no side effects on the context (identity map).
343
348
  */
344
349
  nativeUpdate<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, data: EntityData<Entity>, options?: UpdateOptions<Entity>): Promise<number>;
350
+ /**
351
+ * Invokes a stored procedure or function declared via the {@link Routine} class. Arg and return
352
+ * types are inferred from the literal config; use {@link Routine.create} to refine when needed.
353
+ *
354
+ * Procedures emitting result sets (MySQL `SELECT`s, PG/Oracle refcursor OUT params) return
355
+ * `Dictionary[][]`; MSSQL multi-result procs aren't exposed here, use `em.getConnection().execute()`.
356
+ *
357
+ * Mongo throws. SQLite bridges `bodyJs` functions via better-sqlite3 UDFs (procedures and
358
+ * function-without-bodyJs throw); libSQL throws unconditionally. Oracle calls run on their own
359
+ * pool connection with `autoCommit: true`, so wrapping in `em.transactional(...)` throws.
360
+ *
361
+ * @example
362
+ * ```ts
363
+ * const hash = await em.callRoutine(HashUser, { name: 'jon', salt: 'pepper' });
364
+ *
365
+ * const hashRef = new ScalarReference<string>();
366
+ * await em.callRoutine(AddRecord, { p_name: 'jon', p_age: 30, p_hash: hashRef });
367
+ *
368
+ * const TwoCursors = Routine.create<Record<string, never>, unknown[][]>({ ... });
369
+ * const [users, books] = await em.transactional(em => em.callRoutine(TwoCursors, {}));
370
+ * ```
371
+ */
372
+ callRoutine<R extends Routine>(routine: R, args: RoutineArgs<R>): Promise<RoutineReturn<R>>;
345
373
  /**
346
374
  * Fires native delete query. Calling this has no side effects on the context (identity map).
347
375
  */
@@ -441,6 +469,27 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
441
469
  * Returns total number of entities matching your `where` query.
442
470
  */
443
471
  count<Entity extends object, Hint extends string = never>(entityName: EntityName<Entity>, where?: FilterQuery<NoInfer<Entity>>, options?: CountOptions<Entity, Hint>): Promise<number>;
472
+ /**
473
+ * Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
474
+ * field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
475
+ *
476
+ * SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
477
+ *
478
+ * @example
479
+ * ```ts
480
+ * // Count books per author
481
+ * const counts = await em.countBy(Book, 'author');
482
+ * // { '1': 2, '2': 1, '3': 3 }
483
+ *
484
+ * // Count with a filter
485
+ * const counts = await em.countBy(Book, 'author', { where: { active: true } });
486
+ *
487
+ * // Composite groupBy — keys joined with ~~~
488
+ * const counts = await em.countBy(Order, ['status', 'country']);
489
+ * // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
490
+ * ```
491
+ */
492
+ countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
444
493
  /**
445
494
  * Tells the EntityManager to make an instance managed and persistent.
446
495
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -511,6 +560,13 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
511
560
  * Gets the transaction context (driver dependent object used to make sure queries are executed on same connection).
512
561
  */
513
562
  getTransactionContext<T extends Transaction = Transaction>(): T | undefined;
563
+ /**
564
+ * Returns the cancellation defaults configured on this EntityManager (via `em.fork({ signal })`
565
+ * or inherited from a transactional fork). Returns `undefined` when no signal is set.
566
+ *
567
+ * @internal — exposed for subclass drivers and `UnitOfWork`; not part of the public API.
568
+ */
569
+ protected getAbortOptions(): AbortQueryOptions | undefined;
514
570
  /**
515
571
  * Sets the transaction context.
516
572
  */
@@ -538,12 +594,14 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
538
594
  private buildFields;
539
595
  /** @internal */
540
596
  preparePopulate<Entity extends object>(entityName: EntityName<Entity>, options: Pick<FindOptions<Entity, any, any, any>, 'populate' | 'strategy' | 'fields' | 'flags' | 'filters' | 'exclude' | 'populateHints'>, validate?: boolean): Promise<PopulateOptions<Entity>[]>;
597
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
598
+ private forceSelectInForLimitedPopulate;
541
599
  /**
542
600
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
543
601
  * some additional lazy properties, if so, we reload and merge the data from database
544
602
  */
545
603
  protected shouldRefresh<T extends object, P extends string = never, F extends string = never, E extends string = never>(meta: EntityMetadata<T>, entity: T, options: FindOneOptions<T, P, F, E>): boolean;
546
- protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any>): void;
604
+ protected prepareOptions(options: (FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>) & AbortQueryOptions): void;
547
605
  /**
548
606
  * @internal
549
607
  */
@@ -585,7 +643,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
585
643
  */
586
644
  set schema(schema: string | null | undefined);
587
645
  /** @internal */
588
- getDataLoader(type: 'ref' | '1:m' | 'm:n'): Promise<any>;
646
+ getDataLoader(type: 'ref' | '1:m' | 'm:n' | 'count'): Promise<any>;
589
647
  /**
590
648
  * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
591
649
  * if executed inside request context handler.
@@ -638,4 +696,11 @@ export interface ForkOptions {
638
696
  schema?: string;
639
697
  /** default logger context, can be overridden via {@apilink FindOptions} */
640
698
  loggerContext?: Dictionary;
699
+ /**
700
+ * Default `AbortSignal` applied to every operation on this fork (queries and UoW flushes).
701
+ * Per-call options.signal still takes precedence.
702
+ */
703
+ signal?: AbortSignal;
704
+ /** Default cancellation strategy paired with {@link signal}. */
705
+ inflightQueryAbortStrategy?: InflightQueryAbortStrategy;
641
706
  }
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
@@ -318,13 +322,13 @@ export class EntityManager {
318
322
  }
319
323
  const types = Object.values(meta.root.discriminatorMap).map(cls => this.metadata.get(cls));
320
324
  const children = [];
321
- const lookUpChildren = (ret, type) => {
322
- const children = types.filter(meta2 => meta2.extends === type);
323
- 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));
324
328
  ret.push(...children.filter(c => c.discriminatorValue));
325
329
  return children;
326
330
  };
327
- lookUpChildren(children, meta.class);
331
+ lookUpChildren(children, meta);
328
332
  /* v8 ignore next */
329
333
  where[meta.root.discriminatorColumn] =
330
334
  children.length > 0
@@ -354,10 +358,18 @@ export class EntityManager {
354
358
  const field = hint.field.split(':')[0];
355
359
  const prop = meta.properties[field];
356
360
  const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
357
- 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;
358
363
  if (!joined && !hint.filter) {
359
364
  continue;
360
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
+ }
361
373
  const filters = QueryHelper.mergePropertyFilters(prop.filters, options.filters);
362
374
  const where = await this.applyFilters(prop.targetMeta.class, {}, filters, 'read', {
363
375
  ...options,
@@ -423,11 +435,13 @@ export class EntityManager {
423
435
  const populated = options.populate.filter(({ field }) => field.split(':')[0] === prop.name);
424
436
  let found = false;
425
437
  for (const hint of populated) {
426
- 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) {
427
442
  hint.filter = true;
428
443
  }
429
- const strategy = getLoadingStrategy(prop.strategy || hint.strategy || options.strategy || this.config.get('loadStrategy'), prop.kind);
430
- if (hint.field === `${prop.name}:ref` || (hint.filter && strategy === LoadStrategy.JOINED)) {
444
+ if (hint.field === `${prop.name}:ref` || (hint.filter && willJoin)) {
431
445
  found = true;
432
446
  }
433
447
  }
@@ -441,7 +455,23 @@ export class EntityManager {
441
455
  const prop = meta?.properties[field];
442
456
  if (prop && !ref) {
443
457
  hint.children ??= [];
444
- 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
+ }
445
475
  }
446
476
  }
447
477
  }
@@ -649,7 +679,7 @@ export class EntityManager {
649
679
  where = await em.processWhere(entityName, where, options, 'read');
650
680
  validateEmptyWhere(where);
651
681
  em.checkLockRequirements(options.lockMode, meta);
652
- const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.OPTIMISTIC;
682
+ const isOptimisticLocking = options.lockMode == null || options.lockMode === LockMode.NONE || options.lockMode === LockMode.OPTIMISTIC;
653
683
  if (entity && !em.shouldRefresh(meta, entity, options) && isOptimisticLocking) {
654
684
  return em.lockAndPopulate(meta, entity, where, options);
655
685
  }
@@ -790,7 +820,10 @@ export class EntityManager {
790
820
  data = QueryHelper.processObjectParams(data);
791
821
  validateParams(data, 'insert data');
792
822
  if (em.eventManager.hasListeners(EventType.beforeUpsert, meta)) {
793
- 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
+ }
794
827
  }
795
828
  const ret = await em.driver.nativeUpdate(entityName, where, data, {
796
829
  ctx: em.#transactionContext,
@@ -839,6 +872,7 @@ export class EntityManager {
839
872
  em.getHydrator().hydrate(entity, meta, data2, em.#entityFactory, 'full', false, true);
840
873
  }
841
874
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
875
+ resetUntouchedCollections(meta, entity);
842
876
  const snapshot = this.#comparator.prepareEntity(entity);
843
877
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
844
878
  if (em.eventManager.hasListeners(EventType.afterUpsert, meta)) {
@@ -905,6 +939,7 @@ export class EntityManager {
905
939
  const allWhere = [];
906
940
  const entities = new Map();
907
941
  const entitiesByData = new Map();
942
+ const entitiesByAllDataIdx = new Map();
908
943
  for (let i = 0; i < data.length; i++) {
909
944
  let row = data[i];
910
945
  let where;
@@ -918,6 +953,7 @@ export class EntityManager {
918
953
  }
919
954
  where = helper(entity).getPrimaryKey();
920
955
  em.#entityFactory.assignDefaultValues(entity, meta);
956
+ entitiesByAllDataIdx.set(allData.length, entity);
921
957
  row = em.#comparator.prepareEntity(entity);
922
958
  }
923
959
  else {
@@ -965,6 +1001,9 @@ export class EntityManager {
965
1001
  const entity = entitiesByData.get(dto) ?? dto;
966
1002
  await em.eventManager.dispatchEvent(EventType.beforeUpsert, { entity, em, meta }, meta);
967
1003
  }
1004
+ for (const [idx, entity] of entitiesByAllDataIdx) {
1005
+ allData[idx] = em.#comparator.prepareEntity(entity);
1006
+ }
968
1007
  }
969
1008
  const res = await em.driver.nativeUpdateMany(entityName, allWhere, allData, {
970
1009
  ctx: em.#transactionContext,
@@ -1065,6 +1104,7 @@ export class EntityManager {
1065
1104
  }
1066
1105
  for (const [entity] of entities) {
1067
1106
  // recompute the data as there might be some values missing (e.g. those with db column defaults)
1107
+ resetUntouchedCollections(meta, entity);
1068
1108
  const snapshot = this.#comparator.prepareEntity(entity);
1069
1109
  em.#unitOfWork.register(entity, snapshot, { refresh: true });
1070
1110
  }
@@ -1158,6 +1198,7 @@ export class EntityManager {
1158
1198
  */
1159
1199
  async lock(entity, lockMode, options = {}) {
1160
1200
  options = Utils.isPlainObject(options) ? options : { lockVersion: options };
1201
+ this.getContext(false).prepareOptions(options);
1161
1202
  await this.getUnitOfWork().lock(entity, { lockMode, ...options });
1162
1203
  }
1163
1204
  /**
@@ -1179,9 +1220,6 @@ export class EntityManager {
1179
1220
  helper(data).setSchema(options.schema);
1180
1221
  }
1181
1222
  if (!helper(data).__managed) {
1182
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1183
- em.#unitOfWork.getPersistStack().delete(data);
1184
- // it can be also in the identity map if it had a PK value already
1185
1223
  em.#unitOfWork.unsetIdentity(data);
1186
1224
  }
1187
1225
  const meta = helper(data).__meta;
@@ -1270,9 +1308,6 @@ export class EntityManager {
1270
1308
  helper(row).setSchema(options.schema);
1271
1309
  }
1272
1310
  if (!helper(row).__managed) {
1273
- // the entity might have been created via `em.create()`, which adds it to the persist stack automatically
1274
- em.#unitOfWork.getPersistStack().delete(row);
1275
- // it can be also in the identity map if it had a PK value already
1276
1311
  em.#unitOfWork.unsetIdentity(row);
1277
1312
  }
1278
1313
  const payload = em.#comparator.prepareEntity(row);
@@ -1310,6 +1345,37 @@ export class EntityManager {
1310
1345
  });
1311
1346
  return res.affectedRows;
1312
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
+ }
1313
1379
  /**
1314
1380
  * Fires native delete query. Calling this has no side effects on the context (identity map).
1315
1381
  */
@@ -1458,6 +1524,29 @@ export class EntityManager {
1458
1524
  await em.storeCache(options.cache, cached, () => +count);
1459
1525
  return +count;
1460
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
+ }
1461
1550
  /**
1462
1551
  * Tells the EntityManager to make an instance managed and persistent.
1463
1552
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -1563,7 +1652,7 @@ export class EntityManager {
1563
1652
  const em = this.getContext();
1564
1653
  em.prepareOptions(options);
1565
1654
  const entityName = arr[0].constructor;
1566
- 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);
1567
1656
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
1568
1657
  return entities;
1569
1658
  }
@@ -1596,6 +1685,8 @@ export class EntityManager {
1596
1685
  fork.#filterParams = Utils.copy(em.#filterParams);
1597
1686
  fork.loggerContext = Utils.merge({}, em.loggerContext, options.loggerContext);
1598
1687
  fork.#schema = options.schema ?? em.#schema;
1688
+ fork.signal = options.signal ?? em.signal;
1689
+ fork.inflightQueryAbortStrategy = options.inflightQueryAbortStrategy ?? em.inflightQueryAbortStrategy;
1599
1690
  if (!options.clear) {
1600
1691
  for (const entity of em.#unitOfWork.getIdentityMap()) {
1601
1692
  fork.#unitOfWork.register(entity);
@@ -1671,6 +1762,22 @@ export class EntityManager {
1671
1762
  getTransactionContext() {
1672
1763
  return this.getContext(false).#transactionContext;
1673
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
+ }
1674
1781
  /**
1675
1782
  * Sets the transaction context.
1676
1783
  */
@@ -1821,8 +1928,17 @@ export class EntityManager {
1821
1928
  return [];
1822
1929
  }
1823
1930
  const meta = this.metadata.find(entityName);
1824
- // infer populate hint if only `fields` are available
1825
- 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) {
1826
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
1827
1943
  const pruneToOneRelations = (meta, fields) => {
1828
1944
  const ret = [];
@@ -1856,7 +1972,19 @@ export class EntityManager {
1856
1972
  }
1857
1973
  return Utils.unique(ret);
1858
1974
  };
1859
- 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
+ }
1860
1988
  }
1861
1989
  if (!options.populate) {
1862
1990
  const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
@@ -1896,9 +2024,21 @@ export class EntityManager {
1896
2024
  }
1897
2025
  if (options.populateHints) {
1898
2026
  applyPopulateHints(populate, options.populateHints);
2027
+ this.forceSelectInForLimitedPopulate(populate);
1899
2028
  }
1900
2029
  return populate;
1901
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
+ }
1902
2042
  /**
1903
2043
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
1904
2044
  * some additional lazy properties, if so, we reload and merge the data from database
@@ -1930,6 +2070,8 @@ export class EntityManager {
1930
2070
  throw new ValidationError(`Cannot combine 'fields' and 'exclude' option.`);
1931
2071
  }
1932
2072
  options.schema ??= this.#schema;
2073
+ options.signal ??= this.signal;
2074
+ options.inflightQueryAbortStrategy ??= this.inflightQueryAbortStrategy;
1933
2075
  options.logging = options.loggerContext = Utils.merge({ id: this.id }, this.loggerContext, options.loggerContext, options.logging);
1934
2076
  }
1935
2077
  /**
@@ -1938,7 +2080,15 @@ export class EntityManager {
1938
2080
  cacheKey(entityName, options, method, where) {
1939
2081
  const { ...opts } = options;
1940
2082
  // ignore some irrelevant options, e.g. logger context can contain dynamic data for the same query
1941
- 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
+ ]) {
1942
2092
  delete opts[k];
1943
2093
  }
1944
2094
  return [Utils.className(entityName), method, opts, where];
@@ -2031,6 +2181,8 @@ export class EntityManager {
2031
2181
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
2032
2182
  case 'm:n':
2033
2183
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
2184
+ case 'count':
2185
+ return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
2034
2186
  }
2035
2187
  }
2036
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
+ }