mongoose 5.0.17 → 5.1.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.
package/lib/model.js CHANGED
@@ -11,6 +11,7 @@ var DocumentNotFoundError = require('./error').DocumentNotFoundError;
11
11
  var DivergentArrayError = require('./error').DivergentArrayError;
12
12
  var Error = require('./error');
13
13
  var EventEmitter = require('events').EventEmitter;
14
+ var MongooseMap = require('./types/map');
14
15
  var OverwriteModelError = require('./error').OverwriteModelError;
15
16
  var PromiseProvider = require('./promise_provider');
16
17
  var Query = require('./query');
@@ -35,9 +36,9 @@ var parallelLimit = require('async/parallelLimit');
35
36
  var setDefaultsOnInsert = require('./services/setDefaultsOnInsert');
36
37
  var utils = require('./utils');
37
38
 
38
- var VERSION_WHERE = 1,
39
- VERSION_INC = 2,
40
- VERSION_ALL = VERSION_WHERE | VERSION_INC;
39
+ const VERSION_WHERE = 1;
40
+ const VERSION_INC = 2;
41
+ const VERSION_ALL = VERSION_WHERE | VERSION_INC;
41
42
 
42
43
  /**
43
44
  * Model constructor
@@ -129,21 +130,27 @@ Model.prototype.baseModelName;
129
130
  */
130
131
 
