mongoose 6.4.7 → 6.5.2

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 (47) hide show
  1. package/lib/connection.js +23 -3
  2. package/lib/cursor/ChangeStream.js +39 -1
  3. package/lib/document.js +129 -154
  4. package/lib/error/index.js +1 -0
  5. package/lib/error/validation.js +9 -0
  6. package/lib/helpers/document/applyDefaults.js +115 -0
  7. package/lib/helpers/document/cleanModifiedSubpaths.js +3 -3
  8. package/lib/helpers/document/compile.js +7 -6
  9. package/lib/helpers/firstKey.js +8 -0
  10. package/lib/helpers/model/applyDefaultsToPOJO.js +52 -0
  11. package/lib/helpers/model/castBulkWrite.js +1 -1
  12. package/lib/helpers/model/pushNestedArrayPaths.js +15 -0
  13. package/lib/helpers/populate/markArraySubdocsPopulated.js +1 -0
  14. package/lib/helpers/projection/hasIncludedChildren.js +1 -0
  15. package/lib/helpers/promiseOrCallback.js +24 -15
  16. package/lib/helpers/update/applyTimestampsToChildren.js +6 -2
  17. package/lib/index.js +2 -1
  18. package/lib/internal.js +3 -1
  19. package/lib/model.js +237 -95
  20. package/lib/options/SchemaArrayOptions.js +19 -0
  21. package/lib/options/SchemaNumberOptions.js +2 -0
  22. package/lib/options/SchemaObjectIdOptions.js +1 -0
  23. package/lib/plugins/trackTransaction.js +2 -2
  24. package/lib/query.js +30 -11
  25. package/lib/schema/SubdocumentPath.js +10 -0
  26. package/lib/schema/array.js +2 -1
  27. package/lib/schema/documentarray.js +14 -1
  28. package/lib/schema/string.js +3 -0
  29. package/lib/schema.js +22 -3
  30. package/lib/schematype.js +5 -1
  31. package/lib/statemachine.js +23 -9
  32. package/lib/types/buffer.js +23 -21
  33. package/lib/types/map.js +2 -0
  34. package/lib/utils.js +8 -0
  35. package/lib/validoptions.js +1 -0
  36. package/package.json +14 -14
  37. package/{build-browser.js → scripts/build-browser.js} +1 -1
  38. package/types/connection.d.ts +7 -1
  39. package/types/document.d.ts +8 -1
  40. package/types/expressions.d.ts +1 -1
  41. package/types/index.d.ts +31 -28
  42. package/types/inferschematype.d.ts +3 -20
  43. package/types/models.d.ts +53 -49
  44. package/types/mongooseoptions.d.ts +6 -0
  45. package/types/schemaoptions.d.ts +15 -4
  46. package/types/utility.d.ts +19 -0
  47. package/types/virtuals.d.ts +14 -0
package/lib/model.js CHANGED
@@ -22,6 +22,8 @@ const ServerSelectionError = require('./error/serverSelection');
22
22
  const ValidationError = require('./error/validation');
23
23
  const VersionError = require('./error/version');
24
24
  const ParallelSaveError = require('./error/parallelSave');
25
+ const applyDefaultsHelper = require('./helpers/document/applyDefaults');
26
+ const applyDefaultsToPOJO = require('./helpers/model/applyDefaultsToPOJO');
25
27
  const applyQueryMiddleware = require('./helpers/query/applyQueryMiddleware');
26
28
  const applyHooks = require('./helpers/model/applyHooks');
27
29
  const applyMethods = require('./helpers/model/applyMethods');
@@ -35,6 +37,7 @@ const castBulkWrite = require('./helpers/model/castBulkWrite');
35
37
  const createPopulateQueryFilter = require('./helpers/populate/createPopulateQueryFilter');
36
38
  const getDefaultBulkwriteResult = require('./helpers/getDefaultBulkwriteResult');
37
39
  const discriminator = require('./helpers/model/discriminator');
40
+ const firstKey = require('./helpers/firstKey');
38
41
  const each = require('./helpers/each');
39
42
  const get = require('./helpers/get');
40
43
  const getConstructorName = require('./helpers/getConstructorName');
@@ -54,6 +57,7 @@ const leanPopulateMap = require('./helpers/populate/leanPopulateMap');
54
57
  const modifiedPaths = require('./helpers/update/modifiedPaths');
