@depup/mongoose 9.4.1-depup.0 → 9.8.0-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +3 -9
  2. package/changes.json +3 -8
  3. package/eslint.config.mjs +4 -1
  4. package/lib/aggregate.js +54 -24
  5. package/lib/cast.js +1 -1
  6. package/lib/connection.js +1 -1
  7. package/lib/cursor/aggregationCursor.js +47 -16
  8. package/lib/cursor/queryCursor.js +23 -7
  9. package/lib/document.js +133 -58
  10. package/lib/drivers/node-mongodb-native/collection.js +10 -4
  11. package/lib/error/cast.js +18 -9
  12. package/lib/error/divergentArray.js +1 -1
  13. package/lib/error/messages.js +1 -0
  14. package/lib/helpers/clone.js +22 -5
  15. package/lib/helpers/document/applyDefaults.js +5 -0
  16. package/lib/helpers/populate/createPopulateQueryFilter.js +9 -1
  17. package/lib/helpers/populate/getModelsMapForPopulate.js +8 -6
  18. package/lib/helpers/populate/splitPopulateQuery.js +81 -0
  19. package/lib/helpers/query/castUpdate.js +4 -0
  20. package/lib/helpers/schema/getKeysInSchemaOrder.js +13 -16
  21. package/lib/helpers/update/applyTimestampsToUpdate.js +4 -2
  22. package/lib/model.js +153 -38
  23. package/lib/mongoose.js +3 -3
  24. package/lib/options/schemaTypeOptions.js +14 -0
  25. package/lib/plugins/saveSubdocs.js +16 -13
  26. package/lib/query.js +208 -102
  27. package/lib/queryHelpers.js +13 -12
  28. package/lib/schema/array.js +3 -3
  29. package/lib/schema/date.js +7 -5
  30. package/lib/schema/documentArray.js +1 -1
  31. package/lib/schema/objectId.js +7 -1
  32. package/lib/schema/string.js +19 -2
  33. package/lib/schema/subdocument.js +1 -1
  34. package/lib/schema/union.js +2 -2
  35. package/lib/schema.js +38 -12
  36. package/lib/schemaType.js +58 -10
  37. package/lib/standardSchema/convertErrorToIssues.js +20 -0
  38. package/lib/stateMachine.js +11 -6
  39. package/lib/tracing.js +38 -0
  40. package/lib/types/documentArray/methods/index.js +117 -3
  41. package/lib/types/subdocument.js +4 -2
  42. package/lib/validOptions.js +1 -0
  43. package/package.json +27 -30
  44. package/tstyche.json +2 -1
  45. package/types/augmentations.d.ts +2 -2
  46. package/types/document.d.ts +21 -4
  47. package/types/expressions.d.ts +16 -0
  48. package/types/index.d.ts +13 -10
  49. package/types/inferhydrateddoctype.d.ts +1 -1
  50. package/types/inferrawdoctype.d.ts +6 -3
  51. package/types/inferschematype.d.ts +10 -2
  52. package/types/models.d.ts +28 -11
  53. package/types/mongooseoptions.d.ts +9 -1
  54. package/types/populate.d.ts +8 -0
  55. package/types/query.d.ts +13 -3
  56. package/types/schemaoptions.d.ts +5 -0
  57. package/types/schematypes.d.ts +10 -0
  58. package/types/tracing.d.ts +10 -0
  59. package/types/utility.d.ts +9 -0
@@ -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
+ }
@@ -98,6 +98,10 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
98
98
  moveImmutableProperties(schema, obj, context);
99
99
  }
100
100
 
101
+ if (obj?.$__ && typeof obj.toObject === 'function') {
102
+ obj = obj.toObject(internalToObjectOptions);
103
+ }
104
+
101
105
  const ops = Object.keys(obj);
102
106
  let i = ops.length;
103
107
  const ret = {};
@@ -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;
@@ -82,7 +82,9 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio
82
82
 
