@mikro-orm/mongodb 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.
@@ -28,13 +28,13 @@ export declare class MongoConnection extends Connection {
28
28
  find<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, opts?: MongoFindOptions<T>): Promise<EntityData<T>[]>;
29
29
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, opts?: MongoFindOptions<T>): AsyncIterableIterator<T>;
30
30
  private _find;
31
- insertOne<T extends object>(entityName: EntityName<T>, data: Partial<T>, ctx?: Transaction<ClientSession>): Promise<QueryResult<T>>;
32
- insertMany<T extends object>(entityName: EntityName<T>, data: Partial<T>[], ctx?: Transaction<ClientSession>): Promise<QueryResult<T>>;
33
- updateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: Partial<T>, ctx?: Transaction<ClientSession>, upsert?: boolean, upsertOptions?: UpsertOptions<T>): Promise<QueryResult<T>>;
34
- bulkUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: Partial<T>[], ctx?: Transaction<ClientSession>, upsert?: boolean, upsertOptions?: UpsertManyOptions<T>): Promise<QueryResult<T>>;
35
- deleteMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, ctx?: Transaction<ClientSession>): Promise<QueryResult<T>>;
36
- aggregate<T extends object = any>(entityName: EntityName<T>, pipeline: any[], ctx?: Transaction<ClientSession>, loggerContext?: LoggingOptions): Promise<T[]>;
37
- streamAggregate<T extends object>(entityName: EntityName<T>, pipeline: any[], ctx?: Transaction<ClientSession>, loggerContext?: LoggingOptions, stream?: boolean): AsyncIterableIterator<T>;
31
+ insertOne<T extends object>(entityName: EntityName<T>, data: Partial<T>, ctx?: Transaction<ClientSession>, signal?: AbortSignal): Promise<QueryResult<T>>;
32
+ insertMany<T extends object>(entityName: EntityName<T>, data: Partial<T>[], ctx?: Transaction<ClientSession>, signal?: AbortSignal): Promise<QueryResult<T>>;
33
+ updateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: Partial<T>, ctx?: Transaction<ClientSession>, upsert?: boolean, upsertOptions?: UpsertOptions<T>, signal?: AbortSignal): Promise<QueryResult<T>>;
34
+ bulkUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: Partial<T>[], ctx?: Transaction<ClientSession>, upsert?: boolean, upsertOptions?: UpsertManyOptions<T>, signal?: AbortSignal): Promise<QueryResult<T>>;
35
+ deleteMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, ctx?: Transaction<ClientSession>, signal?: AbortSignal): Promise<QueryResult<T>>;
36
+ aggregate<T extends object = any>(entityName: EntityName<T>, pipeline: any[], ctx?: Transaction<ClientSession>, loggerContext?: LoggingOptions, signal?: AbortSignal): Promise<T[]>;
37
+ streamAggregate<T extends object>(entityName: EntityName<T>, pipeline: any[], ctx?: Transaction<ClientSession>, loggerContext?: LoggingOptions, stream?: boolean, signal?: AbortSignal): AsyncIterableIterator<T>;
38
38
  countDocuments<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, opts?: MongoCountOptions): Promise<number>;
