@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
@@ -177,8 +177,9 @@ export class EntityLoader {
177
177
  Utils.isObject(orderBy[prop.name]))
178
178
  .flatMap(orderBy => orderBy[prop.name]);
179
179
  const where = await this.extractChildCondition(options, prop);
180
+ const mergedOrderBy = populate.orderBy ? Utils.asArray(populate.orderBy) : innerOrderBy;
180
181
  if (prop.kind === ReferenceKind.MANY_TO_MANY && this.#driver.getPlatform().usesPivotTable()) {
181
- const pivotOrderBy = QueryHelper.mergeOrderBy(innerOrderBy, prop.orderBy, prop.targetMeta?.orderBy);
182
+ const pivotOrderBy = QueryHelper.mergeOrderBy(mergedOrderBy, prop.orderBy, prop.targetMeta?.orderBy);
182
183
  const res = await this.findChildrenFromPivotTable(filtered, prop, options, pivotOrderBy, populate, !!ref);
183
184
  return Utils.flatten(res);
184
185
  }
@@ -188,10 +189,12 @@ export class EntityLoader {
188
189
  const { items, partial } = await this.findChildren(options.filtered ?? entities, prop, populate, {
189
190
  ...options,
190
191
  where,
191
- orderBy: innerOrderBy,
192
+ orderBy: mergedOrderBy,
192
193
  }, !!(ref || prop.mapToPk));
193
- const customOrder = innerOrderBy.length > 0 || !!prop.orderBy || !!prop.targetMeta?.orderBy;
194
- this.initializeCollections(filtered, prop, field, items, customOrder, partial);
194
+ const hasLimit = populate.limit != null;
195
+ const isPartial = partial || hasLimit;
196
+ const customOrder = mergedOrderBy.length > 0 || !!prop.orderBy || !!prop.targetMeta?.orderBy;
197
+ this.initializeCollections(filtered, prop, field, items, customOrder, isPartial, hasLimit);
195
198
  return items;
196
199
  }
197
200
  async populateScalar(meta, filtered, options) {
@@ -269,15 +272,15 @@ export class EntityLoader {
269
272
  }));
270
273
  return allItems;
271
274
  }
272
- initializeCollections(filtered, prop, field, children, customOrder, partial) {
275
+ initializeCollections(filtered, prop, field, children, customOrder, partial, readonly) {
273
276
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
274
- this.initializeOneToMany(filtered, children, prop, field, partial);
277
+ this.initializeOneToMany(filtered, children, prop, field, partial, readonly);
275
278
  }
276
279
  if (prop.kind === ReferenceKind.MANY_TO_MANY && !this.#driver.getPlatform().usesPivotTable()) {
277
- this.initializeManyToMany(filtered, children, prop, field, customOrder, partial);
280
+ this.initializeManyToMany(filtered, children, prop, field, customOrder, partial, readonly);
278
281
  }
279
282
  }
