diff --git a/History.md b/History.md index 94e8e7bca50..ffc73974aab 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,20 @@ +4.6.5 / 2016-10-23 +================== + * docs: fix grammar issues #4642 #4640 #4639 [silvermanj7](https://github.com/silvermanj7) + * fix(populate): filter out nonexistant values for dynref #4637 + * fix(query): handle $type as a schematype operator #4632 + * fix(schema): better handling for uppercase: false and lowercase: false #4622 + * fix(query): don't run transforms on updateForExec() #4621 + * fix(query): handle id = 0 in findById #4610 + * fix(query): handle buffers in mergeClone #4609 + * fix(document): handle undefined with conditional validator for validateSync #4607 + * fix: upgrade to mongodb driver 2.2.11 #4581 + * docs(schematypes): clarify schema.path() #4518 + * fix(query): ensure path is defined before checking in timestamps #4514 + * fix(model): set version key in upsert #4505 + * fix(document): never depopulate top-level doc #3057 + * refactor: ensure sync for setting non-capped collections #2690 + 4.6.4 / 2016-10-16 ================== * fix(query): cast $not correctly #4616 #4592 [prssn](https://github.com/prssn) diff --git a/docs/guide.jade b/docs/guide.jade index 00edbc2e54d..8b18ca90cac 100644 --- a/docs/guide.jade +++ b/docs/guide.jade @@ -127,7 +127,7 @@ block content h3#query-helpers Query Helpers :markdown - You can also add query helper functions, which are like instance methods, + You can also add query helper functions, which are like instance methods but for mongoose queries. Query helper methods let you extend mongoose's [chainable query builder API](./queries.html). diff --git a/docs/populate.jade b/docs/populate.jade index fc148f44cf3..5bc7a0387d7 100644 --- a/docs/populate.jade +++ b/docs/populate.jade @@ -344,6 +344,19 @@ block content Band.find({}).populate('members').exec(function(error, bands) { /* `bands.members` is now an array of instances of `Person` */ }); + + :markdown + Keep in mind that virtuals are _not_ included in `toJSON()` output by + default. If you want populate virtuals to show up when using functions + that rely on `JSON.stringify()`, like Express' + [`res.json()` function](http://expressjs.com/en/4x/api.html#res.json), + set the `virtuals: true` option on your schema's `toJSON` options. + + :js + // Set `virtuals: true` so `res.json()` works + var BandSchema = new Schema({ + name: String + }, { toJSON: { virtuals: true } }); h3#next Next Up :markdown diff --git a/docs/schematypes.jade b/docs/schematypes.jade index c0a74ee8891..06bbe96393c 100644 --- a/docs/schematypes.jade +++ b/docs/schematypes.jade @@ -244,6 +244,26 @@ block content | To create your own custom schema take a look at a(href="/docs/customschematypes.html") Creating a Basic Custom Schema Type |. + + h3#path The `schema.path()` Function + :markdown + The `schema.path()` function returns the instantiated schema type for a + given path. + :js + var sampleSchema = new Schema({ name: { type: String, required: true } }); + console.log(sampleSchema.path('name')); + // Output looks like: + /** + * SchemaString { + * enumValues: [], + * regExp: null, + * path: 'name', + * instance: 'String', + * validators: ... + */ + :markdown + You can use this function to inspect the schema type for a given path, + including what validators it has and what the type is. h3#next Next Up :markdown diff --git a/docs/subdocs.jade b/docs/subdocs.jade index e32b7927dc0..90a4306eca7 100644 --- a/docs/subdocs.jade +++ b/docs/subdocs.jade @@ -4,7 +4,7 @@ block content h2 Sub Docs :markdown [Sub-documents](./api.html#types-embedded-js) are docs with schemas of - their own which are elements of a parents document array: + their own which are elements of a parent document array: :js var childSchema = new Schema({ name: 'string' }); @@ -24,7 +24,7 @@ block content parent.save(callback); :markdown - If an error occurs in a sub-documents' middleware, it is bubbled up to the `save()` callback of the parent, so error handling is a snap! + If an error occurs in a sub-document's middleware, it is bubbled up to the `save()` callback of the parent, so error handling is a snap! :js childSchema.pre('save', function (next) { @@ -104,8 +104,8 @@ block content }); :markdown A single embedded sub-document behaves similarly to an embedded array. - It only gets saved when the parent document gets saved, and its pre/post - document middleware get executed. + It only gets saved when the parent document gets saved and its pre/post + document middleware gets executed. :js childSchema.pre('save', function(next) { console.log(this.name); // prints 'Leia' diff --git a/lib/cast.js b/lib/cast.js index 3f687d0d7f8..6a071969dad 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -196,13 +196,6 @@ module.exports = function cast(schema, obj) { continue; } - if ($cond === '$type') { - if (typeof nested !== 'number' && typeof nested !== 'string') { - throw new Error('$type parameter must be number or string'); - } - continue; - } - if ($cond === '$not') { if (nested && schematype && !schematype.caster) { _keys = Object.keys(nested); diff --git a/lib/document.js b/lib/document.js index 8d460f3ee44..1c20ae29445 100644 --- a/lib/document.js +++ b/lib/document.js @@ -2023,17 +2023,13 @@ Document.prototype.$toObject = function(options, json) { retainKeyOrder: this.schema.options.retainKeyOrder }; - if (options && options.depopulate && !options._skipDepopulateTopLevel && this.$__.wasPopulated) { + // _isNested will only be true if this is not the top level document, we + // should never depopulate + if (options && options.depopulate && options._isNested && this.$__.wasPopulated) { // populated paths that we set to a document return clone(this._id, options); } - // If we're calling toObject on a populated doc, we may want to skip - // depopulated on the top level - if (options && options._skipDepopulateTopLevel) { - options._skipDepopulateTopLevel = false; - } - // When internally saving this document we always pass options, // bypassing the custom schema options. if (!(options && utils.getFunctionName(options.constructor) === 'Object') || @@ -2065,6 +2061,8 @@ Document.prototype.$toObject = function(options, json) { // to save it from being overwritten by sub-transform functions var originalTransform = options.transform; + options._isNested = true; + var ret = clone(this._doc, options) || {}; if (options.getters) { diff --git a/lib/drivers/node-mongodb-native/collection.js b/lib/drivers/node-mongodb-native/collection.js index 7e88140b2c2..9bb1bef4b9e 100644 --- a/lib/drivers/node-mongodb-native/collection.js +++ b/lib/drivers/node-mongodb-native/collection.js @@ -40,7 +40,8 @@ NativeCollection.prototype.onOpen = function() { if (!_this.opts.capped.size) { // non-capped - return _this.conn.db.collection(_this.name, callback); + callback(null, _this.conn.db.collection(_this.name)); + return _this.collection; } // capped diff --git a/lib/model.js b/lib/model.js index 7583a852f95..c1a198baafc 100644 --- a/lib/model.js +++ b/lib/model.js @@ -407,7 +407,7 @@ function handleAtomics(self, where, delta, data, value) { // $set if (isMongooseObject(value)) { - value = value.toObject({depopulate: 1}); + value = value.toObject({depopulate: 1, _isNested: true}); } else if (value.valueOf) { value = value.valueOf(); } @@ -417,7 +417,7 @@ function handleAtomics(self, where, delta, data, value) { function iter(mem) { return isMongooseObject(mem) - ? mem.toObject({depopulate: 1}) + ? mem.toObject({depopulate: 1, _isNested: true}) : mem; } @@ -426,7 +426,7 @@ function handleAtomics(self, where, delta, data, value) { val = atomics[op]; if (isMongooseObject(val)) { - val = val.toObject({depopulate: true, transform: false}); + val = val.toObject({depopulate: true, transform: false, _isNested: true}); } else if (Array.isArray(val)) { val = val.map(iter); } else if (val.valueOf) { @@ -506,7 +506,7 @@ Model.prototype.$__delta = function() { value = value.toObject(); operand(this, where, delta, data, value); } else { - value = utils.clone(value, {depopulate: 1}); + value = utils.clone(value, {depopulate: 1, _isNested: true}); operand(this, where, delta, data, value); } } @@ -1556,7 +1556,7 @@ Model.findOneAndUpdate = function(conditions, update, options, callback) { fields = options.fields; } - update = utils.clone(update, {depopulate: 1}); + update = utils.clone(update, {depopulate: 1, _isNested: true}); if (this.schema.options.versionKey && options && options.upsert) { if (!update.$setOnInsert) { update.$setOnInsert = {}; @@ -2081,6 +2081,13 @@ Model.update = function update(conditions, doc, options, callback) { } options = typeof options === 'function' ? options : utils.clone(options); + if (this.schema.options.versionKey && options && options.upsert) { + if (!doc.$setOnInsert) { + doc.$setOnInsert = {}; + } + doc.$setOnInsert[this.schema.options.versionKey] = 0; + } + return mq.update(conditions, doc, options, callback); }; @@ -2927,6 +2934,11 @@ function getModelsMapForPopulate(model, docs, options) { if (refPath) { modelNames = utils.getValue(refPath, doc); + if (Array.isArray(modelNames)) { + modelNames = modelNames.filter(function(v) { + return v != null; + }); + } } else { if (!modelNameFromQuery) { var modelForCurrentDoc = model; diff --git a/lib/query.js b/lib/query.js index a02873371bf..136934d5723 100644 --- a/lib/query.js +++ b/lib/query.js @@ -946,7 +946,11 @@ Query.prototype.getUpdate = function() { */ Query.prototype._updateForExec = function() { - var update = utils.clone(this._update, { retainKeyOrder: true }); + var update = utils.clone(this._update, { + retainKeyOrder: true, + transform: false, + depopulate: true + }); var ops = Object.keys(update); var i = ops.length; var ret = {}; diff --git a/lib/schema.js b/lib/schema.js index adb76dc5425..45cd986d8ea 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -878,10 +878,12 @@ function applyTimestampsToChildren(query) { if (hasDollarKey) { if (update.$push) { for (key in update.$push) { + var $path = schema.path(key); if (update.$push[key] && - schema.path(key).$isMongooseDocumentArray && - schema.path(key).schema.options.timestamps) { - timestamps = schema.path(key).schema.options.timestamps; + $path && + $path.$isMongooseDocumentArray && + $path.schema.options.timestamps) { + timestamps = $path.schema.options.timestamps; createdAt = timestamps.createdAt || 'createdAt'; updatedAt = timestamps.updatedAt || 'updatedAt'; update.$push[key][updatedAt] = now; diff --git a/lib/schema/number.js b/lib/schema/number.js index 3fec612dc43..a91f6dda6df 100644 --- a/lib/schema/number.js +++ b/lib/schema/number.js @@ -203,9 +203,9 @@ SchemaNumber.prototype.cast = function(value, doc, init) { return ret; } - var val = value && value._id - ? value._id // documents - : value; + var val = value && typeof value._id !== 'undefined' ? + value._id : // documents + value; if (!isNaN(val)) { if (val === null) { diff --git a/lib/schema/string.js b/lib/schema/string.js index 02ff495f934..cf49af6dbfc 100644 --- a/lib/schema/string.js +++ b/lib/schema/string.js @@ -129,7 +129,10 @@ SchemaString.prototype.enum = function() { * @return {SchemaType} this */ -SchemaString.prototype.lowercase = function() { +SchemaString.prototype.lowercase = function(shouldApply) { + if (arguments.length > 0 && !shouldApply) { + return this; + } return this.set(function(v, self) { if (typeof v !== 'string') { v = self.cast(v); @@ -155,7 +158,10 @@ SchemaString.prototype.lowercase = function() { * @return {SchemaType} this */ -SchemaString.prototype.uppercase = function() { +SchemaString.prototype.uppercase = function(shouldApply) { + if (arguments.length > 0 && !shouldApply) { + return this; + } return this.set(function(v, self) { if (typeof v !== 'string') { v = self.cast(v); diff --git a/lib/schematype.js b/lib/schematype.js index 8c796d9bb18..3ca3423618b 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -776,12 +776,16 @@ SchemaType.prototype.doValidateSync = function(value, scope) { } }; - var _this = this; - if (value === undefined && !_this.isRequired) { - return null; + var validators = this.validators; + if (value === void 0) { + if (this.validators.length > 0 && this.validators[0].type === 'required') { + validators = [this.validators[0]]; + } else { + return null; + } } - this.validators.forEach(function(v) { + validators.forEach(function(v) { if (err) { return; } @@ -874,7 +878,14 @@ SchemaType.prototype.$conditionalHandlers = { $eq: handleSingle, $in: handleArray, $ne: handleSingle, - $nin: handleArray + $nin: handleArray, + $type: function(val) { + if (typeof val !== 'number' && typeof val !== 'string') { + throw new Error('$type parameter must be number or string'); + } + + return val; + } }; /** diff --git a/lib/types/array.js b/lib/types/array.js index e63c02367e8..715b6b65afb 100644 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -232,7 +232,7 @@ MongooseArray.mixin = { var i = keys.length; if (i === 0) { - ret[0] = ['$set', this.toObject({depopulate: 1, transform: false})]; + ret[0] = ['$set', this.toObject({depopulate: 1, transform: false, _isNested: true})]; return ret; } @@ -244,9 +244,9 @@ MongooseArray.mixin = { // need to convert their elements as if they were MongooseArrays // to handle populated arrays versus DocumentArrays properly. if (isMongooseObject(val)) { - val = val.toObject({depopulate: 1, transform: false}); + val = val.toObject({depopulate: 1, transform: false, _isNested: true}); } else if (Array.isArray(val)) { - val = this.toObject.call(val, {depopulate: 1, transform: false}); + val = this.toObject.call(val, {depopulate: 1, transform: false, _isNested: true}); } else if (val.valueOf) { val = val.valueOf(); } @@ -713,6 +713,7 @@ MongooseArray.mixin = { toObject: function(options) { if (options && options.depopulate) { + options._isNested = true; return this.map(function(doc) { return doc instanceof Document ? doc.toObject(options) diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 606cfa76ce6..911fdf44093 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -19,7 +19,7 @@ Subdocument.prototype = Object.create(Document.prototype); Subdocument.prototype.toBSON = function() { return this.toObject({ transform: false }); -}, +}; /** * Used as a stub for [hooks.js](https://github.com/bnoguchi/hooks-js/tree/31ec571cef0332e21121ee7157e0cf9728572cc3) diff --git a/lib/utils.js b/lib/utils.js index a5aa720060a..391325f0641 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -840,6 +840,9 @@ exports.mergeClone = function(to, fromObj) { if (isMongooseObject(fromObj[key]) && !fromObj[key].isMongooseBuffer) { obj = obj.toObject({ transform: false }); } + if (fromObj[key].isMongooseBuffer) { + obj = new Buffer(obj); + } exports.mergeClone(to[key], obj); } else { // make sure to retain key order here because of a bug handling the diff --git a/package.json b/package.json index 7429a4b6e1a..8ccd487c5f9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mongoose", "description": "Mongoose MongoDB ODM", - "version": "4.6.4", + "version": "4.6.5", "author": "Guillermo Rauch ", "keywords": [ "mongodb", @@ -23,7 +23,7 @@ "bson": "~0.5.4", "hooks-fixed": "1.2.0", "kareem": "1.1.3", - "mongodb": "2.2.10", + "mongodb": "2.2.11", "mpath": "0.2.1", "mpromise": "0.5.5", "mquery": "2.0.0", @@ -46,7 +46,7 @@ "markdown": "0.3.1", "marked": "0.3.6", "mocha": "3.0.2", - "mongoose-long": "0.1.0", + "mongoose-long": "0.1.1", "node-static": "0.7.7", "power-assert": "1.2.0", "q": "1.4.1", diff --git a/test/document.test.js b/test/document.test.js index 89475867bb7..b14d6667f4d 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -3254,6 +3254,30 @@ describe('document', function() { catch(done); }); + it('validateSync with undefined and conditional required (gh-4607)', function(done) { + var schema = new mongoose.Schema({ + type: mongoose.SchemaTypes.Number, + conditional: { + type: mongoose.SchemaTypes.String, + required: function() { + return this.type === 1; + }, + maxlength: 128 + } + }); + + var Model = db.model('gh4607', schema); + + assert.doesNotThrow(function() { + new Model({ + type: 2, + conditional: void 0 + }).validateSync(); + }); + + done(); + }); + it('setting full path under single nested schema works (gh-4578) (gh-4528)', function(done) { var ChildSchema = new mongoose.Schema({ age: Number @@ -3280,6 +3304,21 @@ describe('document', function() { }); }); + it('toObject() does not depopulate top level (gh-3057)', function(done) { + var Cat = db.model('gh3057', { name: String }); + var Human = db.model('gh3057_0', { + name: String, + petCat: { type: mongoose.Schema.Types.ObjectId, ref: 'gh3057' } + }); + + var kitty = new Cat({ name: 'Zildjian' }); + var person = new Human({ name: 'Val', petCat: kitty }); + + assert.equal(kitty.toObject({ depopulate: true }).name, 'Zildjian'); + assert.ok(!person.toObject({ depopulate: true }).petCat.name); + done(); + }); + it('modify multiple subdoc paths (gh-4405)', function(done) { var ChildObjectSchema = new Schema({ childProperty1: String, diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index bddb45eb580..5c87913b591 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -964,7 +964,7 @@ describe('model: findByIdAndUpdate:', function() { } }); - it('adds __v on upsert (gh-2122)', function(done) { + it('adds __v on upsert (gh-2122) (gh-4505)', function(done) { var db = start(); var accountSchema = new Schema({ @@ -974,14 +974,21 @@ describe('model: findByIdAndUpdate:', function() { var Account = db.model('2122', accountSchema); Account.findOneAndUpdate( - {name: 'account'}, - {}, - {upsert: true, new: true}, - function(error, doc) { + {name: 'account'}, + {name: 'test'}, + {upsert: true, new: true}, + function(error, doc) { + assert.ifError(error); + assert.equal(doc.__v, 0); + Account.update({ name: 'test' }, {}, { upsert: true }, function(error) { assert.ifError(error); - assert.equal(doc.__v, 0); - db.close(done); + Account.findOne({ name: 'test' }, function(error, doc) { + assert.ifError(error); + assert.equal(doc.__v, 0); + db.close(done); + }); }); + }); }); it('works with nested schemas and $pull+$or (gh-1932)', function(done) { diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 3338b0557b3..d907e87176b 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -3001,6 +3001,54 @@ describe('model: populate:', function() { done(); }); }); + + it('with nonexistant refPath (gh-4637)', function(done) { + var baseballSchema = mongoose.Schema({ + seam: String + }); + var Baseball = db.model('Baseball', baseballSchema); + + var ballSchema = mongoose.Schema({ + league: String, + kind: String, + ball: { + type: mongoose.Schema.Types.ObjectId, + refPath: 'balls.kind' + } + }); + + var basketSchema = mongoose.Schema({ + balls: [ballSchema] + }); + var Basket = db.model('Basket', basketSchema); + + new Baseball({seam: 'yarn'}). + save(). + then(function(baseball) { + return new Basket({ + balls: [ + { + league: 'MLB', + kind: 'Baseball', + ball: baseball._id + }, + { + league: 'NBA' + } + ] + }).save(); + }). + then(function(basket) { + return basket.populate('balls.ball').execPopulate(); + }). + then(function(basket) { + assert.equal(basket.balls[0].ball.seam, 'yarn'); + assert.ok(!basket.balls[1].kind); + assert.ok(!basket.balls[1].ball); + done(); + }). + catch(done); + }); }); describe('leaves Documents within Mixed properties alone (gh-1471)', function() { diff --git a/test/model.query.casting.test.js b/test/model.query.casting.test.js index aeaf4903a02..421df199d03 100644 --- a/test/model.query.casting.test.js +++ b/test/model.query.casting.test.js @@ -965,6 +965,33 @@ describe('model query casting', function() { }); }); + it('date with $not + $type (gh-4632)', function(done) { + var db = start(); + + var MyModel = db.model('gh4632', { test: Date }); + + MyModel.find({ test: { $not: { $type: 9 } } }, function(error) { + assert.ifError(error); + done(); + }); + }); + + it('_id = 0 (gh-4610)', function(done) { + var db = start(); + + var MyModel = db.model('gh4610', { _id: Number }); + + MyModel.create({ _id: 0 }, function(error) { + assert.ifError(error); + MyModel.findById({ _id: 0 }, function(error, doc) { + assert.ifError(error); + assert.ok(doc); + assert.equal(doc._id, 0); + done(); + }); + }); + }); + it('minDistance (gh-4197)', function(done) { var db = start(); diff --git a/test/model.update.test.js b/test/model.update.test.js index 877341c27d3..9c026a5cff0 100644 --- a/test/model.update.test.js +++ b/test/model.update.test.js @@ -1664,6 +1664,40 @@ describe('model: update:', function() { }); }); + it('with single nested and transform (gh-4621)', function(done) { + var SubdocSchema = new Schema({ + name: String + }, { + toObject: { + transform: function(doc, ret) { + ret.id = ret._id.toString(); + delete ret._id; + } + } + }); + + var CollectionSchema = new Schema({ + field2: SubdocSchema + }); + + var Collection = db.model('gh4621', CollectionSchema); + + Collection.create({}, function(error, doc) { + assert.ifError(error); + var update = { 'field2': { name: 'test' } }; + Collection.update({ _id: doc._id }, update, function(err) { + assert.ifError(err); + Collection.collection.findOne({ _id: doc._id }, function(err, doc) { + assert.ifError(err); + assert.ok(doc.field2._id); + assert.ok(!doc.field2.id); + done(); + }); + }); + }); + + }); + it('works with buffers (gh-3496)', function(done) { var Schema = mongoose.Schema({myBufferField: Buffer}); var Model = db.model('gh3496', Schema); @@ -1981,6 +2015,51 @@ describe('model: update:', function() { }); }); + it('push with timestamps (gh-4514)', function(done) { + var sampleSchema = new mongoose.Schema({ + sampleArray: [{ + values: [String] + }] + }, { timestamps: true }); + + var sampleModel = db.model('gh4514', sampleSchema); + var newRecord = new sampleModel({ + sampleArray: [{ values: ['record1'] }] + }); + + newRecord.save(function(err) { + assert.ifError(err); + sampleModel.update({ 'sampleArray.values': 'record1' }, { + $push: { 'sampleArray.$.values': 'another record' } + }, + { runValidators: true }, + function(err) { + assert.ifError(err); + done(); + }); + }); + }); + + it('update with buffer and exec (gh-4609)', function(done) { + var arrSchema = new Schema({ + ip: mongoose.SchemaTypes.Buffer + }); + var schema = new Schema({ + arr: [arrSchema] + }); + + var M = db.model('gh4609', schema); + + var m = new M({ arr: [{ ip: new Buffer(1) }] }); + m.save(function(error, m) { + assert.ifError(error); + m.update({ $push: { arr: { ip: new Buffer(1) } } }).exec(function(error) { + assert.ifError(error); + done(); + }); + }); + }); + it('update handles casting with mongoose-long (gh-4283)', function(done) { require('mongoose-long')(mongoose);