@mikro-orm/core 7.1.0-dev.7 → 7.1.0-dev.9

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.
@@ -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, IndexFilterQuery, WithUsingOptions } from './typings.js';
9
+ import type { CountByOptions, CountOptions, DeleteOptions, FilterOptions, FindAllOptions, FindByCursorOptions, FindOneOptions, FindOneOrFailOptions, FindOptions, GetReferenceOptions, IDatabaseDriver, LockOptions, NativeInsertUpdateOptions, StreamOptions, UpdateOptions, UpsertManyOptions, UpsertOptions } from './drivers/IDatabaseDriver.js';
10
+ import type { AnyString, ArrayElement, AutoPath, ConnectionType, Dictionary, EntityClass, EntityData, EntityDictionary, EntityDTO, EntityKey, EntityMetadata, EntityName, FilterDef, FilterQuery, FromEntityType, GetRepository, IHydrator, IsSubset, Loaded, MergeLoaded, MergeSelected, ObjectQuery, PopulateOptions, Primary, Ref, RequiredEntityData, 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';
@@ -441,6 +441,27 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
441
441
  * Returns total number of entities matching your `where` query.
442
442
  */
443
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>>;
444
465
  /**
445
466
  * Tells the EntityManager to make an instance managed and persistent.
446
467
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -543,7 +564,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
543
564
  * some additional lazy properties, if so, we reload and merge the data from database
544
565
  */
545
566
  protected shouldRefresh<T extends object, P extends string = never, F extends string = never, E extends string = never>(meta: EntityMetadata<T>, entity: T, options: FindOneOptions<T, P, F, E>): boolean;
546
- protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any>): void;
567
+ protected prepareOptions(options: FindOptions<any, any, any, any> | FindOneOptions<any, any, any, any> | CountOptions<any, any> | CountByOptions<any>): void;
547
568
  /**
548
569
  * @internal
549
570
  */
@@ -585,7 +606,7 @@ export declare class EntityManager<Driver extends IDatabaseDriver = IDatabaseDri
585
606
  */
586
607
  set schema(schema: string | null | undefined);
587
608
  /** @internal */
588
- getDataLoader(type: 'ref' | '1:m' | 'm:n'): Promise<any>;
609
+ getDataLoader(type: 'ref' | '1:m' | 'm:n' | 'count'): Promise<any>;
589
610
  /**
590
611
  * Returns the ID of this EntityManager. Respects the context, so global EM will give you the contextual ID
591
612
  * if executed inside request context handler.
package/EntityManager.js CHANGED
@@ -1458,6 +1458,29 @@ export class EntityManager {
1458
1458
  await em.storeCache(options.cache, cached, () => +count);
1459
1459
  return +count;
1460
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
+ }
1461
1484
  /**
1462
1485
  * Tells the EntityManager to make an instance managed and persistent.
1463
1486
  * The entity will be entered into the database at or before transaction commit or as a result of the flush operation.
@@ -2031,6 +2054,8 @@ export class EntityManager {
2031
2054
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getColBatchLoadFn(em)));
2032
2055
  case 'm:n':
2033
2056
  return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getManyToManyColBatchLoadFn(em)));
2057
+ case 'count':
2058
+ return (em.#loaders[type] ??= new DataLoader(DataloaderUtils.getCountBatchLoadFn(em)));
2034
2059
  }
2035
2060
  }
2036
2061
  /**
@@ -325,6 +325,16 @@ export interface CountOptions<T extends object, P extends string = never> {
325
325
  /** @internal used to apply filters to the auto-joined relations */
326
326
  em?: EntityManager;
327
327
  }
328
+ /** Options for `em.countBy()` queries. */
329
+ export interface CountByOptions<T extends object> {
330
+ where?: FilterQuery<T>;
331
+ filters?: FilterOptions;
332
+ having?: FilterQuery<T>;
333
+ schema?: string;
334
+ flushMode?: FlushMode | `${FlushMode}`;
335
+ loggerContext?: LogContext;
336
+ logging?: LoggingOptions;
337
+ }
328
338
  /** Options for `em.qb().update()` operations. */