280
- initializeOneToMany(filtered, children, prop, field, partial) {
283
+ initializeOneToMany(filtered, children, prop, field, partial, readonly) {
281
284
  const mapToPk = prop.targetMeta.properties[prop.mappedBy].mapToPk;
282
285
  const map = {};
283
286
  for (const entity of filtered) {
@@ -293,14 +296,14 @@ export class EntityLoader {
293
296
  }
294
297
  for (const entity of filtered) {
295
298
  const key = helper(entity).getSerializedPrimaryKey();
296
- entity[field].hydrate(map[key], undefined, partial);
299
+ entity[field].hydrate(map[key], undefined, partial, readonly);
297
300
  }
298
301
  }
299
- initializeManyToMany(filtered, children, prop, field, customOrder, partial) {
302
+ initializeManyToMany(filtered, children, prop, field, customOrder, partial, readonly) {
300
303
  if (prop.mappedBy) {
301
304
  for (const entity of filtered) {
302
305
  const items = children.filter(child => child[prop.mappedBy].contains(entity, false));
303
- entity[field].hydrate(items, true, partial);
306
+ entity[field].hydrate(items, true, partial, readonly);
304
307
  }
305
308
  }
306
309
  else {
@@ -311,12 +314,12 @@ export class EntityLoader {
311
314
  if (!customOrder) {
312
315
  items.sort((a, b) => order.indexOf(a) - order.indexOf(b));
313
316
  }
314
- entity[field].hydrate(items, true, partial);
317
+ entity[field].hydrate(items, true, partial, readonly);
315
318
  }
316
319
  }
317
320
  }
318
321
  async findChildren(entities, prop, populate, options, ref) {
319
- const children = Utils.unique(this.getChildReferences(entities, prop, options, ref));
322
+ const children = Utils.unique(this.getChildReferences(entities, prop, options, ref, populate));
320
323
  const meta = prop.targetMeta;
321
324
  // When targetKey is set, use it for FK lookup instead of the PK
322
325
  let fk = prop.targetKey ?? Utils.getPrimaryKeyHash(meta.primaryKeys);
@@ -334,10 +337,10 @@ export class EntityLoader {
334
337
  fk = ownerProp.name;
335
338
  }
336
339
  }
337
- if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !ref) {
340
+ if (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && (!ref || (!prop.mapToPk && !populate.filter))) {
338
341
  children.length = 0;
339
342
  fk = meta.properties[prop.mappedBy].name;
340
- children.push(...this.filterByReferences(entities, prop.name, options.refresh));
343
+ children.push(...this.filterByReferences(entities, prop, options, ref));
341
344
  }
342
345
  if (children.length === 0) {
343
346
  return { items: [], partial };
@@ -366,8 +369,11 @@ export class EntityLoader {
366
369
  }
367
370
  const fields = this.buildFields(options.fields, prop, ref);
368
371
  /* eslint-disable prefer-const */
369
- let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere = 'infer', connectionType, logging, } = options;
372
+ let { refresh, filters, convertCustomTypes, lockMode, strategy, populateWhere, connectionType, logging } = options;
370
373
  /* eslint-enable prefer-const */
374
+ // `populateWhere` is typed as required via `Required<EntityLoaderOptions>`, but `em.populate(...)`
375
+ // forwards user-supplied options without filling it in — keep the runtime default.
376
+ populateWhere ??= 'infer';
371
377
  if (typeof populateWhere === 'object') {
372
378
  populateWhere = await this.extractChildCondition({ where: populateWhere }, prop);
373
379
  }
@@ -375,7 +381,7 @@ export class EntityLoader {
375
381
  where = { $and: [where, prop.where] };
376
382
  }
377
383
  const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
378
- const items = await this.#em.find(meta.class, where, {
384
+ const findOptions = {
379
385
  filters,
380
386
  convertCustomTypes,
381
387
  lockMode,
@@ -390,11 +396,21 @@ export class EntityLoader {
390
396
  fields,
391
397
  schema,
392
398
  connectionType,
399
+ signal: options.signal,
400
+ inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
393
401
  // @ts-ignore not a public option, will be propagated to the populate call
394
402
  refresh: refresh && !children.every(item => options.visited.has(item)),
395
403
  // @ts-ignore not a public option, will be propagated to the populate call
396
404
  visited: options.visited,
397
- });
405
+ };
406
+ if (populate.limit != null) {
407
+ findOptions._partitionLimit = {
408
+ partitionBy: fk,
409
+ limit: populate.limit,
410
+ offset: populate.offset,
411
+ };
412
+ }
413
+ const items = await this.#em.find(meta.class, where, findOptions);
398
414
  // For targetKey relations, wire up loaded entities to parent references
399
415
  // This is needed because the references were created under alternate key,
400
416
  // but loaded entities are stored under PK, so they don't automatically merge
@@ -582,11 +598,18 @@ export class EntityLoader {
582
598
  const options2 = { ...options, fields, exclude, populateFilter };
583
599
  ['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
584
600
  options2.populate = populate?.children ?? [];
601
+ if (populate?.limit != null) {
602
+ options2._partitionLimit = {
603
+ limit: populate.limit,
604
+ offset: populate.offset,
605
+ };
606
+ }
585
607
  if (!Utils.isEmpty(prop.where)) {
586
608
  where = { $and: [where, prop.where] };
587
609
  }
588
610
  const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy, this.#em.getTransactionContext(), options2, pivotJoin);
589
611
  const children = [];
612
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
590
613
  for (let i = 0; i < filtered.length; i++) {
591
614
  const entity = filtered[i];
592
615
  const items = map[Utils.getPrimaryKeyHash(ids[i])].map(item => {
@@ -596,7 +619,11 @@ export class EntityLoader {
596
619
  schema: options.schema ?? this.#em.config.get('schema'),
597
620
  });
598
621
  }
599
- const entity = this.#em.getEntityFactory().create(prop.targetMeta.class, item, {
622
+ // Union-target items carry their concrete class via `constructor` — dispatch to the right factory call.
623
+ const targetClass = isUnionTargetMN && item.constructor !== Object
624
+ ? item.constructor
625
+ : prop.targetMeta.class;
626
+ const entity = this.#em.getEntityFactory().create(targetClass, item, {
600
627
  refresh,
601
628
  merge: true,
602
629
  convertCustomTypes: true,
@@ -604,7 +631,8 @@ export class EntityLoader {
604
631
  });
605
632
  return this.#em.getUnitOfWork().register(entity, item, { refresh, loaded: true });
606
633
  });
607
- entity[prop.name].hydrate(items, true);
634
+ const hasLimit = populate?.limit != null;
635
+ entity[prop.name].hydrate(items, true, hasLimit, hasLimit);
608
636
  children.push(items);
609
637
  }
610
638
  return children;
@@ -665,8 +693,11 @@ export class EntityLoader {
665
693
  }
666
694
  return ret;
667
695
  }, []);
