mongoose 9.6.3 → 9.7.1

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.
package/lib/aggregate.js CHANGED
@@ -15,6 +15,8 @@ const { buildMiddlewareFilter } = require('./helpers/buildMiddlewareFilter');
15
15
  const stringifyFunctionOperators = require('./helpers/aggregate/stringifyFunctionOperators');
16
16
  const utils = require('./utils');
17
17
  const { modelSymbol } = require('./helpers/symbols');
18
+ const { createTracedChannel } = require('./tracing');
19
+ const { trace: traceAggregate } = createTracedChannel('mongoose:aggregate');
18
20
  const read = Query.prototype.read;
19
21
  const readConcern = Query.prototype.readConcern;
20
22
 
@@ -1071,8 +1073,20 @@ Aggregate.prototype.exec = async function exec() {
1071
1073
 
1072
1074
  this._optionsForExec();
1073
1075
 
1074
- const cursor = await this._connection.client.db().aggregate(this._pipeline, this.options);
1075
- return await cursor.toArray();
1076
+ const _this = this;
1077
+ return traceAggregate(async function maybeTracedConnectionAggregate() {
1078
+ const cursor = await _this._connection.client.db().aggregate(_this._pipeline, _this.options);
1079
+ return await cursor.toArray();
1080
+ }, () => ({
1081
+ operation: 'aggregate',
1082
+ database: _this._connection.name,
1083
+ serverAddress: _this._connection.host,
1084
+ serverPort: _this._connection.port,
1085
+ args: {
1086
+ pipeline: _this._pipeline,
1087
+ options: _this.options
1088
+ }
1089
+ }));
1076
1090
  }
1077
1091
 
1078
1092
  const model = this._model;
@@ -1090,32 +1104,45 @@ Aggregate.prototype.exec = async function exec() {
1090
1104
  prepareDiscriminatorPipeline(this._pipeline, this._model.schema);
1091
1105
  stringifyFunctionOperators(this._pipeline);
1092
1106
 
1093
- const preFilter = buildMiddlewareFilter(this.options, 'pre');
1094
- const postFilter = buildMiddlewareFilter(this.options, 'post');
1107
+ const _this = this;
1108
+ return traceAggregate(async function maybeTracedAggregateExec() {
1109
+ const preFilter = buildMiddlewareFilter(_this.options, 'pre');
1110
+ const postFilter = buildMiddlewareFilter(_this.options, 'post');
1095
1111
 
1096
- try {
1097
- await model.hooks.execPre('aggregate', this, [], { filter: preFilter });
1098
- } catch (error) {
1099
- return await model.hooks.execPost('aggregate', this, [null], { error, filter: postFilter });
1100
- }
1112
+ try {
1113
+ await model.hooks.execPre('aggregate', _this, [], { filter: preFilter });
1114
+ } catch (error) {
1115
+ return await model.hooks.execPost('aggregate', _this, [null], { error, filter: postFilter });
1116
+ }
1101
1117
 
1102
- if (!this._pipeline.length) {
1103
- throw new MongooseError('Aggregate has empty pipeline');
1104
- }
1118
+ if (!_this._pipeline.length) {
1119
+ throw new MongooseError('Aggregate has empty pipeline');
1120
+ }
1105
1121
 
1106
- const options = clone(this.options || {});
1107
- delete options.middleware;
1122
+ const options = clone(_this.options || {});
1123
+ delete options.middleware;
1108
1124
 
1109
- let result;
1110
- try {
1111
- const cursor = await collection.aggregate(this._pipeline, options);
1112
- result = await cursor.toArray();
1113
- } catch (error) {
1114
- return await model.hooks.execPost('aggregate', this, [null], { error, filter: postFilter });
1115
- }
1125
+ let result;
1126
+ try {
1127
+ const cursor = await collection.aggregate(_this._pipeline, options);
1128
+ result = await cursor.toArray();
1129
+ } catch (error) {
1130
+ return await model.hooks.execPost('aggregate', _this, [null], { error, filter: postFilter });
1131
+ }
1116
1132
 
1117
- await model.hooks.execPost('aggregate', this, [result], { error: null, filter: postFilter });
1118
- return result;
1133
+ await model.hooks.execPost('aggregate', _this, [result], { error: null, filter: postFilter });
1134
+ return result;
1135
+ }, () => ({
1136
+ operation: 'aggregate',
1137
+ collection: collection.name,
1138
+ database: model.db?.name,
1139
+ serverAddress: model.db?.host,
1140
+ serverPort: model.db?.port,
1141
+ args: {
1142
+ pipeline: _this._pipeline,
1143
+ options: _this.options
1144
+ }
1145
+ }));
1119
1146
  };