39
39
  transactional<T>(cb: (trx: Transaction<ClientSession>) => Promise<T>, options?: {
40
40
  isolationLevel?: IsolationLevel;
@@ -61,6 +61,11 @@ export interface MongoQueryOptions {
61
61
  indexHint?: string | Dictionary;
62
62
  maxTimeMS?: number;
63
63
  allowDiskUse?: boolean;
64
+ /**
65
+ * Forwarded to the MongoDB driver. When fired, the driver aborts the in-flight operation
66
+ * by closing the underlying socket; the rejection reason is `signal.reason`.
67
+ */
68
+ signal?: AbortSignal;
64
69
  }
65
70
  /** Options for MongoDB find operations. */
66
71
  export interface MongoFindOptions<T extends object> extends MongoQueryOptions {
@@ -68,6 +73,7 @@ export interface MongoFindOptions<T extends object> extends MongoQueryOptions {
68
73
  limit?: number;
69
74
  offset?: number;
70
75
  fields?: string[];
76
+ chunkSize?: number;
71
77
  ctx?: Transaction<ClientSession>;
72
78
  loggerContext?: LoggingOptions;
73
79
  }
@@ -143,6 +143,12 @@ export class MongoConnection extends Connection {
143
143
  if (opts.allowDiskUse != null) {
144
144
  options.allowDiskUse = opts.allowDiskUse;
145
145
  }
146
+ if (opts.chunkSize != null) {
147
+ options.batchSize = opts.chunkSize;
148
+ }
149
+ if (opts.signal) {
150
+ options.signal = opts.signal;
151
+ }
146
152
  const resultSet = this.getCollection(entityName).find(where, options);
147
153
  let query = `db.getCollection('${collection}').find(${this.logObject(where)}, ${this.logObject(options)})`;
148
154
  const orderBy = Utils.asArray(opts.orderBy);
@@ -174,37 +180,43 @@ export class MongoConnection extends Connection {
174
180
  }
175
181
  return { cursor: resultSet, query };
176
182
  }
177
- async insertOne(entityName, data, ctx) {
178
- return this.runQuery('insertOne', entityName, data, undefined, ctx);
183
+ async insertOne(entityName, data, ctx, signal) {
184
+ return this.runQuery('insertOne', entityName, data, undefined, ctx, { signal });
179
185
  }
180
- async insertMany(entityName, data, ctx) {
181
- return this.runQuery('insertMany', entityName, data, undefined, ctx);
186
+ async insertMany(entityName, data, ctx, signal) {
187
+ return this.runQuery('insertMany', entityName, data, undefined, ctx, { signal });
182
188
  }
183
- async updateMany(entityName, where, data, ctx, upsert, upsertOptions) {
184
- return this.runQuery('updateMany', entityName, data, where, ctx, { upsert, upsertOptions });
189
+ async updateMany(entityName, where, data, ctx, upsert, upsertOptions, signal) {
190
+ return this.runQuery('updateMany', entityName, data, where, ctx, { upsert, upsertOptions, signal });
185
191
  }
186
- async bulkUpdateMany(entityName, where, data, ctx, upsert, upsertOptions) {
187
- return this.runQuery('bulkUpdateMany', entityName, data, where, ctx, { upsert, upsertOptions });
192
+ async bulkUpdateMany(entityName, where, data, ctx, upsert, upsertOptions, signal) {
193
+ return this.runQuery('bulkUpdateMany', entityName, data, where, ctx, { upsert, upsertOptions, signal });
188
194
  }
189
- async deleteMany(entityName, where, ctx) {
190
- return this.runQuery('deleteMany', entityName, undefined, where, ctx);
195
+ async deleteMany(entityName, where, ctx, signal) {
196
+ return this.runQuery('deleteMany', entityName, undefined, where, ctx, { signal });
191
197
  }
192
- async aggregate(entityName, pipeline, ctx, loggerContext) {
198
+ async aggregate(entityName, pipeline, ctx, loggerContext, signal) {
193
199
  await this.ensureConnection();
194
200
  const collection = this.getCollectionName(entityName);
195
201
  /* v8 ignore next */
196
202
  const options = ctx ? { session: ctx } : {};
203
+ if (signal) {
204
+ options.signal = signal;
205
+ }
197
206
  const query = `db.getCollection('${collection}').aggregate(${this.logObject(pipeline)}, ${this.logObject(options)}).toArray();`;
198
207
  const now = Date.now();
199
208
  const res = await this.getCollection(entityName).aggregate(pipeline, options).toArray();
200
209
  this.logQuery(query, { took: Date.now() - now, results: res.length, ...loggerContext });
201
210
  return res;
202
211
  }
203
- async *streamAggregate(entityName, pipeline, ctx, loggerContext, stream = false) {
212
+ async *streamAggregate(entityName, pipeline, ctx, loggerContext, stream = false, signal) {
204
213
  await this.ensureConnection();
205
214
  const collection = this.getCollectionName(entityName);
206
215
  /* v8 ignore next */
207
216
  const options = ctx ? { session: ctx } : {};
217
+ if (signal) {
218
+ options.signal = signal;
219
+ }
208
220
  const query = `db.getCollection('${collection}').aggregate(${this.logObject(pipeline)}, ${this.logObject(options)})};`;
209
221
  const cursor = this.getCollection(entityName).aggregate(pipeline, options);
210
222
  this.logQuery(query, { ...loggerContext });
@@ -216,6 +228,7 @@ export class MongoConnection extends Connection {
216
228
  collation: opts.collation,
217
229
  indexHint: opts.indexHint,
218
230
  maxTimeMS: opts.maxTimeMS,
231
+ signal: opts.signal,
219
232
  });
220
233
  }
221
234
  async transactional(cb, options = {}) {
@@ -262,13 +275,16 @@ export class MongoConnection extends Connection {
262
275
  }
263
276
  async runQuery(method, entityName, data, where, ctx, opts) {
264
277
  await this.ensureConnection();
265
- const { upsert, upsertOptions, loggerContext, collation, indexHint, maxTimeMS } = opts ?? {};
278
+ const { upsert, upsertOptions, loggerContext, collation, indexHint, maxTimeMS, signal } = opts ?? {};
266
279
  const collection = this.getCollectionName(entityName);
267
280
  const logger = this.config.getLogger();
268
281
  const options = ctx ? { session: ctx, upsert } : { upsert };
269
282
  if (options.upsert === undefined) {
270
283
  delete options.upsert;
271
284
  }
285
+ if (signal) {
286
+ options.signal = signal;
287
+ }
272
288
  const now = Date.now();
273
289
  let res;
274
290
  let query;
package/MongoDriver.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare class MongoDriver extends DatabaseDriver<MongoConnection> {
15
15
  rawResults?: boolean;
16
16
  }): AsyncIterableIterator<T>;
17
17
  find<T extends object, P extends string = never, F extends string = never, E extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOptions<T, P, F, E>): Promise<EntityData<T>[]>;
18
+ private findWithPartitionLimit;
18
19
  findOne<T extends object, P extends string = never, F extends string = never, E extends string = never>(entityName: EntityName<T>, where: FilterQuery<T>, options?: FindOneOptions<T, P, F, E>): Promise<EntityData<T> | null>;
19
20
  findVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: FindOptions<T, any, any, any>): Promise<EntityData<T>[]>;
20
21
  streamVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: FindOptions<T, any, any, any>): AsyncIterableIterator<EntityData<T>>;
@@ -26,12 +27,14 @@ export declare class MongoDriver extends DatabaseDriver<MongoConnection> {
26
27
  nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateOptions<T> & UpsertManyOptions<T>): Promise<QueryResult<T>>;
27
28
  nativeDelete<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options?: {
28
29
  ctx?: Transaction<ClientSession>;
30
+ signal?: AbortSignal;
29
31
  }): Promise<QueryResult<T>>;
30
- aggregate(entityName: EntityName, pipeline: any[], ctx?: Transaction<ClientSession>): Promise<any[]>;
31
- streamAggregate<T extends object>(entityName: EntityName<T>, pipeline: any[], ctx?: Transaction<ClientSession>): AsyncIterableIterator<T>;
32
+ aggregate(entityName: EntityName, pipeline: any[], ctx?: Transaction<ClientSession>, signal?: AbortSignal): Promise<any[]>;
33
+ streamAggregate<T extends object>(entityName: EntityName<T>, pipeline: any[], ctx?: Transaction<ClientSession>, signal?: AbortSignal): AsyncIterableIterator<T>;
32
34
  getPlatform(): MongoPlatform;
33
35
  private buildQueryOptions;
34
- private renameFields;
36
+ /** @internal */
37
+ renameFields<T extends object>(entityName: EntityName<T>, data: T, dotPaths?: boolean, object?: boolean, root?: boolean): T;
35
38
  private convertObjectIds;
36
39
  private buildFilterById;
37
40
  protected buildFields<T extends object, P extends string = never>(entityName: EntityName<T>, populate: PopulateOptions<T>[], fields?: readonly EntityField<T, P>[], exclude?: string[]): string[] | undefined;
package/MongoDriver.js CHANGED
@@ -30,6 +30,7 @@ export class MongoDriver extends DatabaseDriver {
30
30
  offset: options.offset,
31
31
  fields,
32
32
  ctx: options.ctx,
33
+ chunkSize: options.chunkSize ?? 100,
33
34
  ...this.buildQueryOptions(options),
34
35
  });
35
36
  for await (const item of res) {
@@ -78,6 +79,10 @@ export class MongoDriver extends DatabaseDriver {
78
79
  return res.map(r => this.mapResult(r, this.metadata.find(entityName)));
79
80
  }
80
81
  const orderBy = Utils.asArray(options.orderBy).map(orderBy => this.renameFields(entityName, orderBy, true));
82
+ const partitionLimit = options._partitionLimit;
83
+ if (partitionLimit) {
84
+ return this.findWithPartitionLimit(entityName, where, orderBy, fields ?? [], partitionLimit, options);
85
+ }
81
86
  const res = await this.rethrow(this.getConnection('read').find(entityName, where, {
82
87
  orderBy,
83
88
  limit: options.limit,
@@ -88,6 +93,48 @@ export class MongoDriver extends DatabaseDriver {
88
93
  }));
89
94
  return res.map(r => this.mapResult(r, this.metadata.find(entityName)));
90
95
  }
96
+ async findWithPartitionLimit(entityName, where, orderBy, fields, partitionLimit, options) {
97
+ const meta = this.metadata.find(entityName);
98
+ const { limit, offset = 0 } = partitionLimit;
99
+ // Resolve the partition property to its DB field name; callers always pass
100
+ // a declared property on the target meta (FK of the owning side).
101
+ const prop = meta.properties[partitionLimit.partitionBy];
102
+ const partitionField = prop.fieldNames[0];
103
+ const pipeline = [];
104
+ pipeline.push({ $match: where });
105
+ if (orderBy.length > 0) {
106
+ const sortSpec = {};
107
+ for (const order of orderBy) {
108
+ for (const [key, dir] of Object.entries(order)) {
109
+ sortSpec[key] = dir === 'ASC' || dir === 'asc' || dir === 1 ? 1 : -1;
110
+ }
111
+ }
112
+ pipeline.push({ $sort: sortSpec });
113
+ }
114
+ // $sort before $group ensures $push collects in correct order
115
+ pipeline.push({
116
+ $group: {
117
+ _id: `$${partitionField}`,
118
+ __docs: { $push: '$$ROOT' },
119
+ },
120
+ });
121
+ pipeline.push({
122
+ $project: {
123
+ __docs: { $slice: ['$__docs', offset, limit] },
124
+ },
125
+ });
126
+ pipeline.push({ $unwind: '$__docs' });
127
+ pipeline.push({ $replaceRoot: { newRoot: '$__docs' } });
128
+ if (fields.length > 0) {
129
+ const projection = {};
130
+ for (const field of fields) {
131
+ projection[field] = 1;
132
+ }
133
+ pipeline.push({ $project: projection });
134
+ }
135
+ const res = await this.rethrow(this.getConnection('read').aggregate(entityName, pipeline, options.ctx, options.logging, options.signal));
136
+ return res.map(r => this.mapResult(r, meta));
137
+ }
91
138
  async findOne(entityName, where, options = { populate: [], orderBy: {} }) {
92
139
  if (this.metadata.find(entityName)?.virtual) {
93
140
  const [item] = await this.findVirtual(entityName, where, options);
@@ -146,7 +193,7 @@ export class MongoDriver extends DatabaseDriver {
146
193
  async nativeInsert(entityName, data, options = {}) {
147
194
  this.handleVersionProperty(entityName, data);
148
195
  data = this.renameFields(entityName, data);
149
- return this.rethrow(this.getConnection('write').insertOne(entityName, data, options.ctx));
196
+ return this.rethrow(this.getConnection('write').insertOne(entityName, data, options.ctx, options.signal));
150
197
  }
151
198
  async nativeClone(entityName, where, overrides, options = {}) {
152
199
  const meta = this.metadata.find(entityName);
@@ -177,7 +224,7 @@ export class MongoDriver extends DatabaseDriver {
177
224
  const meta = this.metadata.find(entityName);
178
225
  /* v8 ignore next */
179
226
  const pk = meta?.getPrimaryProps()[0].fieldNames[0] ?? '_id';
180
- const res = await this.rethrow(this.getConnection('write').insertMany(entityName, data, options.ctx));
227
+ const res = await this.rethrow(this.getConnection('write').insertMany(entityName, data, options.ctx, options.signal));
181
228
  res.rows = res.insertedIds.map(id => ({ [pk]: id }));
182
229
  return res;
183
230
  }
@@ -201,7 +248,7 @@ export class MongoDriver extends DatabaseDriver {
201
248
  if (options.onConflictExcludeFields) {
202
249
  options.onConflictExcludeFields = options.onConflictExcludeFields.map(rename);
203
250
  }
204
- return this.rethrow(this.getConnection('write').updateMany(entityName, where, data, options.ctx, options.upsert, options));
251
+ return this.rethrow(this.getConnection('write').updateMany(entityName, where, data, options.ctx, options.upsert, options, options.signal));
205
252
  }
206
253
  async nativeUpdateMany(entityName, where, data, options = {}) {
207
254
  where = where.map(row => {
@@ -229,7 +276,7 @@ export class MongoDriver extends DatabaseDriver {
229
276
  }
230
277
  /* v8 ignore next */
231
278
  const pk = meta?.getPrimaryProps()[0].fieldNames[0] ?? '_id';
232
- const res = await this.rethrow(this.getConnection('write').bulkUpdateMany(entityName, where, data, options.ctx, options.upsert, options));
279
+ const res = await this.rethrow(this.getConnection('write').bulkUpdateMany(entityName, where, data, options.ctx, options.upsert, options, options.signal));
233
280
  if (res.insertedIds) {
234
281
  let i = 0;
235
282
  res.rows = where.map(cond => {
@@ -246,13 +293,13 @@ export class MongoDriver extends DatabaseDriver {
246
293
  where = this.buildFilterById(entityName, where);
247
294
  }
248
295
  where = this.renameFields(entityName, where, true);
249
- return this.rethrow(this.getConnection('write').deleteMany(entityName, where, options.ctx));
296
+ return this.rethrow(this.getConnection('write').deleteMany(entityName, where, options.ctx, options.signal));
250
297
  }
251
- async aggregate(entityName, pipeline, ctx) {
252
- return this.rethrow(this.getConnection('read').aggregate(entityName, pipeline, ctx));
298
+ async aggregate(entityName, pipeline, ctx, signal) {
299
+ return this.rethrow(this.getConnection('read').aggregate(entityName, pipeline, ctx, undefined, signal));
253
300
  }
254
- async *streamAggregate(entityName, pipeline, ctx) {
255
- yield* this.getConnection('read').streamAggregate(entityName, pipeline, ctx);
301
+ async *streamAggregate(entityName, pipeline, ctx, signal) {
302
+ yield* this.getConnection('read').streamAggregate(entityName, pipeline, ctx, undefined, false, signal);
256
303
  }
257
304
  getPlatform() {
258
305
  return this.platform;
@@ -268,14 +315,25 @@ export class MongoDriver extends DatabaseDriver {
268
315
  if (options.indexHint != null) {
269
316
  ret.indexHint = options.indexHint;
270
317
  }
318
+ else if (options.using != null) {
319
+ const names = Utils.asArray(options.using);
320
+ if (names.length > 1) {
321
+ throw new Error('MongoDB only supports a single index hint per query. Provide one index name instead of an array.');
322
+ }
323
+ ret.indexHint = names[0];
324
+ }
271
325
  if (options.maxTimeMS != null) {
272
326
  ret.maxTimeMS = options.maxTimeMS;
273
327
  }
274
328
  if (options.allowDiskUse != null) {
275
329
  ret.allowDiskUse = options.allowDiskUse;
276
330
  }
331
+ if (options.signal) {
332
+ ret.signal = options.signal;
333
+ }
277
334
  return ret;
278
335
  }
336
+ /** @internal */
279
337
  renameFields(entityName, data, dotPaths = false, object, root = true) {
280
338
  if (data == null && root) {
281
339
  return {};
@@ -1,4 +1,4 @@
1
- import { EntityManager, type EntityName, type EntityRepository, type GetRepository, type TransactionOptions, type StreamOptions, type Loaded } from '@mikro-orm/core';
1
+ import { EntityManager, type CountByOptions, type Dictionary, type EntityKey, type EntityName, type EntityRepository, type GetRepository, type Loaded, type StreamOptions, type TransactionOptions, type WithUsingOptions } from '@mikro-orm/core';
2
2
  import type { Collection, Document, TransactionOptions as MongoTransactionOptions } from 'mongodb';
3
3
  import type { MongoDriver } from './MongoDriver.js';
4
4
  import type { MongoEntityRepository } from './MongoEntityRepository.js';
@@ -17,8 +17,12 @@ export declare class MongoEntityManager<Driver extends MongoDriver = MongoDriver
17
17
  /**
18
18
  * @inheritDoc
19
19
  */
20
- stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never>(entityName: EntityName<Entity>, options?: StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
20
+ stream<Entity extends object, Hint extends string = never, Fields extends string = never, Excludes extends string = never, Using extends string = never>(entityName: EntityName<Entity>, options?: WithUsingOptions<StreamOptions<NoInfer<Entity>, Hint, Fields, Excludes>, NoInfer<Entity>, Using>): AsyncIterableIterator<Loaded<Entity, Hint, Fields, Excludes>>;
21
21
  getCollection<T extends Document>(entityOrCollectionName: EntityName<T> | string): Collection<T>;
22
+ /**
23
+ * @inheritDoc
24
+ */
25
+ countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
22
26
  /**
23
27
  * @inheritDoc
24
28
  */
@@ -7,13 +7,13 @@ export class MongoEntityManager extends EntityManager {
7
7
  * Shortcut to driver's aggregate method. Available in MongoDriver only.
8
8
  */
9
9
  async aggregate(entityName, pipeline) {
10
- return this.getDriver().aggregate(entityName, pipeline, this.getTransactionContext());
10
+ return this.getDriver().aggregate(entityName, pipeline, this.getTransactionContext(), this.signal);
11
11
  }
12
12
  /**
13
13
  * Shortcut to driver's aggregate method. Returns a stream. Available in MongoDriver only.
14
14
  */
15
15
  async *streamAggregate(entityName, pipeline) {
16
- yield* this.getDriver().streamAggregate(entityName, pipeline, this.getTransactionContext());
16
+ yield* this.getDriver().streamAggregate(entityName, pipeline, this.getTransactionContext(), this.signal);
17
17
  }
18
18
  /**
19
19
  * @inheritDoc
@@ -27,6 +27,37 @@ export class MongoEntityManager extends EntityManager {
27
27
  getCollection(entityOrCollectionName) {
28
28
  return this.getConnection().getCollection(entityOrCollectionName);
29
29
  }
30
+ /**
31
+ * @inheritDoc
32
+ */
33
+ async countBy(entityName, groupBy, options = {}) {
34
+ const em = this.getContext(false);
35
+ options = { ...options };
36
+ em.prepareOptions(options);
37
+ const meta = em.getMetadata().find(entityName);
38
+ const fields = Utils.asArray(groupBy);
39
+ if (options.having) {
40
+ throw new Error('The `having` option is not supported for MongoDB in `countBy`.');
41
+ }
42
+ await em.tryFlush(entityName, options);
43
+ const rawWhere = options.where;
44
+ const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
45
+ const renamedWhere = em.getDriver().renameFields(meta.class, where, true);
46
+ const fieldNames = fields.map(f => meta.properties[f]?.fieldNames?.[0] ?? f);
47
+ const groupId = fieldNames.length === 1 ? `$${fieldNames[0]}` : Object.fromEntries(fieldNames.map(f => [f, `$${f}`]));
48
+ const pipeline = [];
49
+ if (renamedWhere && Object.keys(renamedWhere).length > 0) {
50
+ pipeline.push({ $match: renamedWhere });
51
+ }
52
+ pipeline.push({ $group: { _id: groupId, count: { $sum: 1 } } });
53
+ const rows = await em.getDriver().aggregate(meta.class, pipeline, em.getTransactionContext(), em.signal);
54
+ const results = {};
55
+ for (const row of rows) {
56
+ const key = fieldNames.length === 1 ? String(row._id) : fieldNames.map(f => String(row._id[f])).join(Utils.PK_SEPARATOR);
57
+ results[key] = +row.count;
58
+ }
59
+ return results;
60
+ }
30
61
  /**
31
62
  * @inheritDoc
32
63
  */
package/MongoPlatform.js CHANGED
@@ -73,9 +73,13 @@ export class MongoPlatform extends Platform {
73
73
  return prop.kind === ReferenceKind.MANY_TO_MANY && prop.owner;
74
74
  }
75
75
  validateMetadata(meta) {
76
+ super.validateMetadata(meta);
76
77
  if (meta.inheritanceType === 'tpt') {
77
78
  throw MetadataError.tptNotSupportedByDriver(meta);
78
79
  }
80
+ if (meta.triggers?.length > 0) {
81
+ throw MetadataError.triggersNotSupportedByDriver(meta);
82
+ }
79
83
  const pk = meta.getPrimaryProps()[0];
80
84
  if (pk && pk.fieldNames?.[0] !== '_id') {
81
85
  throw MetadataError.invalidPrimaryKey(meta, pk, '_id');
@@ -21,6 +21,11 @@ export declare class MongoSchemaGenerator extends AbstractSchemaGenerator<MongoD
21
21
  ensureIndexes(options?: EnsureIndexesOptions): Promise<void>;
22
22
  private mapIndexProperties;
23
23
  private createIndexes;
24
+ /**
25
+ * An explicit `options.partialFilterExpression` wins over `where` — this preserves the
26
+ * long-standing `options: { partialFilterExpression }` escape hatch.
27
+ */
28
+ private applyPartialFilter;
24
29
  private executeQuery;
25
30
  private createUniqueIndexes;
26
31
  private createPropertyIndexes;
@@ -20,10 +20,13 @@ export class MongoSchemaGenerator extends AbstractSchemaGenerator {
20
20
  throw err;
21
21
  }
22
22
  }));
23
+ // Collections must exist before we touch indexes. `createIndex` on a missing collection
24
+ // races with the explicit `createCollection` above, and on retries the recovery path can
25
+ // see partial state and drop indexes that were already created successfully.
26
+ await Promise.all(promises);
23
27
  if (options.ensureIndexes) {
24
28
  await this.ensureIndexes({ ensureCollections: false });
25
29
  }
26
- await Promise.all(promises);
27
30
  }
28
31
  async drop(options = {}) {
29
32
  await this.connection.ensureConnection();
@@ -132,10 +135,21 @@ export class MongoSchemaGenerator extends AbstractSchemaGenerator {
132
135
  const properties = this.mapIndexProperties(index, meta);
133
136
  const collection = this.connection.getCollection(meta.class);
134
137
  if (Array.isArray(index.options) && index.options.length === 2 && properties.length === 0) {
135
- res.push([collection.collectionName, collection.createIndex(index.options[0], index.options[1])]);
138
+ // The array-form escape hatch passes raw [spec, options] to the driver; fold `where`
139
+ // into the options dict (on a clone, to avoid mutating the user's metadata).
140
+ const opts = { ...index.options[1] };
141
+ this.applyPartialFilter(opts, index.where, index.name, meta.className);
142
+ res.push([collection.collectionName, collection.createIndex(index.options[0], opts)]);
136
143
  return;
137
144
  }
138
145
  if (index.options && properties.length === 0) {
146
+ // The plain escape hatch forwards `index.options` as the sole argument; there is
147
+ // no options slot for `partialFilterExpression`, so warn instead of silently dropping.
148
+ if (index.where != null) {
149
+ this.config
150
+ .getLogger()
151
+ .warn('schema', `Index '${index.name ?? '(unnamed)'}' on entity '${meta.className}': \`where\` was ignored because \`options\` is used as the raw index spec and leaves no slot for \`partialFilterExpression\` — pass \`options: [spec, { partialFilterExpression }]\` (array form) to combine both.`);
152
+ }
139
153
  res.push([collection.collectionName, collection.createIndex(index.options)]);
140
154
  return;
141
155
  }
@@ -162,10 +176,30 @@ export class MongoSchemaGenerator extends AbstractSchemaGenerator {
162
176
  if (index.invisible) {
163
177
  indexOptions.hidden = true;
164
178
  }
179
+ this.applyPartialFilter(indexOptions, index.where, index.name, meta.className);
165
180
  res.push([collection.collectionName, this.executeQuery(collection, 'createIndex', fieldOrSpec, indexOptions)]);
166
181
  });
167
182
  return res;
168
183
  }
184
+ /**
185
+ * An explicit `options.partialFilterExpression` wins over `where` — this preserves the
186
+ * long-standing `options: { partialFilterExpression }` escape hatch.
187
+ */
188
+ applyPartialFilter(options, where, indexName, entityName) {
189
+ if (where == null) {
190
+ return;
191
+ }
192
+ if (options.partialFilterExpression != null) {
193
+ this.config
194
+ .getLogger()
195
+ .warn('schema', `Index '${indexName ?? '(unnamed)'}' on entity '${entityName}': both \`where\` and \`options.partialFilterExpression\` are set; \`options.partialFilterExpression\` wins.`);
196
+ return;
197
+ }
198
+ if (typeof where === 'string') {
199
+ throw new Error(`Index '${indexName ?? '(unnamed)'}' on entity '${entityName}': string \`where\` is not supported on MongoDB; pass an object/FilterQuery (it maps to MongoDB's \`partialFilterExpression\`).`);
200
+ }
201
+ options.partialFilterExpression = where;
202
+ }
169
203
  async executeQuery(collection, method, ...args) {
170
204
  const now = Date.now();
171
205
  return collection[method](...args).then((res) => {
@@ -188,14 +222,13 @@ export class MongoSchemaGenerator extends AbstractSchemaGenerator {
188
222
  return o;
189
223
  }, {});
190
224
  const collection = this.connection.getCollection(meta.class);
191
- res.push([
192
- collection.collectionName,
193
- this.executeQuery(collection, 'createIndex', fieldOrSpec, {
194
- name: index.name,
195
- unique: true,
196
- ...index.options,
197
- }),
198
- ]);
225
+ const indexOptions = {
226
+ name: index.name,
227
+ unique: true,
228
+ ...index.options,
229
+ };
230
+ this.applyPartialFilter(indexOptions, index.where, index.name, meta.className);
231
+ res.push([collection.collectionName, this.executeQuery(collection, 'createIndex', fieldOrSpec, indexOptions)]);
199
232
  });
200
233
  return res;
201
234
  }
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
@@ -19,6 +19,7 @@ Install a driver package for your database:
19
19
 
20
20
  ```sh
21
21
  npm install @mikro-orm/postgresql # PostgreSQL
22
+ npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
22
23
  npm install @mikro-orm/mysql # MySQL
23
24
  npm install @mikro-orm/mariadb # MariaDB
24
25
  npm install @mikro-orm/sqlite # SQLite
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/mongodb",
3
- "version": "7.1.0-dev.5",
3
+ "version": "7.1.0-dev.50",
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",
@@ -47,13 +47,13 @@
47
47
  "copy": "node ../../scripts/copy.mjs"
48
48
  },
49
49
  "dependencies": {
50
- "mongodb": "7.1.1"
50
+ "mongodb": "7.2.0"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.0.11"
53
+ "@mikro-orm/core": "^7.0.17"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.5"
56
+ "@mikro-orm/core": "7.1.0-dev.50"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"