668
- // we need to automatically select the FKs too, e.g. for 1:m relations to be able to wire them with the items
669
- if (prop.kind === ReferenceKind.ONE_TO_MANY || prop.kind === ReferenceKind.MANY_TO_MANY) {
696
+ // we need to automatically select the FKs too, e.g. for 1:m and inverse 1:1 relations
697
+ // to be able to wire them with the items
698
+ if (prop.kind === ReferenceKind.ONE_TO_MANY ||
699
+ prop.kind === ReferenceKind.MANY_TO_MANY ||
700
+ (prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner)) {
670
701
  const owner = prop.targetMeta.properties[prop.mappedBy];
671
702
  // when the owning FK is lazy, we need to explicitly select it even without user-provided fields,
672
703
  // otherwise the driver will exclude it and we won't be able to map children to their parent collections
@@ -679,7 +710,7 @@ export class EntityLoader {
679
710
  }
680
711
  return ret;
681
712
  }
682
- getChildReferences(entities, prop, options, ref) {
713
+ getChildReferences(entities, prop, options, ref, populate) {
683
714
  const filtered = this.filterCollections(entities, prop.name, options, ref);
684
715
  if (prop.kind === ReferenceKind.ONE_TO_MANY) {
685
716
  return filtered.map(e => e[prop.name].owner);
@@ -695,7 +726,7 @@ export class EntityLoader {
695
726
  return filtered;
696
727
  }
697
728
  // MANY_TO_ONE or ONE_TO_ONE
698
- return this.filterReferences(entities, prop.name, options, ref);
729
+ return this.filterReferences(entities, prop.name, options, ref, populate);
699
730
  }
700
731
  filterCollections(entities, field, options, ref) {
701
732
  if (options.refresh) {
@@ -709,7 +740,16 @@ export class EntityLoader {
709
740
  }
710
741
  const wrapped = helper(entity);
711
742
  if (!field.includes('.')) {
712
- return wrapped.__loadedProperties.has(field);
743
+ if (wrapped.__loadedProperties.has(field)) {
744
+ return true;
745
+ }
746
+ // Inlined embeddables track each leaf child separately, not the parent property,
747
+ // so we need to recurse for nested inline embeddables.
748
+ const prop = wrapped.__meta.properties[field];
749
+ if (prop?.kind === ReferenceKind.EMBEDDED && !prop.object) {
750
+ return Object.values(prop.embeddedProps).every(child => this.isPropertyLoaded(entity, child.name));
751
+ }
752
+ return false;
713
753
  }
714
754
  const [f, ...r] = field.split('.');
715
755
  /* v8 ignore next */
@@ -721,7 +761,7 @@ export class EntityLoader {
721
761
  }
722
762
  return this.isPropertyLoaded(entity[f], r.join('.'));
723
763
  }
724
- filterReferences(entities, field, options, ref) {
764
+ filterReferences(entities, field, options, ref, populate) {
725
765
  if (ref) {
726
766
  return [];
727
767
  }
@@ -730,13 +770,19 @@ export class EntityLoader {
730
770
  return children.map(e => Reference.unwrapReference(e[field]));
731
771
  }
732
772
  if (options.fields) {
773
+ // `:ref` populate children are satisfied by FK/PK and will be loaded lazily — they don't force a parent reload
774
+ const refChildren = new Set((populate?.children ?? [])
775
+ .map(c => c.field.split(':'))
776
+ .filter(parts => parts[1] === 'ref')
777
+ .map(parts => parts[0]));
733
778
  return children
734
779
  .map(e => Reference.unwrapReference(e[field]))
735
780
  .filter(target => {
736
781
  const wrapped = helper(target);
737
782
  const childFields = options.fields
738
783
  .filter(f => f.startsWith(`${field}.`))
739
- .map(f => f.substring(field.length + 1));
784
+ .map(f => f.substring(field.length + 1))
785
+ .filter(cf => !refChildren.has(cf.split('.')[0]));
740
786
  return !wrapped.__initialized || !childFields.every(cf => this.isPropertyLoaded(target, cf));
741
787
  });
742
788
  }
@@ -744,12 +790,39 @@ export class EntityLoader {
744
790
  .filter(e => !e[field].__helper.__initialized)
745
791
  .map(e => Reference.unwrapReference(e[field]));
746
792
  }
747
- filterByReferences(entities, field, refresh) {
793
+ filterByReferences(entities, prop, options, ref) {
748
794
  /* v8 ignore next */
749
- if (refresh) {
795
+ if (options.refresh) {
750
796
  return entities;
751
797
  }
752
- return entities.filter(e => e[field] !== null && !e[field]?.__helper?.__initialized);
798
+ const field = prop.name;
799
+ return entities.filter(e => {
800
+ const value = e[field];
801
+ if (value === null) {
802
+ return false;
803
+ }
804
+ if (value === undefined || !Utils.isEntity(value, true)) {
805
+ return true;
806
+ }
807
+ const target = Reference.unwrapReference(value);
808
+ const wrapped = helper(target);
809
+ if (!wrapped.__initialized) {
810
+ return true;
811
+ }
812
+ if (ref) {
813
+ return false;
814
+ }
815
+ if (options.fields) {
816
+ const childFields = options.fields
817
+ .filter(f => f.startsWith(`${prop.name}.`))
818
+ .map(f => f.substring(prop.name.length + 1));
819
+ return childFields.length > 0 && !childFields.every(f => this.isPropertyLoaded(target, f));
820
+ }
821
+ return prop.targetMeta.comparableProps.some(targetProp => {
822
+ const inlineEmbedded = targetProp.kind === ReferenceKind.EMBEDDED && !targetProp.object;
823
+ return !inlineEmbedded && !targetProp.lazy && !wrapped.__loadedProperties.has(targetProp.name);
824
+ });
825
+ });
753
826
  }
754
827
  lookupAllRelationships(entityName) {
755
828
  const ret = [];
@@ -1,8 +1,8 @@
1
1
  import type { PopulatePath } from '../enums.js';
2
2
  import type { CreateOptions, EntityManager, MergeOptions } from '../EntityManager.js';
3
3
  import type { AssignOptions } from './EntityAssigner.js';
4
- import type { EntityData, EntityName, Primary, Loaded, FilterQuery, EntityDictionary, AutoPath, RequiredEntityData, Ref, EntityType, EntityDTO, MergeSelected, FromEntityType, IsSubset, MergeLoaded, ArrayElement } from '../typings.js';
5
- import type { CountOptions, DeleteOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from '../drivers/IDatabaseDriver.js';
4
+ import type { Dictionary, EntityData, EntityDictionary, EntityKey, EntityName, FilterQuery, Loaded, Primary, AutoPath, RequiredEntityData, Ref, EntityType, EntityDTO, MergeSelected, FromEntityType, IsSubset, MergeLoaded, ArrayElement, IndexFilterQuery, WithUsingOptions } from '../typings.js';
5
+ import type { CountByOptions, CountOptions, DeleteOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from '../drivers/IDatabaseDriver.js';
6
6
  import type { EntityLoaderOptions } from './EntityLoader.js';
7
7
  import type { Cursor } from '../utils/Cursor.js';
8
8
  /** Repository class providing a type-safe API for querying and persisting a specific entity type. */
@@ -13,13 +13,17 @@ export declare class EntityRepository<Entity extends object> {
13
13
  /**
14
14
  * Finds first entity matching your `where` query.
15
15
  */
16
- findOne<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity>, options?: FindOneOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
16
+ findOne<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOneOptions<Entity, Hint, Fields, Excludes> & {
17
+ using?: Using | Using[];
18
+ }): Promise<Loaded<Entity, Hint, Fields, Excludes> | null>;
17
19
  /**
18
20
  * Finds first entity matching your `where` query. If nothing is found, it will throw an error.
19
21
  * You can override the factory for creating this method via `options.failHandler` locally
20
22
  * or via `Configuration.findOneOrFailHandler` globally.
21
23
  */
22
- findOneOrFail<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
24
+ findOneOrFail<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOneOrFailOptions<Entity, Hint, Fields, Excludes> & {
25
+ using?: Using | Using[];
26
+ }): Promise<Loaded<Entity, Hint, Fields, Excludes>>;
23
27
  /**
24
28
  * Creates or updates the entity, based on whether it is already present in the database.
25
29
  * This method performs an `insert on conflict merge` query ensuring the database is in sync, returning a managed
@@ -72,24 +76,28 @@ export declare class EntityRepository<Entity extends object> {
72
76
  /**
73
77
  * Finds all entities matching your `where` query. You can pass additional options via the `options` parameter.
74
78
  */
75
- find<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity>, options?: FindOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
79
+ find<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
80
+ using?: Using | Using[];
81
+ }): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
76
82
  /**
77
83
  * Calls `em.find()` and `em.count()` with the same arguments (where applicable) and returns the results as tuple
78
84
  * where first element is the array of entities, and the second is the count.
79
85
  */
80
- findAndCount<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(where: FilterQuery<Entity>, options?: FindOptions<Entity, Hint, Fields, Excludes>): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
86
+ findAndCount<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(where: [Using] extends [never] ? FilterQuery<Entity> : IndexFilterQuery<Entity, Using>, options?: FindOptions<Entity, Hint, Fields, Excludes> & {
87
+ using?: Using | Using[];
88
+ }): Promise<[Loaded<Entity, Hint, Fields, Excludes>[], number]>;
81
89
  /**
82
90
  * @inheritDoc EntityManager.findByCursor
83
91
  */
84
- findByCursor<Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true>(options: FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
92
+ findByCursor<Hint extends string = never, Fields extends string = never, Excludes extends string = never, IncludeCount extends boolean = true, Using extends string = never>(options: WithUsingOptions<FindByCursorOptions<Entity, Hint, Fields, Excludes, IncludeCount>, Entity, Using>): Promise<Cursor<Entity, Hint, Fields, Excludes, IncludeCount>>;
85
93
  /**
86
94
  * Finds all entities of given type. You can pass additional options via the `options` parameter.
87
95
  */
88
- findAll<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: FindAllOptions<Entity, Hint, Fields, Excludes>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
96
+ findAll<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(options?: WithUsingOptions<FindAllOptions<Entity, Hint, Fields, Excludes>, Entity, Using>): Promise<Loaded<Entity, Hint, Fields, Excludes>[]>;
89
97
  /**
90
98
  * @inheritDoc EntityManager.stream
91
99
  */
92
- stream<Hint extends string = never, Fields extends string = never, Excludes extends string = never>(options?: StreamOptions<Entity, Hint, Fields, Excludes>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
100
+ stream<Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(options?: WithUsingOptions<StreamOptions<Entity, Hint, Fields, Excludes>, Entity, Using>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
93
101
  /**
94
102
  * @inheritDoc EntityManager.insert
95
103
  */
@@ -199,6 +207,16 @@ export declare class EntityRepository<Entity extends object> {
199
207
  * Returns total number of entities matching your `where` query.
200
208
  */
201
209
  count<Hint extends string = never>(where?: FilterQuery<Entity>, options?: CountOptions<Entity, Hint>): Promise<number>;
210
+ /**
211
+ * Counts entities grouped by one or more properties.
212
+ *
213
+ * @example
214
+ * ```ts
215
+ * const counts = await repo.countBy('status');
216
+ * // { 'active': 5, 'inactive': 2 }
217
+ * ```
218
+ */
219
+ countBy(groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
202
220
  /** Returns the entity class name associated with this repository. */
203
221
  getEntityName(): string;
204
222
  /**
@@ -194,6 +194,18 @@ export class EntityRepository {
194
194
  async count(where = {}, options = {}) {
195
195
  return this.getEntityManager().count(this.entityName, where, options);
196
196
  }
197
+ /**
198
+ * Counts entities grouped by one or more properties.
199
+ *
200
+ * @example
201
+ * ```ts
202
+ * const counts = await repo.countBy('status');
203
+ * // { 'active': 5, 'inactive': 2 }
204
+ * ```
205
+ */
206
+ async countBy(groupBy, options) {
207
+ return this.getEntityManager().countBy(this.entityName, groupBy, options);
208
+ }
197
209
  /** Returns the entity class name associated with this repository. */
198
210
  getEntityName() {
199
211
  return Utils.className(this.entityName);
@@ -1,4 +1,4 @@
1
- import type { AddEager, AddOptional, Dictionary, EntityClass, EntityKey, EntityProperty, Loaded, LoadedReference, Primary, Ref } from '../typings.js';
1
+ import type { AddEager, AddOptional, Dictionary, EntityClass, EntityKey, EntityProperty, LazyRef, Loaded, LoadedReference, Primary, Ref } from '../typings.js';
2
2
  import type { FindOneOptions, FindOneOrFailOptions } from '../drivers/IDatabaseDriver.js';
3
3
  /** Wrapper around an entity that provides lazy loading capabilities and identity-preserving reference semantics. */
4
4
  export declare class Reference<T extends object> {
@@ -98,6 +98,47 @@ export declare function ref<I extends unknown | Ref<unknown> | undefined | null,
98
98
  * shortcut for `Reference.createFromPK(entityType, pk)`
99
99
  */
100
100
  export declare function ref<I extends unknown | undefined | null, T, PKV extends Primary<T> = Primary<T>>(entityType: EntityClass<T>, pk: I): Ref<T> | AddOptional<typeof pk>;
101
+ /**
102
+ * Unwraps a reference to its underlying value, returning the entity or scalar. Works on any of:
103
+ *
104
+ * - `Ref<T>` / `Reference<T>` (entity) — calls `.unwrap()` to extract the wrapped entity, returns `T`.
105
+ * - `LazyRef<T>` — type-only marker; runtime value is already `T`, so returned as-is.
106
+ * - plain entity `T` — returned as-is.
107
+ * - `ScalarReference<V>` / `ScalarRef<V>` — calls `.unwrap()` to extract the scalar value, returns `V | undefined`
108
+ * (a scalar reference may be bound without an initial value).
109
+ *
110
+ * Inverse of the {@link ref} helper. Use as a typed escape hatch when you know a relation is populated
111
+ * but don't want to (or can't) thread `Loaded<T, 'relation'>` through a function signature.
112
+ *
113
+ * ```ts
114
+ * function logAuthor(book: Book) {
115
+ * // `book.author` is typed as `LazyRef<Author>`, so `.name` is not directly accessible
116
+ * console.log(unref(book.author).name);
117
+ * }
118
+ *
119
+ * // Scalar reference:
120
+ * const scalar = ref('hello');
121
+ * unref(scalar); // 'hello' (type: string | undefined)
122
+ * ```
123
+ *
124
+ * Note: `unref` is a compile-time narrowing only for entity refs. On a `LazyRef<T>` or plain entity
125
+ * relation that is a stub (PK only, never populated), reads of non-PK properties will still return
126
+ * `undefined` at runtime — same footgun as a plain non-`Ref` relation.
127
+ */
128
+ export declare function unref<V>(value: ScalarReference<V>): V | undefined;
129
+ export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T): T;
130
+ /**
131
+ * Unwraps a reference to its underlying value. Nullable variant — returns `null` unchanged.
132
+ */
133
+ export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T | null): T | null;
134
+ /**
135
+ * Unwraps a reference to its underlying value. Optional variant — returns `undefined` unchanged.
136
+ */
137
+ export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T | undefined): T | undefined;
138
+ /**
139
+ * Unwraps a reference to its underlying value. Nullable variant — returns `null`/`undefined` unchanged.
140
+ */
141
+ export declare function unref<T extends object>(value: Ref<T> | LazyRef<T> | T | null | undefined): T | null | undefined;
101
142
  /**
102
143
  * shortcut for `Reference.createNakedFromPK(entityType, pk)`
103
144
  */
@@ -303,6 +303,15 @@ export function ref(entityOrType, pk) {
303
303
  }
304
304
  return Reference.createFromPK(entityOrType, pk);
305
305
  }
306
+ export function unref(value) {
307
+ if (value == null) {
308
+ return value;
309
+ }
310
+ if (ScalarReference.isScalarReference(value)) {
311
+ return value.unwrap();
312
+ }
313
+ return Reference.unwrapReference(value);
314
+ }
306
315
  /**
307
316
  * shortcut for `Reference.createNakedFromPK(entityType, pk)`
308
317
  */