@mikro-orm/core 7.1.0-dev.2 → 7.1.0-dev.20

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 (51) hide show
  1. package/EntityManager.d.ts +63 -11
  2. package/EntityManager.js +180 -21
  3. package/drivers/DatabaseDriver.d.ts +1 -0
  4. package/drivers/DatabaseDriver.js +3 -0
  5. package/drivers/IDatabaseDriver.d.ts +44 -1
  6. package/entity/BaseEntity.d.ts +68 -1
  7. package/entity/BaseEntity.js +18 -0
  8. package/entity/Collection.d.ts +6 -3
  9. package/entity/Collection.js +15 -4
  10. package/entity/EntityFactory.js +20 -1
  11. package/entity/EntityLoader.d.ts +3 -1
  12. package/entity/EntityLoader.js +39 -16
  13. package/entity/EntityRepository.d.ts +27 -9
  14. package/entity/EntityRepository.js +12 -0
  15. package/entity/Reference.d.ts +42 -1
  16. package/entity/Reference.js +9 -0
  17. package/entity/defineEntity.d.ts +84 -15
  18. package/entity/defineEntity.js +10 -6
  19. package/entity/utils.js +4 -5
  20. package/errors.d.ts +2 -0
  21. package/errors.js +4 -0
  22. package/index.d.ts +2 -2
  23. package/index.js +1 -1
  24. package/metadata/EntitySchema.js +3 -0
  25. package/metadata/MetadataDiscovery.d.ts +12 -0
  26. package/metadata/MetadataDiscovery.js +155 -10
  27. package/metadata/MetadataValidator.d.ts +24 -0
  28. package/metadata/MetadataValidator.js +201 -0
  29. package/metadata/types.d.ts +71 -3
  30. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  31. package/naming-strategy/NamingStrategy.d.ts +1 -1
  32. package/package.json +1 -1
  33. package/platforms/Platform.d.ts +7 -0
  34. package/platforms/Platform.js +15 -1
  35. package/typings.d.ts +139 -10
  36. package/typings.js +21 -4
  37. package/utils/AbstractMigrator.d.ts +11 -1
  38. package/utils/AbstractMigrator.js +203 -0
  39. package/utils/Configuration.d.ts +7 -0
  40. package/utils/Configuration.js +1 -0
  41. package/utils/DataloaderUtils.d.ts +10 -1
  42. package/utils/DataloaderUtils.js +78 -0
  43. package/utils/QueryHelper.d.ts +16 -0
  44. package/utils/QueryHelper.js +15 -0
  45. package/utils/Utils.js +1 -1
  46. package/utils/fs-utils.d.ts +2 -0
  47. package/utils/fs-utils.js +7 -1
  48. package/utils/index.d.ts +1 -0
  49. package/utils/index.js +1 -0
  50. package/utils/partition-utils.d.ts +17 -0
  51. package/utils/partition-utils.js +79 -0
@@ -6,8 +6,8 @@ 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 } 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, UnboxArray, IndexFilterQuery, WithUsingOptions } from './typings.js';
11
11
  import { FlushMode, LockMode, PopulatePath, type TransactionOptions } from './enums.js';
12
12
  import type { MetadataStorage } from './metadata/MetadataStorage.js';
13
13
  import type { Transaction } from './connections/Connection.js';
@@ -61,7 +61,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
61
61
  /**
62
62
  * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
63
63
  */
64
- find<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options?: FindOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
64
+ find<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
65
+ using?: Using | Using[];
66
+ }): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
65
67
  /**
66
68
  * Finds all entities and returns an async iterable (async generator) that yields results one by one.
67
69
  * The results are merged and mapped to entity instances, without adding them to the identity map.
@@ -79,11 +81,11 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
79
81
  * }
80
82
  * ```
81
83
  */
82
- stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
84
+ stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
83
85
  /**
84
86
  * Finds all entities of given type, optionally matching the `where` condition provided in the `options` parameter.
85
87
  */
86
- findAll<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: FindAllOptions<NoInfer<Entity>, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
88
+ findAll<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<FindAllOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
87
89
  private getPopulateWhere;
88
90
  /**
89
91
  * Registers global filter to this entity manager. Global filters are enabled by default (unless disabled via last parameter).
@@ -130,7 +132,9 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
130
132
  * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
131
133
  * where the first element is the array of entities, and the second is the count.
132
134
  */
