@mikro-orm/core 7.1.0-dev.3 → 7.1.0-dev.30

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 (54) hide show
  1. package/EntityManager.d.ts +44 -11
  2. package/EntityManager.js +139 -31
  3. package/drivers/IDatabaseDriver.d.ts +39 -1
  4. package/entity/BaseEntity.d.ts +68 -1
  5. package/entity/BaseEntity.js +18 -0
  6. package/entity/Collection.d.ts +6 -3
  7. package/entity/Collection.js +15 -4
  8. package/entity/EntityFactory.js +20 -1
  9. package/entity/EntityLoader.d.ts +3 -1
  10. package/entity/EntityLoader.js +39 -16
  11. package/entity/EntityRepository.d.ts +27 -9
  12. package/entity/EntityRepository.js +12 -0
  13. package/entity/Reference.d.ts +42 -1
  14. package/entity/Reference.js +9 -0
  15. package/entity/defineEntity.d.ts +105 -23
  16. package/entity/defineEntity.js +17 -6
  17. package/entity/utils.js +4 -5
  18. package/errors.d.ts +2 -0
  19. package/errors.js +4 -0
  20. package/index.d.ts +2 -2
  21. package/index.js +1 -1
  22. package/metadata/EntitySchema.js +3 -0
  23. package/metadata/MetadataDiscovery.d.ts +12 -0
  24. package/metadata/MetadataDiscovery.js +185 -16
  25. package/metadata/MetadataValidator.d.ts +24 -0
  26. package/metadata/MetadataValidator.js +202 -1
  27. package/metadata/types.d.ts +77 -4
  28. package/naming-strategy/AbstractNamingStrategy.d.ts +1 -1
  29. package/naming-strategy/NamingStrategy.d.ts +1 -1
  30. package/package.json +1 -1
  31. package/platforms/Platform.d.ts +8 -1
  32. package/platforms/Platform.js +40 -4
  33. package/serialization/EntitySerializer.js +2 -1
  34. package/typings.d.ts +176 -18
  35. package/typings.js +22 -5
  36. package/unit-of-work/UnitOfWork.js +3 -0
  37. package/utils/AbstractMigrator.d.ts +20 -5
  38. package/utils/AbstractMigrator.js +263 -28
  39. package/utils/AbstractSchemaGenerator.d.ts +1 -1
  40. package/utils/AbstractSchemaGenerator.js +4 -1
  41. package/utils/Configuration.d.ts +25 -0
  42. package/utils/Configuration.js +1 -0
  43. package/utils/DataloaderUtils.d.ts +10 -1
  44. package/utils/DataloaderUtils.js +78 -0
  45. package/utils/EntityComparator.js +1 -1
  46. package/utils/QueryHelper.d.ts +16 -0
  47. package/utils/QueryHelper.js +15 -0
  48. package/utils/Utils.js +1 -1
  49. package/utils/fs-utils.d.ts +2 -0
  50. package/utils/fs-utils.js +7 -1
  51. package/utils/index.d.ts +1 -0
  52. package/utils/index.js +1 -0
  53. package/utils/partition-utils.d.ts +17 -0
  54. package/utils/partition-utils.js +79 -0
