mongoose 5.4.23 → 5.5.3

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/History.md CHANGED
@@ -1,3 +1,53 @@
1
+ 5.5.3 / 2019-04-22
2
+ ==================
3
+ * fix: add findAndModify deprecation warning that references the useFindAndModify option #7644
4
+ * fix(document): handle pushing a doc onto a discriminator that contains a doc array #7704
5
+ * fix(update): run setters on array elements when doing $set #7679
6
+ * fix: correct usage of arguments while buffering commands #7718 [rzymek](https://github.com/rzymek)
7
+ * fix(document): avoid error clearing modified subpaths if doc not defined #7715 [bitflower](https://github.com/bitflower)
8
+ * refactor(array): move `_parent` property behind a symbol #7726 #7700
9
+ * docs(model): list out all operations and options for `bulkWrite()` #7055
10
+ * docs(aggregate): use `eachAsync()` instead of nonexistent `each()` #7699
11
+ * docs(validation): add CastError validation example #7514
12
+ * docs(query+model): list out all options and callback details for Model.updateX() and Query#updateX() #7646
13
+
14
+ 5.5.2 / 2019-04-16
15
+ ==================
16
+ * fix(document): support setting nested path to non-POJO object #7639
17
+ * perf(connection): remove leaked event handler in `Model.init()` so `deleteModel()` frees all memory #7682
18
+ * fix(timestamps): handle custom statics that conflict with built-in functions (like mongoose-delete plugin) #7698
19
+ * fix(populate): make `Document#populated()` work for populated subdocs #7685
20
+ * fix(document): support `.set()` on document array underneath embedded discriminator path #7656
21
+
22
+ 5.5.1 / 2019-04-11
23
+ ==================
24
+ * fix(document): correctly overwrite all properties when setting a single nested subdoc #7660 #7681
25
+ * fix(array): allow customization of array required validator #7696 [freewil](https://github.com/freewil)
26
+ * fix(discriminator): handle embedded discriminators when casting array defaults #7687
27
+ * fix(collection): ensure collection functions return a promise even if disconnected #7676
28
+ * fix(schematype): avoid indexing properties with `{ unique: false, index: false }` #7620
29
+ * fix(aggregate): make `Aggregate#model()` with no arguments return the aggregation's model #7608
30
+
31
+ 5.5.0 / 2019-04-08
32
+ ==================
33
+ * feat(model): support applying hooks to custom static functions #5982
34
+ * feat(populate): support specifying a function as `match` #7397
35
+ * perf(buffer): avoid calling `defineProperties()` in Buffer constructor #7331
36
+ * feat(connection): add `plugin()` for connection-scoped plugins #7378
37
+ * feat(model): add Model#deleteOne() and corresponding hooks #7538
38
+ * feat(query): support hooks for `Query#distinct()` #5938
39
+ * feat(model): print warning when calling create() incorrectly with a session #7535
40
+ * feat(document): add Document#isEmpty() and corresponding helpers for nested paths #5369
41
+ * feat(document): add `getters` option to Document#get() #7233
42
+ * feat(query): add Query#projection() to get or overwrite the current projection #7384
43
+ * fix(document): set full validator path on validatorProperties if `propsParameter` set on validator #7447
44
+ * feat(document): add Document#directModifiedPaths() #7373
45
+ * feat(document): add $locals property #7691
46
+ * feat(document): add validateUpdatedOnly option that only validates modified paths in `save()` #7492 [captaincaius](https://github.com/captaincaius)
47
+ * chore: upgrade MongoDB driver to v3.2.0 #7641
48
+ * fix(schematype): deprecate `isAsync` option for custom validators #6700
49
+ * chore(mongoose): deprecate global.MONGOOSE_DRIVER_PATH so we can be webpack-warning-free in 6.0 #7501
50
+
1
51
  5.4.23 / 2019-04-08
2
52
  ===================