133
- findAndCount<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options?: FindOptions<Entity, Hint, Fields, Excludes>): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
135
+ findAndCount<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
136
+ using?: Using | Using[];
137
+ }): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
134
138
  /**
135
139
  * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as {@apilink Cursor} object.
136
140
  * Supports `before`, `after`, `first` and `last` options while disallowing `limit` and `offset`. Explicit `orderBy` option
@@ -187,7 +191,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
187
191
  * }
188
192
  * ```
189
193
  */
190
- findByCursor<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true>(entityName: EntityName<Entity>, options: FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
194
+ findByCursor<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true, Using extends string = never>(entityName: EntityName<Entity>, options: WithUsingOptions<FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>, Entity, Using>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
191
195
  /**
192
196
  * Refreshes the persistent state of an entity from the database, overriding any local changes that have not yet been
193
197
  * persisted. Returns the same entity instance (same object reference), but re-hydrated. If the entity is no longer
@@ -203,14 +207,18 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
203
207
  /**
204
208
  * Finds first entity matching your `where` query.
205
209
  */
206
- findOne<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options?: FindOneOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
210
+ findOne<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOneOptions<Entity, Hint, Fields, Excludes> & {
211
+ using?: Using | Using[];
212
+ }): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
207
213
  /**
208
214
  * Finds first entity matching your `where` query. If nothing found, it will throw an error.
209
215
  * If the `strict` option is specified and nothing is found or more than one matching entity is found, it will throw an error.
210
216
  * You can override the factory for creating this method via `options.failHandler` locally
211
217
  * or via `Configuration.findOneOrFailHandler` (`findExactlyOneOrFailHandler` when specifying `strict`) globally.
212
218
  */
213
- findOneOrFail<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, where: FilterQuery<NoInfer<Entity>>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
219
+ findOneOrFail<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, where: [Using] extends [never] ? FilterQuery<NoInfer<Entity>> : IndexFilterQuery<NoInfer<Entity>, Using>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes> & {
220
+ using?: Using | Using[];
221
+ }): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
214
222
  /**
215
223
  * Creates or updates the entity, based on whether it is already present in the database.
216
224
  * This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
@@ -307,6 +315,25 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
307
315
  * Fires native insert query. Calling this has no side effects on the context (identity map).
308
316
  */
309
317
  insert<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity, data?: RequiredEntityData<Entity> | Entity, options?: NativeInsertUpdateOptions<Entity>): Promise<Primary<Entity>>;
318
+ /**
319
+ * Clones rows matching the condition at the database level via INSERT...SELECT.
320
+ * Automatically excludes auto-increment PKs, generated columns, and resets version properties.
321
+ * Returns the cloned entity, registered in the identity map.
322
+ *
323
+ * @example
324
+ * ```ts
325
+ * // Clone by entity class + where + overrides
326
+ * const cloned = await em.clone(Author, { id: 1 }, { email: 'new@email.com' });
327
+ *
328
+ * // Pure clone (all non-PK fields copied)
329
+ * const cloned = await em.clone(Author, { id: 1 });
330
+ *
331
+ * // Clone a loaded entity
332
+ * const author = await em.findOneOrFail(Author, 1);
333
+ * const cloned = await em.clone(author, { email: 'new@email.com' });
334
+ * ```
335
+ */
336
+ clone<Entity extends object>(entityNameOrEntity: EntityName<Entity> | Entity, whereOrOverrides?: FilterQuery<NoInfer<Entity>> | EntityData<Entity>, overridesOrOptions?: EntityData<Entity> | NativeInsertUpdateOptions<Entity>, options?: NativeInsertUpdateOptions<Entity>): Promise<Entity>;
310
337
  /**
311
338
  * Fires native multi-insert query. Calling this has no side effects on the context (identity map).
312
339
  */
@@ -414,6 +441,27 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
414
441
  * Returns total number of entities matching your `where` query.
415
442
  */
416
443
  count<Entity extends object, Hint extends string = never>(entityName: EntityName<Entity>, where?: FilterQuery<NoInfer<Entity>>, options?: CountOptions<Entity, Hint>): Promise<number>;
444
+ /**
445
+ * Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
446
+ * field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
447
+ *
448
+ * SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
449
+ *
450
+ * @example
451
+ * ```ts
452
+ * // Count books per author
453
+ * const counts = await em.countBy(Book, 'author');
454
+ * // { '1': 2, '2': 1, '3': 3 }
455
+ *
456
+ * // Count with a filter
457
+ * const counts = await em.countBy(Book, 'author', { where: { active: true } });
458
+ *
459
+ * // Composite groupBy — keys joined with ~~~
460
+ * const counts = await em.countBy(Order, ['status', 'country']);
461
+ * // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
462
+ * ```
463
+ */
464
+ countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
417
465
  /**
418
466
  * Tells the EntityManager to make an instance managed and persistent.
419
467
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -505,16 +553,20 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
505
553
  */