1120
1147
 
1121
1148
  /**
package/lib/connection.js CHANGED
@@ -1354,7 +1354,7 @@ Connection.prototype.onClose = function onClose(force) {
1354
1354
 
1355
1355
  /**
1356
1356
  * Retrieves a raw collection instance, creating it if not cached.
1357
- * This method returns a thin wrapper around a [MongoDB Node.js driver collection]([MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html)).
1357
+ * This method returns a thin wrapper around a [MongoDB Node.js driver collection](https://mongodb.github.io/node-mongodb-native/Next/classes/Collection.html).
1358
1358
  * Using a Collection bypasses Mongoose middleware, validation, and casting,
1359
1359
  * letting you use [MongoDB Node.js driver](https://mongodb.github.io/node-mongodb-native/) functionality directly.
1360
1360
  *
@@ -10,6 +10,7 @@ const eachAsync = require('../helpers/cursor/eachAsync');
10
10
  const immediate = require('../helpers/immediate');
11
11
  const kareem = require('kareem');
12
12
  const util = require('util');
13
+ const { cursorNextChannel } = require('../tracing');
13
14
 
14
15
  /**
15
16
  * An AggregationCursor is a concurrency primitive for processing aggregation
@@ -62,13 +63,7 @@ util.inherits(AggregationCursor, Readable);
62
63
  */
63
64
 