131
132
  Model.prototype.$__handleSave = function(options, callback) {
132
- var _this = this;
133
- var i;
134
- var keys;
135
- var len;
133
+ const _this = this;
134
+ let i;
135
+ let keys;
136
+ let len;
136
137
  if (!options.safe && this.schema.options.safe) {
137
138
  options.safe = this.schema.options.safe;
138
139
  }
139
140
  if (typeof options.safe === 'boolean') {
140
141
  options.safe = null;
141
142
  }
142
- var safe = options.safe ? utils.clone(options.safe) : options.safe;
143
+ let safe = options.safe ? utils.clone(options.safe) : options.safe;
144
+
145
+ const session = 'session' in options ? options.session : this.$session();
146
+ if (session != null) {
147
+ safe = typeof safe === 'object' && safe != null ? safe : {};
148
+ safe.session = session;
149
+ }
143
150
 
144
151
  if (this.isNew) {
145
152
  // send entire doc
146
- var obj = this.toObject(internalToObjectOptions);
153
+ const obj = this.toObject(internalToObjectOptions);
147
154
 
148
155
  if ((obj || {})._id === void 0) {
149
156
  // documents must have an _id else mongoose won't know
@@ -158,7 +165,7 @@ Model.prototype.$__handleSave = function(options, callback) {
158
165
  }
159
166
 
160
167
  this.$__version(true, obj);
161
- this.collection.insert(obj, safe, function(err, ret) {
168
+ this.collection.insertOne(obj, safe, function(err, ret) {
162
169
  if (err) {
163
170
  _this.isNew = true;
164
171
  _this.emit('isNew', true);
@@ -181,7 +188,7 @@ Model.prototype.$__handleSave = function(options, callback) {
181
188
  // since it already exists
182
189
  this.$__.inserting = false;
183
190
 
184
- var delta = this.$__delta();
191
+ const delta = this.$__delta();
185
192
 
186
193
  if (delta) {
187
194
  if (delta instanceof Error) {
@@ -189,7 +196,7 @@ Model.prototype.$__handleSave = function(options, callback) {
189
196
  return;
190
197
  }
191
198
 
192
- var where = this.$__where(delta[0]);
199
+ const where = this.$__where(delta[0]);
193
200
  if (where instanceof Error) {
194
201
  callback(where);
195
202
  return;
@@ -203,7 +210,7 @@ Model.prototype.$__handleSave = function(options, callback) {
203
210
  }
204
211
  }
205
212
 
206
- this.collection.update(where, delta[1], safe, function(err, ret) {
213
+ this.collection.updateOne(where, delta[1], safe, function(err, ret) {
207
214
  if (err) {
208
215
  callback(err);
209
216
  return;
@@ -234,6 +241,9 @@ Model.prototype.$__save = function(options, callback) {
234
241
  });
235
242
  }
236
243
 
244
+ // store the modified paths before the document is reset
245
+ const modifiedPaths = this.modifiedPaths();
246
+
237
247
  this.$__reset();
238
248
 
239
249
  let numAffected = 0;
@@ -257,16 +267,17 @@ Model.prototype.$__save = function(options, callback) {
257
267
  let doIncrement = VERSION_INC === (VERSION_INC & this.$__.version);
258
268
  this.$__.version = undefined;
259
269
 
270
+ let key = this.schema.options.versionKey;
271
+ let version = this.getValue(key) || 0;
272
+
260
273
  if (numAffected <= 0) {
261
274
  // the update failed. pass an error back
262
- let err = new VersionError(this);
275
+ let err = new VersionError(this, version, modifiedPaths);
263
276
  return callback(err);
264
277
  }
265
278
 
266
279
  // increment version if was successful
267
280
  if (doIncrement) {
268
- let key = this.schema.options.versionKey;
269
- let version = this.getValue(key) | 0;
270
281
  this.setValue(key, version + 1);
271
282
  }
272
283
  }
@@ -321,7 +332,9 @@ Model.prototype.save = function(options, fn) {
321
332
  options = undefined;
322
333
  }
323
334
 
324
- if (!options) {
335
+ if (options != null) {
336
+ options = utils.clone(options);
337
+ } else {
325
338
  options = {};
326
339
  }
327
340
 
@@ -505,36 +518,40 @@ function handleAtomics(self, where, delta, data, value) {
505
518
  */
506
519
 
507
520
  Model.prototype.$__delta = function() {
508
- var dirty = this.$__dirty();
509
- if (!dirty.length && VERSION_ALL !== this.$__.version) return;
521
+ const dirty = this.$__dirty();
522
+ if (!dirty.length && VERSION_ALL !== this.$__.version) {
523
+ return;
524
+ }
510
525
 
511
- var where = {},
512
- delta = {},
513
- len = dirty.length,
514
- divergent = [],
515
- d = 0;
526
+ let where = {};
527
+ let delta = {};
528
+ const len = dirty.length;
529
+ const divergent = [];
530
+ let d = 0;
516
531
 
517
532
  where._id = this._doc._id;
518
- if (where._id.toObject) {
533
+ // If `_id` is an object, need to depopulate, but also need to be careful
534
+ // because `_id` can technically be null (see gh-6406)
535
+ if (get(where, '_id.$__', null) != null) {
519
536
  where._id = where._id.toObject({ transform: false, depopulate: true });
520
537
  }
521
538
 
522
539
  for (; d < len; ++d) {
523
- var data = dirty[d];
524
- var value = data.value;
540
+ const data = dirty[d];
541
+ let value = data.value;
525
542
 
526
- var match = checkDivergentArray(this, data.path, value);
543
+ const match = checkDivergentArray(this, data.path, value);
527
544
  if (match) {
528
545
  divergent.push(match);
529
546
  continue;
530
547
  }
531
548
 
532
- var pop = this.populated(data.path, true);
549
+ const pop = this.populated(data.path, true);
533
550
  if (!pop && this.$__.selected) {
534
551
  // If any array was selected using an $elemMatch projection, we alter the path and where clause
535
552
  // NOTE: MongoDB only supports projected $elemMatch on top level array.
536
- var pathSplit = data.path.split('.');
537
- var top = pathSplit[0];
553
+ const pathSplit = data.path.split('.');
554
+ const top = pathSplit[0];
538
555
  if (this.$__.selected[top] && this.$__.selected[top].$elemMatch) {
539
556
  // If the selected array entry was modified
540
557
  if (pathSplit.length > 1 && pathSplit[1] == 0 && typeof where[top] === 'undefined') {
@@ -714,7 +731,7 @@ Model.prototype.$__where = function _where(where) {
714
731
  where._id = this._doc._id;
715
732
  }
716
733
 
717
- if (this._doc._id == null) {
734
+ if (this._doc._id === void 0) {
718
735
  return new Error('No _id found on document!');
719
736
  }
720
737
 
@@ -1064,8 +1081,8 @@ function _ensureIndexes(model, options, callback) {
1064
1081
  var index = indexes.shift();
1065
1082
  if (!index) return done();
1066
1083
 
1067
- var indexFields = index[0];
1068
- var indexOptions = index[1];
1084
+ var indexFields = utils.clone(index[0]);
1085
+ var indexOptions = utils.clone(index[1]);
1069
1086
  _handleSafe(options);
1070
1087
 
1071
1088
  indexSingleStart(indexFields, options);
@@ -1843,6 +1860,126 @@ Model.findByIdAndUpdate = function(id, update, options, callback) {
1843
1860
  return this.findOneAndUpdate.call(this, {_id: id}, update, options, callback);
1844
1861
  };
1845
1862
 
1863
+ /**
1864
+ * Issue a MongoDB `findOneAndDelete()` command.
1865
+ *
1866
+ * Finds a matching document, removes it, and passes the found document
1867
+ * (if any) to the callback.
1868
+ *
1869
+ * Executes immediately if `callback` is passed else a Query object is returned.
1870
+ *
1871
+ * This function triggers the following middleware.
1872
+ *
1873
+ * - `findOneAndDelete()`
1874
+ *
1875
+ * This function differs slightly from `Model.findOneAndRemove()` in that
1876
+ * `findOneAndRemove()` becomes a [MongoDB `findAndModify()` command](https://docs.mongodb.com/manual/reference/method/db.collection.findAndModify/),
1877
+ * as opposed to a `findOneAndDelete()` command. For most mongoose use cases,
1878
+ * this distinction is purely pedantic. You should use `findOneAndDelete()`
1879
+ * unless you have a good reason not to.
1880
+ *
1881
+ * ####Options:
1882
+ *
1883
+ * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
1884
+ * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
1885
+ * - `select`: sets the document fields to return
1886
+ * - `rawResult`: if true, returns the [raw result from the MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify)
1887
+ * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update
1888
+ *
1889
+ * ####Examples:
1890
+ *
1891
+ * A.findOneAndDelete(conditions, options, callback) // executes
1892
+ * A.findOneAndDelete(conditions, options) // return Query
1893
+ * A.findOneAndDelete(conditions, callback) // executes
1894
+ * A.findOneAndDelete(conditions) // returns Query
1895
+ * A.findOneAndDelete() // returns Query
1896
+ *
1897
+ * Values are cast to their appropriate types when using the findAndModify helpers.
1898
+ * However, the below are not executed by default.
1899
+ *
1900
+ * - defaults. Use the `setDefaultsOnInsert` option to override.
1901
+ *
1902
+ * `findAndModify` helpers support limited validation. You can
1903
+ * enable these by setting the `runValidators` options,
1904
+ * respectively.
1905
+ *
1906
+ * If you need full-fledged validation, use the traditional approach of first
1907
+ * retrieving the document.
1908
+ *
1909
+ * Model.findById(id, function (err, doc) {
1910
+ * if (err) ..
1911
+ * doc.name = 'jason bourne';
1912
+ * doc.save(callback);
1913
+ * });
1914
+ *
1915
+ * @param {Object} conditions
1916
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
1917
+ * @param {Function} [callback]
1918
+ * @return {Query}
1919
+ * @api public
1920
+ */
1921
+
1922
+ Model.findOneAndDelete = function(conditions, options, callback) {
1923
+ if (arguments.length === 1 && typeof conditions === 'function') {
1924
+ var msg = 'Model.findOneAndDelete(): First argument must not be a function.\n\n'
1925
+ + ' ' + this.modelName + '.findOneAndDelete(conditions, callback)\n'
1926
+ + ' ' + this.modelName + '.findOneAndDelete(conditions)\n'
1927
+ + ' ' + this.modelName + '.findOneAndDelete()\n';
1928
+ throw new TypeError(msg);
1929
+ }
1930
+
1931
+ if (typeof options === 'function') {
1932
+ callback = options;
1933
+ options = undefined;
1934
+ }
1935
+ if (callback) {
1936
+ callback = this.$wrapCallback(callback);
1937
+ }
1938
+
1939
+ var fields;
1940
+ if (options) {
1941
+ fields = options.select;
1942
+ options.select = undefined;
1943
+ }
1944
+
1945
+ var mq = new this.Query({}, {}, this, this.collection);
1946
+ mq.select(fields);
1947
+
1948
+ return mq.findOneAndDelete(conditions, options, callback);
1949
+ };
1950
+
1951
+ /**
1952
+ * Issue a MongoDB `findOneAndDelete()` command by a document's _id field.
1953
+ * In other words, `findByIdAndDelete(id)` is a shorthand for
1954
+ * `findOneAndDelete({ _id: id })`.
1955
+ *
1956
+ * This function triggers the following middleware.
1957
+ *
1958
+ * - `findOneAndDelete()`
1959
+ *
1960
+ * @param {Object|Number|String} id value of `_id` to query by
1961
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
1962
+ * @param {Function} [callback]
1963
+ * @return {Query}
1964
+ * @see Model.findOneAndRemove #model_Model.findOneAndRemove
1965
+ * @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
1966
+ */
1967
+
1968
+ Model.findByIdAndDelete = function(id, options, callback) {
1969
+ if (arguments.length === 1 && typeof id === 'function') {
1970
+ var msg = 'Model.findByIdAndDelete(): First argument must not be a function.\n\n'
1971
+ + ' ' + this.modelName + '.findByIdAndDelete(id, callback)\n'
1972
+ + ' ' + this.modelName + '.findByIdAndDelete(id)\n'
1973
+ + ' ' + this.modelName + '.findByIdAndDelete()\n';
1974
+ throw new TypeError(msg);
1975
+ }
1976
+ if (callback) {
1977
+ callback = this.$wrapCallback(callback);
1978
+ }
1979
+
1980
+ return this.findOneAndDelete({_id: id}, options, callback);
1981
+ };
1982
+
1846
1983
  /**
1847
1984
  * Issue a mongodb findAndModify remove command.
1848
1985
  *
@@ -2013,15 +2150,15 @@ Model.findByIdAndRemove = function(id, options, callback) {
2013
2150
  */
2014
2151
 
2015
2152
  Model.create = function create(doc, callback) {
2016
- var args;
2017
- var cb;
2018
- var discriminatorKey = this.schema.options.discriminatorKey;
2153
+ let args;
2154
+ let cb;
2155
+ const discriminatorKey = this.schema.options.discriminatorKey;
2019
2156
 
2020
2157
  if (Array.isArray(doc)) {
2021
2158
  args = doc;
2022
2159
  cb = callback;
2023
2160
  } else {
2024
- var last = arguments[arguments.length - 1];
2161
+ let last = arguments[arguments.length - 1];
2025
2162
  // Handle falsy callbacks re: #5061
2026
2163
  if (typeof last === 'function' || !last) {
2027
2164
  cb = last;
@@ -2040,15 +2177,19 @@ Model.create = function create(doc, callback) {
2040
2177
  return cb(null);
2041
2178
  }
2042
2179
 
2043
- var toExecute = [];
2044
- var firstError;
2180
+ const toExecute = [];
2181
+ let firstError;
2045
2182
  args.forEach(doc => {
2046
2183
  toExecute.push(callback => {
2047
- var Model = this.discriminators && doc[discriminatorKey] ?
2184
+ const Model = this.discriminators && doc[discriminatorKey] != null ?
2048
2185
  this.discriminators[doc[discriminatorKey]] || getDiscriminatorByValue(this, doc[discriminatorKey]) :
2049
2186
  this;
2050
- var toSave = doc;
2051
- var callbackWrapper = (error, doc) => {
2187
+ if (Model == null) {
2188
+ throw new Error(`Discriminator "${doc[discriminatorKey]}" not ` +
2189
+ `found for model "${this.modelName}"`);
2190
+ }
2191
+ let toSave = doc;
2192
+ const callbackWrapper = (error, doc) => {
2052
2193
  if (error) {
2053
2194
  if (!firstError) {
2054
2195
  firstError = error;
@@ -2071,9 +2212,9 @@ Model.create = function create(doc, callback) {
2071
2212
  });
2072
2213
 
2073
2214
  parallel(toExecute, (error, res) => {
2074
- var savedDocs = [];
2075
- var len = res.length;
2076
- for (var i = 0; i < len; ++i) {
2215
+ const savedDocs = [];
2216
+ const len = res.length;
2217
+ for (let i = 0; i < len; ++i) {
2077
2218
  if (res[i].doc) {
2078
2219
  savedDocs.push(res[i].doc);
2079
2220
  }
@@ -2113,7 +2254,7 @@ Model.create = function create(doc, callback) {
2113
2254
  *
2114
2255
  * @param {Array} [pipeline]
2115
2256
  * @param {Object} [options] see the [mongodb driver options](http://mongodb.github.io/node-mongodb-native/3.0/api/Collection.html#watch)
2116
- * @return {ChangeStream} mongoose-specific change stream wrapper
2257
+ * @return {ChangeStream} mongoose-specific change stream wrapper, inherits from EventEmitter
2117
2258
  * @api public
2118
2259
  */
2119
2260
 
@@ -2121,6 +2262,41 @@ Model.watch = function(pipeline, options) {
2121
2262
  return new ChangeStream(this, pipeline, options);
2122
2263
  };
2123
2264
 
2265
+ /**
2266
+ * _Requires MongoDB >= 3.6.0._ Starts a [MongoDB session](https://docs.mongodb.com/manual/release-notes/3.6/#client-sessions)
2267
+ * for benefits like causal consistency and [retryable writes](https://docs.mongodb.com/manual/core/retryable-writes/).
2268
+ *
2269
+ * This function does not trigger any middleware.
2270
+ *
2271
+ * ####Example:
2272
+ *
2273
+ * const session = await Person.startSession();
2274
+ * let doc = await Person.findOne({ name: 'Ned Stark' }, { session });
2275
+ * await doc.remove();
2276
+ * // `doc` will always be null, even if reading from a replica set
2277
+ * // secondary. Without causal consistency, it is possible to
2278
+ * // get a doc back from the below query if the query reads from a
2279
+ * // secondary that is experiencing replication lag.
2280
+ * doc = await Person.findOne({ name: 'Ned Stark' }, { readPreference: 'secondary' });
2281
+ *
2282
+ * @param {Object} [options] see the [mongodb driver options](http://mongodb.github.io/node-mongodb-native/3.0/api/MongoClient.html#startSession)
2283
+ * @param {Boolean} [options.causalConsistency=true] set to false to disable causal consistency
2284
+ * @param {Function} [callback]
2285
+ * @return {Promise<ClientSession>} promise that resolves to a MongoDB driver `ClientSession`
2286
+ * @api public
2287
+ */
2288
+
2289
+ Model.startSession = function(options, callback) {
2290
+ return utils.promiseOrCallback(callback, cb => {
2291
+ if (this.collection.buffer) {
2292
+ return this.collection.addQueue(() => {
2293
+ cb(null, this.db.client.startSession(options));
2294
+ });
2295
+ }
2296
+ return cb(null, this.db.client.startSession(options));
2297
+ });
2298
+ };
2299
+
2124
2300
  /**
2125
2301
  * Shortcut for validating an array of documents and inserting them into
2126
2302
  * MongoDB if they're all valid. This function is faster than `.create()`
@@ -2189,7 +2365,9 @@ Model.$__insertMany = function(arr, options, callback) {
2189
2365
  var validationErrors = [];
2190
2366
  arr.forEach(function(doc) {
2191
2367
  toExecute.push(function(callback) {
2192
- doc = new _this(doc);
2368
+ if (!(doc instanceof _this)) {
2369
+ doc = new _this(doc);
2370
+ }
2193
2371
  doc.validate({ __noPromise: true }, function(error) {
2194
2372
  if (error) {
2195
2373
  // Option `ordered` signals that insert should be continued after reaching
@@ -2646,8 +2824,10 @@ function _update(model, op, conditions, doc, options, callback) {
2646
2824
  * ####Example:
2647
2825
  *
2648
2826
  * var o = {};
2649
- * o.map = function () { emit(this.name, 1) }
2650
- * o.reduce = function (k, vals) { return vals.length }
2827
+ * // `map()` and `reduce()` are run on the MongoDB server, not Node.js,
2828
+ * // these functions are converted to strings
2829
+ * o.map = function () { emit(this.name, 1) };
2830
+ * o.reduce = function (k, vals) { return vals.length };
2651
2831
  * User.mapReduce(o, function (err, results) {
2652
2832
  * console.log(results)
2653
2833
  * })
@@ -2677,8 +2857,10 @@ function _update(model, op, conditions, doc, options, callback) {
2677
2857
  * ####Example:
2678
2858
  *
2679
2859
  * var o = {};
2680
- * o.map = function () { emit(this.name, 1) }
2681
- * o.reduce = function (k, vals) { return vals.length }
2860
+ * // You can also define `map()` and `reduce()` as strings if your
2861
+ * // linter complains about `emit()` not being defined
2862
+ * o.map = 'function () { emit(this.name, 1) }';
2863
+ * o.reduce = 'function (k, vals) { return vals.length }';
2682
2864
  * o.out = { replace: 'createdCollectionNameForResults' }
2683
2865
  * o.verbose = true;
2684
2866
  *
@@ -2969,6 +3151,7 @@ Model.geoSearch = function(conditions, options, callback) {
2969
3151
  *
2970
3152
  * @param {Document|Array} docs Either a single document or array of documents to populate.
2971
3153
  * @param {Object} options A hash of key/val (path, options) used for population.
3154
+ * @param {Boolean} [options.retainNullValues=false] by default, mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
2972
3155
  * @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
2973
3156
  * @return {Promise}
2974
3157
  * @api public
@@ -3035,8 +3218,6 @@ const excludeIdReg = /\s?-_id\s?/;
3035
3218
  const excludeIdRegGlobal = /\s?-_id\s?/g;
3036
3219
 
3037
3220
  function populate(model, docs, options, callback) {
3038
- var modelsMap;
3039
-
3040
3221
  // normalize single / multiple docs passed
3041
3222
  if (!Array.isArray(docs)) {
3042
3223
  docs = [docs];
@@ -3046,15 +3227,20 @@ function populate(model, docs, options, callback) {
3046
3227
  return callback();
3047
3228
  }
3048
3229
 
3049
- modelsMap = getModelsMapForPopulate(model, docs, options);
3230
+ const modelsMap = getModelsMapForPopulate(model, docs, options);
3231
+
3050
3232
  if (modelsMap instanceof Error) {
3051
3233
  return utils.immediate(function() {
3052
3234
  callback(modelsMap);
3053
3235
  });
3054
3236
  }
3055
3237
 
3056
- var i, len = modelsMap.length,
3057
- mod, match, select, vals = [];
3238
+ let i;
3239
+ const len = modelsMap.length;
3240
+ let mod;
3241
+ let match;
3242
+ let select;
3243
+ let vals = [];
3058
3244
 
3059
3245
  function flatten(item) {
3060
3246
  // no need to include undefined values in our query
@@ -3073,7 +3259,7 @@ function populate(model, docs, options, callback) {
3073
3259
  match = {};
3074
3260
  }
3075
3261
 
3076
- var ids = utils.array.flatten(mod.ids, flatten);
3262
+ let ids = utils.array.flatten(mod.ids, flatten);
3077
3263
  ids = utils.array.unique(ids);
3078
3264
 
3079
3265
  if (ids.length === 0 || ids.every(utils.isNullOrUndefined)) {
@@ -3247,24 +3433,31 @@ function populate(model, docs, options, callback) {
3247
3433
  */
3248
3434
 
3249
3435
  function assignVals(o) {
3436
+ // Glob all options together because `populateOptions` is confusing
3437
+ const retainNullValues = get(o, 'allOptions.options.options.retainNullValues', false);
3438
+ const populateOptions = Object.assign({}, o.options, {
3439
+ justOne: o.justOne,
3440
+ retainNullValues: retainNullValues
3441
+ });
3442
+
3250
3443
  // replace the original ids in our intermediate _ids structure
3251
3444
  // with the documents found by query
3252
- assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, o.options,
3445
+ assignRawDocsToIdStructure(o.rawIds, o.rawDocs, o.rawOrder, populateOptions,
3253
3446
  o.localField, o.foreignField);
3254
3447
 
3255
3448
  // now update the original documents being populated using the
3256
3449
  // result structure that contains real documents.
3257
- var docs = o.docs;
3258
- var rawIds = o.rawIds;
3259
- var options = o.options;
3450
+ const docs = o.docs;
3451
+ const rawIds = o.rawIds;
3452
+ const options = o.options;
3260
3453
 
3261
3454
  function setValue(val) {
3262
- return valueFilter(val, options, o.justOne);
3455
+ return valueFilter(val, options, populateOptions);
3263
3456
  }
3264
3457
 
3265
- for (var i = 0; i < docs.length; ++i) {
3266
- if (utils.getValue(o.path, docs[i]) == null &&
3267
- !getVirtual(o.originalModel.schema, o.path)) {
3458
+ for (let i = 0; i < docs.length; ++i) {
3459
+ const existingVal = utils.getValue(o.path, docs[i]);
3460
+ if (existingVal == null && !getVirtual(o.originalModel.schema, o.path)) {
3268
3461
  continue;
3269
3462
  }
3270
3463
 
@@ -3278,10 +3471,27 @@ function assignVals(o) {
3278
3471
  rawIds[i] = rawIds[i][0];
3279
3472
  }
3280
3473
 
3474
+
3475
+ // If we're populating a map, the existing value will be an object, so
3476
+ // we need to transform again
3477
+ const isMap = docs[i].constructor.name === 'model' ?
3478
+ existingVal instanceof Map :
3479
+ existingVal != null && existingVal.constructor.name === 'Object';
3480
+ if (!o.isVirtual && isMap) {
3481
+ const _keys = existingVal instanceof Map ?
3482
+ Array.from(existingVal.keys()) :
3483
+ Object.keys(existingVal);
3484
+ rawIds[i] = rawIds[i].reduce((cur, v, i) => {
3485
+ // Avoid casting because that causes infinite recursion
3486
+ cur.$init(_keys[i], v);
3487
+ return cur;
3488
+ }, new MongooseMap({}, docs[i]));
3489
+ }
3490
+
3281
3491
  if (o.isVirtual && docs[i].constructor.name === 'model') {
3282
3492
  // If virtual populate and doc is already init-ed, need to walk through
3283
3493
  // the actual doc to set rather than setting `_doc` directly
3284
- mpath.set(o.path, rawIds[i], docs[i]);
3494
+ mpath.set(o.path, rawIds[i], docs[i], setValue);
3285
3495
  } else {
3286
3496
  var parts = o.path.split('.');
3287
3497
  var cur = docs[i];
@@ -3294,6 +3504,7 @@ function assignVals(o) {
3294
3504
  if (docs[i].$__) {
3295
3505
  docs[i].populated(o.path, o.allIds[i], o.allOptions);
3296
3506
  }
3507
+
3297
3508
  utils.setValue(o.path, rawIds[i], docs[i], setValue, false);
3298
3509
  }
3299
3510
  }
@@ -3374,9 +3585,6 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, lo
3374
3585
  // can safely use this to our advantage dealing with sorted
3375
3586
  // result sets too.
3376
3587
  newOrder.forEach(function(doc, i) {
3377
- if (!doc) {
3378
- return;
3379
- }
3380
3588
  rawIds[i] = doc;
3381
3589
  });
3382
3590
  }
@@ -3477,6 +3685,7 @@ function getModelsMapForPopulate(model, docs, options) {
3477
3685
  if (typeof foreignField === 'function') {
3478
3686
  foreignField = foreignField.call(doc);
3479
3687
  }
3688
+
3480
3689
  const ret = convertTo_id(utils.getValue(localField, doc));
3481
3690
  const id = String(utils.getValue(foreignField, doc));
3482
3691
  options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;
@@ -3539,6 +3748,9 @@ function getModelsMapForPopulate(model, docs, options) {
3539
3748
  if (schema && schema.caster) {
3540
3749
  schema = schema.caster;
3541
3750
  }
3751
+ if (schema && schema.$isSchemaMap) {
3752
+ schema = schema.$__schemaType;
3753
+ }
3542
3754
 
3543
3755
  if (!schema && model.discriminators) {
3544
3756
  discriminatorKey = model.schema.discriminatorMapping.key;
@@ -3548,7 +3760,11 @@ function getModelsMapForPopulate(model, docs, options) {
3548
3760
 
3549
3761
  if (refPath) {
3550
3762
  modelNames = utils.getValue(refPath, doc);
3551
- isRefPathArray = Array.isArray(modelNames);
3763
+ isRefPathArray = false;
3764
+ if (Array.isArray(modelNames)) {
3765
+ isRefPathArray = true;
3766
+ modelNames = utils.array.flatten(modelNames);
3767
+ }
3552
3768
  } else {
3553
3769
  if (!modelNameFromQuery) {
3554
3770
  var modelForCurrentDoc = model;
@@ -3627,18 +3843,33 @@ function convertTo_id(val) {
3627
3843
  if (val instanceof Model) return val._id;
3628
3844
 
3629
3845
  if (Array.isArray(val)) {
3630
- for (var i = 0; i < val.length; ++i) {
3846
+ for (let i = 0; i < val.length; ++i) {
3631
3847
  if (val[i] instanceof Model) {
3632
3848
  val[i] = val[i]._id;
3633
3849
  }
3634
3850
  }
3635
- if (val.isMongooseArray) {
3851
+ if (val.isMongooseArray && val._schema) {
3636
3852
  return val._schema.cast(val, val._parent);
3637
3853
  }
3638
3854
 
3639
3855
  return [].concat(val);
3640
3856
  }
3641
3857
 
3858
+ // `populate('map')` may be an object if populating on a doc that hasn't
3859
+ // been hydrated yet
3860
+ if (val != null && val.constructor.name === 'Object') {
3861
+ const ret = [];
3862
+ for (const key of Object.keys(val)) {
3863
+ ret.push(val[key]);
3864
+ }
3865
+ return ret;
3866
+ }
3867
+ // If doc has already been hydrated, e.g. `doc.populate('map').execPopulate()`
3868
+ // then `val` will already be a map
3869
+ if (val instanceof Map) {
3870
+ return Array.from(val.values());
3871
+ }
3872
+
3642
3873
  return val;
3643
3874
  }
3644
3875
 
@@ -3660,14 +3891,16 @@ function convertTo_id(val) {
3660
3891
  * that population mapping can occur.
3661
3892
  */
3662
3893
 
3663
- function valueFilter(val, assignmentOpts, justOne) {
3894
+ function valueFilter(val, assignmentOpts, populateOptions) {
3664
3895
  if (Array.isArray(val)) {
3665
3896
  // find logic
3666
- var ret = [];
3667
- var numValues = val.length;
3668
- for (var i = 0; i < numValues; ++i) {
3897
+ const ret = [];
3898
+ const numValues = val.length;
3899
+ for (let i = 0; i < numValues; ++i) {
3669
3900
  var subdoc = val[i];
3670
- if (!isDoc(subdoc)) continue;
3901
+ if (!isDoc(subdoc) && (!populateOptions.retainNullValues || subdoc != null)) {
3902
+ continue;
3903
+ }
3671
3904
  maybeRemoveId(subdoc, assignmentOpts);
3672
3905
  ret.push(subdoc);
3673
3906
  if (assignmentOpts.originalLimit &&
@@ -3681,7 +3914,7 @@ function valueFilter(val, assignmentOpts, justOne) {
3681
3914
  while (val.length > ret.length) {
3682
3915
  Array.prototype.pop.apply(val, []);
3683
3916
  }
3684
- for (i = 0; i < ret.length; ++i) {
3917
+ for (let i = 0; i < ret.length; ++i) {
3685
3918
  val[i] = ret[i];
3686
3919
  }
3687
3920
  return val;
@@ -3693,7 +3926,7 @@ function valueFilter(val, assignmentOpts, justOne) {
3693
3926
  return val;
3694
3927
  }
3695
3928
 
3696
- return justOne ? null : [];
3929
+ return populateOptions.justOne ? (val == null ? val : null) : [];
3697
3930
  }
3698
3931
 
3699
3932
  /*!
@@ -3870,24 +4103,24 @@ function applyQueryMiddleware(Query, model) {
3870
4103
  nullResultByDefault: true
3871
4104
  };
3872
4105
 
3873
- Query.prototype._count = model.hooks.createWrapper('count',
3874
- Query.prototype._count, null, kareemOptions);
4106
+ // `update()` thunk has a different name because `_update` was already taken
3875
4107
  Query.prototype._execUpdate = model.hooks.createWrapper('update',
3876
4108
  Query.prototype._execUpdate, null, kareemOptions);
3877
- Query.prototype._find = model.hooks.createWrapper('find',
3878
- Query.prototype._find, null, kareemOptions);
3879
- Query.prototype._findOne = model.hooks.createWrapper('findOne',
3880
- Query.prototype._findOne, null, kareemOptions);
3881
- Query.prototype._findOneAndRemove = model.hooks.createWrapper('findOneAndRemove',
3882
- Query.prototype._findOneAndRemove, null, kareemOptions);
3883
- Query.prototype._findOneAndUpdate = model.hooks.createWrapper('findOneAndUpdate',
3884
- Query.prototype._findOneAndUpdate, null, kareemOptions);
3885
- Query.prototype._replaceOne = model.hooks.createWrapper('replaceOne',
3886
- Query.prototype._replaceOne, null, kareemOptions);
3887
- Query.prototype._updateMany = model.hooks.createWrapper('updateMany',
3888
- Query.prototype._updateMany, null, kareemOptions);
3889
- Query.prototype._updateOne = model.hooks.createWrapper('updateOne',
3890
- Query.prototype._updateOne, null, kareemOptions);
4109
+
4110
+ [
4111
+ 'count',
4112
+ 'find',
4113
+ 'findOne',
4114
+ 'findOneAndDelete',
4115
+ 'findOneAndRemove',
4116
+ 'findOneAndUpdate',
4117
+ 'replaceOne',
4118
+ 'updateMany',
4119
+ 'updateOne'
4120
+ ].forEach(fn => {
4121
+ Query.prototype[`_${fn}`] = model.hooks.createWrapper(fn,
4122
+ Query.prototype[`_${fn}`], null, kareemOptions);
4123
+ });
3891
4124
  }
3892
4125
 
3893
4126
  /*!