329
339
  export interface UpdateOptions<T> {
330
340
  filters?: FilterOptions;
@@ -37,6 +37,7 @@ export declare class Collection<T extends object, O extends object = object> {
37
37
  /**
38
38
  * Gets the count of collection items from database instead of counting loaded items.
39
39
  * The value is cached (unless you use the `where` option), use `refresh: true` to force reload it.
40
+ * When the dataloader is enabled (globally or per-query), multiple calls are batched into a single grouped query.
40
41
  */
41
42
  loadCount(options?: LoadCountOptions<T> | boolean): Promise<number>;
42
43
  /** Queries a subset of the collection items from the database with custom filtering, ordering, and pagination. */
@@ -193,4 +194,6 @@ export interface LoadCountOptions<T extends object> extends CountOptions<T, '*'>
193
194
  refresh?: boolean;
194
195
  /** Additional filtering conditions for the count query. */
195
196
  where?: FilterQuery<T>;
197
+ /** Whether to use the dataloader for batching count operations. */
198
+ dataloader?: boolean;
196
199
  }
@@ -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
  }
@@ -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, IndexFilterQuery, WithUsingOptions } 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. */
@@ -207,6 +207,16 @@ export declare class EntityRepository<Entity extends object> {
207
207
  * Returns total number of entities matching your `where` query.
208
208
  */
209
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>>;
210
220
  /** Returns the entity class name associated with this repository. */
211
221
  getEntityName(): string;
212
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,6 +1,6 @@
1
1
  import type { EntityManager } from '../EntityManager.js';
2
2
  import type { ColumnType, PropertyOptions, ReferenceOptions, EnumOptions, EmbeddedOptions, ManyToOneOptions, OneToManyOptions, OneToOneOptions, ManyToManyOptions, IndexColumnOptions } from '../metadata/types.js';
3
- import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, IndexCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints } from '../typings.js';
3
+ import type { AnyString, GeneratedColumnCallback, Constructor, CheckCallback, FilterQuery, EntityName, Dictionary, EntityMetadata, PrimaryKeyProp, EntityRepositoryType, Hidden, Opt, Primary, EntityClass, EntitySchemaWithMeta, InferEntity, MaybeReturnType, Ref, IndexCallback, TriggerCallback, FormulaCallback, EntityCtor, IsNever, IWrappedEntity, DefineConfig, Config, MaybePromise, IndexHints } from '../typings.js';
4
4
  import type { Raw } from '../utils/RawQueryFragment.js';
5
5
  import type { ScalarReference } from './Reference.js';
6
6
  import type { SerializeOptions } from '../serialization/EntitySerializer.js';