55
58
  const parallelLimit = require('./helpers/parallelLimit');
56
59
  const prepareDiscriminatorPipeline = require('./helpers/aggregate/prepareDiscriminatorPipeline');
60
+ const pushNestedArrayPaths = require('./helpers/model/pushNestedArrayPaths');
57
61
  const removeDeselectedForeignField = require('./helpers/populate/removeDeselectedForeignField');
58
62
  const setDottedPath = require('./helpers/path/setDottedPath');
59
63
  const util = require('util');
@@ -589,6 +593,7 @@ function operand(self, where, delta, data, val, op) {
589
593
  case '$pullAll':
590
594
  case '$push':
591
595
  case '$addToSet':
596
+ case '$inc':
592
597
  break;
593
598
  default:
594
599
  // nothing to do
@@ -764,15 +769,21 @@ Model.prototype.$__delta = function() {
764
769
  value = value.toObject();
765
770
  operand(this, where, delta, data, value);
766
771
  } else {
767
- value = utils.clone(value, {
768
- depopulate: true,
769
- transform: false,
770
- virtuals: false,
771
- getters: false,
772
- omitUndefined: true,
773
- _isNested: true
774
- });
775
- operand(this, where, delta, data, value);
772
+ if (this.$__.primitiveAtomics && this.$__.primitiveAtomics[data.path] != null) {
773
+ const val = this.$__.primitiveAtomics[data.path];
774
+ const op = firstKey(val);
775
+ operand(this, where, delta, data, val[op], op);
776
+ } else {
777
+ value = utils.clone(value, {
778
+ depopulate: true,
779
+ transform: false,
780
+ virtuals: false,
781
+ getters: false,
782
+ omitUndefined: true,
783
+ _isNested: true
784
+ });
785
+ operand(this, where, delta, data, value);
786
+ }
776
787
  }
777
788
  }
778
789
 
@@ -3109,10 +3120,9 @@ Model.create = function create(doc, options, callback) {
3109
3120
  }
3110
3121
  const _done = (error, res) => {
3111
3122
  const savedDocs = [];
3112
- const len = res.length;
3113
- for (let i = 0; i < len; ++i) {
3114
- if (res[i].doc) {
3115
- savedDocs.push(res[i].doc);
3123
+ for (const val of res) {
3124
+ if (val.doc) {
3125
+ savedDocs.push(val.doc);
3116
3126
  }
3117
3127
  }
3118
3128
 
@@ -3167,6 +3177,7 @@ Model.create = function create(doc, options, callback) {
3167
3177
  *
3168
3178
  * @param {Array} [pipeline]
3169
3179
  * @param {Object} [options] see the [mongodb driver options](https://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#watch)
3180
+ * @param {Boolean} [options.hydrate=false] if true and `fullDocument: 'updateLookup'` is set, Mongoose will automatically hydrate `fullDocument` into a fully fledged Mongoose document
3170
3181
  * @return {ChangeStream} mongoose-specific change stream wrapper, inherits from EventEmitter
3171
3182
  * @api public
3172
3183
  */
@@ -3191,6 +3202,9 @@ Model.watch = function(pipeline, options) {
3191
3202
  }
3192
3203
  };
3193
3204
 
3205
+ options = options || {};
3206
+ options.model = this;
3207
+
3194
3208
  return new ChangeStream(changeStreamThunk, pipeline, options);
3195
3209
  };
3196
3210
 
@@ -3560,13 +3574,17 @@ Model.bulkWrite = function(ops, options, callback) {
3560
3574
  return cb(null, getDefaultBulkwriteResult());
3561
3575
  }
3562
3576
 
3563
- this.$__collection.bulkWrite(ops, options, (error, res) => {
3564
- if (error) {
3565
- return cb(error);
3566
- }
3577
+ try {
3578
+ this.$__collection.bulkWrite(ops, options, (error, res) => {
3579
+ if (error) {
3580
+ return cb(error);
3581
+ }
3567
3582
 
3568
- cb(null, res);
3569
- });
3583
+ cb(null, res);
3584
+ });
3585
+ } catch (err) {
3586
+ return cb(err);
3587
+ }
3570
3588
  });
3571
3589
  }, this.events);
3572
3590
  };