64
65
  function _init(model, c, agg) {
65
- if (!model.collection.buffer) {
66
- model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
67
- } else {
68
- model.collection.emitter.once('queue', function() {
69
- model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
70
- });
71
- }
66
+ model.hooks.execPre('aggregate', agg).then(() => onPreComplete(null), err => onPreComplete(err));
72
67
 
73
68
  function onPreComplete(err) {
74
69
  if (err != null) {
@@ -79,8 +74,28 @@ function _init(model, c, agg) {
79
74
  c._transforms.push(agg.options.cursor.transform);
80
75
  }
81
76
 
82
- c.cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
83
- c.emit('cursor', c.cursor);
77
+ if (model.collection._shouldBufferCommands() && model.collection.buffer) {
78
+ model.collection.queue.push([
79
+ () => _getRawCursor(model, c, agg)
80
+ ]);
81
+ } else {
82
+ _getRawCursor(model, c, agg);
83
+ }
84
+ }
85
+ }
86
+
87
+ /*!
88
+ * ignore
89
+ */
90
+
91
+ function _getRawCursor(model, aggregationCursor, agg) {
92
+ try {
93
+ const cursor = model.collection.aggregate(agg._pipeline, agg.options || {});
94
+ aggregationCursor.cursor = cursor;
95
+ aggregationCursor.emit('cursor', cursor);
96
+ } catch (err) {
97
+ aggregationCursor._markError(err);
98
+ aggregationCursor.listeners('error').length > 0 && aggregationCursor.emit('error', aggregationCursor._error);
84
99
  }
85
100
  }
86
101
 
@@ -277,14 +292,30 @@ AggregationCursor.prototype.next = async function next() {
277
292
  if (typeof arguments[0] === 'function') {
278
293
  throw new MongooseError('AggregationCursor.prototype.next() no longer accepts a callback');
279
294
  }
280
- return new Promise((resolve, reject) => {
281
- _next(this, (err, res) => {
282
- if (err != null) {
283
- return reject(err);
284
- }
285
- resolve(res);
295
+ const _this = this;
296
+ const model = this.agg._model;
297
+ return cursorNextChannel.trace(function maybeTracedAggCursorNext() {
298
+ return new Promise((resolve, reject) => {
299
+ _next(_this, (err, res) => {
300
+ if (err != null) {
301
+ return reject(err);
302
+ }
303
+ resolve(res);
304
+ });
286
305
  });
287
- });
306
+ }, () => ({
307
+ operation: 'aggregate',
308
+ collection: model?.collection?.name,
309
+ database: model?.db?.name,
310
+ serverAddress: model?.db?.host,
311
+ serverPort: model?.db?.port,
312
+ batchSize: _this.agg.options?.cursor?.batchSize,
313
+ tailable: false,
314
+ args: {
315
+ pipeline: _this.agg._pipeline,
316
+ options: _this.agg.options
317
+ }
318
+ }));
288
319
  };
289
320
 
290
321
  /**
@@ -12,6 +12,7 @@ const kareem = require('kareem');
12
12
  const immediate = require('../helpers/immediate');
13
13
  const { once } = require('events');
14
14
  const util = require('util');
15
+ const { cursorNextChannel } = require('../tracing');
15
16
 
16
17
  /**
17
18
  * A QueryCursor is a concurrency primitive for processing query results
@@ -310,14 +311,29 @@ QueryCursor.prototype.next = async function next() {
310
311
  if (this._closed) {
311
312
  throw new MongooseError('Cannot call `next()` on a closed cursor');
312
313
  }
313
- return new Promise((resolve, reject) => {
314
- _next(this, function(error, doc) {
315
- if (error) {
316
- return reject(error);
317
- }
318
- resolve(doc);
314
+ const _this = this;
315
+ return cursorNextChannel.trace(function maybeTracedQueryCursorNext() {
316
+ return new Promise((resolve, reject) => {
317
+ _next(_this, function(error, doc) {
318
+ if (error) {
319
+ return reject(error);
320
+ }
321
+ resolve(doc);
322
+ });
319
323
  });
320
- });
324
+ }, () => ({
325
+ operation: _this.query.op || 'find',
326
+ collection: _this.query.mongooseCollection.name,
327
+ database: _this.model.db?.name,
328
+ serverAddress: _this.model.db?.host,
329
+ serverPort: _this.model.db?.port,
330
+ batchSize: _this.options.batchSize || _this.query.options?.batchSize,
331
+ tailable: _this.options.tailable || _this.query.options?.tailable || false,
332
+ args: {
333
+ filter: _this.query.getFilter(),
334
+ options: _this.query._mongooseOptions
335
+ }
336
+ }));
321
337
  };
322
338
 
323
339
  /**
package/lib/document.js CHANGED
@@ -562,27 +562,26 @@ function $applyDefaultsToNested(val, path, doc) {
562
562
  Document.prototype.$__buildDoc = function(obj, fields, skipId, exclude, hasIncludedChildren) {
563
563
  const doc = {};
564
564
 
565
- const paths = Object.keys(this.$__schema.paths).
566
- // Don't build up any paths that are underneath a map, we don't know
567
- // what the keys will be
568
- filter(p => !p.includes('$*'));
565
+ const paths = Object.keys(this.$__schema.paths);
569
566
  const plen = paths.length;
570
567
  let ii = 0;
571
568
 
572
569
  for (; ii < plen; ++ii) {
573
570
  const p = paths[ii];
574
571
 
575
- if (p === '_id') {
576
- if (skipId) {
577
- continue;
578
- }
579
- if (obj && '_id' in obj) {
580
- continue;
581
- }
572
+ // Don't build up any paths that are underneath a map, we don't know
573
+ // what the keys will be
574
+ if (p.includes('$*')) {
575
+ continue;
582
576
  }
583
577
 
584
578
  const path = this.$__schema.paths[p].splitPath();
585
579
  const len = path.length;
580
+ // This loop only creates intermediate objects for nested paths: it never
581
+ // sets any leaf values, so single-segment paths are no-ops.
582
+ if (len === 1) {
583
+ continue;
584
+ }
586
585
  const last = len - 1;
587
586
  let curPath = '';
588
587
  let doc_ = doc;
@@ -1133,7 +1132,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
1133
1132
  return this;
1134
1133
  }
1135
1134
 
1136
- options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1135
+ // Only need to clone if we're overwriting `_skipMinimizeTopLevel`, recursive
1136
+ // calls treat a missing `_skipMinimizeTopLevel` as `false`
1137
+ if (_skipMinimizeTopLevel) {
1138
+ options = Object.assign({}, options, { _skipMinimizeTopLevel: false });
1139
+ }
1137
1140
 
1138
1141
  for (let i = 0; i < len; ++i) {
1139
1142
  key = keys[i];
@@ -1220,15 +1223,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
1220
1223
  val = handleSpreadDoc(val, true);
1221
1224
 
1222
1225
  // if this doc is being constructed we should not trigger getters
1223
- const priorVal = (() => {
1224
- if (this.$__.priorDoc != null) {
1225
- return this.$__.priorDoc.$__getValue(path);
1226
- }
1227
- if (constructing) {
1228
- return void 0;
1229
- }
1230
- return this.$__getValue(path);
1231
- })();
1226
+ const priorVal = this.$__.priorDoc != null ?
1227
+ this.$__.priorDoc.$__getValue(path) :
1228
+ (constructing ? void 0 : this.$__getValue(path));
1232
1229
 
1233
1230
  if (pathType === 'nested' && val) {
1234
1231
  if (typeof val === 'object' && val != null) {
@@ -1410,13 +1407,10 @@ Document.prototype.$set = function $set(path, val, type, options) {
1410
1407
  try {
1411
1408
  // If the user is trying to set a ref path to a document with
1412
1409
  // the correct model name, treat it as populated
1413
- const refMatches = (() => {
1410
+ const refMatches = !(val instanceof Document) ? false : (() => {
1414
1411
  if (schema.options == null) {
1415
1412
  return false;
1416
1413
  }
1417
- if (!(val instanceof Document)) {
1418
- return false;
1419
- }
1420
1414
  const model = val.constructor;
1421
1415
 
1422
1416
  // Check ref
@@ -2870,7 +2864,8 @@ Document.prototype.validate = async function validate(pathsToValidate, options)
2870
2864
 
2871
2865
  await this._execDocumentPostHooks('validate', options, error);
2872
2866
  } finally {
2873
- delete this.$__.validateModifiedOnly;
2867
+ // Assign rather than `delete` to avoid putting `$__` in dictionary mode
2868
+ this.$__.validateModifiedOnly = undefined;
2874
2869
  this.$op = null;
2875
2870
  this.$__.validating = null;
2876
2871
  }
@@ -2913,7 +2908,7 @@ function _completeValidate(doc) {
2913
2908
  }
2914
2909
  }
2915
2910
 
2916
- doc.$__.cachedRequired = {};
2911
+ doc.$__.cachedRequired = null;
2917
2912
  doc.$emit('validate', doc);
2918
2913
  doc.constructor.emit('validate', doc);
2919
2914
 
@@ -3204,7 +3199,7 @@ function _pushNestedArrayPaths(val, paths, path) {
3204
3199
  * ignore
3205
3200
  */
3206
3201
 
3207
- Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(opName, options, argsForHooks) {
3202
+ Document.prototype._execDocumentPreHooks = function _execDocumentPreHooks(opName, options, argsForHooks) {
3208
3203
  const filter = buildMiddlewareFilter(options, 'pre');
3209
3204
  return this.$__middleware.execPre(opName, this, argsForHooks || [], { filter });
3210
3205
  };
@@ -3213,7 +3208,7 @@ Document.prototype._execDocumentPreHooks = async function _execDocumentPreHooks(
3213
3208
  * ignore
3214
3209
  */
3215
3210
 
3216
- Document.prototype._execDocumentPostHooks = async function _execDocumentPostHooks(opName, options, error) {
3211
+ Document.prototype._execDocumentPostHooks = function _execDocumentPostHooks(opName, options, error) {
3217
3212
  const filter = buildMiddlewareFilter(options, 'post');
3218
3213
  return this.$__middleware.execPost(opName, this, [this], { error, filter });
3219
3214
  };
@@ -3264,6 +3259,9 @@ function _handlePathsToSkip(paths, pathsToSkip) {
3264
3259
  /**
3265
3260
  * Executes registered validation rules (skipping asynchronous validators) for this document.
3266
3261
  *
3262
+ * **Deprecated.** Use [`validate()`](https://mongoosejs.com/docs/api/document.html#Document.prototype.validate()) instead.
3263
+ * `validateSync()` does not run validation middleware and will be removed in Mongoose 10.
3264
+ *
3267
3265
  * #### Note:
3268
3266
  *
3269
3267
  * This method is useful if you need synchronous validation.
@@ -3281,14 +3279,21 @@ function _handlePathsToSkip(paths, pathsToSkip) {
3281
3279
  * @param {object} [options] options for validation
3282
3280
  * @param {boolean} [options.validateModifiedOnly=false] If `true`, Mongoose will only validate modified paths, as opposed to modified paths and `required` paths.
3283
3281
  * @param {Array|string} [options.pathsToSkip] list of paths to skip. If set, Mongoose will validate every modified path that is not in this list.
3284
- * @param {boolean|object} [options.middleware=true] set to `false` to skip all user-defined middleware
3285
- * @param {boolean} [options.middleware.pre=true] set to `false` to skip only pre hooks
3286
- * @param {boolean} [options.middleware.post=true] set to `false` to skip only post hooks
3287
3282
  * @return {ValidationError|undefined} ValidationError if there are errors during validation, or undefined if there is no error.
3283
+ * @deprecated Use `validate()` instead. `validateSync()` does not run middleware and will be removed in Mongoose 10.
3288
3284
  * @api public
3289
3285
  */
3290
3286
 
3291
- Document.prototype.validateSync = function(pathsToValidate, options) {
3287
+ Document.prototype.validateSync = function() {
3288
+ utils.warn('Mongoose: `Document.prototype.validateSync()` is deprecated and will be removed in Mongoose 10. Use `Document.prototype.validate()` instead.');
3289
+ return this.$__validateSync.apply(this, arguments);
3290
+ };
3291
+
3292
+ /*!
3293
+ * ignore
3294
+ */
3295
+
3296
+ Document.prototype.$__validateSync = function(pathsToValidate, options) {
3292
3297
  const _this = this;
3293
3298
 
3294
3299
  if (arguments.length === 1 && typeof arguments[0] === 'object' && !Array.isArray(arguments[0])) {
@@ -3620,7 +3625,11 @@ Document.prototype.$__reset = function reset() {
3620
3625
  // Clear atomics on dirty paths. Walk the modified and default paths
3621
3626
  // directly instead of calling $__dirty(), which builds intermediate
3622
3627
  // arrays, a Map, and does parent-path deduplication we don't need here.
3623
- this.$__resetAtomics();
3628
+ // Skip entirely if the doc only has primitive values, because only arrays
3629
+ // and maps have atomics.
3630
+ if (!onlyPrimitiveValues) {
3631
+ this.$__resetAtomics();
3632
+ }
3624
3633
 
3625
3634
  this.$__.backup = {};
3626
3635
  this.$__.backup.activePaths = {
@@ -5266,6 +5275,12 @@ function checkDivergentArray(doc, path, array) {
5266
5275
  // If any array was selected using an $elemMatch projection, we deny the update.
5267
5276
  // NOTE: MongoDB only supports projected $elemMatch on top level array.
5268
5277
  const top = path.split('.')[0];
5278
+ if (doc.$__.selected[top] && doc.$__.selected[top].$slice != null && utils.isMongooseArray(array)) {
5279
+ const atomics = array[arrayAtomicsSymbol];
5280
+ if (atomics.$set) {
5281
+ return top;
5282
+ }
5283
+ }
5269
5284
  if (doc.$__.selected[top + '.$']) {
5270
5285
  return top;
5271
5286
  }
@@ -17,7 +17,7 @@ class DivergentArrayError extends MongooseError {
17
17
 
18
18
  constructor(paths) {
19
19
  const msg = 'For your own good, using `document.save()` to update an array '
20
- + 'which was selected using an $elemMatch projection OR '
20
+ + 'which was selected using an $elemMatch or $slice projection OR '
21
21
  + 'populated using skip, limit, query conditions, or exclusion of '
22
22
  + 'the _id field when the operation results in a $pop or $set of '
23
23
  + 'the entire array is not supported. The following '
@@ -17,6 +17,11 @@ module.exports = function applyDefaults(doc, fields, exclude, hasIncludedChildre
17
17
  }
18
18
 
19
19
  const type = doc.$__schema.paths[p];
20
+ // `getDefault()` returns undefined when `defaultValue` is undefined, in which
21
+ // case this loop never modifies the doc, so skip traversing the path entirely.
22
+ if (type.defaultValue === undefined) {
23
+ continue;
24
+ }
20
25
  const path = type.splitPath();
21
26
  const len = path.length;
22
27
  if (path[len - 1] === '$*') {
@@ -25,7 +25,15 @@ module.exports = function createPopulateQueryFilter(ids, _match, _foreignField,
25
25
  for (let i = 0; i < _parentPaths.length - 1; ++i) {
26
26
  const cur = _parentPaths[i];
27
27
  if (match[cur] != null && match[cur].$elemMatch != null) {
28
- match[cur].$elemMatch[foreignField.slice(cur.length + 1)] = trusted({ $in: ids });
28
+ // Copy rather than mutate so the user's `match` stays unchanged and split populate
29
+ // queries (gh-5890) each get their own `$in` rather than sharing one object
30
+ match[cur] = {
31
+ ...match[cur],
32
+ $elemMatch: {
33
+ ...match[cur].$elemMatch,
34
+ [foreignField.slice(cur.length + 1)]: trusted({ $in: ids })
35
+ }
36
+ };
29
37
  delete match[foreignField];
30
38
  break;
31
39
  }
@@ -649,11 +649,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
649
649
  ids = matchIdsToRefPaths(ret, modelNamesForRefPath, modelName);
650
650
  }
651
651
 
652
- const perDocumentLimit = options.perDocumentLimit == null ?
653
- get(options, 'options.perDocumentLimit', null) :
654
- options.perDocumentLimit;
655
-
656
- if (!available[modelName] || perDocumentLimit != null) {
652
+ if (!available[modelName]) {
657
653
  const currentOptions = {
658
654
  model: Model
659
655
  };
@@ -682,6 +678,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
682
678
  isVirtual: data.isVirtual,
683
679
  virtual: data.virtual,
684
680
  count: data.count,
681
+ isRefPath: !!data.isRefPath,
685
682
  [populateModelSymbol]: Model
686
683
  };
687
684
  map.push(available[modelName]);
@@ -692,6 +689,7 @@ function addModelNamesToMap(model, map, available, modelNames, options, data, re
692
689
  available[modelName].ids.push(ids);
693
690
  available[modelName].allIds.push(ret);
694
691
  available[modelName].unpopulatedValues.push(unpopulatedValue);
692
+ available[modelName].isRefPath = available[modelName].isRefPath || !!data.isRefPath;
695
693
  if (data.hasMatchFunction) {
696
694
  available[modelName].match.push(data.match);
697
695
  }
@@ -873,7 +871,11 @@ function _findRefPathForDiscriminators(doc, modelSchema, data, options, normaliz
873
871
  }
874
872
 
875
873
  /**
876
- * Throw an error if there are any $where keys
874
+ * Throw an error if there are any $where keys to defend against [CVE-2024-53900](https://nvd.nist.gov/vuln/detail/CVE-2024-53900)
875
+ *
876
+ * Note that this is ONLY for $where because sift executes $where in Node.js memory.
877
+ * Other forms of MongoDB server-side execution, like $expr, are NOT filtered out.
878
+ * This function is not meant to protect against server-side execution in MongoDB.
877
879
  */
878
880
 
879
881
  function throwOn$where(match) {
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ const createPopulateQueryFilter = require('./createPopulateQueryFilter');
4
+ const get = require('../get');
5
+ const utils = require('../../utils');
6
+
7
+ module.exports = splitPopulateQuery;
8
+
9
+ /*!
10
+ * If a single populate query would have more than this many elements in its `$in` filter,
11
+ * Mongoose splits the populate into a separate query per document to avoid going over
12
+ * MongoDB's 16 MB BSON size limit on queries. Overwritable for testing purposes. See gh-5890.
13
+ */
14
+
15
+ splitPopulateQuery.maxInFilterLength = 50000;
16
+
17
+ /*!
18
+ * Split a populate models-map entry into a separate query per document if either:
19
+ *
20
+ * 1. The `perDocumentLimit` option is set, so each document needs its own query with its
21
+ * own `limit` (gh-7318), or
22
+ * 2. A single populate query for `mod` would have too many elements in its `$in` filter
23
+ * (gh-5890). With multiple foreign fields, `createPopulateQueryFilter()` repeats the ids
24
+ * under `$or` once per foreign field, so the threshold counts one copy of `ids` per
25
+ * foreign field.
26
+ *
27
+ * Returns a list of `[mod, match, select, assignmentOpts]` params, one per document, for
28
+ * `_execPopulateQuery()`. Returns `null` if the populate query doesn't need to be split.
29
+ * A `null` `match` means the document has no ids to query: `_execPopulateQuery()` skips
30
+ * executing a query, and `_assign()` just sets the document's populated path to the
31
+ * default value.
32
+ *
33
+ * Splitting on document boundaries means each document's populated value is the result of
34
+ * exactly one query, so split entries can typically be assigned from only their own query's
35
+ * results (`_assignFromOwnResults`) rather than scanning every populate query's results.
36
+ * refPath is the exception: a single document's array can contain ids for multiple models,
37
+ * so assigning refPath populate results relies on every query's results being available for
38
+ * every document. refPath entries are therefore only split when `perDocumentLimit` requires
39
+ * it, not to keep the `$in` filter small. A single document whose ids alone overflow the
40
+ * BSON size limit cannot be split.
41
+ */
42
+
43
+ function splitPopulateQuery(mod, ids, select, assignmentOpts) {
44
+ if (mod.docs.length <= 1) {
45
+ return null;
46
+ }
47
+ const perDocumentLimit = mod.options.perDocumentLimit == null ?
48
+ get(mod.options, 'options.perDocumentLimit', null) :
49
+ mod.options.perDocumentLimit;
50
+ const numInFilterElements = ids.length * mod.foreignField.size;
51
+ if (perDocumentLimit == null && (numInFilterElements <= splitPopulateQuery.maxInFilterLength || mod.isRefPath)) {
52
+ return null;
53
+ }
54
+
55
+ return mod.docs.map((doc, i) => {
56
+ const subMod = {
57
+ ...mod,
58
+ docs: [doc],
59
+ ids: [mod.ids[i]],
60
+ allIds: [mod.allIds[i]],
61
+ unpopulatedValues: [mod.unpopulatedValues[i]],
62
+ match: Array.isArray(mod.match) ? [mod.match[i]] : mod.match,
63
+ _assignFromOwnResults: !mod.isRefPath
64
+ };
65
+ let subIds = utils.array.flatten(subMod.ids, flatten);
66
+ subIds = utils.array.unique(subIds);
67
+ const match = subIds.length === 0 || subIds.every(utils.isNullOrUndefined) ?
68
+ null :
69
+ createPopulateQueryFilter(subIds, subMod.match, subMod.foreignField, subMod.model, subMod.options.skipInvalidIds);
70
+ return [subMod, match, select, assignmentOpts];
71
+ });
72
+ }
73
+
74
+ /*!
75
+ * ignore
76
+ */
77
+
78
+ function flatten(item) {
79
+ // no need to include undefined values in our query
80
+ return undefined !== item;
81
+ }
@@ -3,25 +3,22 @@
3
3
  const get = require('../get');
4
4
 
5
5
  module.exports = function getKeysInSchemaOrder(schema, val, path) {
6
+ const valKeys = Object.keys(val);
7
+ if (valKeys.length <= 1) {
8
+ return valKeys;
9
+ }
10
+
6
11
  const schemaKeys = path != null ? Object.keys(get(schema.tree, path, {})) : Object.keys(schema.tree);
7
- const valKeys = new Set(Object.keys(val));
12
+ const remaining = new Set(valKeys);
8
13
 
9
- let keys;
10
- if (valKeys.size > 1) {
11
- keys = new Set();
12
- for (const key of schemaKeys) {
13
- if (valKeys.has(key)) {
14
- keys.add(key);
15
- }
14
+ const keys = [];
15
+ for (const key of schemaKeys) {
16
+ if (remaining.delete(key)) {
17
+ keys.push(key);
16
18
  }
17
- for (const key of valKeys) {
18
- if (!keys.has(key)) {
19
- keys.add(key);
20
- }
21
- }
22
- keys = Array.from(keys);
23
- } else {
24
- keys = Array.from(valKeys);
19
+ }
20
+ for (const key of remaining) {
21
+ keys.push(key);
25
22
  }
26
23
 
27
24
  return keys;