@@ -79,10 +79,11 @@ export class Collection {
79
79
  /**
80
80
  * Gets the count of collection items from database instead of counting loaded items.
81
81
  * The value is cached (unless you use the `where` option), use `refresh: true` to force reload it.
82
+ * When the dataloader is enabled (globally or per-query), multiple calls are batched into a single grouped query.
82
83
  */
83
84
  async loadCount(options = {}) {
84
85
  options = typeof options === 'boolean' ? { refresh: options } : options;
85
- const { refresh, where, ...countOptions } = options;
86
+ const { refresh, where, dataloader, ...countOptions } = options;
86
87
  if (!refresh && !where && this.#count != null) {
87
88
  return this.#count;
88
89
  }
@@ -92,8 +93,15 @@ export class Collection {
92
93
  this.property.owner) {
93
94
  return (this.#count = this.length);
94
95
  }
95
- const cond = this.createLoadCountCondition(where ?? {});
96
- const count = await em.count(this.property.targetMeta.class, cond, countOptions);
96
+ let count;
97
+ if (dataloader ?? [DataloaderType.ALL, DataloaderType.COLLECTION].includes(em.config.getDataloaderType())) {
98
+ const loader = await em.getDataLoader('count');
99
+ count = await loader.load([this, { where, ...countOptions }]);
100
+ }
101
+ else {
102
+ const cond = this.createLoadCountCondition(where ?? {});
103
+ count = await em.count(this.property.targetMeta.class, cond, countOptions);
104
+ }
97
105
  if (!where) {
98
106
  this.#count = count;
99
107
  }
@@ -462,7 +470,7 @@ export class Collection {
462
470
  /**
463
471
  * @internal
464
472
  */
465
- hydrate(items, forcePropagate, partial) {
473
+ hydrate(items, forcePropagate, partial, readonly) {
466
474
  for (let i = 0; i < this.#items.size; i++) {
467
475
  delete this[i];
468
476
  }
@@ -472,6 +480,9 @@ export class Collection {
472
480
  this.#count = 0;
473
481
  this.add(items);
474
482
  this.takeSnapshot(forcePropagate);
483
+ if (readonly) {
484
+ this.#readonly = true;
485
+ }
475
486
  }
476
487
  /**
477
488
  * Remove all items from the collection. Note that removing items from collection does not necessarily imply deleting the target entity,
@@ -316,11 +316,30 @@ export class EntityFactory {
316
316
  else if (!onCreateOnly && prop.default != null && !isRaw(prop.default) && entity[prop.name] === undefined) {
317
317
  entity[prop.name] = prop.default;
318
318
  }
319
+ else if (!onCreateOnly &&
320
+ this.#config.get('initNullableProperties') &&
321
+ prop.nullable &&
322
+ prop.default == null &&
323
+ !prop.defaultRaw &&
324
+ entity[prop.name] === undefined) {
325
+ entity[prop.name] = (this.#config.get('forceUndefined') ? undefined : null);
326
+ }
319
327
  if (prop.kind === ReferenceKind.EMBEDDED && entity[prop.name]) {
320
328
  const items = prop.array ? entity[prop.name] : [entity[prop.name]];
321
329
  for (const item of items) {
322
330
  // Embedded sub-properties need all defaults since the DB can't apply them within JSON columns.
323
- this.assignDefaultValues(item, prop.targetMeta);
331
+ // For polymorphic embeddables, resolve the actual subtype to avoid setting
332
+ // properties from other subtypes (e.g. Cat's canMeow on a Dog instance).
333
+ let targetMeta = prop.targetMeta;
334
+ if (targetMeta.polymorphs && targetMeta.discriminatorColumn) {
335
+ const discValue = item[targetMeta.discriminatorColumn];
336
+ // eslint-disable-next-line eqeqeq
337
+ const resolved = targetMeta.polymorphs.find(m => m.discriminatorValue == discValue);
338
+ if (resolved) {
339
+ targetMeta = resolved;
340
+ }
341
+ }
342
+ this.assignDefaultValues(item, targetMeta);
324
343
  }
325
344
  }
326
345
  }
@@ -1,4 +1,4 @@
1
- import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, PopulateOptions } from '../typings.js';
1
+ import type { AnyEntity, AutoPath, ConnectionType, EntityName, EntityProperty, FilterQuery, PopulateHintOptions, PopulateOptions } from '../typings.js';
2
2
  import type { EntityManager } from '../EntityManager.js';
3
3
  import { LoadStrategy, type LockMode, type PopulateHint, PopulatePath, type QueryOrderMap } from '../enums.js';
4
4
  import type { FilterOptions } from '../drivers/IDatabaseDriver.js';
@@ -37,6 +37,8 @@ export interface EntityLoaderOptions<Entity, Fields extends string = never, Excl
37
37
  connectionType?: ConnectionType;
38
38
  /** Logging options for the query. */
39
39
  logging?: LoggingOptions;
40
+ /** Per-relation populate overrides (limit, offset, orderBy). */
41
+ populateHints?: Record<string, PopulateHintOptions>;
40
42
  }
41
43
  /** Responsible for batch-loading entity relations using either select-in or joined loading strategies. */
42
44
  export declare class EntityLoader {
@@ -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,7 +314,7 @@ 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
  }
@@ -375,7 +378,7 @@ export class EntityLoader {
375
378
  where = { $and: [where, prop.where] };
376
379
  }
377
380
  const orderBy = QueryHelper.mergeOrderBy(options.orderBy, prop.orderBy);
378
- const items = await this.#em.find(meta.class, where, {
381
+ const findOptions = {
379
382
  filters,
380
383
  convertCustomTypes,
381
384
  lockMode,
@@ -394,7 +397,15 @@ export class EntityLoader {
394
397
  refresh: refresh && !children.every(item => options.visited.has(item)),
395
398
  // @ts-ignore not a public option, will be propagated to the populate call
396
399
  visited: options.visited,
397
- });
400
+ };
401
+ if (populate.limit != null) {
402
+ findOptions._partitionLimit = {
403
+ partitionBy: fk,
404
+ limit: populate.limit,
405
+ offset: populate.offset,
406
+ };
407
+ }
408
+ const items = await this.#em.find(meta.class, where, findOptions);
398
409
  // For targetKey relations, wire up loaded entities to parent references
399
410
  // This is needed because the references were created under alternate key,
400
411
  // but loaded entities are stored under PK, so they don't automatically merge
@@ -582,11 +593,18 @@ export class EntityLoader {
582
593
  const options2 = { ...options, fields, exclude, populateFilter };
583
594
  ['limit', 'offset', 'first', 'last', 'before', 'after', 'overfetch'].forEach(prop => delete options2[prop]);
584
595
  options2.populate = populate?.children ?? [];
596
+ if (populate?.limit != null) {
597
+ options2._partitionLimit = {
598
+ limit: populate.limit,
599
+ offset: populate.offset,
600
+ };
601
+ }
585
602
  if (!Utils.isEmpty(prop.where)) {
586
603
  where = { $and: [where, prop.where] };
587
604
  }
588
605
  const map = await this.#driver.loadFromPivotTable(prop, ids, where, orderBy, this.#em.getTransactionContext(), options2, pivotJoin);
589
606
  const children = [];
607
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(prop);
590
608
  for (let i = 0; i < filtered.length; i++) {
591
609
  const entity = filtered[i];
592
610
  const items = map[Utils.getPrimaryKeyHash(ids[i])].map(item => {
@@ -596,7 +614,11 @@ export class EntityLoader {
596
614
  schema: options.schema ?? this.#em.config.get('schema'),
597
615
  });
598
616
  }
599
- const entity = this.#em.getEntityFactory().create(prop.targetMeta.class, item, {
617
+ // Union-target items carry their concrete class via `constructor` — dispatch to the right factory call.
618
+ const targetClass = isUnionTargetMN && item.constructor !== Object
619
+ ? item.constructor
620
+ : prop.targetMeta.class;
621
+ const entity = this.#em.getEntityFactory().create(targetClass, item, {
600
622
  refresh,
601
623
  merge: true,
602
624
  convertCustomTypes: true,
@@ -604,7 +626,8 @@ export class EntityLoader {
604
626
  });
605
627
  return this.#em.getUnitOfWork().register(entity, item, { refresh, loaded: true });
606
628
  });
607
- entity[prop.name].hydrate(items, true);
629
+ const hasLimit = populate?.limit != null;
630
+ entity[prop.name].hydrate(items, true, hasLimit, hasLimit);
608
631
  children.push(items);
609
632
  }
610
633
  return children;
@@ -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
  */