@@ -576,7 +576,7 @@ export type PropertyBuilders = {
576
576
  /** Own keys + base entity keys (when TBase is not `never`). Guards against `keyof never = string | number | symbol`. */
577
577
  type AllKeys<TProperties, TBase> = keyof TProperties | (IsNever<TBase> extends true ? never : keyof TBase);
578
578
  /** Metadata descriptor for `defineEntity()`, combining entity options with property definitions. */
579
- export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'repository' | 'filters' | 'orderBy'> {
579
+ export interface EntityMetadataWithProperties<TName extends string, TTableName extends string, TProperties extends Record<string, any>, TPK extends (keyof TProperties)[] | undefined = undefined, TBase = never, TRepository = never, TForceObject extends boolean = false> extends Omit<Partial<EntityMetadata<InferEntityFromProperties<TProperties, TPK, TBase, TRepository>>>, 'properties' | 'extends' | 'primaryKeys' | 'hooks' | 'discriminatorColumn' | 'versionProperty' | 'concurrencyCheckKeys' | 'serializedPrimaryKey' | 'indexes' | 'uniques' | 'triggers' | 'repository' | 'filters' | 'orderBy'> {
580
580
  name: TName;
581
581
  tableName?: TTableName;
582
582
  extends?: {
@@ -629,6 +629,15 @@ export interface EntityMetadataWithProperties<TName extends string, TTableName e
629
629
  fillFactor?: number;
630
630
  disabled?: boolean;
631
631
  }[];
632
+ triggers?: {
633
+ name?: string;
634
+ timing: 'before' | 'after' | 'instead of';
635
+ events: ('insert' | 'update' | 'delete' | 'truncate')[];
636
+ forEach?: 'row' | 'statement';
637
+ body?: string | Raw | TriggerCallback<InferEntityFromProperties<TProperties, TPK, TBase>>;
638
+ when?: string;
639
+ expression?: string;
640
+ }[];
632
641
  }
633
642
  /** Defines an entity schema using property builders, with full type inference from the property definitions. */
634
643
  export declare function defineEntity<const TName extends string, const TTableName extends string, const TProperties extends Record<string, any>, const TPK extends (keyof TProperties)[] | undefined = undefined, const TBase = never, const TRepository = never, const TForceObject extends boolean = false>(meta: EntityMetadataWithProperties<TName, TTableName, TProperties, TPK, TBase, TRepository, TForceObject>): EntitySchemaWithMeta<TName, TTableName, InferEntityFromProperties<TProperties, TPK, TBase, TRepository, TForceObject>, TBase, TProperties>;
package/errors.d.ts CHANGED
@@ -74,6 +74,8 @@ export declare class MetadataError<T extends AnyEntity = AnyEntity> extends Vali
74
74
  static viewEntityWithoutExpression(meta: EntityMetadata): MetadataError;
75
75
  static mixedInheritanceStrategies(root: EntityMetadata, child: EntityMetadata): MetadataError;
76
76
  static tptNotSupportedByDriver(meta: EntityMetadata): MetadataError;
77
+ /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
78
+ static triggersNotSupportedByDriver(meta: EntityMetadata): MetadataError;
77
79
  private static fromMessage;
78
80
  }
79
81
  /** Error thrown when an entity lookup fails to find the expected result. */
package/errors.js CHANGED
@@ -243,6 +243,10 @@ export class MetadataError extends ValidationError {
243
243
  static tptNotSupportedByDriver(meta) {
244
244
  return new MetadataError(`Entity ${meta.className} uses TPT (Table-Per-Type) inheritance which is not supported by the current driver. TPT requires SQL JOINs and is only available with SQL drivers.`);
245
245
  }
246
+ /** Thrown when database triggers are defined on an entity using a driver that does not support them. */
247
+ static triggersNotSupportedByDriver(meta) {
248
+ return new MetadataError(`Entity ${meta.className} defines database triggers which are not supported by the current driver. Triggers are only available with SQL drivers.`);
249
+ }
246
250
  static fromMessage(meta, prop, message) {
247
251
  return new MetadataError(`${meta.className}.${prop.name} ${message}`);
248
252
  }
package/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  * @module core
4
4
  */
5
5
  export { EntityMetadata, PrimaryKeyProp, EntityRepositoryType, OptionalProps, EagerProps, HiddenProps, Config, EntityName, IndexHints, } from './typings.js';
6
- export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
6
+ export type { CompiledFunctions, Constructor, ConnectionType, Dictionary, Primary, IPrimaryKey, ObjectQuery, FilterQuery, IWrappedEntity, InferEntityName, EntityData, Highlighter, MaybePromise, AnyEntity, EntityClass, EntityProperty, PopulateOptions, Populate, Loaded, New, LoadedReference, LoadedCollection, IMigrator, IMigrationGenerator, MigratorEvent, GetRepository, MigrationObject, DeepPartial, PrimaryProperty, Cast, IsUnknown, EntityDictionary, EntityDTO, EntityDTOFlat, EntityDTOProp, SerializeDTO, MigrationDiff, GenerateOptions, FilterObject, IndexFilterQuery, ExtractIndexHints, IndexName, IndexColumns, WithUsingOptions, IMigrationRunner, IEntityGenerator, ISeedManager, SeederObject, IMigratorStorage, RequiredEntityData, CheckCallback, TriggerCallback, IndexCallback, FormulaCallback, FormulaTable, SchemaTable, SchemaColumns, SimpleColumnMeta, Rel, Ref, ScalarRef, EntityRef, ISchemaGenerator, MigrationInfo, MigrateOptions, MigrationResult, MigrationRow, EntityKey, EntityValue, EntityDataValue, FilterKey, EntityType, FromEntityType, Selected, IsSubset, EntityProps, ExpandProperty, ExpandScalar, FilterItemValue, ExpandQuery, Scalar, ExpandHint, FilterValue, MergeLoaded, MergeSelected, TypeConfig, AnyString, ClearDatabaseOptions, CreateSchemaOptions, EnsureDatabaseOptions, UpdateSchemaOptions, DropSchemaOptions, RefreshDatabaseOptions, AutoPath, UnboxArray, MetadataProcessor, ImportsResolver, RequiredNullable, DefineConfig, Opt, Hidden, EntitySchemaWithMeta, InferEntity, CheckConstraint, TriggerDef, GeneratedColumnCallback, FilterDef, EntityCtor, Subquery, PopulateHintOptions, Prefixes, } from './typings.js';
7
7
  export * from './enums.js';
8
8
  export * from './errors.js';
9
9
  export * from './exceptions.js';
@@ -115,6 +115,7 @@ export declare class MetadataDiscovery {
115
115
  private initAutoincrement;
116
116
  private createSchemaTable;
117
117
  private initCheckConstraints;
118
+ private initTriggers;
118
119
  private initGeneratedColumn;
119
120
  private getDefaultVersionValue;
120
121
  private inferDefaultValue;
@@ -159,6 +159,7 @@ export class MetadataDiscovery {
159
159
  forEachProp((m, p) => this.initGeneratedColumn(m, p));
160
160
  filtered.forEach(meta => this.initAutoincrement(meta)); // once again after we init custom types
161
161
  filtered.forEach(meta => this.initCheckConstraints(meta));
162
+ filtered.forEach(meta => this.initTriggers(meta));
162
163
  forEachProp((_m, p) => {
163
164
  this.initDefaultValue(p);
164
165
  this.inferTypeFromDefault(p);
@@ -1021,6 +1022,7 @@ export class MetadataDiscovery {
1021
1022
  meta.indexes = Utils.unique([...base.indexes, ...meta.indexes]);
1022
1023
  meta.uniques = Utils.unique([...base.uniques, ...meta.uniques]);
1023
1024
  meta.checks = Utils.unique([...base.checks, ...meta.checks]);
1025
+ meta.triggers = Utils.unique([...base.triggers, ...meta.triggers]);
1024
1026
  const pks = Object.values(meta.properties)
1025
1027
  .filter(p => p.primary)
1026
1028
  .map(p => p.name);
@@ -1584,6 +1586,27 @@ export class MetadataDiscovery {
1584
1586
  }
1585
1587
  }
1586
1588
  }
1589
+ initTriggers(meta) {
1590
+ if (meta.triggers.length === 0) {
1591
+ return;
1592
+ }
1593
+ const columns = meta.createSchemaColumnMappingObject();
1594
+ const table = this.createSchemaTable(meta);
1595
+ for (const trigger of meta.triggers) {
1596
+ if (trigger.body && trigger.expression) {
1597
+ throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} defines both 'body' and 'expression'. Use one or the other.`);
1598
+ }
1599
+ if (!trigger.body && !trigger.expression) {
1600
+ throw new MetadataError(`Trigger "${trigger.name ?? '(unnamed)'}" on entity ${meta.className} must define either 'body' or 'expression'.`);
1601
+ }
1602
+ trigger.name ??= this.#namingStrategy.indexName(meta.tableName, trigger.events, 'trigger');
1603
+ trigger.forEach ??= 'row';
1604
+ if (trigger.body instanceof Function) {
1605
+ trigger.body = trigger.body(columns, table);
1606
+ }
1607
+ }
1608
+ meta.hasTriggers = true;
1609
+ }
1587
1610
  initGeneratedColumn(meta, prop) {
1588
1611
  if (!prop.generated && prop.columnTypes) {
1589
1612
  const match = /(.*) generated always as (.*)/i.exec(prop.columnTypes[0]);
@@ -1,4 +1,4 @@
1
- import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw } from '../typings.js';
1
+ import type { AnyEntity, Constructor, EntityName, AnyString, CheckCallback, GeneratedColumnCallback, FormulaCallback, FilterQuery, Dictionary, AutoPath, EntityClass, IndexCallback, ObjectQuery, Raw, TriggerDef } from '../typings.js';
2
2
  import type { Cascade, LoadStrategy, DeferMode, QueryOrderMap, EmbeddedPrefixMode } from '../enums.js';
3
3
  import type { Type, types } from '../types/index.js';
4
4
  import type { EntityManager } from '../EntityManager.js';
@@ -57,6 +57,8 @@ export type EntityOptions<T, E = T extends EntityClass<infer P> ? P : T> = {
57
57
  };
58
58
  /** Used to make ORM aware of externally defined triggers. This is needed for MS SQL Server multi inserts, ignored in other dialects. */
59
59
  hasTriggers?: boolean;
60
+ /** Database triggers to create for this entity's table. (SQL drivers only) */
61
+ triggers?: TriggerDef<E>[];
60
62
  /** SQL query that maps to a {@doclink virtual-entities | virtual entity}, or for view entities, the view definition. */
61
63
  expression?: string | ((em: any, where: ObjectQuery<E>, options: FindOptions<E, any, any, any>, stream?: boolean) => object);
62
64
  /** Set {@doclink repositories#custom-repository | custom repository class}. */
@@ -4,7 +4,7 @@ import { type ReferenceKind } from '../enums.js';
4
4
  export declare abstract class AbstractNamingStrategy implements NamingStrategy {
5
5
  getClassName(file: string, separator?: string): string;
6
6
  classToMigrationName(timestamp: string, customMigrationName?: string): string;
7
- indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
7
+ indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
8
8
  /**
9
9
  * @inheritDoc
10
10
  */
@@ -75,7 +75,7 @@ export interface NamingStrategy {
75
75
  /**
76
76
  * Returns key/constraint name for the given type. Some drivers might not support all the types (e.g. mysql and sqlite enforce the PK name).
77
77
  */
78
- indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default'): string;
78
+ indexName(tableName: string, columns: string[], type: 'primary' | 'foreign' | 'unique' | 'index' | 'sequence' | 'check' | 'default' | 'trigger'): string;
79
79
  /**
80
80
  * Returns alias name for given entity. The alias needs to be unique across the query, which is by default
81
81
  * ensured via appended index parameter. It is optional to use it as long as you ensure it will be unique.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/core",
3
- "version": "7.1.0-dev.7",
3
+ "version": "7.1.0-dev.9",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
package/typings.d.ts CHANGED
@@ -656,6 +656,8 @@ export type IndexCallback<T> = (columns: Record<PropertyName<T>, string>, table:
656
656
  export type FormulaCallback<T> = (columns: FormulaColumns<T>, table: FormulaTable) => string | Raw;
657
657
  /** Callback for CHECK constraint expressions. Receives column mappings and table info. */
658
658
  export type CheckCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
659
+ /** Callback for trigger body expressions. Receives column mappings and table info. */
660
+ export type TriggerCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
659
661
  /** Callback for generated (computed) column expressions. Receives column mappings and table info. */
660
662
  export type GeneratedColumnCallback<T> = (columns: Record<PropertyName<T>, string>, table: SchemaTable) => string | Raw;
661
663
  /** Definition of a CHECK constraint on a table or property. */
@@ -664,6 +666,23 @@ export interface CheckConstraint<T = any> {
664
666
  property?: string;
665
667
  expression: string | Raw | CheckCallback<T>;
666
668
  }
669
+ /** Definition of a database trigger on a table. */
670
+ export interface TriggerDef<T = any> {
671
+ /** Trigger name. Auto-generated if omitted. */
672
+ name?: string;
673
+ /** When the trigger fires relative to the event. */
674
+ timing: 'before' | 'after' | 'instead of';
675
+ /** Which DML events activate the trigger. */
676
+ events: ('insert' | 'update' | 'delete' | 'truncate')[];
677
+ /** Whether the trigger fires once per row or per statement. Defaults to `'row'`. */
678
+ forEach?: 'row' | 'statement';
679
+ /** SQL body of the trigger. Can be a string, Raw query, or callback receiving column name mappings. */
680
+ body?: string | Raw | TriggerCallback<T>;
681
+ /** Optional SQL WHEN condition for the trigger. */
682
+ when?: string;
683
+ /** Raw DDL escape hatch — full CREATE TRIGGER statement. Mutually exclusive with `body`. */
684
+ expression?: string;
685
+ }
667
686
  /** Branded string that accepts any string value while preserving autocompletion for known literals. */
668
687
  export type AnyString = string & {};
669
688
  /** Describes a single property (column, relation, or embedded) within an entity's metadata. */
@@ -883,6 +902,7 @@ export interface EntityMetadata<Entity = any, Class extends EntityCtor<Entity> =
883
902
  disabled?: boolean;
884
903
  }[];
885
904
  checks: CheckConstraint<Entity>[];
905
+ triggers: TriggerDef<Entity>[];
886
906
  repositoryClass?: string;
887
907
  repository: () => EntityClass<EntityRepository<any>>;
888
908
  hooks: {
package/typings.js CHANGED
@@ -48,6 +48,7 @@ export class EntityMetadata {
48
48
  this.indexes = [];
49
49
  this.uniques = [];
50
50
  this.checks = [];
51
+ this.triggers = [];
51
52
  this.referencingProperties = [];
52
53
  this.concurrencyCheckKeys = new Set();
53
54
  Object.assign(this, meta);
@@ -1,5 +1,5 @@
1
1
  import type { Constructor, Primary, Ref } from '../typings.js';
2
- import { Collection, type InitCollectionOptions } from '../entity/Collection.js';
2
+ import { Collection, type InitCollectionOptions, type LoadCountOptions } from '../entity/Collection.js';
3
3
  import { type EntityManager } from '../EntityManager.js';
4
4
  import { type LoadReferenceOptions } from '../entity/Reference.js';
5
5
  type BatchLoadFn<K, V> = (keys: readonly K[]) => PromiseLike<ArrayLike<V | Error>>;
@@ -44,6 +44,15 @@ export declare class DataloaderUtils {
44
44
  * makes one query per entity and maps each input collection to the corresponding result.
45
45
  */
46
46
  static getManyToManyColBatchLoadFn(em: EntityManager): BatchLoadFn<[Collection<any>, Omit<InitCollectionOptions<any, any>, 'dataloader'>?], any>;
47
+ /**
48
+ * Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
49
+ * by entity and relation, issues a single grouped count query per entity+options combination
50
+ * via `em.countBy()`, and maps each input collection to the corresponding count.
51
+ *
52
+ * For 1:M relations, groups by the FK property on the target entity.
53
+ * For M:N relations, groups by the owner FK on the pivot entity.
54
+ */
55
+ static getCountBatchLoadFn(em: EntityManager): BatchLoadFn<[Collection<any>, Omit<LoadCountOptions<any>, 'dataloader' | 'refresh'>?], number>;
47
56
  static getDataLoader(): Promise<Constructor<{
48
57
  load: (...args: unknown[]) => Promise<unknown>;
49
58
  }>>;
@@ -1,6 +1,7 @@
1
1
  import { Collection } from '../entity/Collection.js';
2
2
  import { helper } from '../entity/wrap.js';
3
3
  import { Reference } from '../entity/Reference.js';
4
+ import { ReferenceKind } from '../enums.js';
4
5
  import { Utils } from './Utils.js';
5
6
  export class DataloaderUtils {
6
7
  static DataLoader;
@@ -216,6 +217,83 @@ export class DataloaderUtils {
216
217
  return ret;
217
218
  };
218
219
  }
220
+ /**
221
+ * Returns the count dataloader batchLoadFn, which aggregates `Collection.loadCount()` calls
222
+ * by entity and relation, issues a single grouped count query per entity+options combination
223
+ * via `em.countBy()`, and maps each input collection to the corresponding count.
224
+ *
225
+ * For 1:M relations, groups by the FK property on the target entity.
226
+ * For M:N relations, groups by the owner FK on the pivot entity.
227
+ */
228
+ static getCountBatchLoadFn(em) {
229
+ return async (collsWithOpts) => {
230
+ const groups = new Map();
231
+ const keys = [];
232
+ for (const [col, opts] of collsWithOpts) {
233
+ const prop = col.property;
234
+ let fkProp;
235
+ let countByClass;
236
+ let targetFilterProp;
237
+ if (prop.kind === ReferenceKind.ONE_TO_MANY) {
238
+ fkProp = prop.mappedBy;
239
+ countByClass = prop.targetMeta.class;
240
+ }
241
+ else {
242
+ // M:N: group by the owner FK on the pivot entity
243
+ const pivotMeta = em.getMetadata().get(prop.pivotEntity);
244
+ const ownerPivotProp = pivotMeta.relations[prop.owner ? 0 : 1];
245
+ const targetPivotProp = pivotMeta.relations[prop.owner ? 1 : 0];
246
+ fkProp = ownerPivotProp.name;
247
+ countByClass = pivotMeta.class;
248
+ targetFilterProp = targetPivotProp.name;
249
+ }
250
+ // Include the owner-side uniqueName in the key so that two distinct owner entity types
251
+ // sharing a relation with the same property name (e.g. `Author.books` and `Publisher.books`)
252
+ // don't collide into a single batch that would use one owner's FK mapping for the other.
253
+ const ownerUniqueName = helper(col.owner).__meta.uniqueName;
254
+ const key = `${ownerUniqueName}.${prop.name}|${JSON.stringify(opts ?? {})}`;
255
+ keys.push(key);
256
+ let group = groups.get(key);
257
+ if (!group) {
258
+ group = { fkProp, countByClass, targetFilterProp, ownerPKs: new Map(), opts: opts ?? {} };
259
+ groups.set(key, group);
260
+ }
261
+ const pk = helper(col.owner).getPrimaryKey();
262
+ group.ownerPKs.set(JSON.stringify(pk), pk);
263
+ }
264
+ const promises = Array.from(groups.entries()).map(async ([key, group]) => {
265
+ const allPKs = Array.from(group.ownerPKs.values());
266
+ const { where, ...countOpts } = group.opts;
267
+ const conditions = [{ [group.fkProp]: { $in: allPKs } }];
268
+ if (where) {
269
+ conditions.push(where);
270
+ }
271
+ // For M:N, apply the target entity's filters through the pivot's target relation
272
+ if (group.targetFilterProp) {
273
+ const targetMeta = em.getMetadata().find(group.countByClass);
274
+ const targetRelMeta = targetMeta.properties[group.targetFilterProp]?.targetMeta;
275
+ if (targetRelMeta) {
276
+ const filterCond = await em.applyFilters(targetRelMeta.class, {}, countOpts.filters, 'read');
277
+ if (filterCond && Object.keys(filterCond).length > 0) {
278
+ conditions.push({ [group.targetFilterProp]: filterCond });
279
+ }
280
+ }
281
+ }
282
+ const fkWhere = conditions.length === 1 ? conditions[0] : { $and: conditions };
283
+ const counts = await em.countBy(group.countByClass, group.fkProp, {
284
+ where: fkWhere,
285
+ ...countOpts,
286
+ });
287
+ return [key, counts];
288
+ });
289
+ const resultsMap = new Map(await Promise.all(promises));
290
+ return collsWithOpts.map(([col], i) => {
291
+ const pk = helper(col.owner).getPrimaryKey();
292
+ const counts = resultsMap.get(keys[i]);
293
+ return counts?.[String(pk)] ?? 0;
294
+ });
295
+ };
296
+ }
219
297
  static async getDataLoader() {
220
298
  if (this.DataLoader) {
221
299
  return this.DataLoader;
package/utils/Utils.js CHANGED
@@ -141,7 +141,7 @@ export function parseJsonSafe(value) {
141
141
  /** Collection of general-purpose utility methods used throughout the ORM. */
142
142
  export class Utils {
143
143
  static PK_SEPARATOR = '~~~';
144
- static #ORM_VERSION = '7.1.0-dev.7';
144
+ static #ORM_VERSION = '7.1.0-dev.9';
145
145
  /**
146
146
  * Checks if the argument is instance of `Object`. Returns false for arrays.
147
147
  */