506
554
  getComparator(): EntityComparator;
507
555
  private checkLockRequirements;
556
+ private validateIndexUsage;
557
+ private validateWhereKeysForIndex;
508
558
  private lockAndPopulate;
509
559
  private buildFields;
510
560
  /** @internal */
511
561
  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>[]>;
562
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
563
+ private forceSelectInForLimitedPopulate;
512
564
  /**
513
565
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
514
566
  * some additional lazy properties, if so, we reload and merge the data from database
515
567
  */
516
568
  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;
517
- protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any>): void;
569
+ protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>): void;
518
570
  /**
519
571
  * @internal
520
572
  */
@@ -556,7 +608,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
556
608
  */
557
609
  set schema(schema: string | null | undefined);
558
610
  /** @internal */
559
- getDataLoader(type: 'ref' | '1:m' | 'm:n'): Promise<any>;
611
+ getDataLoader(type: 'ref' | '1:m' | 'm:n' | 'count'): Promise<any>;
560
612
  /**
561
613
  * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
562
614
  * if executed inside request context handler.
package/EntityManager.js CHANGED
@@ -103,9 +103,6 @@ export class EntityManager {
103
103
  repo(entityName) {
104
104
  return this.getRepository(entityName);
105
105
  }
106
- /**
107
- * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
108
- */
109
106
  async find(entityName, where, options = {}) {
110
107
  if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
111
108
  const em = this.getContext(false);
@@ -116,10 +113,11 @@ export class EntityManager {
116
113
  }
117
114
  const em = this.getContext();
118
115
  em.prepareOptions(options);
116
+ const meta = this.metadata.get(entityName);
117
+ em.validateIndexUsage(meta, where, options);
119
118
  await em.tryFlush(entityName, options);
120
119
  where = await em.processWhere(entityName, where, options, 'read');
121
120
  validateParams(where);
122
- const meta = this.metadata.get(entityName);
123
121
  if (meta.orderBy) {
124
122
  options.orderBy = QueryHelper.mergeOrderBy(options.orderBy, meta.orderBy);
125
123
  }
@@ -203,6 +201,7 @@ export class EntityManager {
203
201
  options.orderBy = options.orderBy || {};
204
202
  options.populate = (await em.preparePopulate(entityName, options));
205
203
  const meta = this.metadata.get(entityName);
204
+ em.validateIndexUsage(meta, options.where ?? {}, options);
206
205
  options = { ...options };
207
206
  // save the original hint value so we know it was infer/all
208
207
  options._populateWhere = options.populateWhere ?? this.config.get('populateWhere');
@@ -494,10 +493,6 @@ export class EntityManager {
494
493
  const conds = [...ret, where].filter(c => Utils.hasObjectKeys(c) || Raw.hasObjectFragments(c));
495
494
  return conds.length > 1 ? { $and: conds } : conds[0];
496
495
  }
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
496
  async findAndCount(entityName, where, options = {}) {
502
497
  const em = this.getContext(false);
503
498
  await em.tryFlush(entityName, options);
@@ -631,9 +626,6 @@ export class EntityManager {
631
626
  }
632
627
  return entity;
633
628
  }
634
- /**
635
- * Finds first entity matching your `where` query.
636
- */
637
629
  async findOne(entityName, where, options = {}) {
638
630
  if (options.disableIdentityMap ?? this.config.get('disableIdentityMap')) {
639
631
  const em = this.getContext(false);
@@ -653,6 +645,7 @@ export class EntityManager {
653
645
  }
654
646
  await em.tryFlush(entityName, options);
655
647
  const meta = em.metadata.get(entityName);
648
+ em.validateIndexUsage(meta, where, options);
656
649
  where = await em.processWhere(entityName, where, options, 'read');
657
650
  validateEmptyWhere(where);
658
651
  em.checkLockRequirements(options.lockMode, meta);
@@ -701,12 +694,6 @@ export class EntityManager {
701
694
  await em.storeCache(options.cache, cached, () => helper(entity).toPOJO());
702
695
  return entity;
703
696
  }
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
697
  async findOneOrFail(entityName, where, options = {}) {
711
698
  let entity;
712
699
  let isStrictViolation = false;
@@ -1211,6 +1198,54 @@ export class EntityManager {
1211
1198
  });
1212
1199
  return res.insertId;
1213
1200
  }
1201
+ /**
1202
+ * Clones rows matching the condition at the database level via INSERT...SELECT.
1203
+ * Automatically excludes auto-increment PKs, generated columns, and resets version properties.
1204
+ * Returns the cloned entity, registered in the identity map.
1205
+ *
1206
+ * @example
1207
+ * ```ts
1208
+ * // Clone by entity class + where + overrides
1209
+ * const cloned = await em.clone(Author, { id: 1 }, { email: 'new@email.com' });
1210
+ *
1211
+ * // Pure clone (all non-PK fields copied)
1212
+ * const cloned = await em.clone(Author, { id: 1 });
1213
+ *
1214
+ * // Clone a loaded entity
1215
+ * const author = await em.findOneOrFail(Author, 1);
1216
+ * const cloned = await em.clone(author, { email: 'new@email.com' });
1217
+ * ```
1218
+ */
1219
+ async clone(entityNameOrEntity, whereOrOverrides, overridesOrOptions, options) {
1220
+ const em = this.getContext(false);
1221
+ let entityName;
1222
+ let where;
1223
+ let overrides;
1224
+ if (Utils.isEntity(entityNameOrEntity)) {
1225
+ // clone(entity, overrides?, options?)
1226
+ entityName = entityNameOrEntity.constructor;
1227
+ where = helper(entityNameOrEntity).getPrimaryKey();
1228
+ overrides = whereOrOverrides;
1229
+ options = overridesOrOptions;
1230
+ }
1231
+ else {
1232
+ // clone(EntityClass, where, overrides?, options?)
1233
+ entityName = entityNameOrEntity;
1234
+ where = whereOrOverrides;
1235
+ overrides = overridesOrOptions;
1236
+ }
1237
+ options ??= {};
1238
+ em.prepareOptions(options);
1239
+ const meta = em.metadata.get(entityName);
1240
+ const res = await em.driver.nativeClone(entityName, where, overrides, {
1241
+ ctx: em.#transactionContext,
1242
+ ...options,
1243
+ });
1244
+ const pk = res.insertId ?? res.row?.[meta.primaryKeys[0]];
1245
+ return em.findOneOrFail(entityName, pk, {
1246
+ schema: options.schema,
1247
+ });
1248
+ }
1214
1249
  /**
1215
1250
  * Fires native multi-insert query. Calling this has no side effects on the context (identity map).
1216
1251
  */
@@ -1423,6 +1458,29 @@ export class EntityManager {
1423
1458
  await em.storeCache(options.cache, cached, () => +count);
1424
1459
  return +count;
1425
1460
  }
1461
+ /**
1462
+ * Counts entities grouped by one or more properties. Returns a dictionary keyed by the grouped
1463
+ * field value(s), with counts as values. For composite `groupBy`, keys are joined with `~~~`.
1464
+ *
1465
+ * SQL drivers issue a single `GROUP BY` query; MongoDB uses an aggregation pipeline.
1466
+ *
1467
+ * @example
1468
+ * ```ts
1469
+ * // Count books per author
1470
+ * const counts = await em.countBy(Book, 'author');
1471
+ * // { '1': 2, '2': 1, '3': 3 }
1472
+ *
1473
+ * // Count with a filter
1474
+ * const counts = await em.countBy(Book, 'author', { where: { active: true } });
1475
+ *
1476
+ * // Composite groupBy — keys joined with ~~~
1477
+ * const counts = await em.countBy(Order, ['status', 'country']);
1478
+ * // { 'pending~~~US': 5, 'shipped~~~DE': 3 }
1479
+ * ```
1480
+ */
1481
+ async countBy(entityName, groupBy, options) {
1482
+ throw new Error(`${this.constructor.name}.countBy() is not supported by the current driver`);
1483
+ }
1426
1484
  /**
1427
1485
  * Tells the EntityManager to make an instance managed and persistent.
1428
1486
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -1528,7 +1586,7 @@ export class EntityManager {
1528
1586
  const em = this.getContext();
1529
1587
  em.prepareOptions(options);
1530
1588
  const entityName = arr[0].constructor;
1531
- const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters }, options.validate);
1589
+ const preparedPopulate = await em.preparePopulate(entityName, { populate: populate, filters: options.filters, populateHints: options.populateHints }, options.validate);
1532
1590
  await em.#entityLoader.populate(entityName, arr, preparedPopulate, options);
1533
1591
  return entities;
1534
1592
  }
@@ -1679,6 +1737,79 @@ export class EntityManager {
1679
1737
  throw ValidationError.transactionRequired();
1680
1738
  }
1681
1739
  }
1740
+ validateIndexUsage(meta, where, options) {
1741
+ if (!options.using) {
1742
+ return;
1743
+ }
1744
+ const indexNames = Utils.asArray(options.using);
1745
+ const allIndexes = [...meta.indexes, ...meta.uniques];
1746
+ const indexMap = new Map();
1747
+ for (const idx of allIndexes) {
1748
+ if (idx.name) {
1749
+ indexMap.set(idx.name, Utils.asArray(idx.properties ?? []));
1750
+ }
1751
+ }
1752
+ for (const prop of meta.props) {
1753
+ if (typeof prop.index === 'string') {
1754
+ indexMap.set(prop.index, [prop.name]);
1755
+ }
1756
+ if (typeof prop.unique === 'string') {
1757
+ indexMap.set(prop.unique, [prop.name]);
1758
+ }
1759
+ }
1760
+ const allowedProps = new Set();
1761
+ for (const name of indexNames) {
1762
+ const props = indexMap.get(name);
1763
+ if (!props) {
1764
+ const available = [...indexMap.keys()];
1765
+ throw new Error(`Index '${name}' not found on entity '${meta.className}'. ` +
1766
+ (available.length > 0 ? `Available indexes: ${available.join(', ')}` : 'No named indexes defined.'));
1767
+ }
1768
+ for (const prop of props) {
1769
+ allowedProps.add(prop);
1770
+ }
1771
+ }
1772
+ this.validateWhereKeysForIndex(where, allowedProps, indexNames);
1773
+ if (options.orderBy) {
1774
+ const orderMaps = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy];
1775
+ for (const orderMap of orderMaps) {
1776
+ for (const key of Object.keys(orderMap)) {
1777
+ if (!allowedProps.has(key)) {
1778
+ throw new Error(`Property '${key}' in orderBy is not covered by index '${indexNames.join("', '")}'. ` +
1779
+ `Allowed properties: ${[...allowedProps].join(', ')}`);
1780
+ }
1781
+ }
1782
+ }
1783
+ }
1784
+ }
1785
+ validateWhereKeysForIndex(where, allowedProps, indexNames) {
1786
+ if (typeof where !== 'object' || where === null) {
1787
+ return;
1788
+ }
1789
+ if (Array.isArray(where)) {
1790
+ for (const item of where) {
1791
+ this.validateWhereKeysForIndex(item, allowedProps, indexNames);
1792
+ }
1793
+ return;
1794
+ }
1795
+ for (const key of Object.keys(where)) {
1796
+ if (key === '$and' || key === '$or') {
1797
+ for (const item of Utils.asArray(where[key])) {
1798
+ this.validateWhereKeysForIndex(item, allowedProps, indexNames);
1799
+ }
1800
+ }
1801
+ else if (key === '$not') {
1802
+ this.validateWhereKeysForIndex(where[key], allowedProps, indexNames);
1803
+ }
1804
+ else if (key.startsWith('$')) {
1805
+ continue;
1806
+ }
1807
+ else if (!allowedProps.has(key)) {
1808
+ throw new Error(`Property '${key}' in where clause is not covered by index '${indexNames.join("', '")}'. ` +
1809
+ `Allowed properties: ${[...allowedProps].join(', ')}`);
1810
+ }
1811
+ }
1812
+ }
1682
1813
  async lockAndPopulate(meta, entity, where, options) {
1683
1814
  if (!meta.virtual && options.lockMode === LockMode.OPTIMISTIC) {
1684
1815
  await this.lock(entity, options.lockMode, {
@@ -1713,8 +1844,13 @@ export class EntityManager {
1713
1844
  return [];
1714
1845
  }
1715
1846
  const meta = this.metadata.find(entityName);
1716
- // infer populate hint if only `fields` are available
1717
- if (!options.populate && options.fields) {
1847
+ // infer populate hints from `fields` when present; merge with explicit `populate` if both are set
1848
+ const fields = options.fields ? this.buildFields(options.fields) : undefined;
1849
+ const populateArray = Array.isArray(options.populate) ? options.populate : undefined;
1850
+ const hasNestedFields = fields?.some(f => f.includes('.')) ?? false;
1851
+ const shouldInfer = fields !== undefined && options.populate === undefined;
1852
+ const shouldMerge = fields !== undefined && populateArray !== undefined && populateArray.length > 0 && hasNestedFields;
1853
+ if (shouldInfer || shouldMerge) {
1718
1854
  // 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
1719
1855
  const pruneToOneRelations = (meta, fields) => {
1720
1856
  const ret = [];
@@ -1748,7 +1884,16 @@ export class EntityManager {
1748
1884
  }
1749
1885
  return Utils.unique(ret);
1750
1886
  };
1751
- options.populate = pruneToOneRelations(meta, this.buildFields(options.fields));
1887
+ const fromFields = pruneToOneRelations(meta, fields);
1888
+ if (shouldInfer) {
1889
+ options.populate = fromFields;
1890
+ }
1891
+ else {
1892
+ // with an explicit populate, only nested paths (e.g. `item.id`) need to be merged in — they
1893
+ // require a JOIN to access sub-fields, whereas bare names are already handled by the driver
1894
+ const extras = fromFields.filter(f => f.includes('.'));
1895
+ options.populate = [...populateArray, ...extras];
1896
+ }
1752
1897
  }
1753
1898
  if (!options.populate) {
1754
1899
  const populate = this.#entityLoader.normalizePopulate(entityName, [], options.strategy, true, options.exclude);
@@ -1788,9 +1933,21 @@ export class EntityManager {
1788
1933
  }
1789
1934
  if (options.populateHints) {
1790
1935
  applyPopulateHints(populate, options.populateHints);
1936
+ this.forceSelectInForLimitedPopulate(populate);
1791
1937
  }
1792
1938
  return populate;
1793
1939
  }
1940
+ /** Force SELECT_IN strategy on populate entries with `limit`, since JOINED cannot do per-parent limiting. */
1941
+ forceSelectInForLimitedPopulate(populate) {
1942
+ for (const entry of populate) {
1943
+ if (entry.limit != null) {
1944
+ entry.strategy = LoadStrategy.SELECT_IN;
1945
+ }
1946
+ if (entry.children?.length) {
1947
+ this.forceSelectInForLimitedPopulate(entry.children);
1948
+ }
1949
+ }
1950
+ }
1794
1951
  /**
1795
1952
  * when the entity is found in identity map, we check if it was partially loaded or we are trying to populate
1796
1953
  * some additional lazy properties, if so, we reload and merge the data from database
@@ -1923,6 +2080,8 @@ export class EntityManager {
1923
2080
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
1924
2081
  case 'm:n':
1925
2082
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
2083
+ case 'count':
2084
+ return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
1926
2085
  }
1927
2086
  }
1928
2087
  /**
@@ -28,6 +28,7 @@ export declare abstract class DatabaseDriver<C extends Connection> implements ID
28
28
  abstract nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
29
29
  nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
30
30
  abstract nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: DeleteOptions<T>): Promise<QueryResult<T>>;
31
+ nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
31
32
  abstract count<T extends object, P extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: CountOptions<T, P>): Promise<number>;
32
33
  /** Creates a new EntityManager instance bound to this driver. */
33
34
  createEntityManager(useContext?: boolean): this[typeof EntityManagerType];
@@ -27,6 +27,9 @@ export class DatabaseDriver {
27
27
  async nativeUpdateMany(entityName, where, data, options) {
28
28
  throw new Error(`Batch updates are not supported by ${this.constructor.name} driver`);
29
29
  }
30
+ async nativeClone(entityName, where, overrides, options) {
31
+ throw new Error(`${this.constructor.name} does not support nativeClone`);
32
+ }
30
33
  /** Creates a new EntityManager instance bound to this driver. */
31
34
  createEntityManager(useContext) {
32
35
  const EntityManagerClass = this.config.get('entityManager', EntityManager);
@@ -1,4 +1,4 @@
1
- import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes } from '../typings.js';
1
+ import type { ConnectionType, Constructor, EntityData, EntityMetadata, EntityProperty, FilterQuery, Primary, Dictionary, IPrimaryKey, PopulateOptions, EntityDictionary, AutoPath, ObjectQuery, FilterObject, Populate, EntityName, PopulateHintOptions, Prefixes, IndexName } from '../typings.js';
2
2
  import type { Connection, QueryResult, Transaction } from '../connections/Connection.js';
3
3
  import type { FlushMode, LockMode, QueryOrderMap, QueryFlag, LoadStrategy, PopulateHint, PopulatePath } from '../enums.js';
4
4
  import type { Platform } from '../platforms/Platform.js';
@@ -52,6 +52,11 @@ export interface IDatabaseDriver<C extends Connection = Connection> {
52
52
  nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>): Promise<QueryResult<T>>;
53
53
  /** Deletes rows matching the given condition. */
54
54
  nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: NativeDeleteOptions<T>): Promise<QueryResult<T>>;
55
+ /**
56
+ * Clones rows matching the condition at the database level via INSERT...SELECT.
57
+ * Automatically excludes auto-increment PKs, generated columns, and resets version properties.
58
+ */
59
+ nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
55
60
  /** Persists changes to M:N collections (inserts/deletes pivot table rows). */
56
61
  syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>;
57
62
  /** Counts entities matching the given condition. */
@@ -105,6 +110,20 @@ export interface FindAllOptions<T, P extends string = never, F extends string =
105
110
  }
106
111
  /** Options for streaming query results via `em.stream()`. */
107
112
  export interface StreamOptions<Entity, Populate extends string = never, Fields extends string = never, Exclude extends string = never> extends Omit<FindAllOptions<Entity, Populate, Fields, Exclude>, 'cache' | 'before' | 'after' | 'first' | 'last' | 'overfetch' | 'strategy'> {
113
+ /**
114
+ * How many rows to fetch in one round-trip.
115
+ * Lower values will result in more queries and network bandwidth, but less memory usage.
116
+ * Higher values will result in fewer queries and network bandwidth, but higher memory usage.
117
+ * Note that the results are iterated one row at a time regardless of this value.
118
+ *
119
+ * Honored on PostgreSQL (cursor-based fetch), MSSQL (tedious stream chunk size),
120
+ * Oracle (mapped to `fetchArraySize`) and MongoDB (mapped to `batchSize`). Ignored
121
+ * on MySQL, MariaDB, SQLite and libSQL, where the underlying driver already streams
122
+ * row-by-row with no batching knob.
123
+ *
124
+ * @default 100 (on dialects that honor it)
125
+ */
126
+ chunkSize?: number;
108
127
  /**
109
128
  * When populating to-many relations, the ORM streams fully merged entities instead of yielding every row.
110
129
  * You can opt out of this behavior by specifying `mergeResults: false`. This will yield every row from
@@ -211,6 +230,20 @@ export interface FindOptions<Entity, Hint extends string = never, Fields extends
211
230
  connectionType?: ConnectionType;
212
231
  /** SQL: appended to FROM clause (e.g. `'force index(my_index)'`); MongoDB: index name or spec passed as `hint`. */
213
232
  indexHint?: string | Dictionary;
233
+ /**
234
+ * Named index(es) for this query. When provided:
235
+ * - Validates that `where` and `orderBy` only reference columns covered by the specified index(es).
236
+ * - Emits SQL index hints where supported (MySQL/MariaDB: `USE INDEX`, MSSQL: `WITH (INDEX(...))`).
237
+ * - If `indexHint` is also set, `indexHint` takes precedence for SQL generation.
238
+ *
239
+ * Accepts a single index name or an array. For `defineEntity` entities with named indexes
240
+ * and decorator entities with `[IndexHints]`, index names are autocompleted.
241
+ *
242
+ * @example
243
+ * await em.find(Book, { title: 'foo' }, { using: 'idx_book_title' });
244
+ * await em.find(Book, { title: 'foo', author: 1 }, { using: ['idx_book_title', 'idx_book_author'] });
245
+ */
246
+ using?: IndexName<Entity> | IndexName<Entity>[];
214
247
  /** sql only */
215
248
  comments?: string | string[];
216
249
  /** sql only */
@@ -306,6 +339,16 @@ export interface CountOptions<T extends object, P extends string = never> {
306
339
  /** @internal used to apply filters to the auto-joined relations */
307
340
  em?: EntityManager;
308
341
  }
342
+ /** Options for `em.countBy()` queries. */
343
+ export interface CountByOptions<T extends object> {
344
+ where?: FilterQuery<T>;
345
+ filters?: FilterOptions;
346
+ having?: FilterQuery<T>;
347
+ schema?: string;
348
+ flushMode?: FlushMode | `${FlushMode}`;
349
+ loggerContext?: LogContext;
350
+ logging?: LoggingOptions;
351
+ }
309
352
  /** Options for `em.qb().update()` operations. */
310
353
  export interface UpdateOptions<T> {
311
354
  filters?: FilterOptions;
@@ -1,4 +1,4 @@
1
- import { type Ref } from './Reference.js';
1
+ import { type LoadReferenceOptions, type LoadReferenceOrFailOptions, type Ref } from './Reference.js';
2
2
  import type { AutoPath, EntityData, EntityDTO, ExtractFieldsHint, Loaded, LoadedReference, AddEager, EntityKey, FromEntityType, IsSubset, MergeSelected, ResolveSerializeFields, SerializeDTO, SerializeFieldsKeepPK } from '../typings.js';
3
3
  import { type AssignOptions } from './EntityAssigner.js';
4
4
  import type { EntityLoaderOptions } from './EntityLoader.js';
@@ -90,3 +90,70 @@ export declare abstract class BaseEntity {
90
90
  /** Sets the database schema for this entity. */
91
91
  setSchema(schema?: string): void;
92
92
  }
93
+ type EntityConstructor<T extends object = object> = abstract new (...args: any[]) => T;
94
+ /**
95
+ * The `load()` / `loadOrFail()` methods added by the {@link Loadable} mixin. Declared as an interface so the
96
+ * mixin function can have an explicit return type (required by JSR fast-check).
97
+ */
98
+ export interface LoadableEntity {
99
+ /**
100
+ * Ensures this entity is loaded (without reloading it if it already is). Returns the entity, or `null` if it
101
+ * was not found in the database (e.g. it was deleted in the meantime, or active filters disallow loading it).
102
+ * Use `loadOrFail()` if you want an error to be thrown in such a case.
103
+ */
104
+ load<Entity extends this = this, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: LoadReferenceOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
105
+ /**
106
+ * Ensures this entity is loaded (without reloading it if it already is). Returns the entity, or throws an error
107
+ * just like `em.findOneOrFail()` (and respects the same config options) if it was not found.
108
+ */
109
+ loadOrFail<Entity extends this = this, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: LoadReferenceOrFailOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
110
+ }
111
+ /** Return-type shape of {@link Loadable} — a constructor that produces instances of `TBase` enriched with {@link LoadableEntity}. */
112
+ export type LoadableConstructor<TBase extends EntityConstructor> = abstract new (...args: any[]) => InstanceType<TBase> & LoadableEntity;
113
+ /** Internal: rejects a base class that already defines `load` or `loadOrFail` to prevent silent override. */
114
+ type EnsureNoLoadConflict<TBase extends EntityConstructor> = InstanceType<TBase> extends {
115
+ load: any;
116
+ } ? 'Loadable: base class already defines `load` — remove it or do not apply the mixin' : InstanceType<TBase> extends {
117
+ loadOrFail: any;
118
+ } ? 'Loadable: base class already defines `loadOrFail` — remove it or do not apply the mixin' : TBase;
119
+ /** Empty base for {@link Loadable} when called without arguments — standalone mixin, no inherited base. */
120
+ declare abstract class EmptyBase {
121
+ }
122
+ /**
123
+ * Mixin that adds `load()` / `loadOrFail()` methods to an entity class. These methods ensure the entity is loaded
124
+ * from the database without reloading it if it already is — unlike `init()`, which always refreshes.
125
+ *
126
+ * Useful when migrating from a non-`Ref`-based codebase where lazy loading support is desired without the
127
+ * `.$` / `.get()` indirection that the `Reference` wrapper requires. Opt-in so it does not conflict with entities
128
+ * that already define a `load` or `loadOrFail` property — applying the mixin to a base class that already has
129
+ * either method is a compile error to prevent silent override.
130
+ *
131
+ * Call without arguments (`Loadable()`) for a standalone base with no other inheritance, or pass a base class
132
+ * (`Loadable(BaseEntity)`) to compose. The convenience alias {@link LoadableBaseEntity} is shorthand for the
133
+ * latter.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * // compose with BaseEntity
138
+ * class User extends Loadable(BaseEntity) {
139
+ * @PrimaryKey()
140
+ * id!: number;
141
+ * }
142
+ *
143
+ * // standalone — no inherited base
144
+ * class Product extends Loadable() {
145
+ * @PrimaryKey()
146
+ * id!: number;
147
+ * }
148
+ *
149
+ * const user = orm.em.getReference(User, 1);
150
+ * await user.load();
151
+ * ```
152
+ */
153
+ export declare function Loadable(): LoadableConstructor<typeof EmptyBase> & typeof EmptyBase;
154
+ export declare function Loadable<TBase extends EntityConstructor>(Base: EnsureNoLoadConflict<TBase> extends TBase ? TBase : never): LoadableConstructor<TBase> & TBase;
155
+ declare const LoadableBaseEntityBase: LoadableConstructor<typeof BaseEntity> & typeof BaseEntity;
156
+ /** Convenience: `BaseEntity` pre-composed with the `Loadable` mixin. */
157
+ export declare abstract class LoadableBaseEntity extends LoadableBaseEntityBase {
158
+ }
159
+ export {};