@@ -3579,41 +3597,50 @@ Model.bulkWrite = function(ops, options, callback) {
3579
3597
  *
3580
3598
  * @param {Array<Document>} documents
3581
3599
  * @param {Object} [options] options passed to the underlying `bulkWrite()`
3600
+ * @param {Boolean} [options.timestamps] defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
3582
3601
  * @param {ClientSession} [options.session=null] The session associated with this bulk write. See [transactions docs](/docs/transactions.html).
3583
3602
  * @param {String|number} [options.w=1] The [write concern](https://docs.mongodb.com/manual/reference/write-concern/). See [`Query#w()`](/docs/api.html#query_Query-w) for more information.
3584
3603
  * @param {number} [options.wtimeout=null] The [write concern timeout](https://docs.mongodb.com/manual/reference/write-concern/#wtimeout).
3585
3604
  * @param {Boolean} [options.j=true] If false, disable [journal acknowledgement](https://docs.mongodb.com/manual/reference/write-concern/#j-option)
3586
3605
  *
3587
3606
  */
3588
- Model.bulkSave = function(documents, options) {
3589
- const preSavePromises = documents.map(buildPreSavePromise);
3590
-
3591
- const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true });
3592
-
3593
- let bulkWriteResultPromise;
3594
- return Promise.all(preSavePromises)
3595
- .then(() => bulkWriteResultPromise = this.bulkWrite(writeOperations, options))
3596
- .then(() => documents.map(buildSuccessfulWriteHandlerPromise))
3597
- .then(() => bulkWriteResultPromise)
3598
- .catch((err) => {
3599
- if (!(err && err.writeErrors && err.writeErrors.length)) {
3600
- throw err;
3601
- }
3602
- return Promise.all(
3603
- documents.map((document) => {
3604
- const documentError = err.writeErrors.find(writeError => {
3605
- const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3606
- return writeErrorDocumentId.toString() === document._id.toString();
3607
- });
3607
+ Model.bulkSave = async function(documents, options) {
3608
+ options = options || {};
3608
3609
 
3609
- if (documentError == null) {
3610
- return buildSuccessfulWriteHandlerPromise(document);
3611
- }
3612
- })
3613
- ).then(() => {
3614
- throw err;
3610
+ const writeOperations = this.buildBulkWriteOperations(documents, { skipValidation: true, timestamps: options.timestamps });
3611
+
3612
+ if (options.timestamps != null) {
3613
+ for (const document of documents) {
3614
+ document.$__.saveOptions = document.$__.saveOptions || {};
3615
+ document.$__.saveOptions.timestamps = options.timestamps;
3616
+ }
3617
+ }
3618
+
3619
+ await Promise.all(documents.map(buildPreSavePromise));
3620
+
3621
+ const { bulkWriteResult, bulkWriteError } = await this.bulkWrite(writeOperations, options).then(
3622
+ (res) => ({ bulkWriteResult: res, bulkWriteError: null }),
3623
+ (err) => ({ bulkWriteResult: null, bulkWriteError: err })
3624
+ );
3625
+
3626
+ await Promise.all(
3627
+ documents.map(async(document) => {
3628
+ const documentError = bulkWriteError && bulkWriteError.writeErrors.find(writeError => {
3629
+ const writeErrorDocumentId = writeError.err.op._id || writeError.err.op.q._id;
3630
+ return writeErrorDocumentId.toString() === document._id.toString();
3615
3631
  });
3616
- });
3632
+
3633
+ if (documentError == null) {
3634
+ await handleSuccessfulWrite(document);
3635
+ }
3636
+ })
3637
+ );
3638
+
3639
+ if (bulkWriteError && bulkWriteError.writeErrors && bulkWriteError.writeErrors.length) {
3640
+ throw bulkWriteError;
3641
+ }
3642
+
3643
+ return bulkWriteResult;
3617
3644
  };
3618
3645
 
3619
3646
  function buildPreSavePromise(document) {
@@ -3628,26 +3655,138 @@ function buildPreSavePromise(document) {
3628
3655
  });
3629
3656
  }
3630
3657
 
3631
- function buildSuccessfulWriteHandlerPromise(document) {
3658
+ function handleSuccessfulWrite(document) {
3632
3659
  return new Promise((resolve, reject) => {
3633
- handleSuccessfulWrite(document, resolve, reject);
3660
+ if (document.$isNew) {
3661
+ _setIsNew(document, false);
3662
+ }
3663
+
3664
+ document.$__reset();
3665
+ document.schema.s.hooks.execPost('save', document, {}, (err) => {
3666
+ if (err) {
3667
+ reject(err);
3668
+ return;
3669
+ }
3670
+ resolve();
3671
+ });
3672
+
3634
3673
  });
3635
3674
  }
3636
3675
 
3637
- function handleSuccessfulWrite(document, resolve, reject) {
3638
- if (document.$isNew) {
3639
- _setIsNew(document, false);
3676
+ /**
3677
+ * Apply defaults to the given document or POJO.
3678
+ *
3679
+ * @param {Object|Document} obj object or document to apply defaults on
3680
+ * @returns {Object|Document}
3681
+ * @api public
3682
+ */
3683
+
3684
+ Model.applyDefaults = function applyDefaults(doc) {
3685
+ if (doc.$__ != null) {
3686
+ applyDefaultsHelper(doc, doc.$__.fields, doc.$__.exclude);
3687
+
3688
+ for (const subdoc of doc.$getAllSubdocs()) {
3689
+ applyDefaults(subdoc, subdoc.$__.fields, subdoc.$__.exclude);
3690
+ }
3691
+
3692
+ return doc;
3640
3693
  }
3641
3694
 
3642
- document.$__reset();
3643
- document.schema.s.hooks.execPost('save', document, {}, (err) => {
3644
- if (err) {
3645
- reject(err);
3646
- return;
3695
+ applyDefaultsToPOJO(doc, this.schema);
3696
+
3697
+ return doc;
3698
+ };
3699
+
3700
+ /**
3701
+ * Cast the given POJO to the model's schema
3702
+ *
3703
+ * #### Example:
3704
+ *
3705
+ * const Test = mongoose.model('Test', Schema({ num: Number }));
3706
+ *
3707
+ * const obj = Test.castObject({ num: '42' });
3708
+ * obj.num; // 42 as a number
3709
+ *
3710
+ * Test.castObject({ num: 'not a number' }); // Throws a ValidationError
3711
+ *
3712
+ * @param {Object} obj object or document to cast
3713
+ * @returns {Object} POJO casted to the model's schema
3714
+ * @throws {ValidationError} if casting failed for at least one path
3715
+ * @api public
3716
+ */
3717
+
3718
+ Model.castObject = function castObject(obj) {
3719
+ const ret = {};
3720
+
3721
+ const schema = this.schema;
3722
+ const paths = Object.keys(schema.paths);
3723
+
3724
+ for (const path of paths) {
3725
+ const schemaType = schema.path(path);
3726
+ if (!schemaType || !schemaType.$isMongooseArray) {
3727
+ continue;
3647
3728
  }
3648
- resolve();
3649
- });
3650
- }
3729
+
3730
+ const val = get(obj, path);
3731
+ pushNestedArrayPaths(paths, val, path);
3732
+ }
3733
+
3734
+ let error = null;
3735
+
3736
+ for (const path of paths) {
3737
+ const schemaType = schema.path(path);
3738
+ if (schemaType == null) {
3739
+ continue;
3740
+ }
3741
+
3742
+ let val = get(obj, path, void 0);
3743
+
3744
+ if (val == null) {
3745
+ continue;
3746
+ }
3747
+
3748
+ const pieces = path.indexOf('.') === -1 ? [path] : path.split('.');
3749
+ let cur = ret;
3750
+ for (let i = 0; i < pieces.length - 1; ++i) {
3751
+ if (cur[pieces[i]] == null) {
3752
+ cur[pieces[i]] = isNaN(pieces[i + 1]) ? {} : [];
3753
+ }
3754
+ cur = cur[pieces[i]];
3755
+ }
3756
+
3757
+ if (schemaType.$isMongooseDocumentArray) {
3758
+ continue;
3759
+ }
3760
+ if (schemaType.$isSingleNested || schemaType.$isMongooseDocumentArrayElement) {
3761
+ try {
3762
+ val = Model.castObject.call(schemaType.caster, val);
3763
+ } catch (err) {
3764
+ error = error || new ValidationError();
3765
+ error.addError(path, err);
3766
+ continue;
3767
+ }
3768
+
3769
+ cur[pieces[pieces.length - 1]] = val;
3770
+ continue;
3771
+ }
3772
+
3773
+ try {
3774
+ val = schemaType.cast(val);
3775
+ cur[pieces[pieces.length - 1]] = val;
3776
+ } catch (err) {
3777
+ error = error || new ValidationError();
3778
+ error.addError(path, err);
3779
+
3780
+ continue;
3781
+ }
3782
+ }
3783
+
3784
+ if (error != null) {
3785
+ throw error;
3786
+ }
3787
+
3788
+ return ret;
3789
+ };
3651
3790
 
3652
3791
  /**
3653
3792
  * Build bulk write operations for `bulkSave()`.
@@ -3655,9 +3794,11 @@ function handleSuccessfulWrite(document, resolve, reject) {
3655
3794
  * @param {Array<Document>} documents The array of documents to build write operations of
3656
3795
  * @param {Object} options
3657
3796
  * @param {Boolean} options.skipValidation defaults to `false`, when set to true, building the write operations will bypass validating the documents.
3797
+ * @param {Boolean} options.timestamps defaults to `null`, when set to false, mongoose will not add/update timestamps to the documents.
3658
3798
  * @return {Array<Promise>} Returns a array of all Promises the function executes to be awaited.
3659
3799
  * @api private
3660
3800
  */
3801
+
3661
3802
  Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, options) {
3662
3803
  if (!Array.isArray(documents)) {
3663
3804
  throw new Error(`bulkSave expects an array of documents to be passed, received \`${documents}\` instead`);
@@ -3678,9 +3819,9 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3678
3819
 
3679
3820
  const isANewDocument = document.isNew;
3680
3821
  if (isANewDocument) {
3681
- accumulator.push({
3682
- insertOne: { document }
3683
- });
3822
+ const writeOperation = { insertOne: { document } };
3823
+ utils.injectTimestampsOption(writeOperation.insertOne, options.timestamps);
3824
+ accumulator.push(writeOperation);
3684
3825
 
3685
3826
  return accumulator;
3686
3827
  }
@@ -3695,13 +3836,9 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3695
3836
  _applyCustomWhere(document, where);
3696
3837
 
3697
3838
  document.$__version(where, delta);
3698
-
3699
- accumulator.push({
3700
- updateOne: {
3701
- filter: where,
3702
- update: changes
3703
- }
3704
- });
3839
+ const writeOperation = { updateOne: { filter: where, update: changes } };
3840
+ utils.injectTimestampsOption(writeOperation.updateOne, options.timestamps);
3841
+ accumulator.push(writeOperation);
3705
3842
 
3706
3843
  return accumulator;
3707
3844
  }
@@ -3720,6 +3857,7 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3720
3857
  }
3721
3858
  };
3722
3859
 
3860
+
3723
3861
  /**
3724
3862
  * Shortcut for creating a new Document from existing raw data, pre-saved in the DB.
3725
3863
  * The document returned has no paths marked as modified initially.
@@ -3731,11 +3869,13 @@ Model.buildBulkWriteOperations = function buildBulkWriteOperations(documents, op
3731
3869
  *
3732
3870
  * @param {Object} obj
3733
3871
  * @param {Object|String|String[]} [projection] optional projection containing which fields should be selected for this document
3872
+ * @param {Object} [options] optional options
3873
+ * @param {Boolean} [options.setters=false] if true, apply schema setters when hydrating
3734
3874
  * @return {Document} document instance
3735
3875
  * @api public
3736
3876
  */
3737
3877
 
3738
- Model.hydrate = function(obj, projection) {
3878
+ Model.hydrate = function(obj, projection, options) {
3739
3879
  _checkContext(this, 'hydrate');
3740
3880
 
3741
3881
  if (projection != null) {
@@ -3746,7 +3886,7 @@ Model.hydrate = function(obj, projection) {
3746
3886
  }
3747
3887
 
3748
3888
  const document = require('./queryhelpers').createModel(this, obj, projection);
3749
- document.$init(obj);
3889
+ document.$init(obj, options);
3750
3890
  return document;
3751
3891
  };
3752
3892
 
@@ -3843,6 +3983,7 @@ Model.update = function update(conditions, doc, options, callback) {
3843
3983
  * and `post('updateMany')` instead.
3844
3984
  *
3845
3985
  * #### Example:
3986
+ *
3846
3987
  * const res = await Person.updateMany({ name: /Stark$/ }, { isDeleted: true });
3847
3988
  * res.matchedCount; // Number of documents matched
3848
3989
  * res.modifiedCount; // Number of documents modified
@@ -3883,6 +4024,7 @@ Model.updateMany = function updateMany(conditions, doc, options, callback) {
3883
4024
  * - Use `replaceOne()` if you want to overwrite an entire document rather than using atomic operators like `$set`.
3884
4025
  *
3885
4026
  * #### Example:
4027
+ *
3886
4028
  * const res = await Person.updateOne({ name: 'Jean-Luc Picard' }, { ship: 'USS Enterprise' });
3887
4029
  * res.matchedCount; // Number of documents matched
3888
4030
  * res.modifiedCount; // Number of documents modified
@@ -3920,6 +4062,7 @@ Model.updateOne = function updateOne(conditions, doc, options, callback) {
3920
4062
  * given document (no atomic operators like `$set`).
3921
4063
  *
3922
4064
  * #### Example:
4065
+ *
3923
4066
  * const res = await Person.replaceOne({ _id: 24601 }, { name: 'Jean Valjean' });
3924
4067
  * res.matchedCount; // Number of documents matched
3925
4068
  * res.modifiedCount; // Number of documents modified
@@ -4049,7 +4192,7 @@ function _update(model, op, conditions, doc, options, callback) {
4049
4192
  * @param {String} [opts.out.reduce] add the results to collectionName: if dups are detected, uses the reducer / finalize functions
4050
4193
  * @param {String} [opts.out.merge] add the results to collectionName: if dups exist the new docs overwrite the old
4051
4194
  * @param {Function} [callback] optional callback
4052
- * @see https://www.mongodb.org/display/DOCS/MapReduce
4195
+ * @see MongoDB MapReduce https://www.mongodb.org/display/DOCS/MapReduce
4053
4196
  * @return {Promise}
4054
4197
  * @api public
4055
4198
  */
@@ -4245,7 +4388,7 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
4245
4388
  }
4246
4389
 
4247
4390
  const val = get(obj, path);
4248
- pushNestedArrayPaths(val, path);
4391
+ pushNestedArrayPaths(paths, val, path);
4249
4392
  }
4250
4393
 
4251
4394
  let remaining = paths.length;
@@ -4258,7 +4401,7 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
4258
4401
  continue;
4259
4402
  }
4260
4403
 
4261
- const pieces = path.split('.');
4404
+ const pieces = path.indexOf('.') === -1 ? [path] : path.split('.');
4262
4405
  let cur = obj;
4263
4406
  for (let i = 0; i < pieces.length - 1; ++i) {
4264
4407
  cur = cur[pieces[i]];
@@ -4282,32 +4425,12 @@ Model.validate = function validate(obj, pathsToValidate, context, callback) {
4282
4425
  schemaType.doValidate(val, err => {
4283
4426
  if (err) {
4284
4427
  error = error || new ValidationError();
4285
- if (err instanceof ValidationError) {
4286
- for (const _err of Object.keys(err.errors)) {
4287
- error.addError(`${path}.${err.errors[_err].path}`, _err);
4288
- }
4289
- } else {
4290
- error.addError(err.path, err);
4291
- }
4428
+ error.addError(path, err);
4292
4429
  }
4293
4430
  _checkDone();
4294
4431
  }, context, { path: path });
4295
4432
  }
4296
4433
 
4297
- function pushNestedArrayPaths(nestedArray, path) {
4298
- if (nestedArray == null) {
4299
- return;
4300
- }
4301
-
4302
- for (let i = 0; i < nestedArray.length; ++i) {
4303
- if (Array.isArray(nestedArray[i])) {
4304
- pushNestedArrayPaths(nestedArray[i], path + '.' + i);
4305
- } else {
4306
- paths.push(path + '.' + i);
4307
- }
4308
- }
4309
- }
4310
-
4311
4434
  function _checkDone() {
4312
4435
  if (--remaining <= 0) {
4313
4436
  return cb(error);
@@ -4479,6 +4602,17 @@ function populate(model, docs, options, callback) {
4479
4602
  mod.options.options.sort || void 0;
4480
4603
  assignmentOpts.excludeId = excludeIdReg.test(select) || (select && select._id === 0);
4481
4604
 
4605
+ // Lean transform may delete `_id`, which would cause assignment
4606
+ // to fail. So delay running lean transform until _after_
4607
+ // `_assign()`
4608
+ if (mod.options &&
4609
+ mod.options.options &&
4610
+ mod.options.options.lean &&
4611
+ mod.options.options.lean.transform) {
4612
+ mod.options.options._leanTransform = mod.options.options.lean.transform;
4613
+ mod.options.options.lean = true;
4614
+ }
4615
+
4482
4616
  if (ids.length === 0 || ids.every(utils.isNullOrUndefined)) {
4483
4617
  // Ensure that we set to 0 or empty array even
4484
4618
  // if we don't actually execute a query to make sure there's a value
@@ -4553,6 +4687,14 @@ function populate(model, docs, options, callback) {
4553
4687
  for (const arr of params) {
4554
4688
  removeDeselectedForeignField(arr[0].foreignField, arr[0].options, vals);
4555
4689
  }
4690
+ for (const arr of params) {
4691
+ const mod = arr[0];
4692
+ if (mod.options && mod.options.options && mod.options.options._leanTransform) {
4693
+ for (const doc of vals) {
4694
+ mod.options.options._leanTransform(doc);
4695
+ }
4696
+ }
4697
+ }
4556
4698
  callback();
4557
4699
  }
4558
4700
  }
@@ -52,6 +52,25 @@ Object.defineProperty(SchemaArrayOptions.prototype, 'enum', opts);
52
52
 
53
53
  Object.defineProperty(SchemaArrayOptions.prototype, 'of', opts);
54
54
 
55
+ /**
56
+ * If set to `false`, will always deactivate casting non-array values to arrays.
57
+ * If set to `true`, will cast non-array values to arrays if `init` and `SchemaArray.options.castNonArrays` are also `true`
58
+ *
59
+ * #### Example:
60
+ *
61
+ * const Model = db.model('Test', new Schema({ x1: { castNonArrays: false, type: [String] } }));
62
+ * const doc = new Model({ x1: "some non-array value" });
63
+ * await doc.validate(); // Errors with "CastError"
64
+ *
65
+ * @api public
66
+ * @property castNonArrays
67
+ * @memberOf SchemaArrayOptions
68
+ * @type {Boolean}
69
+ * @instance
70
+ */
71
+
72
+ Object.defineProperty(SchemaArrayOptions.prototype, 'castNonArrays', opts);
73
+
55
74
  /*!
56
75
  * ignore
57
76
  */
@@ -50,6 +50,7 @@ Object.defineProperty(SchemaNumberOptions.prototype, 'max', opts);
50
50
  * equal to one of the given values.
51
51
  *
52
52
  * #### Example:
53
+ *
53
54
  * const schema = new Schema({
54
55
  * favoritePrime: {
55
56
  * type: Number,
@@ -71,6 +72,7 @@ Object.defineProperty(SchemaNumberOptions.prototype, 'enum', opts);
71
72
  * Sets default [populate options](/docs/populate.html#query-conditions).
72
73
  *
73
74
  * #### Example:
75
+ *
74
76
  * const schema = new Schema({
75
77
  * child: {
76
78
  * type: Number,
@@ -35,6 +35,7 @@ Object.defineProperty(SchemaObjectIdOptions.prototype, 'auto', opts);
35
35
  * Sets default [populate options](/docs/populate.html#query-conditions).
36
36
  *
37
37
  * #### Example:
38
+ *
38
39
  * const schema = new Schema({
39
40
  * child: {
40
41
  * type: 'ObjectId',
@@ -23,14 +23,14 @@ module.exports = function trackTransaction(schema) {
23
23
  initialState.versionKey = this.get(this.$__schema.options.versionKey);
24
24
  }
25
25
 
26
- initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.states.modify));
26
+ initialState.modifiedPaths = new Set(Object.keys(this.$__.activePaths.getStatePaths('modify')));
27
27
  initialState.atomics = _getAtomics(this);
28
28
 
29
29
  session[sessionNewDocuments].set(this, initialState);
30
30
  } else {
31
31
  const state = session[sessionNewDocuments].get(this);
32
32
 
33
- for (const path of Object.keys(this.$__.activePaths.states.modify)) {
33
+ for (const path of Object.keys(this.$__.activePaths.getStatePaths('modify'))) {
34
34
  state.modifiedPaths.add(path);
35
35
  }
36
36
  state.atomics = _getAtomics(this, state.atomics);