3
53
  * fix(document): report cast error when string path in schema is an array in MongoDB #7619
package/lib/aggregate.js CHANGED
@@ -80,14 +80,26 @@ function Aggregate(pipeline) {
80
80
  Aggregate.prototype.options;
81
81
 
82
82
  /**
83
- * Binds this aggregate to a model.
83
+ * Get/set the model that this aggregation will execute on.
84
84
  *
85
- * @param {Model} model the model to which the aggregate is to be bound
86
- * @return {Aggregate}
85
+ * ####Example:
86
+ * const aggregate = MyModel.aggregate([{ $match: { answer: 42 } }]);
87
+ * aggregate.model() === MyModel; // true
88
+ *
89
+ * // Change the model. There's rarely any reason to do this.
90
+ * aggregate.model(SomeOtherModel);
91
+ * aggregate.model() === SomeOtherModel; // true
92
+ *
93
+ * @param {Model} [model] the model to which the aggregate is to be bound
94
+ * @return {Aggregate|Model} if model is passed, will return `this`, otherwise will return the model
87
95
  * @api public
88
96
  */
89
97
 
90
98
  Aggregate.prototype.model = function(model) {
99
+ if (arguments.length === 0) {
100
+ return this._model;
101
+ }
102
+
91
103
  this._model = model;
92
104
  if (model.schema != null) {
93
105
  if (this.options.readPreference == null &&
@@ -790,7 +802,7 @@ Aggregate.prototype.option = function(value) {
790
802
  * ####Example:
791
803
  *
792
804
  * var cursor = Model.aggregate(..).cursor({ batchSize: 1000 }).exec();
793
- * cursor.each(function(error, doc) {
805
+ * cursor.eachAsync(function(error, doc) {
794
806
  * // use doc
795
807
  * });
796
808
  *
package/lib/connection.js CHANGED
@@ -10,6 +10,7 @@ const Collection = require('./driver').get().Collection;
10
10
  const STATES = require('./connectionstate');
11
11
  const MongooseError = require('./error');
12
12
  const PromiseProvider = require('./promise_provider');
13
+ const applyPlugins = require('./helpers/schema/applyPlugins');
13
14
  const get = require('./helpers/get');
14
15
  const mongodb = require('mongodb');
15
16
  const utils = require('./utils');
@@ -59,6 +60,7 @@ function Connection(base) {
59
60
  this._readyState = STATES.disconnected;
60
61
  this._closeCalled = false;
61
62
  this._hasOpened = false;
63
+ this.plugins = [];
62
64
 
63
65
  this.$internalEmitter = new EventEmitter();
64
66
  this.$internalEmitter.setMaxListeners(0);
@@ -147,6 +149,29 @@ Connection.prototype.collections;
147
149
 
148
150
  Connection.prototype.name;
149
151
 
152
+ /**
153
+ * The plugins that will be applied to all models created on this connection.
154
+ *
155
+ * ####Example:
156
+ *
157
+ * const db = mongoose.createConnection('mongodb://localhost:27017/mydb');
158
+ * db.plugin(() => console.log('Applied'));
159
+ * db.plugins.length; // 1
160
+ *
161
+ * db.model('Test', new Schema({})); // Prints "Applied"
162
+ *
163
+ * @property plugins
164
+ * @memberOf Connection
165
+ * @instance
166
+ * @api public
167
+ */
168
+
169
+ Object.defineProperty(Connection.prototype, 'plugins', {
170
+ configurable: false,
171
+ enumerable: true,
172
+ writable: true
173
+ });
174
+
150
175
  /**
151
176
  * The host name portion of the URI. If multiple hosts, such as a replica set,
152
177
  * this will contain the first host name in the URI
@@ -336,7 +361,13 @@ Connection.prototype.dropCollection = _wrapConnHelper(function dropCollection(co
336
361
  */
337
362
 
338
363
  Connection.prototype.dropDatabase = _wrapConnHelper(function dropDatabase(cb) {
339
- this.$internalEmitter.emit('dropDatabase');
364
+ // If `dropDatabase()` is called, this model's collection will not be
365
+ // init-ed. It is sufficiently common to call `dropDatabase()` after
366
+ // `mongoose.connect()` but before creating models that we want to
367
+ // support this. See gh-6967
368
+ for (const name of Object.keys(this.models)) {
369
+ delete this.models[name].$init;
370
+ }
340
371
  this.db.dropDatabase(cb);
341
372
  });
342
373
 
@@ -729,6 +760,30 @@ Connection.prototype.collection = function(name, options) {
729
760
  return this.collections[name];
730
761
  };
731
762
 
763
+ /**
764
+ * Declares a plugin executed on all schemas you pass to `conn.model()`
765
+ *
766
+ * Equivalent to calling `.plugin(fn)` on each schema you create.
767
+ *
768
+ * ####Example:
769
+ * const db = mongoose.createConnection('mongodb://localhost:27017/mydb');
770
+ * db.plugin(() => console.log('Applied'));
771
+ * db.plugins.length; // 1
772
+ *
773
+ * db.model('Test', new Schema({})); // Prints "Applied"
774
+ *
775
+ * @param {Function} fn plugin callback
776
+ * @param {Object} [opts] optional options
777
+ * @return {Connection} this
778
+ * @see plugins ./plugins.html
779
+ * @api public
780
+ */
781
+
782
+ Connection.prototype.plugin = function(fn, opts) {
783
+ this.plugins.push([fn, opts]);
784
+ return this;
785
+ };
786
+
732
787
  /**
733
788
  * Defines or retrieves a model.
734
789
  *
@@ -800,6 +855,8 @@ Connection.prototype.model = function(name, schema, collection) {
800
855
  let model;
801
856
 
802
857
  if (schema && schema.instanceOfSchema) {
858
+ applyPlugins(schema, this.plugins, null, '$connectionPluginsApplied');
859
+
803
860
  // compile a model
804
861
  model = this.base.model(fn || name, schema, collection, opts);
805
862
 
package/lib/document.js CHANGED
@@ -100,13 +100,15 @@ function Document(obj, fields, skipId, options) {
100
100
  $__hasIncludedChildren(fields) :
101
101
  {};
102
102
 
103
- this.$__buildDoc(obj, fields, skipId, exclude, hasIncludedChildren, false);
103
+ if (this._doc == null) {
104
+ this.$__buildDoc(obj, fields, skipId, exclude, hasIncludedChildren, false);
104
105
 
105
- // By default, defaults get applied **before** setting initial values
106
- // Re: gh-6155
107
- $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, true, {
108
- isNew: this.isNew
109
- });
106
+ // By default, defaults get applied **before** setting initial values
107
+ // Re: gh-6155
108
+ $__applyDefaults(this, fields, skipId, exclude, hasIncludedChildren, true, {
109
+ isNew: this.isNew
110
+ });
111
+ }
110
112
 
111
113
  if (obj) {
112
114
  if (obj instanceof Document) {
@@ -136,6 +138,7 @@ function Document(obj, fields, skipId, options) {
136
138
  }
137
139
 
138
140
  this.$__._id = this._id;
141
+ this.$locals = {};
139
142
 
140
143
  if (!schema.options.strict && obj) {
141
144
  const _this = this;
@@ -177,6 +180,35 @@ Document.prototype.constructor = Document;
177
180
 
178
181
  Document.prototype.schema;
179
182
 
183
+ /**
184
+ * Empty object that you can use for storing properties on the document. This
185
+ * is handy for passing data to middleware without conflicting with Mongoose
186
+ * internals.
187
+ *
188
+ * ####Example:
189
+ *
190
+ * schema.pre('save', function() {
191
+ * // Mongoose will set `isNew` to `false` if `save()` succeeds
192
+ * this.$locals.wasNew = this.isNew;
193
+ * });
194
+ *
195
+ * schema.post('save', function() {
196
+ * // Prints true if `isNew` was set before `save()`
197
+ * console.log(this.$locals.wasNew);
198
+ * });
199
+ *
200
+ * @api public
201
+ * @property $locals
202
+ * @memberOf Document
203
+ * @instance
204
+ */
205
+
206
+ Object.defineProperty(Document.prototype, '$locals', {
207
+ configurable: false,
208
+ enumerable: false,
209
+ writable: true
210
+ });
211
+
180
212
  /**
181
213
  * Boolean flag specifying if the document is new.
182
214
  *
@@ -758,6 +790,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
758
790
 
759
791
  return this;
760
792
  }
793
+ } else {
794
+ this.$__.$setCalled.add(path);
761
795
  }
762
796
 
763
797
  function _handleIndex(i) {
@@ -776,7 +810,8 @@ Document.prototype.$set = function $set(path, val, type, options) {
776
810
  delete this._doc[key];
777
811
  }
778
812
 
779
- if (utils.isPOJO(path[key]) &&
813
+ if (typeof path[key] === 'object' &&
814
+ path[key] != null &&
780
815
  pathtype !== 'virtual' &&
781
816
  pathtype !== 'real' &&
782
817
  !(this.$__path(pathName) instanceof MixedSchema) &&
@@ -819,7 +854,10 @@ Document.prototype.$set = function $set(path, val, type, options) {
819
854
  }
820
855
  }
821
856
 
822
- const pathType = this.schema.pathType(path);
857
+ let pathType = this.schema.pathType(path);
858
+ if (pathType === 'adhocOrUndefined') {
859
+ pathType = getEmbeddedDiscriminatorPath(this, path, { typeOnly: true });
860
+ }
823
861
 
824
862
  // Assume this is a Mongoose document that was copied into a POJO using
825
863
  // `Object.assign()` or `{...doc}`
@@ -828,7 +866,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
828
866
  }
829
867
 
830
868
  if (pathType === 'nested' && val) {
831
- if (utils.isPOJO(val)) {
869
+ if (typeof val === 'object' && val != null) {
832
870
  if (!merge) {
833
871
  this.setValue(path, null);
834
872
  cleanModifiedSubpaths(this, path);
@@ -1026,25 +1064,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
1026
1064
  // a single nested doc. That's to make sure we get the correct context.
1027
1065
  // Otherwise we would double-call the setter, see gh-7196.
1028
1066
  if (this.schema.singleNestedPaths[path] == null) {
1029
- // Init the new subdoc to the previous values of the doc, so
1030
- // getters/setters see the correct current state. We pass the new subdoc
1031
- // instead of the old subdoc because otherwise side effects in setters
1032
- // wouldn't work, see gh-7585
1033
- if (constructing && this.$__.$options.priorDoc) {
1034
- const priorVal = Object.assign({}, this.$__.$options.priorDoc._doc);
1035
- delete priorVal[this.schema.options.discriminatorKey];
1036
- init(this, priorVal, this._doc);
1037
- }
1038
-
1039
1067
  val = schema.applySetters(val, this, false, priorVal);
1040
-
1041
- if (constructing && this.$__.$options.priorDoc) {
1042
- // Clear init-ed paths afterwards, because those should be paths that
1043
- // were in the previous doc and should not be in the new one.
1044
- for (const path of Object.keys(this.$__.activePaths.states.init)) {
1045
- delete this._doc[path];
1046
- }
1047
- }
1048
1068
  }
1049
1069
 
1050
1070
  if (!didPopulate && this.$__.populated) {
@@ -1277,6 +1297,9 @@ Document.prototype.setValue = function(path, val) {
1277
1297
  *
1278
1298
  * @param {String} path
1279
1299
  * @param {Schema|String|Number|Buffer|*} [type] optionally specify a type for on-the-fly attributes
1300
+ * @param {Object} [options]
1301
+ * @param {Boolean} [options.virtuals=false] Apply virtuals before getting this path
1302
+ * @param {Boolean} [options.getters=true] If false, skip applying getters and just get the raw value
1280
1303
  * @api public
1281
1304
  */
1282
1305
 
@@ -1323,7 +1346,7 @@ Document.prototype.get = function(path, type, options) {
1323
1346
  obj = adhoc.cast(obj);
1324
1347
  }
1325
1348
 
1326
- if (schema != null) {
1349
+ if (schema != null && options.getters !== false) {
1327
1350
  obj = schema.applyGetters(obj, this);
1328
1351
  } else if (this.schema.nested[path] && options.virtuals) {
1329
1352
  // Might need to apply virtuals if this is a nested path
@@ -1420,6 +1443,95 @@ Document.prototype.$ignore = function(path) {
1420
1443
  this.$__.activePaths.ignore(path);
1421
1444
  };
1422
1445
 
1446
+ /**
1447
+ * Returns the list of paths that have been directly modified. A direct
1448
+ * modified path is a path that you explicitly set, whether via `doc.foo = 'bar'`,
1449
+ * `Object.assign(doc, { foo: 'bar' })`, or `doc.set('foo', 'bar')`.
1450
+ *
1451
+ * A path `a` may be in `modifiedPaths()` but not in `directModifiedPaths()`
1452
+ * because a child of `a` was directly modified.
1453
+ *
1454
+ * ####Example
1455
+ * const schema = new Schema({ foo: String, nested: { bar: String } });
1456
+ * const Model = mongoose.model('Test', schema);
1457
+ * await Model.create({ foo: 'original', nested: { bar: 'original' } });
1458
+ *
1459
+ * const doc = await Model.findOne();
1460
+ * doc.nested.bar = 'modified';
1461
+ * doc.directModifiedPaths(); // ['nested.bar']
1462
+ * doc.modifiedPaths(); // ['nested', 'nested.bar']
1463
+ *
1464
+ * @return {Array}
1465
+ * @api public
1466
+ */
1467
+
1468
+ Document.prototype.directModifiedPaths = function() {
1469
+ return Object.keys(this.$__.activePaths.states.modify);
1470
+ };
1471
+
1472
+ /**
1473
+ * Returns true if the given path is nullish or only contains empty objects.
1474
+ * Useful for determining whether this subdoc will get stripped out by the
1475
+ * [minimize option](/docs/guide.html#minimize).
1476
+ *
1477
+ * ####Example:
1478
+ * const schema = new Schema({ nested: { foo: String } });
1479
+ * const Model = mongoose.model('Test', schema);
1480
+ * const doc = new Model({});
1481
+ * doc.$isEmpty('nested'); // true
1482
+ * doc.nested.$isEmpty(); // true
1483
+ *
1484
+ * doc.nested.foo = 'bar';
1485
+ * doc.$isEmpty('nested'); // false
1486
+ * doc.nested.$isEmpty(); // false
1487
+ *
1488
+ * @memberOf Document
1489
+ * @instance
1490
+ * @api public
1491
+ * @method $isEmpty
1492
+ * @return {Boolean}
1493
+ */
1494
+
1495
+ Document.prototype.$isEmpty = function(path) {
1496
+ const isEmptyOptions = {
1497
+ minimize: true,
1498
+ virtuals: false,
1499
+ getters: false,
1500
+ transform: false
1501
+ };
1502
+
1503
+ if (arguments.length > 0) {
1504
+ const v = this.get(path);
1505
+ if (v == null) {
1506
+ return true;
1507
+ }
1508
+ if (typeof v !== 'object') {
1509
+ return false;
1510
+ }
1511
+ if (utils.isPOJO(v)) {
1512
+ return _isEmpty(v);
1513
+ }
1514
+ return Object.keys(v.toObject(isEmptyOptions)).length === 0;
1515
+ }
1516
+
1517
+ return Object.keys(this.toObject(isEmptyOptions)).length === 0;
1518
+ };
1519
+
1520
+ function _isEmpty(v) {
1521
+ if (v == null) {
1522
+ return true;
1523
+ }
1524
+ if (typeof v !== 'object' || Array.isArray(v)) {
1525
+ return false;
1526
+ }
1527
+ for (const key of Object.keys(v)) {
1528
+ if (!_isEmpty(v[key])) {
1529
+ return false;
1530
+ }
1531
+ }
1532
+ return true;
1533
+ }
1534
+
1423
1535
  /**
1424
1536
  * Returns the list of paths that have been modified.
1425
1537
  *
@@ -1757,7 +1869,7 @@ Document.prototype.validate = function(options, callback) {
1757
1869
  options = null;
1758
1870
  }
1759
1871
 
1760
- return utils.promiseOrCallback(callback, cb => this.$__validate(function(error) {
1872
+ return utils.promiseOrCallback(callback, cb => this.$__validate(options, function(error) {
1761
1873
  cb(error);
1762
1874
  }), this.constructor.events);
1763
1875
  };
@@ -1889,7 +2001,23 @@ function _getPathsToValidate(doc) {
1889
2001
  * ignore
1890
2002
  */
1891
2003
 
1892
- Document.prototype.$__validate = function(callback) {
2004
+ Document.prototype.$__validate = function(options, callback) {
2005
+ if (typeof options === 'function') {
2006
+ callback = options;
2007
+ options = null;
2008
+ }
2009
+
2010
+ const hasValidateModifiedOnlyOption = options &&
2011
+ (typeof options === 'object') &&
2012
+ ('validateModifiedOnly' in options);
2013
+
2014
+ let shouldValidateModifiedOnly;
2015
+ if (hasValidateModifiedOnlyOption) {
2016
+ shouldValidateModifiedOnly = !!options.validateModifiedOnly;
2017
+ } else {
2018
+ shouldValidateModifiedOnly = this.schema.options.validateModifiedOnly;
2019
+ }
2020
+
1893
2021
  const _this = this;
1894
2022
  const _complete = () => {
1895
2023
  const err = this.$__.validationError;
@@ -1911,7 +2039,9 @@ Document.prototype.$__validate = function(callback) {
1911
2039
 
1912
2040
  // only validate required fields when necessary
1913
2041
  const pathDetails = _getPathsToValidate(this);
1914
- const paths = pathDetails[0];
2042
+ const paths = shouldValidateModifiedOnly ?
2043
+ pathDetails[0].filter((path) => this.isModified(path)) :
2044
+ pathDetails[0];
1915
2045
  const skipSchemaValidators = pathDetails[1];
1916
2046
 
1917
2047
  if (paths.length === 0) {
@@ -1975,7 +2105,7 @@ Document.prototype.$__validate = function(callback) {
1975
2105
  _this.invalidate(path, err, undefined, true);
1976
2106
  }
1977
2107
  --total || complete();
1978
- }, scope, { skipSchemaValidators: skipSchemaValidators[path] });
2108
+ }, scope, { skipSchemaValidators: skipSchemaValidators[path], path: path });
1979
2109
  });
1980
2110
  };
1981
2111
 
@@ -2006,16 +2136,29 @@ Document.prototype.$__validate = function(callback) {
2006
2136
  * @api public
2007
2137
  */
2008
2138
 
2009
- Document.prototype.validateSync = function(pathsToValidate) {
2139
+ Document.prototype.validateSync = function(pathsToValidate, options) {
2010
2140
  const _this = this;
2011
2141
 
2142
+ const hasValidateModifiedOnlyOption = options &&
2143
+ (typeof options === 'object') &&
2144
+ ('validateModifiedOnly' in options);
2145
+
2146
+ let shouldValidateModifiedOnly;
2147
+ if (hasValidateModifiedOnlyOption) {
2148
+ shouldValidateModifiedOnly = !!options.validateModifiedOnly;
2149
+ } else {
2150
+ shouldValidateModifiedOnly = this.schema.options.validateModifiedOnly;
2151
+ }
2152
+
2012
2153
  if (typeof pathsToValidate === 'string') {
2013
2154
  pathsToValidate = pathsToValidate.split(' ');
2014
2155
  }
2015
2156
 
2016
2157
  // only validate required fields when necessary
2017
2158
  const pathDetails = _getPathsToValidate(this);
2018
- let paths = pathDetails[0];
2159
+ let paths = shouldValidateModifiedOnly ?
2160
+ pathDetails[0].filter((path) => this.isModified(path)) :
2161
+ pathDetails[0];
2019
2162
  const skipSchemaValidators = pathDetails[1];
2020
2163
 
2021
2164
  if (pathsToValidate && pathsToValidate.length) {
@@ -2047,7 +2190,8 @@ Document.prototype.validateSync = function(pathsToValidate) {
2047
2190
 
2048
2191
  const val = _this.getValue(path);
2049
2192
  const err = p.doValidateSync(val, _this, {
2050
- skipSchemaValidators: skipSchemaValidators[path]
2193
+ skipSchemaValidators: skipSchemaValidators[path],
2194
+ path: path
2051
2195
  });
2052
2196
  if (err && (!p.$isMongooseDocumentArray || err.$isArrayValidatorError)) {
2053
2197
  if (p.$isSingleNested &&
@@ -3110,7 +3254,6 @@ Document.prototype.populated = function(path, val, options) {
3110
3254
  }
3111
3255
 
3112
3256
  // internal
3113
-
3114
3257
  if (val === true) {
3115
3258
  if (!this.$__.populated) {
3116
3259
  return undefined;
@@ -3120,6 +3263,22 @@ Document.prototype.populated = function(path, val, options) {
3120
3263
 
3121
3264
  this.$__.populated || (this.$__.populated = {});
3122
3265
  this.$__.populated[path] = {value: val, options: options};
3266
+
3267
+ // If this was a nested populate, make sure each populated doc knows
3268
+ // about its populated children (gh-7685)
3269
+ const pieces = path.split('.');
3270
+ for (let i = 0; i < pieces.length - 1; ++i) {
3271
+ const subpath = pieces.slice(0, i + 1).join('.');
3272
+ const subdoc = this.get(subpath);
3273
+ if (subdoc != null && subdoc.$__ != null && this.populated(subpath)) {
3274
+ const rest = pieces.slice(i + 1).join('.');
3275
+ subdoc.populated(rest, val, options);
3276
+ // No need to continue because the above recursion should take care of
3277
+ // marking the rest of the docs as populated
3278
+ break;
3279
+ }
3280
+ }
3281
+
3123
3282
  return val;
3124
3283
  };
3125
3284
 
@@ -20,8 +20,9 @@ const util = require('util');
20
20
  * @api private
21
21
  */
22
22
 
23
- function NativeCollection() {
23
+ function NativeCollection(name, options) {
24
24
  this.collection = null;
25
+ this.Promise = options.Promise || Promise;
25
26
  MongooseCollection.apply(this, arguments);
26
27
  }
27
28
 
@@ -115,9 +116,10 @@ const syncCollectionMethods = { watch: true };
115
116
  function iter(i) {
116
117
  NativeCollection.prototype[i] = function() {
117
118
  const collection = this.collection;
118
- const args = arguments;
119
+ const args = Array.from(arguments);
119
120
  const _this = this;
120
121
  const debug = get(_this, 'conn.base.options.debug');
122
+ const lastArg = arguments[arguments.length - 1];
121
123
 
122
124
  // If user force closed, queueing will hang forever. See #5664
123
125
  if (this.opts.$wasForceClosed) {
@@ -127,8 +129,18 @@ function iter(i) {
127
129
  if (syncCollectionMethods[i]) {
128
130
  throw new Error('Collection method ' + i + ' is synchronous');
129
131
  }
130
- this.addQueue(i, arguments);
131
- return;
132
+ if (typeof lastArg === 'function') {
133
+ this.addQueue(i, args);
134
+ return;
135
+ }
136
+ return new this.Promise((resolve, reject) => {
137
+ this.addQueue(i, [].concat(args).concat([(err, res) => {
138
+ if (err != null) {
139
+ return reject(err);
140
+ }
141
+ resolve(res);
142
+ }]));
143
+ });
132
144
  }
133
145
 
134
146
  if (debug) {
@@ -8,4 +8,4 @@ exports.Binary = require('./binary');
8
8
  exports.Collection = require('./collection');
9
9
  exports.Decimal128 = require('./decimal128');
10
10
  exports.ObjectId = require('./objectid');
11
- exports.ReadPreference = require('./ReadPreference');
11
+ exports.ReadPreference = require('./ReadPreference');
@@ -9,6 +9,9 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) {
9
9
  const skipDocArrays = options.skipDocArrays;
10
10
 
11
11
  let deleted = 0;
12
+ if (!doc) {
13
+ return deleted;
14
+ }
12
15
  for (const modifiedPath of Object.keys(doc.$__.activePaths.states.modify)) {
13
16
  if (skipDocArrays) {
14
17
  const schemaType = doc.schema.path(modifiedPath);
@@ -100,6 +100,21 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) {
100
100
  value: true
101
101
  });
102
102
 
103
+ const _isEmptyOptions = Object.freeze({
104
+ minimize: true,
105
+ virtuals: false,
106
+ getters: false,
107
+ transform: false
108
+ });
109
+ Object.defineProperty(nested, '$isEmpty', {
110
+ enumerable: false,
111
+ configurable: true,
112
+ writable: false,
113
+ value: function() {
114
+ return Object.keys(this.get(path, null, _isEmptyOptions) || {}).length === 0;
115
+ }
116
+ });
117
+
103
118
  compile(subprops, nested, path, options);
104
119
  this.$__.getters[path] = nested;
105
120
  }
@@ -140,7 +155,7 @@ function getOwnPropertyDescriptors(object) {
140
155
  delete result[key];
141
156
  return;
142
157
  }
143
- result[key].enumerable = ['isNew', '$__', 'errors', '_doc'].indexOf(key) === -1;
158
+ result[key].enumerable = ['isNew', '$__', 'errors', '_doc', '$locals'].indexOf(key) === -1;
144
159
  });
145
160
 
146
161
  return result;
@@ -18,8 +18,12 @@ module.exports = function getEmbeddedDiscriminatorPath(doc, path, options) {
18
18
  const subpath = parts.slice(0, i + 1).join('.');
19
19
  schema = doc.schema.path(subpath);
20
20
  if (schema == null) {
21
+ type = 'adhocOrUndefined';
21
22
  continue;
22
23
  }
24
+ if (schema.instance === 'Mixed') {
25
+ return typeOnly ? 'real' : schema;
26
+ }
23
27
  type = doc.schema.pathType(subpath);
24
28
  if ((schema.$isSingleNested || schema.$isMongooseDocumentArrayElement) &&
25
29
  schema.schema.discriminators != null) {
@@ -30,11 +34,7 @@ module.exports = function getEmbeddedDiscriminatorPath(doc, path, options) {
30
34
  continue;
31
35
  }
32
36
  const rest = parts.slice(i + 1).join('.');
33
- schema = discriminators[discriminatorKey].path(rest);
34
- if (schema != null) {
35
- type = discriminators[discriminatorKey].pathType(rest);
36
- break;
37
- }
37
+ return getEmbeddedDiscriminatorPath(doc.get(subpath), rest, options);
38
38
  }
39
39
  }
40
40