83
83
  if (!skipCreatedAt && createdAt) {
84
84
  const overwriteImmutable = get(options, 'overwriteImmutable', false);
85
- const hasUserCreatedAt = currentUpdate[createdAt] != null || currentUpdate.$set?.[createdAt] != null;
85
+ const hasUserCreatedAt = currentUpdate[createdAt] != null
86
+ || currentUpdate.$set?.[createdAt] != null
87
+ || currentUpdate.$setOnInsert?.[createdAt] != null;
86
88
 
87
89
  // If overwriteImmutable is true and user provided createdAt, keep their value
88
90
  if (overwriteImmutable && hasUserCreatedAt) {
@@ -91,7 +93,7 @@ function applyTimestampsToUpdate(now, createdAt, updatedAt, currentUpdate, optio
91
93
  updates.$set[createdAt] = currentUpdate[createdAt];
92
94
  delete currentUpdate[createdAt];
93
95
  }
94
- // User's value is already in $set, nothing more to do
96
+ // User's value is already in $set or $setOnInsert, nothing more to do
95
97
  } else {
96
98
  if (currentUpdate[createdAt]) {
97
99
  delete currentUpdate[createdAt];
package/lib/model.js CHANGED
@@ -7,6 +7,10 @@
7
7
  const Aggregate = require('./aggregate');
8
8
  const ChangeStream = require('./cursor/changeStream');
9
9
  const Document = require('./document');
10
+ const { createTracedChannel } = require('./tracing');
11
+ const { trace: traceSave } = createTracedChannel('mongoose:model:save');
12
+ const { trace: traceInsertMany } = createTracedChannel('mongoose:model:insertMany');
13
+ const { trace: traceBulkWrite } = createTracedChannel('mongoose:model:bulkWrite');
10
14
  const DocumentNotFoundError = require('./error/notFound');
11
15
  const EventEmitter = require('events').EventEmitter;
12
16
  const Kareem = require('kareem');
@@ -37,6 +41,7 @@ const applyVirtualsHelper = require('./helpers/document/applyVirtuals');
37
41
  const assignVals = require('./helpers/populate/assignVals');
38
42
  const castBulkWrite = require('./helpers/model/castBulkWrite');
39
43
  const clone = require('./helpers/clone');
44
+ const convertErrorToStandardSchemaIssues = require('./standardSchema/convertErrorToIssues');
40
45
  const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
41
46
  const decorateUpdateWithVersionKey = require('./helpers/update/decorateUpdateWithVersionKey');
42
47
  const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
@@ -66,6 +71,7 @@ const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscrim
66
71
  const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths');
67
72
  const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField');
68
73
  const setDottedPath = require('./helpers/path/setDottedPath');
74
+ const splitPopulateQuery = require('./helpers/populate/splitPopulateQuery');
69
75
  const { buildMiddlewareFilter } = require('./helpers/buildMiddlewareFilter');
70
76
  const util = require('util');
71
77
  const utils = require('./utils');
@@ -663,19 +669,29 @@ Model.prototype.save = async function save(options) {
663
669
 
664
670
  this.$__.saveOptions = options;
665
671
 
666
- try {
667
- await this.$__save(options);
668
- } catch (error) {
669
- this.$__handleReject(error);
670
- throw error;
671
- } finally {
672
- this.$__.saving = null;
673
- this.$__.saveOptions = null;
674
- this.$__.$versionError = null;
675
- this.$op = null;
676
- }
677
-
678
- return this;
672
+ const _this = this;
673
+ return traceSave(async function maybeTracedSave() {
674
+ try {
675
+ await _this.$__save(options);
676
+ } catch (error) {
677
+ _this.$__handleReject(error);
678
+ throw error;
679
+ } finally {
680
+ _this.$__.saving = null;
681
+ _this.$__.saveOptions = null;
682
+ _this.$__.$versionError = null;
683
+ _this.$op = null;
684
+ }
685
+
686
+ return _this;
687
+ }, () => ({
688
+ operation: 'save',
689
+ collection: _this.constructor.collection.name,
690
+ database: _this.constructor.db?.name,
691
+ serverAddress: _this.constructor.db?.host,
692
+ serverPort: _this.constructor.db?.port,
693
+ args: { options }
694
+ }));
679
695
  };
680
696
 
681
697
  Model.prototype.$save = Model.prototype.save;
@@ -2325,7 +2341,7 @@ Model.where = function where(path, val) {
2325
2341
  *
2326
2342
  * Sometimes you need to query for things in mongodb using a JavaScript expression. You can do so via `find({ $where: javascript })`, or you can use the mongoose shortcut method $where via a Query chain or from your mongoose Model.
2327
2343
  *
2328
- * Blog.$where('this.username.indexOf("val") !== -1').exec(function (err, docs) {});
2344
+ * const result = await Blog.$where('this.username.indexOf("val") !== -1').exec();
2329
2345
  *
2330
2346
  * @param {string|Function} argument is a javascript string or anonymous function
2331
2347
  * @method $where
@@ -2418,13 +2434,6 @@ Model.findOneAndUpdate = function(conditions, update, options) {
2418
2434
  fields = options.fields || options.projection;
2419
2435
  }
2420
2436
 
2421
- update = clone(update, {
2422
- depopulate: true,
2423
- _isNested: true
2424
- });
2425
-
2426
- decorateUpdateWithVersionKey(update, options, this.schema.options.versionKey);
2427
-
2428
2437
  const mq = new this.Query({}, {}, this, this.$__collection);
2429
2438
  mq.select(fields);
2430
2439
 
@@ -2864,6 +2873,10 @@ Model.create = async function create(doc, options) {
2864
2873
  Model.insertOne = async function insertOne(doc, options) {
2865
2874
  _checkContext(this, 'insertOne');
2866
2875
 
2876
+ if (doc == null || typeof doc !== 'object') {
2877
+ throw new ObjectParameterError(doc, 'doc', 'insertOne');
2878
+ }
2879
+
2867
2880
  const discriminatorKey = this.schema.options.discriminatorKey;
2868
2881
  const Model = this.discriminators && doc[discriminatorKey] != null ?
2869
2882
  this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this.discriminators, doc[discriminatorKey]) :
@@ -3018,6 +3031,18 @@ Model.insertMany = async function insertMany(arr, options) {
3018
3031
  throw new MongooseError('Model.insertMany() no longer accepts a callback');
3019
3032
  }
3020
3033
 
3034
+ const ThisModel = this;
3035
+ return traceInsertMany(function maybeTracedInsertMany() { return _insertMany.call(ThisModel, arr, options); }, () => ({
3036
+ operation: 'insertMany',
3037
+ collection: ThisModel.collection.name,
3038
+ database: ThisModel.db?.name,
3039
+ serverAddress: ThisModel.db?.host,
3040
+ serverPort: ThisModel.db?.port,
3041
+ args: { docs: arr, options }
3042
+ }));
3043
+ };
3044
+
3045
+ async function _insertMany(arr, options) {
3021
3046
  options = options || {};
3022
3047
  const preFilter = buildMiddlewareFilter(options, 'pre');
3023
3048
  const postFilter = buildMiddlewareFilter(options, 'post');
@@ -3253,7 +3278,7 @@ Model.insertMany = async function insertMany(arr, options) {
3253
3278
 
3254
3279
  const [result] = await this._middleware.execPost('insertMany', this, [docAttributes], { filter: postFilter });
3255
3280
  return result;
3256
- };
3281
+ }
3257
3282
 
3258
3283
  /*!
3259
3284
  * ignore
@@ -3381,6 +3406,19 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3381
3406
  typeof arguments[2] === 'function') {
3382
3407
  throw new MongooseError('Model.bulkWrite() no longer accepts a callback');
3383
3408
  }
3409
+
3410
+ const ThisModel = this;
3411
+ return traceBulkWrite(function maybeTracedBulkWrite() { return _bulkWrite.call(ThisModel, ops, options); }, () => ({
3412
+ operation: 'bulkWrite',
3413
+ collection: ThisModel.collection.name,
3414
+ database: ThisModel.db?.name,
3415
+ serverAddress: ThisModel.db?.host,
3416
+ serverPort: ThisModel.db?.port,
3417
+ args: { ops, options }
3418
+ }));
3419
+ };
3420
+
3421
+ async function _bulkWrite(ops, options) {
3384
3422
  options = options || {};
3385
3423
  const preFilter = buildMiddlewareFilter(options, 'pre');
3386
3424
  const postFilter = buildMiddlewareFilter(options, 'post');
@@ -3519,7 +3557,7 @@ Model.bulkWrite = async function bulkWrite(ops, options) {
3519
3557
  await this.hooks.execPost('bulkWrite', this, [res], { filter: postFilter });
3520
3558
 
3521
3559
  return res;
3522
- };
3560
+ }
3523
3561
 
3524
3562
  /**
3525
3563
  * Takes an array of documents, gets the changes and inserts/updates documents in the database
@@ -3618,12 +3656,16 @@ async function buildPreSavePromise(document, options) {
3618
3656
  }
3619
3657
 
3620
3658
  async function handleSuccessfulWrite(document, options) {
3621
- if (document.$isNew) {
3659
+ const wasNew = document.$isNew;
3660
+ if (wasNew) {
3622
3661
  _setIsNew(document, false);
3623
3662
  }
3624
3663
 
3625
3664
  document.$__reset();
3626
- document._applyVersionIncrement();
3665
+ // version key gets initialized to 0 when inserting a new document so avoid incrementing version key here
3666
+ if (!wasNew) {
3667
+ document._applyVersionIncrement();
3668
+ }
3627
3669
  const postFilter = buildMiddlewareFilter(options, 'post');
3628
3670
  return document.schema.s.hooks.execPost('save', document, [document], { filter: postFilter });
3629
3671
  }
@@ -3864,7 +3906,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3864
3906
  throw new MongooseError(`documents.${i} was not a mongoose document, documents must be an array of mongoose documents (instanceof mongoose.Document).`);
3865
3907
  }
3866
3908
  if (options.validateBeforeSave == null || options.validateBeforeSave) {
3867
- const err = document.validateSync();
3909
+ const err = document.$__validateSync();
3868
3910
  if (err != null) {
3869
3911
  throw err;
3870
3912
  }
@@ -3873,6 +3915,12 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3873
3915
 
3874
3916
  const isANewDocument = document.isNew;
3875
3917
  if (isANewDocument) {
3918
+ // Like `$__version()` on insert, initialize the version key so
3919
+ // inserted documents get the same shape as `save()` gives them
3920
+ const versionKey = document.$__schema.options.versionKey;
3921
+ if (versionKey) {
3922
+ document.$__setValue(versionKey, 0);
3923
+ }
3876
3924
  const writeOperation = { insertOne: { document } };
3877
3925
  utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
3878
3926
  return writeOperation;
@@ -4191,13 +4239,24 @@ Model.aggregate = function aggregate(pipeline, options) {
4191
4239
  * age: { type: Number, required: true }
4192
4240
  * });
4193
4241
  *
4242
+ * // Succeeds
4243
+ * await Model.validate({ name: 'John Smith', age: 31 });
4244
+ *
4194
4245
  * try {
4195
- * await Model.validate({ name: null }, ['name'])
4246
+ * await Model.validate({ name: null });
4196
4247
  * } catch (err) {
4197
4248
  * err instanceof mongoose.Error.ValidationError; // true
4198
4249
  * Object.keys(err.errors); // ['name']
4199
4250
  * }
4200
4251
  *
4252
+ * Note: the `pathsToSkip` and `pathsToValidate` options **only** apply to validation, not
4253
+ * casting. This function will still throw an error for values that cannot be casted to the
4254
+ * schema-specified type. Remove any paths you do not want to cast.
4255
+ *
4256
+ * // The following will still throw an error because the value of `age` cannot be
4257
+ * // casted to a number. Remove the `age` property before calling `validate()`.
4258
+ * await Model.validate({ name: 'Test', age: 'not a number' }, ['name']);
4259
+ *
4201
4260
  * @param {object} obj
4202
4261
  * @param {object|Array|string} pathsOrOptions
4203
4262
  * @param {object} [context]
@@ -4302,6 +4361,33 @@ Model.validate = async function validate(obj, pathsOrOptions, context) {
4302
4361
  return obj;
4303
4362
  };
4304
4363
 
4364
+ /**
4365
+ * Standard Schema adapter for this model.
4366
+ * Calls [`Model.validate()`](https://mongoosejs.com/docs/api/model.html#Model.validate()) internally with the provided `libraryOptions`
4367
+ * to cast and validate the given value.
4368
+ *
4369
+ * @api public
4370
+ * @property ~standard
4371
+ * @memberOf Model
4372
+ * @static
4373
+ */
4374
+
4375
+ Object.defineProperty(Model, '~standard', {
4376
+ configurable: true,
4377
+ get() {
4378
+ return {
4379
+ version: 1,
4380
+ vendor: 'mongoose',
4381
+ validate: (value, options) => {
4382
+ return this.validate(value, options?.libraryOptions).then(
4383
+ value => ({ value }),
4384
+ error => ({ issues: convertErrorToStandardSchemaIssues(error) })
4385
+ );
4386
+ }
4387
+ };
4388
+ }
4389
+ });
4390
+
4305
4391
  /**
4306
4392
  * Populates document references.
4307
4393
  *
@@ -4468,7 +4554,6 @@ async function _populatePath(model, docs, populateOptions) {
4468
4554
  mod.foreignField.clear();
4469
4555
  mod.foreignField.add(populateOptions.foreignField);
4470
4556
  }
4471
- const match = createPopulateQueryFilter(ids, mod.match, mod.foreignField, mod.model, mod.options.skipInvalidIds);
4472
4557
  if (assignmentOpts.excludeId) {
4473
4558
  // override the exclusion from the query so we can use the _id
4474
4559
  // for document matching during assignment. we'll delete the
@@ -4489,6 +4574,17 @@ async function _populatePath(model, docs, populateOptions) {
4489
4574
  } else if (mod.options.limit != null) {
4490
4575
  assignmentOpts.originalLimit = mod.options.limit;
4491
4576
  }
4577
+
4578
+ // Execute a separate query per document if `perDocumentLimit` is set (gh-7318), or if
4579
+ // there are so many ids that the `$in` filter may overflow MongoDB's 16 MB BSON size
4580
+ // limit on queries (gh-5890).
4581
+ const splitParams = splitPopulateQuery(mod, ids, select, assignmentOpts);
4582
+ if (splitParams != null) {
4583
+ params.push(...splitParams);
4584
+ continue;
4585
+ }
4586
+
4587
+ const match = createPopulateQueryFilter(ids, mod.match, mod.foreignField, mod.model, mod.options.skipInvalidIds);
4492
4588
  params.push([mod, match, select, assignmentOpts]);
4493
4589
  }
4494
4590
  if (!hasOne) {
@@ -4509,6 +4605,9 @@ async function _populatePath(model, docs, populateOptions) {
4509
4605
 
4510
4606
  // Track deferred populates per-param (per model) to avoid mixing them
4511
4607
  const deferredPopulatesPerParam = new Map();
4608
+ // Query results per param. Batches that were split off of a large populate query (gh-5890)
4609
+ // are assigned from only their own query's results rather than the combined `vals`.
4610
+ const valsByParam = [];
4512
4611
 
4513
4612
  if (populateOptions.ordered) {
4514
4613
  // Populate in series, primarily for transactions because MongoDB doesn't support multiple operations on
@@ -4516,7 +4615,10 @@ async function _populatePath(model, docs, populateOptions) {
4516
4615
  for (let i = 0; i < params.length; i++) {
4517
4616
  const arr = params[i];
4518
4617
  const { docs, deferredPopulates } = await _execPopulateQuery.apply(null, arr);
4519
- vals = vals.concat(docs);
4618
+ valsByParam.push(docs);
4619
+ if (!arr[0]._assignFromOwnResults) {
4620
+ vals = vals.concat(docs);
4621
+ }
4520
4622
  if (deferredPopulates.length > 0) {
4521
4623
  deferredPopulatesPerParam.set(i, deferredPopulates);
4522
4624
  }
@@ -4531,7 +4633,10 @@ async function _populatePath(model, docs, populateOptions) {
4531
4633
  const results = await Promise.all(promises);
4532
4634
  for (let i = 0; i < results.length; i++) {
4533
4635
  const { docs, deferredPopulates } = results[i];
4534
- vals = vals.concat(docs);
4636
+ valsByParam.push(docs);
4637
+ if (!params[i][0]._assignFromOwnResults) {
4638
+ vals = vals.concat(docs);
4639
+ }
4535
4640
  if (deferredPopulates.length > 0) {
4536
4641
  deferredPopulatesPerParam.set(i, deferredPopulates);
4537
4642
  }
@@ -4539,13 +4644,15 @@ async function _populatePath(model, docs, populateOptions) {
4539
4644
  }
4540
4645
 
4541
4646
 
4542
- for (const arr of params) {
4647
+ for (let i = 0; i < params.length; i++) {
4648
+ const arr = params[i];
4543
4649
  const mod = arr[0];
4544
4650
  const assignmentOpts = arr[3];
4545
- for (const val of vals) {
4651
+ const valsForMod = mod._assignFromOwnResults ? valsByParam[i] : vals;
4652
+ for (const val of valsForMod) {
4546
4653
  mod.options._childDocs.push(val);
4547
4654
  }
4548
- _assign(model, vals, mod, assignmentOpts);
4655
+ _assign(model, valsForMod, mod, assignmentOpts);
4549
4656
  }
4550
4657
 
4551
4658
  // Handle deferred populate for cases with per-document match functions.
@@ -4585,13 +4692,15 @@ async function _populatePath(model, docs, populateOptions) {
4585
4692
  }
4586
4693
  }
4587
4694
 
4588
- for (const arr of params) {
4589
- removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
4695
+ for (let i = 0; i < params.length; i++) {
4696
+ const mod = params[i][0];
4697
+ removeDeselectedForeignField(mod.foreignField, mod.options, mod._assignFromOwnResults ? valsByParam[i] : vals);
4590
4698
  }
4591
- for (const arr of params) {
4592
- const mod = arr[0];
4699
+ for (let i = 0; i < params.length; i++) {
4700
+ const mod = params[i][0];
4593
4701
  if (mod.options?.options?._leanTransform) {
4594
- for (const doc of vals) {
4702
+ const valsForMod = mod._assignFromOwnResults ? valsByParam[i] : vals;
4703
+ for (const doc of valsForMod) {
4595
4704
  mod.options.options._leanTransform(doc);
4596
4705
  }
4597
4706
  }
@@ -4603,6 +4712,12 @@ async function _populatePath(model, docs, populateOptions) {
4603
4712
  */
4604
4713
 
4605
4714
  function _execPopulateQuery(mod, match, select) {
4715
+ // `null` match means `mod`'s documents have no ids to query, possible if a large populate
4716
+ // was split into a query per document (gh-5890). Skip executing a query: `_assign()` still
4717
+ // sets the documents' populated paths to the appropriate default value.
4718
+ if (match == null) {
4719
+ return Promise.resolve({ docs: [], deferredPopulates: [] });
4720
+ }
4606
4721
  let subPopulate = clone(mod.options.populate);
4607
4722
  const queryOptions = {};
4608
4723
  if (mod.options.skip !== undefined) {
package/lib/mongoose.js CHANGED
@@ -228,7 +228,7 @@ Mongoose.prototype.setDriver = function setDriver(driver) {
228
228
  * - `bufferCommands`: enable/disable mongoose's buffering mechanism for all connections and models
229
229
  * - `bufferTimeoutMS`: If bufferCommands is on, this option sets the maximum amount of time Mongoose buffering will wait before throwing an error. If not specified, Mongoose will use 10000 (10 seconds).
230
230
  * - `cloneSchemas`: `false` by default. Set to `true` to `clone()` all schemas before compiling into a model.
231
- * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
231
+ * - `debug`: If `true`, prints the operations mongoose sends to MongoDB to the console. If an object is passed, you can set `color`, `shell`, and `timestamp` options. If `timestamp` is `true`, Mongoose prefixes console debug output with an ISO timestamp in brackets. If a writable stream is passed, it will log to that stream, without colorization. If a callback function is passed, it will receive the collection name, the method name, then all arguments passed to the method. For example, if you wanted to replicate the default logging, you could output from the callback `Mongoose: ${collectionName}.${methodName}(${methodArgs.join(', ')})`.
232
232
  * - `id`: If `true`, adds a `id` virtual to all schemas unless overwritten on a per-schema basis.
233
233
  * - `maxTimeMS`: If set, attaches [maxTimeMS](https://www.mongodb.com/docs/manual/reference/operator/meta/maxTimeMS/) to every query
234
234
  * - `objectIdGetter`: `true` by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter.
@@ -398,8 +398,8 @@ Mongoose.prototype.get = Mongoose.prototype.set;
398
398
  * @param {string} [options.user] username for authentication, equivalent to `options.auth.username`. Maintained for backwards compatibility.
399
399
  * @param {string} [options.pass] password for authentication, equivalent to `options.auth.password`. Maintained for backwards compatibility.
400
400
  * @param {boolean} [options.autoIndex=true] Mongoose-specific option. Set to false to disable automatic index creation for all models associated with this connection.
401
- * @param {number} [options.maxPoolSize=5] The maximum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
402
- * @param {number} [options.minPoolSize=1] The minimum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
401
+ * @param {number} [options.maxPoolSize=100] The maximum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
402
+ * @param {number} [options.minPoolSize=0] The minimum number of sockets the MongoDB driver will keep open for this connection. Keep in mind that MongoDB only allows one operation per socket at a time, so you may want to increase this if you find you have a few slow queries that are blocking faster queries from proceeding. See [Slow Trains in MongoDB and Node.js](https://thecodebarbarian.com/slow-trains-in-mongodb-and-nodejs).
403
403
  * @param {number} [options.socketTimeoutMS=0] How long the MongoDB driver will wait before killing a socket due to inactivity _after initial connection_. Defaults to 0, which means Node.js will not time out the socket due to inactivity. A socket may be inactive because of either no activity or a long-running operation. This option is passed to [Node.js `socket#setTimeout()` function](https://nodejs.org/api/net.html#net_socket_settimeout_timeout_callback) after the MongoDB driver successfully completes.
404
404
  * @param {number} [options.family=0] Passed transparently to [Node.js' `dns.lookup()`](https://nodejs.org/api/dns.html#dns_dns_lookup_hostname_options_callback) function. May be either `0`, `4`, or `6`. `4` means use IPv4 only, `6` means use IPv6 only, `0` means try both.
405
405
  * @return {Connection} the created Connection object. Connections are not thenable, so you can't do `await mongoose.createConnection()`. To await use `mongoose.createConnection(uri).asPromise()` instead.
@@ -94,6 +94,20 @@ Object.defineProperty(SchemaTypeOptions.prototype, 'cast', opts);
94
94
 
95
95
  Object.defineProperty(SchemaTypeOptions.prototype, 'required', opts);
96
96
 
97
+ /**
98
+ * Controls whether this path may be set to `null`. By default, Mongoose allows
99
+ * `null` for non-required paths. Set `allowNull: false` to allow `undefined`
100
+ * but disallow `null`.
101
+ *
102
+ * @api public
103
+ * @property allowNull
104
+ * @memberOf SchemaTypeOptions
105
+ * @type {boolean}
106
+ * @instance
107
+ */
108
+
109
+ Object.defineProperty(SchemaTypeOptions.prototype, 'allowNull', opts);
110
+
97
111
  /**
98
112
  * The default value for this path. If a function, Mongoose executes the function
99
113
  * and uses the return value as the default.
@@ -16,7 +16,10 @@ module.exports = function saveSubdocs(schema) {
16
16
  };
17
17
 
18
18
 
19
- async function saveSubdocsPreSave() {
19
+ // These hooks are deliberately not `async` so that they don't allocate a
20
+ // promise and force an extra microtask hop on every `save()` when there are
21
+ // no subdocuments. kareem only awaits hooks that return a promise.
22
+ function saveSubdocsPreSave() {
20
23
  if (this.$isSubdocument) {
21
24
  return;
22
25
  }
@@ -28,15 +31,15 @@ async function saveSubdocsPreSave() {
28
31
  }
29
32
 
30
33
  const options = this.$__.saveOptions;
31
- await Promise.all(subdocs.map(subdoc => subdoc._execDocumentPreHooks('save', options, [options])));
32
-
33
- // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments
34
- if (this.$__.saveOptions) {
35
- this.$__.saveOptions.__subdocs = null;
36
- }
34
+ return Promise.all(subdocs.map(subdoc => subdoc._execDocumentPreHooks('save', options, [options]))).then(() => {
35
+ // Invalidate subdocs cache because subdoc pre hooks can add new subdocuments
36
+ if (this.$__.saveOptions) {
37
+ this.$__.saveOptions.__subdocs = null;
38
+ }
39
+ });
37
40
  }
38
41
 
39
- async function saveSubdocsPostSave() {
42
+ function saveSubdocsPostSave() {
40
43
  if (this.$isSubdocument) {
41
44
  return;
42
45
  }
@@ -53,10 +56,10 @@ async function saveSubdocsPostSave() {
53
56
  promises.push(subdoc._execDocumentPostHooks('save', options));
54
57
  }
55
58
 
56
- await Promise.all(promises);
59
+ return Promise.all(promises);
57
60
  }
58
61
 
59
- async function saveSubdocsPreDeleteOne() {
62
+ function saveSubdocsPreDeleteOne() {
60
63
  const removedSubdocs = this.$__.removedSubdocs;
61
64
  if (!removedSubdocs?.length) {
62
65
  return;
@@ -68,10 +71,10 @@ async function saveSubdocsPreDeleteOne() {
68
71
  promises.push(subdoc._execDocumentPreHooks('deleteOne', options));
69
72
  }
70
73
 
71
- await Promise.all(promises);
74
+ return Promise.all(promises);
72
75
  }
73
76
 
74
- async function saveSubdocsPostDeleteOne() {
77
+ function saveSubdocsPostDeleteOne() {
75
78
  const removedSubdocs = this.$__.removedSubdocs;
76
79
  if (!removedSubdocs?.length) {
77
80
  return;
@@ -84,7 +87,7 @@ async function saveSubdocsPostDeleteOne() {
84
87
  }
85
88
 
86
89
  this.$__.removedSubdocs = null;
87
- await Promise.all(promises);
90
+ return Promise.all(promises);
88
91
  }
89
92
 
90
93