diff --git a/.eslintignore b/.eslintignore index b1109fb5b0a..b03442e5d60 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,3 +2,5 @@ docs/ bin/ test/triage/ test/model.discriminator.test.js +tools/ +test/es6/ diff --git a/.gitignore b/.gitignore index 00dc9ca998a..c2a14d4b815 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,6 @@ benchmarks/v8.log .DS_Store docs/*.json docs/source/_docs -docs/*.html tags test/triage/*.js /.idea diff --git a/.travis.yml b/.travis.yml index 23b799dc7e2..88444bbe6ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,3 +13,6 @@ before_script: - tar -zxvf mongodb-linux-x86_64-2.6.11.tgz - mkdir -p ./data/db - ./mongodb-linux-x86_64-2.6.11/bin/mongod --fork --nopreallocj --dbpath ./data/db --syslog --port 27017 +script: + - npm test + - npm run lint diff --git a/History.md b/History.md index 0c187c9d807..928cc7fc8b1 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,57 @@ +4.9.9 / 2017-05-13 +================== + * docs: correct value for Query#regex() #5230 + * fix(connection): don't throw if .catch() on open() promise #5229 + * fix(schema): allow update with $currentDate for updatedAt to succeed #5222 + * fix(model): versioning doesn't fail if version key undefined #5221 [basileos](https://github.com/basileos) + * fix(document): don't emit model error if callback specified for consistency with docs #5216 + * fix(document): handle errors in subdoc pre validate #5215 + +4.9.8 / 2017-05-07 +================== + * docs(subdocs): rewrite subdocs guide #5217 + * fix(document): avoid circular JSON if error in doc array under single nested subdoc #5208 + * fix(document): set intermediate empty objects for deeply nested undefined paths before path itself #5206 + * fix(schema): throw error if first param to schema.plugin() is not a function #5201 + * perf(document): major speedup in validating subdocs (50x in some cases) #5191 + +4.9.7 / 2017-04-30 +================== + * docs: fix typo #5204 [phutchins](https://github.com/phutchins) + * fix(schema): ensure correct path for deeply nested schema indexes #5199 + * fix(schema): make remove a reserved name #5197 + * fix(model): handle Decimal type in insertMany correctly #5190 + * fix: upgrade kareem to handle async pre hooks correctly #5188 + * docs: add details about unique not being a validator #5179 + * fix(validation): handle returning a promise with isAsync: true #5171 + +4.9.6 / 2017-04-23 +================== + * fix: update `parentArray` references when directly assigning document arrays #5192 [jhob](https://github.com/jhob) + * docs: improve schematype validator docs #5178 [milesbarr](https://github.com/milesbarr) + * fix(model): modify discriminator() class in place #5175 + * fix(model): handle bulkWrite updateMany casting #5172 [tzellman](https://github.com/tzellman) + * docs(model): fix replaceOne example for bulkWrite #5168 + * fix(document): don't create a new array subdoc when creating schema array #5162 + * fix(model): merge query hooks from discriminators #5147 + * fix(document): add parent() function to subdocument to match array subdoc #5134 + +4.9.5 / 2017-04-16 +================== + * fix(query): correct $pullAll casting of null #5164 [Sebmaster](https://github.com/Sebmaster) + * docs: add advanced schemas docs for loadClass #5157 + * fix(document): handle null/undefined gracefully in applyGetters() #5143 + * fix(model): add resolveToObject option for mapReduce with ES6 promises #4945 + +4.9.4 / 2017-04-09 +================== + * fix(schema): clone query middleware correctly #5153 #5141 [clozanosanchez](https://github.com/clozanosanchez) + * docs(aggregate): fix typo #5142 + * fix(query): cast .$ update to underlying array type #5130 + * fix(populate): don't mutate populate result in place #5128 + * fix(query): handle $setOnInsert consistent with $set #5126 + * docs(query): add strict mode option for findOneAndUpdate #5108 + 4.9.3 / 2017-04-02 ================== * docs: document.js fixes for functions prepended with `$` #5131 [krmannix](https://github.com/krmannix) diff --git a/docs/faq.jade b/docs/faq.jade index 59bd3554e1a..9ca9d50f8de 100644 --- a/docs/faq.jade +++ b/docs/faq.jade @@ -30,6 +30,44 @@ block content doc.array[3] = 'changed'; doc.markModified('array'); doc.save(); + + hr#unique-doesnt-work + :markdown + **Q**. I declared a schema property as `unique` but I can still save duplicates. What gives? + + **A**. Mongoose doesn't handle `unique` on it's own, `{ name: { type: String, unique: true } }` + just a shorthand for creating a [MongoDB unique index on `name`](https://docs.mongodb.com/manual/core/index-unique/). + For example, if MongoDB doesn't already have a unique index on `name`, the below code will not error despite the fact that `unique` is true. + :js + var schema = new mongoose.Schema({ + name: { type: String, unique: true } + }); + var Model = db.model('Test', schema); + + Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) { + console.log(err); // No error, unless index was already built + }); + :markdown + However, if you wait for the index to build using the `Model.on('index')` event, attempts to save duplicates will correctly error. + :js + var schema = new mongoose.Schema({ + name: { type: String, unique: true } + }); + var Model = db.model('Test', schema); + + Model.on('index', function(err) { // <-- Wait for model's indexes to finish + assert.ifError(err); + Model.create([{ name: 'Val' }, { name: 'Val' }], function(err) { + console.log(err); + }); + }); + :markdown + MongoDB persists indexes, so you only need to rebuild indexes if you're starting + with a fresh database or you ran `db.dropDatabase()`. In a production environment, + you should [create your indexes using the MongoDB shell])(https://docs.mongodb.com/manual/reference/method/db.collection.createIndex/) + rather than relying on mongoose to do it for you. The `unique` option for schemas is + convenient for development and documentation, but mongoose is *not* an index management solution. + hr#date_changes :markdown **Q**. Why don't in-place modifications to date objects diff --git a/docs/includes/nav.jade b/docs/includes/nav.jade index 2a102cb4d27..5f7afcc3ba2 100644 --- a/docs/includes/nav.jade +++ b/docs/includes/nav.jade @@ -22,6 +22,12 @@ ul a(href="./schematypes.html") span schema | types + li.custom-schema-types + a(href="./customschematypes.html") + | custom schema types + li.advanced-schemas + a(href="./advanced_schemas.html") + | advanced usage li a(href="./models.html") | models @@ -68,9 +74,6 @@ ul li a(href="./browser.html") | schemas in the browser - li - a(href="./customschematypes.html") - | custom schema types li a(href="./compatibility.html") | MongoDB Version Compatibility diff --git a/docs/source/acquit.js b/docs/source/acquit.js index ae4a68c02c3..91ede15c43e 100644 --- a/docs/source/acquit.js +++ b/docs/source/acquit.js @@ -30,6 +30,11 @@ var files = [ input: 'test/docs/validation.test.js', output: 'validation.html', title: 'Validation' + }, + { + input: 'test/docs/schemas.test.es6.js', + output: 'advanced_schemas.html', + title: 'Advanced Schemas' } ]; diff --git a/docs/subdocs.jade b/docs/subdocs.jade index 90a4306eca7..dbbc4c79f5e 100644 --- a/docs/subdocs.jade +++ b/docs/subdocs.jade @@ -3,47 +3,101 @@ extends layout 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 parent document array: + Subdocuments are documents embedded in other documents. In Mongoose, this + means you can nest schemas in other schemas. Mongoose has two + distinct notions of subdocuments: arrays of subdocuments and single nested + subdocuments. :js var childSchema = new Schema({ name: 'string' }); var parentSchema = new Schema({ - children: [childSchema] - }) + // Array of subdocuments + children: [childSchema], + // Single nested subdocuments. Caveat: single nested subdocs only work + // in mongoose >= 4.2.0 + child: childSchema + }); :markdown - Sub-documents enjoy all the same features as normal - [documents](./api.html#document-js). The only difference is that they are + Subdocuments are similar to normal documents. Nested schemas can have + [middleware](http://mongoosejs.com/docs/middleware.html), [custom validation logic](http://mongoosejs.com/docs/middleware.html), + virtuals, and any other feature top-level schemas can use. The major + difference is that subdocuments are not saved individually, they are saved whenever their top-level parent document is saved. :js var Parent = mongoose.model('Parent', parentSchema); var parent = new Parent({ children: [{ name: 'Matt' }, { name: 'Sarah' }] }) parent.children[0].name = 'Matthew'; + + // `parent.children[0].save()` is a no-op, it triggers middleware but + // does **not** actually save the subdocument. You need to save the parent + // doc. parent.save(callback); :markdown - 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! + Subdocuments have `save` and `validate` [middleware](http://mongoosejs.com/docs/middleware.html) + just like top-level documents. Calling `save()` on the parent document triggers + the `save()` middleware for all its subdocuments, and the same for `validate()` + middleware. :js childSchema.pre('save', function (next) { - if ('invalid' == this.name) return next(new Error('#sadpanda')); + if ('invalid' == this.name) { + return next(new Error('#sadpanda')); + } next(); }); var parent = new Parent({ children: [{ name: 'invalid' }] }); parent.save(function (err) { console.log(err.message) // #sadpanda - }) + }); + + :markdown + Subdocuments' `pre('save')` and `pre('validate')` middleware execute + **before** the top-level document's `pre('save')` but **after** the + top-level document's `pre('validate')` middleware. This is because validating + before `save()` is actually a piece of built-in middleware. + + :js + // Below code will print out 1-4 in order + var childSchema = new mongoose.Schema({ name: 'string' }); + + childSchema.pre('validate', function(next) { + console.log('2'); + next(); + }); + + childSchema.pre('save', function(next) { + console.log('3'); + next(); + }); + + var parentSchema = new mongoose.Schema({ + child: childSchema, + }); + + parentSchema.pre('validate', function(next) { + console.log('1'); + next(); + }); + + parentSchema.pre('save', function(next) { + console.log('4'); + next(); + }); + h3 Finding a sub-document :markdown - Each document has an `_id`. DocumentArrays have a special [id](./api.html#types_documentarray_MongooseDocumentArray-id) method for looking up a document by its `_id`. + Each subdocument has an `_id` by default. Mongoose document arrays have a + special [id](./api.html#types_documentarray_MongooseDocumentArray-id) method + for searching a document array to find a document with a given `_id`. :js var doc = parent.children.id(_id); - h3 Adding sub-docs + h3 Adding sub-docs to arrays :markdown MongooseArray methods such as [push](./api.html#types_array_MongooseArray.push), @@ -71,50 +125,36 @@ block content :js var newdoc = parent.children.create({ name: 'Aaron' }); - h3 Removing docs + h3 Removing subdocs :markdown - Each sub-document has it's own - [remove](./api.html#types_embedded_EmbeddedDocument-remove) method. + Each subdocument has it's own + [remove](./api.html#types_embedded_EmbeddedDocument-remove) method. For + an array subdocument, this is equivalent to calling `.pull()` on the + subdocument. For a single nested subdocument, `remove()` is equivalent + to setting the subdocument to `null`. :js - var doc = parent.children.id(_id).remove(); + // Equivalent to `parent.children.pull(_id)` + parent.children.id(_id).remove(); + // Equivalent to `parent.child = null` + parent.child.remove(); parent.save(function (err) { if (err) return handleError(err); - console.log('the sub-doc was removed') + console.log('the subdocs were removed'); }); - h4#altsyntax Alternate declaration syntax + h4#altsyntax Alternate declaration syntax for arrays :markdown - If you don't need access to the sub-document schema instance, - you may also declare sub-docs by simply passing an object literal: + If you create a schema with an array of objects, mongoose will automatically + convert the object to a schema for you: :js var parentSchema = new Schema({ children: [{ name: 'string' }] - }) - - h4#single-embedded Single Embedded Subdocs - :markdown - **New in 4.2.0** - - You can also embed schemas without using arrays. - :js - var childSchema = new Schema({ name: 'string' }); - - var parentSchema = new Schema({ - child: childSchema }); - :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 gets executed. - :js - childSchema.pre('save', function(next) { - console.log(this.name); // prints 'Leia' + // Equivalent + var parentSchema = new Schema({ + children: [new Schema({ name: 'string' })] }); - var Parent = mongoose.model('Parent', parentSchema); - var parent = new Parent({ child: { name: 'Luke' } }) - parent.child.name = 'Leia'; - parent.save(callback); // Triggers the pre middleware. - + h3#next Next Up :markdown Now that we've covered `Sub-documents`, let's take a look at diff --git a/lib/aggregate.js b/lib/aggregate.js index 8bb126b5b5f..623fbaa8737 100644 --- a/lib/aggregate.js +++ b/lib/aggregate.js @@ -171,7 +171,7 @@ Aggregate.prototype.project = function(arg) { * * ####Examples: * - * aggregate.match({ department: { $in: [ "sales", "engineering" } } }); + * aggregate.match({ department: { $in: [ "sales", "engineering" ] } }); * * @see $match http://docs.mongodb.org/manual/reference/aggregation/match/ * @method match diff --git a/lib/cast.js b/lib/cast.js index 406c7d1c1d5..4ff8449dd54 100644 --- a/lib/cast.js +++ b/lib/cast.js @@ -179,6 +179,9 @@ module.exports = function cast(schema, obj, options) { } throw new StrictModeError(path, 'Path "' + path + '" is not in ' + 'schema, strict mode is `true`, and upsert is `true`.'); + } else if (options && options.strictQuery === 'throw') { + throw new StrictModeError(path, 'Path "' + path + '" is not in ' + + 'schema and strictQuery is true.'); } } else if (val === null || val === undefined) { obj[path] = null; diff --git a/lib/connection.js b/lib/connection.js index c2cd448934a..0c7e138238f 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -143,42 +143,15 @@ Connection.prototype.db; Connection.prototype.config; -/** - * Opens the connection to MongoDB. - * - * `options` is a hash with the following possible properties: - * - * config - passed to the connection config instance - * db - passed to the connection db instance - * server - passed to the connection server instance(s) - * replset - passed to the connection ReplSet instance - * user - username for authentication - * pass - password for authentication - * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) - * - * ####Notes: - * - * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. - * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. - * See the node-mongodb-native driver instance for options that it understands. - * - * _Options passed take precedence over options included in connection strings._ - * - * @param {String} connection_string mongodb://uri or the host to which you are connecting - * @param {String} [database] database name - * @param {Number} [port] database port - * @param {Object} [options] options - * @param {Function} [callback] - * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native - * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate - * @api public +/*! + * ignore */ -Connection.prototype.open = function(host, database, port, options, callback) { - var parsed; - var Promise = PromiseProvider.get(); +Connection.prototype._handleOpenArgs = function(host, database, port, options, callback) { var err; + var parsed; + if (typeof database === 'string') { switch (arguments.length) { case 2: @@ -221,11 +194,9 @@ Connection.prototype.open = function(host, database, port, options, callback) { try { parsed = muri(host); - } catch (err) { - this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + } catch (error) { + this.error(error, callback); + throw error; } database = parsed.db; @@ -240,25 +211,19 @@ Connection.prototype.open = function(host, database, port, options, callback) { err = new Error('Trying to open unclosed connection.'); err.state = this.readyState; this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } if (!host) { err = new Error('Missing hostname.'); this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } if (!database) { err = new Error('Missing database name.'); this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } // authentication @@ -275,19 +240,13 @@ Connection.prototype.open = function(host, database, port, options, callback) { if (host.length > 2) { err = new Error('Username and password must be URI encoded if they ' + 'contain "@", see http://bit.ly/2nRYRyq'); - this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } var auth = host.shift().split(':'); if (auth.length > 2) { err = new Error('Username and password must be URI encoded if they ' + 'contain ":", see http://bit.ly/2nRYRyq'); - this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } host = host.pop(); this.user = auth[0]; @@ -305,6 +264,52 @@ Connection.prototype.open = function(host, database, port, options, callback) { this.host = host; this.port = port; + return callback; +}; + +/** + * Opens the connection to MongoDB. + * + * `options` is a hash with the following possible properties: + * + * config - passed to the connection config instance + * db - passed to the connection db instance + * server - passed to the connection server instance(s) + * replset - passed to the connection ReplSet instance + * user - username for authentication + * pass - password for authentication + * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) + * + * ####Notes: + * + * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. + * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. + * See the node-mongodb-native driver instance for options that it understands. + * + * _Options passed take precedence over options included in connection strings._ + * + * @param {String} connection_string mongodb://uri or the host to which you are connecting + * @param {String} [database] database name + * @param {Number} [port] database port + * @param {Object} [options] options + * @param {Function} [callback] + * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native + * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate + * @api public + */ + +Connection.prototype.open = function() { + var Promise = PromiseProvider.get(); + var callback; + + try { + callback = this._handleOpenArgs.apply(this, arguments); + } catch (error) { + return new Promise.ES6(function(resolve, reject) { + reject(error); + }); + } + var _this = this; var promise = new Promise.ES6(function(resolve, reject) { _this._open(true, function(error) { @@ -322,9 +327,44 @@ Connection.prototype.open = function(host, database, port, options, callback) { resolve(); }); }); + + // Monkey-patch `.then()` so if the promise is handled, we don't emit an + // `error` event. + var _then = promise.then; + promise.then = function(resolve, reject) { + promise.$hasHandler = true; + return _then.call(promise, resolve, reject); + }; + return promise; }; +/*! + * ignore + */ + +Connection.prototype._openWithoutPromise = function() { + var callback; + + try { + callback = this._handleOpenArgs.apply(this, arguments); + } catch (error) { + // No need to do anything + } + + var _this = this; + this._open(true, function(error) { + callback && callback(error); + if (error && !callback) { + // Error can be on same tick re: christkv/mongodb-core#157 + setImmediate(function() { + _this.emit('error', error); + }); + return; + } + }); +}; + /** * Helper for `dropDatabase()`. * @@ -363,57 +403,15 @@ Connection.prototype.dropDatabase = function(callback) { return promise; }; -/** - * Opens the connection to a replica set. - * - * ####Example: - * - * var db = mongoose.createConnection(); - * db.openSet("mongodb://user:pwd@localhost:27020,localhost:27021,localhost:27012/mydb"); - * - * The database name and/or auth need only be included in one URI. - * The `options` is a hash which is passed to the internal driver connection object. - * - * Valid `options` - * - * db - passed to the connection db instance - * server - passed to the connection server instance(s) - * replset - passed to the connection ReplSetServer instance - * user - username for authentication - * pass - password for authentication - * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) - * mongos - Boolean - if true, enables High Availability support for mongos - * - * _Options passed take precedence over options included in connection strings._ - * - * ####Notes: - * - * _If connecting to multiple mongos servers, set the `mongos` option to true._ - * - * conn.open('mongodb://mongosA:27501,mongosB:27501', { mongos: true }, cb); - * - * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. - * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. - * See the node-mongodb-native driver instance for options that it understands. - * - * _Options passed take precedence over options included in connection strings._ - * - * @param {String} uris MongoDB connection string - * @param {String} [database] database name if not included in `uris` - * @param {Object} [options] passed to the internal driver - * @param {Function} [callback] - * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native - * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate - * @api public +/*! + * ignore */ -Connection.prototype.openSet = function(uris, database, options, callback) { +Connection.prototype._handleOpenSetArgs = function(uris, database, options, callback) { if (!rgxProtocol.test(uris)) { uris = 'mongodb://' + uris; } - var Promise = PromiseProvider.get(); - switch (arguments.length) { case 3: switch (typeof database) { @@ -457,9 +455,7 @@ Connection.prototype.openSet = function(uris, database, options, callback) { parsed = muri(uris); } catch (err) { this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } if (!this.name) { @@ -473,9 +469,7 @@ Connection.prototype.openSet = function(uris, database, options, callback) { if (!this.name) { var err = new Error('No database name provided for replica set'); this.error(err, callback); - return new Promise.ES6(function(resolve, reject) { - reject(err); - }); + throw err; } // authentication @@ -493,6 +487,88 @@ Connection.prototype.openSet = function(uris, database, options, callback) { if (options && options.config) { this.config.autoIndex = options.config.autoIndex !== false; } +}; + +/*! + * ignore + */ + +Connection.prototype._openSetWithoutPromise = function(uris, database, options, callback) { + try { + callback = this._handleOpenSetArgs.apply(this, arguments); + } catch (err) { + // Nothing to do, `_handleOpenSetArgs` calls callback if error occurred + return; + } + + var _this = this; + var emitted = false; + this._open(true, function(error) { + callback && callback(error); + if (error) { + if (!callback && !emitted) { + emitted = true; + _this.emit('error', error); + } + return; + } + }); +}; + +/** + * Opens the connection to a replica set. + * + * ####Example: + * + * var db = mongoose.createConnection(); + * db.openSet("mongodb://user:pwd@localhost:27020,localhost:27021,localhost:27012/mydb"); + * + * The database name and/or auth need only be included in one URI. + * The `options` is a hash which is passed to the internal driver connection object. + * + * Valid `options` + * + * db - passed to the connection db instance + * server - passed to the connection server instance(s) + * replset - passed to the connection ReplSetServer instance + * user - username for authentication + * pass - password for authentication + * auth - options for authentication (see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate) + * mongos - Boolean - if true, enables High Availability support for mongos + * + * _Options passed take precedence over options included in connection strings._ + * + * ####Notes: + * + * _If connecting to multiple mongos servers, set the `mongos` option to true._ + * + * conn.open('mongodb://mongosA:27501,mongosB:27501', { mongos: true }, cb); + * + * Mongoose forces the db option `forceServerObjectId` false and cannot be overridden. + * Mongoose defaults the server `auto_reconnect` options to true which can be overridden. + * See the node-mongodb-native driver instance for options that it understands. + * + * _Options passed take precedence over options included in connection strings._ + * + * @param {String} uris MongoDB connection string + * @param {String} [database] database name if not included in `uris` + * @param {Object} [options] passed to the internal driver + * @param {Function} [callback] + * @see node-mongodb-native https://github.com/mongodb/node-mongodb-native + * @see http://mongodb.github.com/node-mongodb-native/api-generated/db.html#authenticate + * @api public + */ + +Connection.prototype.openSet = function(uris, database, options, callback) { + var Promise = PromiseProvider.get(); + + try { + callback = this._handleOpenSetArgs.apply(this, arguments); + } catch (err) { + return new Promise.ES6(function(resolve, reject) { + reject(err); + }); + } var _this = this; var emitted = false; @@ -510,6 +586,15 @@ Connection.prototype.openSet = function(uris, database, options, callback) { resolve(); }); }); + + // Monkey-patch `.then()` so if the promise is handled, we don't emit an + // `error` event. + var _then = promise.then; + promise.then = function(resolve, reject) { + promise.$hasHandler = true; + return _then.call(promise, resolve, reject); + }; + return promise; }; diff --git a/lib/document.js b/lib/document.js index 9a982c4df6b..be05e95a3ff 100644 --- a/lib/document.js +++ b/lib/document.js @@ -328,7 +328,6 @@ Document.prototype.init = function(doc, opts, fn) { } init(this, doc, this._doc); - this.$__storeShard(); this.emit('init', this); this.constructor.emit('init', this); @@ -416,44 +415,6 @@ function init(self, obj, doc, prefix) { } } -/** - * Stores the current values of the shard keys. - * - * ####Note: - * - * _Shard key values do not / are not allowed to change._ - * - * @api private - * @method $__storeShard - * @memberOf Document - */ - -Document.prototype.$__storeShard = function() { - // backwards compat - var key = this.schema.options.shardKey || this.schema.options.shardkey; - if (!(key && utils.getFunctionName(key.constructor) === 'Object')) { - return; - } - - var orig = this.$__.shardval = {}, - paths = Object.keys(key), - len = paths.length, - val; - - for (var i = 0; i < len; ++i) { - val = this.getValue(paths[i]); - if (isMongooseObject(val)) { - orig[paths[i]] = val.toObject({depopulate: true, _isNested: true}); - } else if (val !== null && val !== undefined && val.valueOf && - // Explicitly don't take value of dates - (!val.constructor || utils.getFunctionName(val.constructor) !== 'Date')) { - orig[paths[i]] = val.valueOf(); - } else { - orig[paths[i]] = val; - } - } -}; - /*! * Set up middleware support */ @@ -663,6 +624,18 @@ Document.prototype.set = function(path, val, type, options) { var schema; var parts = path.split('.'); + // gh-4578, if setting a deeply nested path that doesn't exist yet, create it + var cur = this._doc; + var curPath = ''; + for (i = 0; i < parts.length - 1; ++i) { + cur = cur[parts[i]]; + curPath += (curPath.length > 0 ? '.' : '') + parts[i]; + if (!cur) { + this.set(curPath, {}); + cur = this.getValue(curPath); + } + } + if (pathType === 'adhocOrUndefined' && strict) { // check for roots that are Mixed types var mixed; @@ -878,6 +851,13 @@ Document.prototype.$__set = function(pathToMark, path, constructing, parts, sche if (val && val.isMongooseArray) { val._registerAtomic('$set', val); + // Update embedded document parent references (gh-5189) + if (val.isMongooseDocumentArray) { + val.forEach(function(item) { + item && item.__parentArray && (item.__parentArray = val); + }); + } + // Small hack for gh-1638: if we're overwriting the entire array, ignore // paths that were modified before the array overwrite this.$__.activePaths.forEach(function(modifiedPath) { @@ -910,7 +890,7 @@ Document.prototype.$__set = function(pathToMark, path, constructing, parts, sche } else if (obj[parts[i]] && Array.isArray(obj[parts[i]])) { obj = obj[parts[i]]; } else { - this.set(cur, {}); + obj[parts[i]] = obj[parts[i]] || {}; obj = obj[parts[i]]; } } @@ -1353,15 +1333,17 @@ function _getPathsToValidate(doc) { paths = paths.concat(Object.keys(doc.$__.activePaths.states.modify)); paths = paths.concat(Object.keys(doc.$__.activePaths.states.default)); - var subdocs = doc.$__getAllSubdocs(); - var subdoc; - var len = subdocs.length; - for (i = 0; i < len; ++i) { - subdoc = subdocs[i]; - if (subdoc.$isSingleNested && - doc.isModified(subdoc.$basePath) && - !doc.isDirectModified(subdoc.$basePath)) { - paths.push(subdoc.$basePath); + if (!doc.ownerDocument) { + var subdocs = doc.$__getAllSubdocs(); + var subdoc; + var len = subdocs.length; + for (i = 0; i < len; ++i) { + subdoc = subdocs[i]; + if (subdoc.$isSingleNested && + doc.isModified(subdoc.$basePath) && + !doc.isDirectModified(subdoc.$basePath)) { + paths.push(subdoc.$basePath); + } } } @@ -1369,8 +1351,14 @@ function _getPathsToValidate(doc) { // the children as well for (i = 0; i < paths.length; ++i) { var path = paths[i]; + + var _pathType = doc.schema.path(path); + if (!_pathType || !_pathType.$isMongooseArray || _pathType.$isMongooseDocumentArray) { + continue; + } + var val = doc.getValue(path); - if (val && val.isMongooseArray && !Buffer.isBuffer(val) && !val.isMongooseDocumentArray) { + if (val) { var numElements = val.length; for (var j = 0; j < numElements; ++j) { paths.push(path + '.' + j); @@ -1424,7 +1412,7 @@ Document.prototype.$__validate = function(callback) { var paths = _getPathsToValidate(this); if (paths.length === 0) { - process.nextTick(function() { + return process.nextTick(function() { var error = _complete(); if (error) { return _this.schema.s.hooks.execPost('validate:error', _this, [ _this], { error: error }, function(error) { @@ -1478,7 +1466,10 @@ Document.prototype.$__validate = function(callback) { }); }; - paths.forEach(validatePath); + var numPaths = paths.length; + for (var i = 0; i < numPaths; ++i) { + validatePath(paths[i]); + } }; /** @@ -1776,25 +1767,44 @@ Document.prototype.$__dirty = function() { */ function compile(tree, proto, prefix, options) { - var keys = Object.keys(tree), - i = keys.length, - limb, - key; + var keys = Object.keys(tree); + var i = keys.length; + var len = keys.length; + var limb; + var key; + + if (options.retainKeyOrder) { + for (i = 0; i < len; ++i) { + key = keys[i]; + limb = tree[key]; + + defineKey(key, + ((utils.getFunctionName(limb.constructor) === 'Object' + && Object.keys(limb).length) + && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) + ? limb + : null) + , proto + , prefix + , keys + , options); + } + } else { + while (i--) { + key = keys[i]; + limb = tree[key]; - while (i--) { - key = keys[i]; - limb = tree[key]; - - defineKey(key, - ((utils.getFunctionName(limb.constructor) === 'Object' - && Object.keys(limb).length) - && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) - ? limb - : null) - , proto - , prefix - , keys - , options); + defineKey(key, + ((utils.getFunctionName(limb.constructor) === 'Object' + && Object.keys(limb).length) + && (!limb[options.typeKey] || (options.typeKey === 'type' && limb.type.type)) + ? limb + : null) + , proto + , prefix + , keys + , options); + } } } @@ -1805,7 +1815,7 @@ function getOwnPropertyDescriptors(object) { Object.getOwnPropertyNames(object).forEach(function(key) { result[key] = Object.getOwnPropertyDescriptor(object, key); - result[key].enumerable = true; + result[key].enumerable = ['isNew', '$__', 'errors', '_doc'].indexOf(key) === -1; }); return result; @@ -1853,7 +1863,7 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { } Object.defineProperty(nested, 'toObject', { - enumerable: true, + enumerable: false, configurable: true, writable: false, value: function() { @@ -1862,7 +1872,7 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { }); Object.defineProperty(nested, 'toJSON', { - enumerable: true, + enumerable: false, configurable: true, writable: false, value: function() { @@ -1871,7 +1881,7 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) { }); Object.defineProperty(nested, '$__isNested', { - enumerable: true, + enumerable: false, configurable: true, writable: false, value: true @@ -2290,27 +2300,41 @@ function minimize(obj) { */ function applyGetters(self, json, type, options) { - var schema = self.schema, - paths = Object.keys(schema[type]), - i = paths.length, - path; + var schema = self.schema; + var paths = Object.keys(schema[type]); + var i = paths.length; + var path; + var cur = self._doc; + var v; + + if (!cur) { + return json; + } while (i--) { path = paths[i]; - var parts = path.split('.'), - plen = parts.length, - last = plen - 1, - branch = json, - part; + var parts = path.split('.'); + var plen = parts.length; + var last = plen - 1; + var branch = json; + var part; + cur = self._doc; for (var ii = 0; ii < plen; ++ii) { part = parts[ii]; + v = cur[part]; if (ii === last) { branch[part] = clone(self.get(path), options); + } else if (v == null) { + if (part in cur) { + branch[part] = v; + } + break; } else { branch = branch[part] || (branch[part] = {}); } + cur = v; } } diff --git a/lib/error/validation.js b/lib/error/validation.js index e3322d4942d..c70a5f61901 100644 --- a/lib/error/validation.js +++ b/lib/error/validation.js @@ -14,10 +14,13 @@ var MongooseError = require('../error.js'); function ValidationError(instance) { this.errors = {}; + this._message = ''; if (instance && instance.constructor.name === 'model') { - MongooseError.call(this, instance.constructor.modelName + ' validation failed'); + this._message = instance.constructor.modelName + ' validation failed'; + MongooseError.call(this, this._message); } else { - MongooseError.call(this, 'Validation failed'); + this._message = 'Validation failed'; + MongooseError.call(this, this._message); } if (Error.captureStackTrace) { Error.captureStackTrace(this); @@ -37,24 +40,49 @@ function ValidationError(instance) { ValidationError.prototype = Object.create(MongooseError.prototype); ValidationError.prototype.constructor = MongooseError; +Object.defineProperty(ValidationError.prototype, 'message', { + get: function() { + return this._message + ': ' + _generateMessage(this); + }, + enumerable: true +}); /** * Console.log helper */ ValidationError.prototype.toString = function() { - var ret = this.name + ': '; + return this.name + ': ' + _generateMessage(this); +}; + +/*! + * inspect helper + */ + +ValidationError.prototype.inspect = function() { + return Object.assign(new Error(this.message), this); +}; + +/*! + * ignore + */ + +function _generateMessage(err) { + var keys = Object.keys(err.errors || {}); + var len = keys.length; var msgs = []; + var key; - Object.keys(this.errors || {}).forEach(function(key) { - if (this === this.errors[key]) { - return; + for (var i = 0; i < len; ++i) { + key = keys[i]; + if (err === err.errors[key]) { + continue; } - msgs.push(String(this.errors[key])); - }, this); + msgs.push(key + ': ' + err.errors[key].message); + } - return ret + msgs.join(', '); -}; + return msgs.join(', '); +} /*! * Module exports diff --git a/lib/index.js b/lib/index.js index e0c644557e6..e5720cc5c87 100644 --- a/lib/index.js +++ b/lib/index.js @@ -17,9 +17,12 @@ var Schema = require('./schema'), pkg = require('../package.json'); var querystring = require('querystring'); +var saveSubdocs = require('./plugins/saveSubdocs'); +var validateBeforeSave = require('./plugins/validateBeforeSave'); var Aggregate = require('./aggregate'); var PromiseProvider = require('./promise_provider'); +var shardingPlugin = require('./plugins/sharding'); /** * Mongoose constructor. @@ -32,7 +35,6 @@ var PromiseProvider = require('./promise_provider'); function Mongoose() { this.connections = []; - this.plugins = []; this.models = {}; this.modelSchemas = {}; // default global options @@ -43,6 +45,21 @@ function Mongoose() { conn.models = this.models; } +/*! + * ignore + */ + +Object.defineProperty(Mongoose.prototype, 'plugins', { + configurable: false, + enumerable: true, + writable: false, + value: [ + [saveSubdocs, { deduplicate: true }], + [validateBeforeSave, { deduplicate: true }], + [shardingPlugin, { deduplicate: true }] + ] +}); + /** * Expose connection states for user-land * @@ -181,12 +198,12 @@ Mongoose.prototype.createConnection = function(uri, options) { var rsOption = options && (options.replset || options.replSet); if (arguments.length) { if (rgxReplSet.test(arguments[0]) || checkReplicaSetInUri(arguments[0])) { - conn.openSet.apply(conn, arguments).catch(function() {}); + conn._openSetWithoutPromise.apply(conn, arguments); } else if (rsOption && (rsOption.replicaSet || rsOption.rs_name)) { - conn.openSet.apply(conn, arguments).catch(function() {}); + conn._openSetWithoutPromise.apply(conn, arguments); } else { - conn.open.apply(conn, arguments).catch(function() {}); + conn._openWithoutPromise.apply(conn, arguments); } } diff --git a/lib/model.js b/lib/model.js index 493b2ce21e6..7e9f2666ed8 100644 --- a/lib/model.js +++ b/lib/model.js @@ -27,12 +27,6 @@ var VERSION_WHERE = 1, VERSION_INC = 2, VERSION_ALL = VERSION_WHERE | VERSION_INC; -var POJO_TO_OBJECT_OPTIONS = { - depopulate: true, - transform: false, - _skipDepopulateTopLevel: true -}; - /** * Model constructor * @@ -40,7 +34,7 @@ var POJO_TO_OBJECT_OPTIONS = { * * @param {Object} doc values with which to create the document * @inherits Document http://mongoosejs.com/docs/api.html#document-js - * @event `error`: If listening to this event, it is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. + * @event `error`: If listening to this event, 'error' is emitted when a document was saved without passing a callback and an `error` occurred. If not listening, the event bubbles to the connection used to create this Model. * @event `index`: Emitted after `Model#ensureIndexes` completes. If an error occurred it is passed with the event. * @event `index-single-start`: Emitted when an individual index starts within `Model#ensureIndexes`. The fields and options being used to build the index are also passed with the event. * @event `index-single-done`: Emitted when an individual index finishes within `Model#ensureIndexes`. If an error occurred it is passed with the event. The fields, options, and index name are also passed. @@ -224,7 +218,6 @@ Model.prototype.$__save = function(options, callback) { } _this.$__reset(); - _this.$__storeShard(); var numAffected = 0; if (result) { @@ -643,7 +636,8 @@ Model.prototype.$__version = function(where, delta) { // $push $addToSet don't need the where clause set if (VERSION_WHERE === (VERSION_WHERE & this.$__.version)) { - where[key] = this.getValue(key); + var value = this.getValue(key); + if (value != null) where[key] = value; } if (VERSION_INC === (VERSION_INC & this.$__.version)) { @@ -674,7 +668,7 @@ Model.prototype.increment = function increment() { }; /** - * Returns a query object which applies shardkeys if they exist. + * Returns a query object * * @api private * @method $__where @@ -684,22 +678,10 @@ Model.prototype.increment = function increment() { Model.prototype.$__where = function _where(where) { where || (where = {}); - var paths, - len; - if (!where._id) { where._id = this._doc._id; } - if (this.$__.shardval) { - paths = Object.keys(this.$__.shardval); - len = paths.length; - - for (var i = 0; i < len; ++i) { - where[paths[i]] = this.$__.shardval[paths[i]]; - } - } - if (this._doc._id == null) { return new Error('No _id found on document!'); } @@ -829,8 +811,10 @@ Model.prototype.model = function model(name) { */ Model.discriminator = function(name, schema) { + var model; if (typeof name === 'function') { - name = utils.getFunctionName(name); + model = name; + name = utils.getFunctionName(model); } schema = discriminator(this, name, schema); @@ -840,7 +824,10 @@ Model.discriminator = function(name, schema) { schema.$isRootDiscriminator = true; - this.discriminators[name] = this.db.model(name, schema, this.collection.name); + if (!model) { + model = this.db.model(name, schema, this.collection.name); + } + this.discriminators[name] = model; var d = this.discriminators[name]; d.prototype.__proto__ = this.prototype; Object.defineProperty(d, 'baseModelName', { @@ -942,14 +929,6 @@ Model.ensureIndexes = function ensureIndexes(options, callback) { function _ensureIndexes(model, options, callback) { var indexes = model.schema.indexes(); - if (!indexes.length) { - setImmediate(function() { - callback && callback(); - }); - return; - } - // Indexes are created one-by-one to support how MongoDB < 2.4 deals - // with background indexes. var done = function(err) { if (err && model.schema.options.emitIndexErrors) { @@ -959,6 +938,15 @@ function _ensureIndexes(model, options, callback) { callback && callback(err); }; + if (!indexes.length) { + setImmediate(function() { + done(); + }); + return; + } + // Indexes are created one-by-one to support how MongoDB < 2.4 deals + // with background indexes. + var indexSingleDone = function(err, fields, options, name) { model.emit('index-single-done', err, fields, options, name); }; @@ -1131,7 +1119,7 @@ Model.deleteOne = function deleteOne(conditions, callback) { }; /** - * Deletes the first document that matches `conditions` from the collection. + * Deletes all of the documents that match `conditions` from the collection. * Behaves like `remove()`, but deletes all documents that match `conditions` * regardless of the `justOne` option. * @@ -1891,6 +1879,7 @@ Model.create = function create(doc, callback) { } var toExecute = []; + var firstError; args.forEach(function(doc) { toExecute.push(function(callback) { var Model = _this.discriminators && doc[discriminatorKey] ? @@ -1899,9 +1888,12 @@ Model.create = function create(doc, callback) { var toSave = doc instanceof Model ? doc : new Model(doc); var callbackWrapper = function(error, doc) { if (error) { - return callback(error); + if (!firstError) { + firstError = error; + } + return callback(null, { error: error }); } - callback(null, doc); + callback(null, { doc: doc }); }; // Hack to avoid getting a promise because of @@ -1914,12 +1906,20 @@ Model.create = function create(doc, callback) { }); }); - parallel(toExecute, function(error, savedDocs) { - if (error) { + parallel(toExecute, function(error, res) { + var savedDocs = []; + var len = res.length; + for (var i = 0; i < len; ++i) { + if (res[i].doc) { + savedDocs.push(res[i].doc); + } + } + + if (firstError) { if (cb) { - cb(error); + cb(firstError, savedDocs); } else { - reject(error); + reject(firstError); } return; } @@ -1930,8 +1930,7 @@ Model.create = function create(doc, callback) { } else { resolve.apply(promise, savedDocs); if (cb) { - savedDocs.unshift(null); - cb.apply(_this, savedDocs); + cb.apply(_this, [null].concat(savedDocs)); } } }); @@ -1940,6 +1939,17 @@ Model.create = function create(doc, callback) { return promise; }; +/*! + * ignore + */ + +var INSERT_MANY_CONVERT_OPTIONS = { + depopulate: true, + transform: false, + _skipDepopulateTopLevel: true, + flattenDecimals: false +}; + /** * Shortcut for validating an array of documents and inserting them into * MongoDB if they're all valid. This function is faster than `.create()` @@ -2012,10 +2022,11 @@ Model.insertMany = function(arr, options, callback) { doc[doc.schema.options.versionKey] = 0; } if (doc.initializeTimestamps) { - return doc.initializeTimestamps().toObject(POJO_TO_OBJECT_OPTIONS); + return doc.initializeTimestamps().toObject(INSERT_MANY_CONVERT_OPTIONS); } - return doc.toObject(POJO_TO_OBJECT_OPTIONS); + return doc.toObject(INSERT_MANY_CONVERT_OPTIONS); }); + _this.collection.insertMany(docObjects, options, function(error) { if (error) { callback && callback(error); @@ -2034,12 +2045,14 @@ Model.insertMany = function(arr, options, callback) { /** * Sends multiple `insertOne`, `updateOne`, `updateMany`, `replaceOne`, * `deleteOne`, and/or `deleteMany` operations to the MongoDB server in one - * command. This is faster than sending multiple independent operations because - * with `bulkWrite()` there is only one round trip to the server. + * command. This is faster than sending multiple independent operations (like) + * if you use `create()`) because with `bulkWrite()` there is only one round + * trip to MongoDB. * * Mongoose will perform casting on all operations you provide. * - * This function does **not** trigger any middleware. + * This function does **not** trigger any middleware. If you need to trigger + * `save()` middleware for every document use [`create()`](http://mongoosejs.com/docs/api.html#model_Model.create) instead. * * ####Example: * @@ -2055,13 +2068,17 @@ Model.insertMany = function(arr, options, callback) { * { * updateOne: { * filter: { name: 'Eddard Stark' }, - * // Mongoose inserts `$set` for you in the update below + * // If you were using the MongoDB driver directly, you'd need to do + * // `update: { $set: { title: ... } }` but mongoose adds $set for + * // you. * update: { title: 'Hand of the King' } * } * }, * { * deleteOne: { - * { name: 'Eddard Stark' } + * { + * filter: { name: 'Eddard Stark' } + * } * } * } * ]).then(handleResult); @@ -2115,7 +2132,7 @@ Model.bulkWrite = function(ops, options, callback) { try { op['updateMany']['filter'] = cast(_this.schema, op['updateMany']['filter']); - op['updateMany']['update'] = castUpdate(_this.schema, op['updateMany']['filter'], { + op['updateMany']['update'] = castUpdate(_this.schema, op['updateMany']['update'], { strict: _this.schema.options.strict, overwrite: false }); @@ -2440,9 +2457,13 @@ function _update(model, op, conditions, doc, options, callback) { * }); * }) * - * // a promise is returned so you may instead write + * // `mapReduce()` returns a promise. However, ES6 promises can only + * // resolve to exactly one value, + * o.resolveToObject = true; * var promise = User.mapReduce(o); - * promise.then(function (model, stats) { + * promise.then(function (res) { + * var model = res.model; + * var stats = res.stats; * console.log('map reduce took %d ms', stats.processtime) * return model.find().where('value').gt(10).exec(); * }).then(function (docs) { @@ -2461,6 +2482,7 @@ Model.mapReduce = function mapReduce(o, callback) { if (callback) { callback = this.$wrapCallback(callback); } + var resolveToObject = o.resolveToObject; var Promise = PromiseProvider.get(); return new Promise.ES6(function(resolve, reject) { if (!Model.mapReduce.schema) { @@ -2500,10 +2522,16 @@ Model.mapReduce = function mapReduce(o, callback) { model._mapreduce = true; callback && callback(null, model, stats); - return resolve(model, stats); + return resolveToObject ? resolve({ + model: model, + stats: stats + }) : resolve(model, stats); } callback && callback(null, ret, stats); + if (resolveToObject) { + return resolve({ model: ret, stats: stats }); + } resolve(ret, stats); }); }); @@ -3619,7 +3647,10 @@ Model.compile = function compile(name, schema, collectionName, connection, base) model.Query.base = Query.base; applyQueryMethods(model, schema.query); - var kareemOptions = { useErrorHandlers: true }; + var kareemOptions = { + useErrorHandlers: true, + numCallbackParams: 1 + }; model.$__insertMany = model.hooks.createWrapper('insertMany', model.insertMany, model, kareemOptions); model.insertMany = function(arr, options, callback) { diff --git a/lib/plugins/saveSubdocs.js b/lib/plugins/saveSubdocs.js new file mode 100644 index 00000000000..ffb90b4d4a1 --- /dev/null +++ b/lib/plugins/saveSubdocs.js @@ -0,0 +1,37 @@ +'use strict'; + +var each = require('async/each'); + +/*! + * ignore + */ + +module.exports = function(schema) { + schema.callQueue.unshift(['pre', ['save', function(next) { + if (this.ownerDocument) { + next(); + return; + } + + var _this = this; + var subdocs = this.$__getAllSubdocs(); + + if (!subdocs.length) { + next(); + return; + } + + each(subdocs, function(subdoc, cb) { + subdoc.save(function(err) { + cb(err); + }); + }, function(error) { + if (error) { + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + next(error); + }); + } + next(); + }); + }]]); +}; diff --git a/lib/plugins/sharding.js b/lib/plugins/sharding.js new file mode 100644 index 00000000000..4a2f85ea1fe --- /dev/null +++ b/lib/plugins/sharding.js @@ -0,0 +1,71 @@ +'use strict'; + +var utils = require('../utils'); + +module.exports = function shardingPlugin(schema) { + schema.post('init', function() { + storeShard.call(this); + return this; + }); + schema.pre('save', function(next) { + applyWhere.call(this); + next(); + }); + schema.post('save', function() { + storeShard.call(this); + }); +}; + +function applyWhere() { + var paths; + var len; + + if (this.$__.shardval) { + paths = Object.keys(this.$__.shardval); + len = paths.length; + + this.$where = this.$where || {}; + for (var i = 0; i < len; ++i) { + this.$where[paths[i]] = this.$__.shardval[paths[i]]; + } + } +} + +/*! + * Stores the current values of the shard keys. + * + * ####Note: + * + * _Shard key values do not / are not allowed to change._ + * + * @api private + * @method $__storeShard + * @memberOf Document + */ +module.exports.storeShard = storeShard; + +function storeShard() { + // backwards compat + var key = this.schema.options.shardKey || this.schema.options.shardkey; + if (!(key && utils.getFunctionName(key.constructor) === 'Object')) { + return; + } + + var orig = this.$__.shardval = {}, + paths = Object.keys(key), + len = paths.length, + val; + + for (var i = 0; i < len; ++i) { + val = this.getValue(paths[i]); + if (utils.isMongooseObject(val)) { + orig[paths[i]] = val.toObject({depopulate: true, _isNested: true}); + } else if (val !== null && val !== undefined && val.valueOf && + // Explicitly don't take value of dates + (!val.constructor || utils.getFunctionName(val.constructor) !== 'Date')) { + orig[paths[i]] = val.valueOf(); + } else { + orig[paths[i]] = val; + } + } +} diff --git a/lib/plugins/validateBeforeSave.js b/lib/plugins/validateBeforeSave.js new file mode 100644 index 00000000000..706cc0692b3 --- /dev/null +++ b/lib/plugins/validateBeforeSave.js @@ -0,0 +1,47 @@ +'use strict'; + +/*! + * ignore + */ + +module.exports = function(schema) { + schema.callQueue.unshift(['pre', ['save', function(next, options) { + var _this = this; + // Nested docs have their own presave + if (this.ownerDocument) { + return next(); + } + + var hasValidateBeforeSaveOption = options && + (typeof options === 'object') && + ('validateBeforeSave' in options); + + var shouldValidate; + if (hasValidateBeforeSaveOption) { + shouldValidate = !!options.validateBeforeSave; + } else { + shouldValidate = this.schema.options.validateBeforeSave; + } + + // Validate + if (shouldValidate) { + // HACK: use $__original_validate to avoid promises so bluebird doesn't + // complain + if (this.$__original_validate) { + this.$__original_validate({__noPromise: true}, function(error) { + return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { + next(error); + }); + }); + } else { + this.validate({__noPromise: true}, function(error) { + return _this.schema.s.hooks.execPost('save:error', _this, [ _this], { error: error }, function(error) { + next(error); + }); + }); + } + } else { + next(); + } + }]]); +}; diff --git a/lib/query.js b/lib/query.js index 58c45f51469..1fdd524256a 100644 --- a/lib/query.js +++ b/lib/query.js @@ -7,6 +7,7 @@ var QueryCursor = require('./querycursor'); var QueryStream = require('./querystream'); var cast = require('./cast'); var castUpdate = require('./services/query/castUpdate'); +var hasDollarKeys = require('./services/query/hasDollarKeys'); var helpers = require('./queryhelpers'); var mquery = require('mquery'); var readPref = require('./drivers').ReadPreference; @@ -455,7 +456,7 @@ Query.prototype.toConstructor = function toConstructor() { * @method regex * @memberOf Query * @param {String} [path] - * @param {Number} val + * @param {String|RegExp} val * @api public */ @@ -2001,29 +2002,36 @@ Query.prototype._findAndModify = function(type, callback) { opts.remove = false; } - castedDoc = castDoc(this, opts.overwrite); - castedDoc = setDefaultsOnInsert(this, schema, castedDoc, opts); - if (!castedDoc) { - if (opts.upsert) { - // still need to do the upsert to empty doc - var doc = utils.clone(castedQuery); - delete doc._id; - castedDoc = {$set: doc}; - } else { - return this.findOne(callback); - } - } else if (castedDoc instanceof Error) { - return callback(castedDoc); + if (opts.overwrite && !hasDollarKeys(this._update)) { + castedDoc = new model(this._update, null, true); + doValidate = function(callback) { + castedDoc.validate(callback); + }; } else { - // In order to make MongoDB 2.6 happy (see - // https://jira.mongodb.org/browse/SERVER-12266 and related issues) - // if we have an actual update document but $set is empty, junk the $set. - if (castedDoc.$set && Object.keys(castedDoc.$set).length === 0) { - delete castedDoc.$set; + castedDoc = castDoc(this, opts.overwrite); + castedDoc = setDefaultsOnInsert(this, schema, castedDoc, opts); + if (!castedDoc) { + if (opts.upsert) { + // still need to do the upsert to empty doc + var doc = utils.clone(castedQuery); + delete doc._id; + castedDoc = {$set: doc}; + } else { + return this.findOne(callback); + } + } else if (castedDoc instanceof Error) { + return callback(castedDoc); + } else { + // In order to make MongoDB 2.6 happy (see + // https://jira.mongodb.org/browse/SERVER-12266 and related issues) + // if we have an actual update document but $set is empty, junk the $set. + if (castedDoc.$set && Object.keys(castedDoc.$set).length === 0) { + delete castedDoc.$set; + } } - } - doValidate = updateValidators(this, schema, castedDoc, opts); + doValidate = updateValidators(this, schema, castedDoc, opts); + } } this._applyPaths(); @@ -2181,7 +2189,14 @@ Query.prototype._execUpdate = function(callback) { if (this.options.runValidators) { _this = this; - doValidate = updateValidators(this, schema, castedDoc, options); + if (this.options.overwrite && !hasDollarKeys(castedDoc)) { + castedDoc = new _this.model(castedDoc, null, true); + doValidate = function(callback) { + castedDoc.validate(callback); + }; + } else { + doValidate = updateValidators(this, schema, castedDoc, options); + } var _callback = function(err) { if (err) { return callback(err); @@ -2919,7 +2934,9 @@ Query.prototype.cast = function(model, obj) { return cast(model.schema, obj, { upsert: this.options && this.options.upsert, strict: (this.options && this.options.strict) || - (model.schema.options && model.schema.options.strict) + (model.schema.options && model.schema.options.strict), + strictQuery: (this.options && this.options.strictQuery) || + (model.schema.options && model.schema.options.strictQuery) }); } catch (err) { // CastError, assign model diff --git a/lib/schema.js b/lib/schema.js index cd376b9a705..e0950353d70 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -86,6 +86,7 @@ function Schema(obj, options) { this.tree = {}; this.query = {}; this.childSchemas = []; + this.plugins = []; this.s = { hooks: new Kareem(), @@ -107,9 +108,9 @@ function Schema(obj, options) { (!this.options.noId && this.options._id) && !_idSubDoc; if (auto_id) { - obj = {_id: {auto: true}}; - obj._id[this.options.typeKey] = Schema.ObjectId; - this.add(obj); + var _obj = {_id: {auto: true}}; + _obj._id[this.options.typeKey] = Schema.ObjectId; + this.add(_obj); } // ensure the documents receive an id getter unless disabled @@ -127,6 +128,41 @@ function Schema(obj, options) { if (this.options.timestamps) { this.setupTimestamp(this.options.timestamps); } + + // Assign virtual properties based on alias option + aliasFields(this); +} + +/*! + * Create virtual properties with alias field + */ +function aliasFields(schema) { + // console.log(schema.paths); + for (var path in schema.paths) { + if (!schema.paths[path].options) continue; + + var prop = schema.paths[path].path; + var alias = schema.paths[path].options.alias; + + if (alias) { + if ('string' === typeof alias && alias.length > 0) { + schema + .virtual(alias) + .get((function(p) { + return function() { + return this.get(p); + }; + })(prop)) + .set((function(p) { + return function(v) { + return this.set(p, v); + }; + })(prop)); + } else { + throw new Error('Invalid value for alias option on ' + prop + ', got ' + alias); + } + } + } } /*! @@ -166,82 +202,6 @@ Object.defineProperty(Schema.prototype, '_defaultMiddleware', { enumerable: false, writable: false, value: [ - { - kind: 'pre', - hook: 'save', - fn: function(next, options) { - var _this = this; - // Nested docs have their own presave - if (this.ownerDocument) { - return next(); - } - - var hasValidateBeforeSaveOption = options && - (typeof options === 'object') && - ('validateBeforeSave' in options); - - var shouldValidate; - if (hasValidateBeforeSaveOption) { - shouldValidate = !!options.validateBeforeSave; - } else { - shouldValidate = this.schema.options.validateBeforeSave; - } - - // Validate - if (shouldValidate) { - // HACK: use $__original_validate to avoid promises so bluebird doesn't - // complain - if (this.$__original_validate) { - this.$__original_validate({__noPromise: true}, function(error) { - return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { - next(error); - }); - }); - } else { - this.validate({__noPromise: true}, function(error) { - return _this.schema.s.hooks.execPost('save:error', _this, [ _this], { error: error }, function(error) { - next(error); - }); - }); - } - } else { - next(); - } - } - }, - { - kind: 'pre', - hook: 'save', - isAsync: true, - fn: function(next, done) { - var _this = this; - var subdocs = this.$__getAllSubdocs(); - - if (!subdocs.length || this.$__preSavingFromParent) { - done(); - next(); - return; - } - - each(subdocs, function(subdoc, cb) { - subdoc.$__preSavingFromParent = true; - subdoc.save(function(err) { - cb(err); - }); - }, function(error) { - for (var i = 0; i < subdocs.length; ++i) { - delete subdocs[i].$__preSavingFromParent; - } - if (error) { - return _this.schema.s.hooks.execPost('save:error', _this, [_this], { error: error }, function(error) { - done(error); - }); - } - next(); - done(); - }); - } - }, { kind: 'pre', hook: 'validate', @@ -348,6 +308,8 @@ Schema.prototype.clone = function() { s.callQueue = this.callQueue.map(function(f) { return f; }); s.methods = utils.clone(this.methods); s.statics = utils.clone(this.statics); + s.plugins = Array.prototype.slice.call(this.plugins); + s.s.hooks = this.s.hooks.clone(); return s; }; @@ -483,6 +445,7 @@ reserved.schema = reserved.set = reserved.toObject = reserved.validate = +reserved.remove = // hooks.js reserved._pres = reserved._posts = 1; @@ -829,18 +792,22 @@ Schema.prototype.setupTimestamp = function(timestamps) { var parts = createdAt.split('.'); var i; var cur = schemaAdditions; - for (i = 0; i < parts.length; ++i) { - cur[parts[i]] = (i < parts.length - 1 ? - cur[parts[i]] || {} : - Date); + if (this.pathType(createdAt) === 'adhocOrUndefined') { + for (i = 0; i < parts.length; ++i) { + cur[parts[i]] = (i < parts.length - 1 ? + cur[parts[i]] || {} : + Date); + } } parts = updatedAt.split('.'); cur = schemaAdditions; - for (i = 0; i < parts.length; ++i) { - cur[parts[i]] = (i < parts.length - 1 ? - cur[parts[i]] || {} : - Date); + if (this.pathType(createdAt) === 'adhocOrUndefined') { + for (i = 0; i < parts.length; ++i) { + cur[parts[i]] = (i < parts.length - 1 ? + cur[parts[i]] || {} : + Date); + } } this.add(schemaAdditions); @@ -875,7 +842,9 @@ Schema.prototype.setupTimestamp = function(timestamps) { updates = { $set: {} }; currentUpdate = currentUpdate || {}; - updates.$set[updatedAt] = now; + if (!currentUpdate.$currentDate || !currentUpdate.$currentDate[updatedAt]) { + updates.$set[updatedAt] = now; + } if (currentUpdate[createdAt]) { delete currentUpdate[createdAt]; @@ -1183,6 +1152,21 @@ Schema.prototype.post = function(method, fn) { */ Schema.prototype.plugin = function(fn, opts) { + if (typeof fn !== 'function') { + throw new Error('First param to `schema.plugin()` must be a function, ' + + 'got "' + (typeof fn) + '"'); + } + + if (opts && + opts.deduplicate) { + for (var i = 0; i < this.plugins.length; ++i) { + if (this.plugins[i].fn === fn) { + return this; + } + } + } + this.plugins.push({ fn: fn, opts: opts }); + fn(this, opts); return this; }; @@ -1367,13 +1351,15 @@ Schema.prototype.indexes = function() { 'use strict'; var indexes = []; - var seenPrefix = {}; + var schemaStack = []; var collectIndexes = function(schema, prefix) { - if (seenPrefix[prefix]) { + // Ignore infinitely nested schemas, if we've already seen this schema + // along this path there must be a cycle + if (schemaStack.indexOf(schema) !== -1) { return; } - seenPrefix[prefix] = true; + schemaStack.push(schema); prefix = prefix || ''; var key, path, index, field, isObject, options, type; @@ -1384,9 +1370,9 @@ Schema.prototype.indexes = function() { path = schema.paths[key]; if ((path instanceof MongooseTypes.DocumentArray) || path.$isSingleNested) { - collectIndexes(path.schema, key + '.'); + collectIndexes(path.schema, prefix + key + '.'); } else { - index = path._index; + index = path._index || (path.caster && path.caster._index); if (index !== false && index !== null && index !== undefined) { field = {}; @@ -1415,6 +1401,8 @@ Schema.prototype.indexes = function() { } } + schemaStack.pop(); + if (prefix) { fixSubIndexPaths(schema, prefix); } else { diff --git a/lib/schema/array.js b/lib/schema/array.js index 0eb902fac25..b1ec0bae72e 100644 --- a/lib/schema/array.js +++ b/lib/schema/array.js @@ -63,7 +63,7 @@ function SchemaArray(key, cast, options, schemaOptions) { : cast; this.casterConstructor = caster; - if (typeof caster === 'function') { + if (typeof caster === 'function' && !caster.$isArraySubdocument) { this.caster = new caster(null, castOptions); } else { this.caster = caster; @@ -74,6 +74,8 @@ function SchemaArray(key, cast, options, schemaOptions) { } } + this.$isMongooseArray = true; + SchemaType.call(this, key, options, 'Array'); var defaultArr; @@ -241,7 +243,7 @@ SchemaArray.prototype.castForQuery = function($conditional, value) { v = method.call(caster, v); return v; } - if (val != null) { + if (v != null) { v = new Constructor(v); return v; } diff --git a/lib/schema/embedded.js b/lib/schema/embedded.js index 9938563adc8..3ca711534b1 100644 --- a/lib/schema/embedded.js +++ b/lib/schema/embedded.js @@ -181,7 +181,7 @@ Embedded.prototype.doValidate = function(value, fn, scope) { if (!(value instanceof Constructor)) { value = new Constructor(value); } - value.validate(fn, {__noPromise: true}); + value.validate({__noPromise: true}, fn); }, scope); }; diff --git a/lib/schematype.js b/lib/schematype.js index 16078815796..35703a03393 100644 --- a/lib/schematype.js +++ b/lib/schematype.js @@ -419,9 +419,8 @@ SchemaType.prototype.get = function(fn) { * * // Can also return a promise * schema.path('name').validate({ - * isAsync: true, - * validator: function (value, respond) { - * return new Promise(resolve => { + * validator: function (value) { + * return new Promise(function (resolve, reject) { * resolve(false); // validation failed * }); * } @@ -800,7 +799,12 @@ SchemaType.prototype.doValidate = function(value, fn, scope) { */ function asyncValidate(validator, scope, value, props, cb) { + var called = false; var returnVal = validator.call(scope, value, function(ok, customMsg) { + if (called) { + return; + } + called = true; if (typeof returnVal === 'boolean') { return; } @@ -810,7 +814,27 @@ function asyncValidate(validator, scope, value, props, cb) { cb(ok, props); }); if (typeof returnVal === 'boolean') { + called = true; cb(returnVal, props); + } else if (returnVal && typeof returnVal.then === 'function') { + // Promise + returnVal.then( + function(ok) { + if (called) { + return; + } + called = true; + cb(ok, props); + }, + function(error) { + if (called) { + return; + } + called = true; + + props.reason = error; + cb(false, props); + }); } } diff --git a/lib/services/model/applyHooks.js b/lib/services/model/applyHooks.js index f07d49fb056..0104df55540 100644 --- a/lib/services/model/applyHooks.js +++ b/lib/services/model/applyHooks.js @@ -123,7 +123,9 @@ function applyHooks(model, schema) { if (error instanceof VersionError) { error.stack = originalError.stack; } - _this.$__handleReject(error); + if (!fn) { + _this.$__handleReject(error); + } reject(error); return; } @@ -140,7 +142,7 @@ function applyHooks(model, schema) { if (this.constructor.$wrapCallback) { fn = this.constructor.$wrapCallback(fn); } - return promise.then( + promise.then( function() { process.nextTick(function() { fn.apply(null, [null].concat($results)); diff --git a/lib/services/model/discriminator.js b/lib/services/model/discriminator.js index c6b18da8f4a..68f0ba8b7dc 100644 --- a/lib/services/model/discriminator.js +++ b/lib/services/model/discriminator.js @@ -82,8 +82,11 @@ module.exports = function discriminator(model, name, schema) { schema.options._id = _id; } schema.options.id = id; + schema.s.hooks = model.schema.s.hooks.merge(schema.s.hooks); - schema.callQueue = baseSchema.callQueue.concat(schema.callQueue.slice(schema._defaultMiddleware.length)); + schema.plugins = Array.prototype.slice(baseSchema.plugins); + schema.callQueue = baseSchema.callQueue. + concat(schema.callQueue.slice(schema._defaultMiddleware.length)); schema._requiredpaths = undefined; // reset just in case Schema#requiredPaths() was called on either schema } diff --git a/lib/services/query/hasDollarKeys.js b/lib/services/query/hasDollarKeys.js new file mode 100644 index 00000000000..2917a847ba6 --- /dev/null +++ b/lib/services/query/hasDollarKeys.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = function(obj) { + var keys = Object.keys(obj); + var len = keys.length; + for (var i = 0; i < len; ++i) { + if (keys[i].charAt(0) === '$') { + return true; + } + } + return false; +}; diff --git a/lib/types/array.js b/lib/types/array.js index 3874078ee1c..510ff1b0e33 100644 --- a/lib/types/array.js +++ b/lib/types/array.js @@ -119,10 +119,10 @@ MongooseArray.mixin = { if (!isDisc) { value = new Model(value); } - return this._schema.caster.cast(value, this._parent, true); + return this._schema.caster.applySetters(value, this._parent, true); } - return this._schema.caster.cast(value, this._parent, false); + return this._schema.caster.applySetters(value, this._parent, false); }, /** @@ -698,10 +698,6 @@ MongooseArray.mixin = { set: function set(i, val) { var value = this._cast(val, i); - value = this._schema.caster instanceof EmbeddedDocument ? - value : - this._schema.caster.applySetters(val, this._parent) - ; this[i] = value; this._markModified(i); return this; diff --git a/lib/types/subdocument.js b/lib/types/subdocument.js index 27534467285..5ad19c4684d 100644 --- a/lib/types/subdocument.js +++ b/lib/types/subdocument.js @@ -71,7 +71,13 @@ Subdocument.prototype.$markValid = function(path) { }; Subdocument.prototype.invalidate = function(path, err, val) { - Document.prototype.invalidate.call(this, path, err, val); + // Hack: array subdocuments' validationError is equal to the owner doc's, + // so validating an array subdoc gives the top-level doc back. Temporary + // workaround for #5208 so we don't have circular errors. + if (err !== this.ownerDocument().$__.validationError) { + Document.prototype.invalidate.call(this, path, err, val); + } + if (this.$parent) { this.$parent.invalidate([this.$basePath, path].join('.'), err, val); } else if (err.kind === 'cast' || err.name === 'CastError') { @@ -102,6 +108,16 @@ Subdocument.prototype.ownerDocument = function() { return this.$__.ownerDocument; }; +/** + * Returns this sub-documents parent document. + * + * @api public + */ + +Subdocument.prototype.parent = function() { + return this.$parent; +}; + /** * Null-out this subdoc * diff --git a/package.json b/package.json index c9f426ef322..83f4475c08e 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "async": "2.1.4", "bson": "~1.0.4", "hooks-fixed": "2.0.0", - "kareem": "1.2.1", - "mongodb": "2.2.25", + "kareem": "1.4.1", + "mongodb": "2.2.26", "mpath": "0.2.1", "mpromise": "0.5.5", "mquery": "2.3.0", @@ -48,6 +48,7 @@ "marked": "0.3.6", "mocha": "3.2.0", "mongoose-long": "0.1.1", + "mongodb-topology-manager": "1.0.11", "node-static": "0.7.7", "power-assert": "1.4.1", "q": "1.4.1", @@ -71,8 +72,8 @@ "scripts": { "fix-lint": "eslint . --fix", "install-browser": "npm install `node format_deps.js`", + "lint": "eslint . --quiet", "test": "mocha test/*.test.js test/**/*.test.js", - "posttest": "eslint . --quiet", "test-cov": "istanbul cover --report text --report html _mocha test/*.test.js" }, "main": "./index.js", diff --git a/test/connection.test.js b/test/connection.test.js index 2ddd6361da5..de65bba7ea2 100644 --- a/test/connection.test.js +++ b/test/connection.test.js @@ -141,11 +141,6 @@ describe('connections:', function() { var mongod = 'mongodb://localhost:27017'; - var repl1 = process.env.MONGOOSE_SET_TEST_URI; - var repl2 = repl1.replace('mongodb://', '').split(','); - repl2.push(repl2.shift()); - repl2 = 'mongodb://' + repl2.join(','); - describe('with different host/port', function() { it('non-replica set', function(done) { var db = mongoose.createConnection(); @@ -195,58 +190,16 @@ describe('connections:', function() { }); }); }); + }); + }); - it('replica set', function(done) { - var db = mongoose.createConnection(); - - db.openSet(repl1, function(err) { - if (err) { - return done(err); - } - - var hosts = db.hosts.slice(); - var db1 = db.db; - - db.close(function(err) { - if (err) { - return done(err); - } - - db.openSet(repl2, function(err) { - if (err) { - return done(err); - } - - db.hosts.forEach(function(host, i) { - assert.notEqual(host.port, hosts[i].port); - }); - assert.ok(db1 !== db.db); - - hosts = db.hosts.slice(); - var db2 = db.db; - - db.close(function(err) { - if (err) { - return done(err); - } - - db.openSet(repl1, function(err) { - if (err) { - return done(err); - } - - db.hosts.forEach(function(host, i) { - assert.notEqual(host.port, hosts[i].port); - }); - assert.ok(db2 !== db.db); + describe('errors', function() { + it('.catch() means error does not get thrown (gh-5229)', function(done) { + var db = mongoose.createConnection(); - db.close(); - done(); - }); - }); - }); - }); - }); + db.open('fail connection').catch(function(error) { + assert.ok(error); + done(); }); }); }); diff --git a/test/docs/es6_gateway.test.js b/test/docs/es6_gateway.test.js new file mode 100644 index 00000000000..b0532ce1fd2 --- /dev/null +++ b/test/docs/es6_gateway.test.js @@ -0,0 +1,5 @@ +'use strict'; + +if (parseInt(process.versions.node.split('.')[0], 10) >= 4) { + require('./schemas.test.es6.js'); +} diff --git a/test/docs/schemas.test.es6.js b/test/docs/schemas.test.es6.js new file mode 100644 index 00000000000..152c176c3b7 --- /dev/null +++ b/test/docs/schemas.test.es6.js @@ -0,0 +1,72 @@ +'use strict'; + +var assert = require('assert'); +var mongoose = require('../../'); + +describe('Advanced Schemas', function () { + var db; + var Schema = mongoose.Schema; + + before(function() { + db = mongoose.createConnection('mongodb://localhost:27017/mongoose_test'); + }); + + after(function(done) { + db.close(done); + }); + + /** + * Mongoose allows creating schemas from [ES6 classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). + * The `loadClass()` function lets you pull in methods, + * statics, and virtuals from an ES6 class. A class method maps to a schema + * method, a static method maps to a schema static, and getters/setters map + * to virtuals. + */ + it('Creating from ES6 Classes Using `loadClass()`', function(done) { + const schema = new Schema({ firstName: String, lastName: String }); + + class PersonClass { + // `fullName` becomes a virtual + get fullName() { + return `${this.firstName} ${this.lastName}`; + } + + set fullName(v) { + const firstSpace = v.indexOf(' '); + this.firstName = v.split(' ')[0]; + this.lastName = firstSpace === -1 ? '' : v.substr(firstSpace + 1); + } + + // `getFullName()` becomes a document method + getFullName() { + return `${this.firstName} ${this.lastName}`; + } + + // `findByFullName()` becomes a static + static findByFullName(name) { + const firstSpace = name.indexOf(' '); + const firstName = name.split(' ')[0]; + const lastName = firstSpace === -1 ? '' : name.substr(firstSpace + 1); + return this.findOne({ firstName, lastName }); + } + } + + schema.loadClass(PersonClass); + var Person = db.model('Person', schema); + + Person.create({ firstName: 'Jon', lastName: 'Snow' }). + then(doc => { + assert.equal(doc.fullName, 'Jon Snow'); + doc.fullName = 'Jon Stark'; + assert.equal(doc.firstName, 'Jon'); + assert.equal(doc.lastName, 'Stark'); + return Person.findByFullName('Jon Snow'); + }). + then(doc => { + assert.equal(doc.fullName, 'Jon Snow'); + // acquit:ignore:start + done(); + // acquit:ignore:end + }); + }); +}); diff --git a/test/docs/validation.test.js b/test/docs/validation.test.js index e2c4c994154..33c9d36efcd 100644 --- a/test/docs/validation.test.js +++ b/test/docs/validation.test.js @@ -107,6 +107,51 @@ describe('validation docs', function() { // acquit:ignore:end }); + /** + * A common gotcha for beginners is that the `unique` option for schemas + * is *not* a validator. It's a convenient helper for building [MongoDB unique indexes](https://docs.mongodb.com/manual/core/index-unique/). + * See the [FAQ](/docs/faq.html) for more information. + */ + + it('The `unique` Option is Not a Validator', function(done) { + var uniqueUsernameSchema = new Schema({ + username: { + type: String, + unique: true + } + }); + var U1 = db.model('U1', uniqueUsernameSchema); + var U2 = db.model('U2', uniqueUsernameSchema); + // acquit:ignore:start + var remaining = 2; + // acquit:ignore:end + + var dup = [{ username: 'Val' }, { username: 'Val' }]; + U1.create(dup, function(error) { + // Will save successfully! + // acquit:ignore:start + assert.ifError(error); + --remaining || done(); + // acquit:ignore:end + }); + + // Need to wait for the index to finish building before saving, + // otherwise unique constraints may be violated. + U2.on('index', function(error) { + assert.ifError(error); + U2.create(dup, function(error) { + // Will error, but will *not* be a mongoose validation error, but + // a duplicate key error. + assert.ok(error); + assert.ok(!error.errors); + assert.ok(error.message.indexOf('duplicate key error') !== -1); + // acquit:ignore:start + --remaining || done(); + // acquit:ignore:end + }); + }); + }); + /** * If the built-in validators aren't enough, you can define custom validators * to suit your needs. diff --git a/test/document.modified.test.js b/test/document.modified.test.js index 2eecd53ca80..1f1ff26e24d 100644 --- a/test/document.modified.test.js +++ b/test/document.modified.test.js @@ -465,6 +465,28 @@ describe('document modified', function() { done(); }); + + it('updates embedded doc parents upon direct assignment (gh-5189)', function(done) { + var db = start(); + var familySchema = new Schema({ + children: [{name: {type: String, required: true}}] + }); + var Family = db.model('Family', familySchema); + Family.create({ + children: [ + {name: 'John'}, + {name: 'Mary'} + ] + }, function(err, family) { + family.set({children: family.children.slice(1)}); + family.children.forEach(function(child) { + child.set({name: 'Maryanne'}); + }); + + assert.equal(family.validateSync(), undefined); + done(); + }); + }); }); it('should support setting mixed paths by string (gh-1418)', function(done) { diff --git a/test/document.test.js b/test/document.test.js index 1793bd7ce0a..0419ee3d5b6 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -1272,7 +1272,7 @@ describe('document', function() { var m = new M({name: 'gh1109-2', arr: [1]}); assert.equal(called, false); m.save(function(err) { - assert.equal(String(err), 'ValidationError: BAM'); + assert.equal(String(err), 'ValidationError: arr: BAM'); assert.equal(called, true); m.arr.push(2); called = false; @@ -1301,7 +1301,7 @@ describe('document', function() { assert.equal(err.errors.arr.message, 'Path `arr` is required.'); m.arr.push({nice: true}); m.save(function(err) { - assert.equal(String(err), 'ValidationError: BAM'); + assert.equal(String(err), 'ValidationError: arr: BAM'); m.arr.push(95); m.save(function(err) { assert.ifError(err); @@ -2054,6 +2054,25 @@ describe('document', function() { done(); }); + it('single embedded parent() (gh-5134)', function(done) { + var userSchema = new mongoose.Schema({ + name: String, + email: {type: String, required: true, match: /.+@.+/} + }, {_id: false, id: false}); + + var eventSchema = new mongoose.Schema({ + user: userSchema, + name: String + }); + + var Event = db.model('gh5134', eventSchema); + + var e = new Event({name: 'test', user: {}}); + assert.strictEqual(e.user.parent(), e.user.ownerDocument()); + + done(); + }); + it('single embedded schemas with markmodified (gh-2689)', function(done) { var userSchema = new mongoose.Schema({ name: String, @@ -3348,6 +3367,36 @@ describe('document', function() { }); }); + it('setting a nested path retains nested modified paths (gh-5206)', function(done) { + var testSchema = new mongoose.Schema({ + name: String, + surnames: { + docarray: [{ name: String }] + } + }); + + var Cat = db.model('gh5206', testSchema); + + var kitty = new Cat({ + name: 'Test', + surnames: { + docarray: [{ name: 'test1' }, { name: 'test2' }] + } + }); + + kitty.save(function(error) { + assert.ifError(error); + + kitty.surnames = { + docarray: [{ name: 'test1' }, { name: 'test2' }, { name: 'test3' }] + }; + + assert.deepEqual(kitty.modifiedPaths(), + ['surnames', 'surnames.docarray']); + done(); + }); + }); + it('toObject() does not depopulate top level (gh-3057)', function(done) { var Cat = db.model('gh3057', { name: String }); var Human = db.model('gh3057_0', { @@ -3870,6 +3919,27 @@ describe('document', function() { }); }); + it('save errors with callback and promise work (gh-5216)', function(done) { + var schema = new mongoose.Schema({}); + + var Model = db.model('gh5216', schema); + + var _id = new mongoose.Types.ObjectId(); + var doc1 = new Model({ _id: _id }); + var doc2 = new Model({ _id: _id }); + + Model.on('error', function(error) { + done(error); + }); + + doc1.save(). + then(function() { return doc2.save(function() {}); }). + catch(function(error) { + assert.ok(error); + done(); + }); + }); + it('post hooks on child subdocs run after save (gh-5085)', function(done) { var ChildModelSchema = new mongoose.Schema({ text: { @@ -3923,6 +3993,157 @@ describe('document', function() { done(); }); + it('toObject() with null (gh-5143)', function(done) { + var schema = new mongoose.Schema({ + customer: { + name: { type: String, required: false } + } + }); + + var Model = db.model('gh5143', schema); + + var model = new Model(); + model.customer = null; + assert.strictEqual(model.toObject().customer, null); + assert.strictEqual(model.toObject({ getters: true }).customer, null); + + done(); + }); + + it('handles array subdocs with single nested subdoc default (gh-5162)', function(done) { + var RatingsItemSchema = new mongoose.Schema({ + value: Number + }, { versionKey: false, _id: false }); + + var RatingsSchema = new mongoose.Schema({ + ratings: { + type: RatingsItemSchema, + default: { id: 1, value: 0 } + }, + _id: false + }); + + var RestaurantSchema = new mongoose.Schema({ + menu: { + type: [RatingsSchema] + } + }); + + var Restaurant = db.model('gh5162', RestaurantSchema); + + // Should not throw + var r = new Restaurant(); + assert.deepEqual(r.toObject().menu, []); + done(); + }); + + it('iterating through nested doc keys (gh-5078)', function(done) { + var schema = new Schema({ + nested: { + test1: String, + test2: String + } + }, { retainKeyOrder: true }); + + schema.virtual('tests').get(function() { + return _.map(this.nested, function(v) { + return v; + }); + }); + + var M = db.model('gh5078', schema); + + var doc = new M({ nested: { test1: 'a', test2: 'b' } }); + + assert.deepEqual(doc.toObject({ virtuals: true }).tests, ['a', 'b']); + + // Should not throw + require('util').inspect(doc); + JSON.stringify(doc); + + done(); + }); + + it('JSON.stringify nested errors (gh-5208)', function(done) { + var AdditionalContactSchema = new Schema({ + contactName: { + type: String, + required: true + }, + contactValue: { + type: String, + required: true + } + }); + + var ContactSchema = new Schema({ + name: { + type: String, + required: true + }, + email: { + type: String, + required: true + }, + additionalContacts: [AdditionalContactSchema] + }); + + var EmergencyContactSchema = new Schema({ + contactName: { + type: String, + required: true + }, + contact: ContactSchema + }); + + var EmergencyContact = + db.model('EmergencyContact', EmergencyContactSchema); + + var contact = new EmergencyContact({ + contactName: 'Electrical Service', + contact: { + name: 'John Smith', + email: 'john@gmail.com', + additionalContacts: [ + { + contactName: 'skype' + // Forgotten value + } + ] + } + }); + contact.validate(function(error) { + assert.ok(error); + assert.ok(error.errors['contact']); + assert.ok(error.errors['contact.additionalContacts.0.contactValue']); + + // This `JSON.stringify()` should not throw + assert.ok(JSON.stringify(error).indexOf('contactValue') !== -1); + done(); + }); + }); + + it('handles errors in subdoc pre validate (gh-5215)', function(done) { + var childSchema = new mongoose.Schema({}); + + childSchema.pre('validate', function(next) { + next(new Error('child pre validate')); + }); + + var parentSchema = new mongoose.Schema({ + child: childSchema + }); + + var Parent = db.model('gh5215', parentSchema); + + Parent.create({ child: {} }, function(error) { + assert.ok(error); + assert.ok(error.errors['child']); + assert.equal(error.errors['child'].message, 'child pre validate'); + done(); + }); + }); + it('modify multiple subdoc paths (gh-4405)', function(done) { var ChildObjectSchema = new Schema({ childProperty1: String, diff --git a/test/document.unit.test.js b/test/document.unit.test.js index 6f491d3a201..3f9748c0a6f 100644 --- a/test/document.unit.test.js +++ b/test/document.unit.test.js @@ -2,8 +2,10 @@ * Module dependencies. */ -var start = require('./common'); var assert = require('power-assert'); +var start = require('./common'); +var storeShard = require('../lib/plugins/sharding').storeShard; + var mongoose = start.mongoose; describe('sharding', function() { @@ -22,7 +24,7 @@ describe('sharding', function() { var currentTime = new Date(); d._doc = {date: currentTime}; - d.$__storeShard(); + storeShard.call(d); assert.equal(d.$__.shardval.date, currentTime); done(); }); diff --git a/test/es6.test.js b/test/es6.test.js new file mode 100644 index 00000000000..545755b8825 --- /dev/null +++ b/test/es6.test.js @@ -0,0 +1,5 @@ +'use strict'; + +if (parseInt(process.versions.node.split('.')[0], 10) >= 4) { + require('./es6/all.test.es6.js'); +} diff --git a/test/es6/all.test.es6.js b/test/es6/all.test.es6.js new file mode 100644 index 00000000000..df398fec44b --- /dev/null +++ b/test/es6/all.test.es6.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Module dependencies. + */ + +const assert = require('assert'); +const start = require('../common'); + +const mongoose = start.mongoose; + +describe('bug fixes', function() { + let db; + + before(function() { + db = start(); + }); + + it('discriminators with classes modifies class in place (gh-5175)', function(done) { + class Vehicle extends mongoose.Model { } + var V = mongoose.model(Vehicle, new mongoose.Schema()); + assert.ok(V === Vehicle); + class Car extends Vehicle { } + var C = Vehicle.discriminator(Car, new mongoose.Schema()); + assert.ok(C === Car); + done(); + }); +}); diff --git a/test/model.discriminator.test.js b/test/model.discriminator.test.js index c53560cf0e9..427a19a4b83 100644 --- a/test/model.discriminator.test.js +++ b/test/model.discriminator.test.js @@ -315,9 +315,7 @@ describe('model', function() { }); it('merges callQueue with base queue defined before discriminator types callQueue', function(done) { - assert.equal(Employee.schema.callQueue.length, 5); - // PersonSchema.post('save') - assert.strictEqual(Employee.schema.callQueue[0], Person.schema.callQueue[0]); + assert.equal(Employee.schema.callQueue.length, 8); // EmployeeSchema.pre('save') var queueIndex = Employee.schema.callQueue.length - 1; @@ -453,6 +451,11 @@ describe('model', function() { name: String }); var childCalls = 0; + var childValidateCalls = 0; + childSchema.pre('validate', function(next) { + ++childValidateCalls; + next(); + }); childSchema.pre('save', function(next) { ++childCalls; next(); @@ -486,6 +489,7 @@ describe('model', function() { assert.equal(doc.heir.name, 'Robb Stark'); assert.equal(doc.children.length, 1); assert.equal(doc.children[0].name, 'Jon Snow'); + assert.equal(childValidateCalls, 2); assert.equal(childCalls, 2); assert.equal(parentCalls, 1); done(); @@ -508,6 +512,33 @@ describe('model', function() { done(); }); + it('copies query hooks (gh-5147)', function(done) { + var options = { discriminatorKey: 'kind' }; + + var eventSchema = new mongoose.Schema({ time: Date }, options); + var eventSchemaCalls = 0; + eventSchema.pre('findOneAndUpdate', function() { + ++eventSchemaCalls; + }); + + var Event = db.model('gh5147', eventSchema); + + var clickedEventSchema = new mongoose.Schema({ url: String }, options); + var clickedEventSchemaCalls = 0; + clickedEventSchema.pre('findOneAndUpdate', function() { + ++clickedEventSchemaCalls; + }); + var ClickedLinkEvent = Event.discriminator('gh5147_0', clickedEventSchema); + + ClickedLinkEvent.findOneAndUpdate({}, { time: new Date() }, {}). + exec(function(error) { + assert.ifError(error); + assert.equal(eventSchemaCalls, 1); + assert.equal(clickedEventSchemaCalls, 1); + done(); + }); + }); + it('embedded discriminators with $push (gh-5009)', function(done) { var eventSchema = new Schema({ message: String }, { discriminatorKey: 'kind', _id: false }); diff --git a/test/model.findOneAndUpdate.test.js b/test/model.findOneAndUpdate.test.js index 467c7262635..97f5107c5a5 100644 --- a/test/model.findOneAndUpdate.test.js +++ b/test/model.findOneAndUpdate.test.js @@ -1781,6 +1781,17 @@ describe('model: findOneAndUpdate:', function() { }); }); + it('strictQuery option (gh-4136)', function(done) { + var modelSchema = new Schema({ field: Number }, { strictQuery: 'throw' }); + + var Model = db.model('gh4136', modelSchema); + Model.find({ nonexistingField: 1 }).exec(function(error) { + assert.ok(error); + assert.ok(error.message.indexOf('strictQuery') !== -1, error.message); + done(); + }); + }); + it('strict option (gh-5108)', function(done) { var modelSchema = new Schema({ field: Number }, { strict: 'throw' }); @@ -1828,6 +1839,27 @@ describe('model: findOneAndUpdate:', function() { }); }); + it('overwrite doc with update validators (gh-3556)', function(done) { + var testSchema = new Schema({ + name: { + type: String, + required: true + }, + otherName: String + }); + var Test = db.model('gh3556', testSchema); + + var opts = { overwrite: true, runValidators: true }; + Test.findOneAndUpdate({}, { otherName: 'test' }, opts, function(error) { + assert.ok(error); + assert.ok(error.errors['name']); + Test.findOneAndUpdate({}, { $set: { otherName: 'test' } }, opts, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + it('properly handles casting nested objects in update (gh-4724)', function(done) { var locationSchema = new Schema({ _id: false, diff --git a/test/model.indexes.test.js b/test/model.indexes.test.js index 890a74656f3..7c0e7a89467 100644 --- a/test/model.indexes.test.js +++ b/test/model.indexes.test.js @@ -178,21 +178,65 @@ describe('model', function() { }); }); + it('nested embedded docs (gh-5199)', function(done) { + var SubSubSchema = mongoose.Schema({ + nested2: String + }); + + SubSubSchema.index({ nested2: 1 }); + + var SubSchema = mongoose.Schema({ + nested1: String, + subSub: SubSubSchema + }); + + SubSchema.index({ nested1: 1 }); + + var ContainerSchema = mongoose.Schema({ + nested0: String, + sub: SubSchema + }); + + ContainerSchema.index({ nested0: 1 }); + + assert.deepEqual(ContainerSchema.indexes().map(function(v) { return v[0]; }), [ + { 'sub.subSub.nested2': 1 }, + { 'sub.nested1': 1 }, + { 'nested0': 1 } + ]); + + done(); + }); + + it('primitive arrays (gh-3347)', function(done) { + var schema = new Schema({ + arr: [{ type: String, unique: true }] + }); + + var indexes = schema.indexes(); + assert.equal(indexes.length, 1); + assert.deepEqual(indexes[0][0], { arr: 1 }); + assert.ok(indexes[0][1].unique); + + done(); + }); + it('error should emit on the model', function(done) { var db = start(), schema = new Schema({name: {type: String}}), Test = db.model('IndexError', schema, 'x' + random()); - Test.on('index', function(err) { - db.close(); - assert.ok(/E11000 duplicate key error/.test(err.message), err); - done(); - }); - Test.create({name: 'hi'}, {name: 'hi'}, function(err) { assert.strictEqual(err, null); Test.schema.index({name: 1}, {unique: true}); Test.schema.index({other: 1}); + + Test.on('index', function(err) { + db.close(); + assert.ok(/E11000 duplicate key error/.test(err.message), err); + done(); + }); + Test.init(); }); }); diff --git a/test/model.mapreduce.test.js b/test/model.mapreduce.test.js index abb7cfc03d3..c445c73bba9 100644 --- a/test/model.mapreduce.test.js +++ b/test/model.mapreduce.test.js @@ -375,4 +375,27 @@ describe('model: mapreduce:', function() { db.close(done); }); }); + + it('resolveToObject (gh-4945)', function(done) { + var db = start(); + var MR = db.model('MapReduce', collection); + + var o = { + map: function() { + }, + reduce: function() { + return 'test'; + }, + verbose: false, + resolveToObject: true + }; + + MR.create({ title: 'test' }, function(error) { + assert.ifError(error); + MR.mapReduce(o).then(function(obj) { + assert.ok(obj.model); + db.close(done); + }).catch(done); + }); + }); }); diff --git a/test/model.populate.test.js b/test/model.populate.test.js index 759a65d3d29..f6a1cfb8006 100644 --- a/test/model.populate.test.js +++ b/test/model.populate.test.js @@ -2023,7 +2023,8 @@ describe('model: populate:', function() { }); comment.save(function(err) { - assert.equal('CommentWithRequiredField validation failed', err && err.message); + assert.ok(err); + assert.ok(err.message.indexOf('CommentWithRequiredField validation failed') === 0, err.message); assert.ok('num' in err.errors); assert.ok('str' in err.errors); assert.ok('user' in err.errors); diff --git a/test/model.test.js b/test/model.test.js index 667eefd74b5..0e25096867f 100644 --- a/test/model.test.js +++ b/test/model.test.js @@ -1139,7 +1139,7 @@ describe('Model', function() { db.close(); assert.equal(err.errors.name.message, 'Name cannot be greater than 1 character for path "name" with value `hi`'); assert.equal(err.name, 'ValidationError'); - assert.equal(err.message, 'IntrospectionValidation validation failed'); + assert.ok(err.message.indexOf('IntrospectionValidation validation failed') !== -1, err.message); done(); }); }); @@ -5345,7 +5345,15 @@ describe('Model', function() { }); var calledPre = 0; var calledPost = 0; - schema.pre('insertMany', function(next) { + schema.pre('insertMany', function(next, docs) { + assert.equal(docs.length, 2); + assert.equal(docs[0].name, 'Star Wars'); + ++calledPre; + next(); + }); + schema.pre('insertMany', function(next, docs) { + assert.equal(docs.length, 2); + assert.equal(docs[0].name, 'Star Wars'); ++calledPre; next(); }); @@ -5358,7 +5366,7 @@ describe('Model', function() { Movie.insertMany(arr, function(error, docs) { assert.ifError(error); assert.equal(docs.length, 2); - assert.equal(calledPre, 1); + assert.equal(calledPre, 2); assert.equal(calledPost, 1); done(); }); @@ -5569,6 +5577,34 @@ describe('Model', function() { }); }); + it('insertMany with Decimal (gh-5190)', function(done) { + start.mongodVersion(function(err, version) { + if (err) { + done(err); + return; + } + var mongo34 = version[0] > 3 || (version[0] === 3 && version[1] >= 4); + if (!mongo34) { + done(); + return; + } + + test(); + }); + + function test() { + var schema = new mongoose.Schema({ + amount : mongoose.Schema.Types.Decimal + }); + var Money = db.model('gh5190', schema); + + Money.insertMany([{ amount : '123.45' }], function(error) { + assert.ifError(error); + done(); + }); + } + }); + it('bulkWrite casting updateMany, deleteOne, deleteMany (gh-3998)', function(done) { var schema = new Schema({ str: String, diff --git a/test/model.update.test.js b/test/model.update.test.js index 6212e43ced1..8e8d4354453 100644 --- a/test/model.update.test.js +++ b/test/model.update.test.js @@ -1978,6 +1978,23 @@ describe('model: update:', function() { catch(done); }); + it('lets $currentDate go through with updatedAt (gh-5222)', function(done) { + var testSchema = new Schema({ + name: String + }, { timestamps: true }); + + var Test = db.model('gh5222', testSchema); + + Test.create({ name: 'test' }, function(error) { + assert.ifError(error); + var u = { $currentDate: { updatedAt: true }, name: 'test2' }; + Test.update({}, u, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + it('update validators on single nested (gh-4332)', function(done) { var AreaSchema = new Schema({ a: String @@ -2448,6 +2465,27 @@ describe('model: update:', function() { }); }); + it('overwrite doc with update validators (gh-3556)', function(done) { + var testSchema = new Schema({ + name: { + type: String, + required: true + }, + otherName: String + }); + var Test = db.model('gh3556', testSchema); + + var opts = { overwrite: true, runValidators: true }; + Test.update({}, { otherName: 'test' }, opts, function(error) { + assert.ok(error); + assert.ok(error.errors['name']); + Test.update({}, { $set: { otherName: 'test' } }, opts, function(error) { + assert.ifError(error); + done(); + }); + }); + }); + it('does not fail if passing whole doc (gh-5088)', function(done) { var schema = new Schema({ username: String, @@ -2495,6 +2533,32 @@ describe('model: update:', function() { catch(done); }); + it('$pullAll with null (gh-5164)', function(done) { + var schema = new Schema({ + name: String, + arr: [{ name: String }] + }, { strict: true }); + var Test = db.model('gh5164', schema); + + var doc = new Test({ name: 'Test', arr: [null, {name: 'abc'}] }); + + doc.save(). + then(function(doc) { + return Test.update({ _id: doc._id }, { + $pullAll: { arr: [null] } + }); + }). + then(function() { + return Test.findById(doc); + }). + then(function(doc) { + assert.equal(doc.arr.length, 1); + assert.equal(doc.arr[0].name, 'abc'); + done(); + }). + catch(done); + }); + it('single embedded schema under document array (gh-4519)', function(done) { var PermissionSchema = new mongoose.Schema({ read: { type: Boolean, required: true }, diff --git a/test/query.middleware.test.js b/test/query.middleware.test.js index 5d3cfa378b7..8a58827673e 100644 --- a/test/query.middleware.test.js +++ b/test/query.middleware.test.js @@ -394,4 +394,29 @@ describe('query middleware', function() { done(); }); }); + + it('with clone() (gh-5153)', function(done) { + var schema = new Schema({}); + var calledPre = 0; + var calledPost = 0; + + schema.pre('find', function(next) { + ++calledPre; + next(); + }); + + schema.post('find', function(res, next) { + ++calledPost; + next(); + }); + + var Test = db.model('gh5153', schema.clone()); + + Test.find().exec(function(error) { + assert.ifError(error); + assert.equal(calledPre, 1); + assert.equal(calledPost, 1); + done(); + }); + }); }); diff --git a/test/schema.alias.test.js b/test/schema.alias.test.js new file mode 100644 index 00000000000..f696e6f992d --- /dev/null +++ b/test/schema.alias.test.js @@ -0,0 +1,105 @@ + +/** + * Module dependencies. + */ + +var start = require('./common'), + mongoose = start.mongoose, + assert = require('power-assert'), + Schema = mongoose.Schema; + +describe('schema alias option', function() { + it('works with all basic schema types', function() { + var db = start(); + + var schema = new Schema({ + string: { type: String, alias: 'StringAlias' }, + number: { type: Number, alias: 'NumberAlias' }, + date: { type: Date, alias: 'DateAlias' }, + buffer: { type: Buffer, alias: 'BufferAlias' }, + boolean: { type: Boolean, alias: 'BooleanAlias' }, + mixed: { type: Schema.Types.Mixed, alias: 'MixedAlias' }, + objectId: { type: Schema.Types.ObjectId, alias: 'ObjectIdAlias'}, + array: { type: [], alias: 'ArrayAlias' } + }); + + var S = db.model('AliasSchemaType', schema); + S.create({ + string: 'hello', + number: 1, + date: new Date(), + buffer: new Buffer('World'), + boolean: false, + mixed: [1, [], 'three', { four: 5 }], + objectId: new Schema.Types.ObjectId(), + array: ['a', 'b', 'c', 'd'] + }, function(err, s) { + assert.ifError(err); + + // Comparing with aliases + assert.equal(s.string, s.StringAlias); + assert.equal(s.number, s.NumberAlias); + assert.equal(s.date, s.DateAlias); + assert.equal(s.buffer, s.BufferAlias); + assert.equal(s.boolean, s.BooleanAlias); + assert.equal(s.mixed, s.MixedAlias); + assert.equal(s.objectId, s.ObjectIdAlias); + assert.equal(s.array, s.ArrayAlias); + }); + }); + + it('works with nested schema types', function() { + var db = start(); + + var schema = new Schema({ + nested: { + type: { + string: { type: String, alias: 'StringAlias' }, + number: { type: Number, alias: 'NumberAlias' }, + date: { type: Date, alias: 'DateAlias' }, + buffer: { type: Buffer, alias: 'BufferAlias' }, + boolean: { type: Boolean, alias: 'BooleanAlias' }, + mixed: { type: Schema.Types.Mixed, alias: 'MixedAlias' }, + objectId: { type: Schema.Types.ObjectId, alias: 'ObjectIdAlias'}, + array: { type: [], alias: 'ArrayAlias' } + }, + alias: 'NestedAlias' + } + }); + + var S = db.model('AliasNestedSchemaType', schema); + S.create({ + nested: { + string: 'hello', + number: 1, + date: new Date(), + buffer: new Buffer('World'), + boolean: false, + mixed: [1, [], 'three', { four: 5 }], + objectId: new Schema.Types.ObjectId(), + array: ['a', 'b', 'c', 'd'] + } + }, function(err, s) { + assert.ifError(err); + + // Comparing with aliases + assert.equal(s.nested, s.NestedAlias); + assert.equal(s.nested.string, s.StringAlias); + assert.equal(s.nested.number, s.NumberAlias); + assert.equal(s.nested.date, s.DateAlias); + assert.equal(s.nested.buffer, s.BufferAlias); + assert.equal(s.nested.boolean, s.BooleanAlias); + assert.equal(s.nested.mixed, s.MixedAlias); + assert.equal(s.nested.objectId, s.ObjectIdAlias); + assert.equal(s.nested.array, s.ArrayAlias); + }); + }); + + it('throws when alias option is invalid', function() { + assert.throws(function() { + new Schema({ + foo: { type: String, alias: 456 } + }); + }); + }); +}); diff --git a/test/schema.test.js b/test/schema.test.js index b7b2dc7e970..d76995a661e 100644 --- a/test/schema.test.js +++ b/test/schema.test.js @@ -772,15 +772,15 @@ describe('schema', function() { Tobi.pre('save', function() { }); - assert.equal(Tobi.callQueue.length, 5); + assert.equal(Tobi.callQueue.length, 3); Tobi.post('save', function() { }); - assert.equal(Tobi.callQueue.length, 6); + assert.equal(Tobi.callQueue.length, 4); Tobi.pre('save', function() { }); - assert.equal(Tobi.callQueue.length, 7); + assert.equal(Tobi.callQueue.length, 5); done(); }); }); diff --git a/test/schema.validation.test.js b/test/schema.validation.test.js index b48cef24850..11cf40d9c93 100644 --- a/test/schema.validation.test.js +++ b/test/schema.validation.test.js @@ -2,16 +2,17 @@ * Module dependencies. */ -var start = require('./common'), - mongoose = start.mongoose, - assert = require('power-assert'), - Schema = mongoose.Schema, - ValidatorError = mongoose.Error.ValidatorError, - SchemaTypes = Schema.Types, - ObjectId = SchemaTypes.ObjectId, - Mixed = SchemaTypes.Mixed, - DocumentObjectId = mongoose.Types.ObjectId, - random = require('../lib/utils').random; +var start = require('./common'); +var mongoose = start.mongoose; +var assert = require('power-assert'); +var Schema = mongoose.Schema; +var ValidatorError = mongoose.Error.ValidatorError; +var SchemaTypes = Schema.Types; +var ObjectId = SchemaTypes.ObjectId; +var Mixed = SchemaTypes.Mixed; +var DocumentObjectId = mongoose.Types.ObjectId; +var random = require('../lib/utils').random; +var Promise = require('bluebird'); describe('schema', function() { describe('validation', function() { @@ -681,6 +682,31 @@ describe('schema', function() { }); }); + it('custom validators with isAsync and promise (gh-5171)', function(done) { + var validate = function(v) { + return Promise.resolve(v === 'test'); + }; + + var schema = new Schema({ + x: { + type: String + } + }); + + schema.path('x').validate({ + isAsync: true, + validator: validate + }); + var M = mongoose.model('gh5171', schema); + + var m = new M({x: 'not test'}); + + m.validate(function(err) { + assert.ok(err.errors['x']); + done(); + }); + }); + it('supports custom properties (gh-2132)', function(done) { var schema = new Schema({ x: { @@ -1031,7 +1057,7 @@ describe('schema', function() { bad.foods = 'waffles'; bad.validate(function(error) { assert.ok(error); - var errorMessage = 'CastError: Cast to Object failed for value ' + + var errorMessage = 'foods: Cast to Object failed for value ' + '"waffles" at path "foods"'; assert.ok(error.toString().indexOf(errorMessage) !== -1, error.toString()); done(); @@ -1045,7 +1071,7 @@ describe('schema', function() { var bad = new Breakfast({}); bad.validate(function(error) { assert.ok(error); - var errorMessage = 'ValidationError: Path `description` is required.'; + var errorMessage = 'ValidationError: description: Path `description` is required.'; assert.equal(errorMessage, error.toString()); done(); }); @@ -1059,7 +1085,7 @@ describe('schema', function() { var error = bad.validateSync(); assert.ok(error); - var errorMessage = 'ValidationError: Path `description` is required.'; + var errorMessage = 'ValidationError: description: Path `description` is required.'; assert.equal(errorMessage, error.toString()); done(); }); @@ -1088,7 +1114,7 @@ describe('schema', function() { var bad = new Breakfast({}); bad.validate(function(error) { assert.ok(error); - var errorMessage = 'ValidationError: Path `description` is required.'; + var errorMessage = 'ValidationError: description: Path `description` is required.'; assert.equal(errorMessage, error.toString()); done(); }); @@ -1106,7 +1132,7 @@ describe('schema', function() { var Breakfast = mongoose.model('gh2832', breakfast, 'gh2832'); Breakfast.create({description: undefined}, function(error) { assert.ok(error); - var errorMessage = 'ValidationError: CastError: Cast to String failed for value "undefined" at path "description"'; + var errorMessage = 'ValidationError: description: Cast to String failed for value "undefined" at path "description"'; assert.equal(errorMessage, error.toString()); assert.ok(error.errors.description); assert.equal(error.errors.description.reason.toString(), 'Error: oops'); diff --git a/test/shard.test.js b/test/shard.test.js index 2dee21ecd19..9d11710668c 100644 --- a/test/shard.test.js +++ b/test/shard.test.js @@ -15,9 +15,6 @@ if (!uri) { '\033[39m' ); - // let expresso shut down this test - exports.r = function expressoHack() { - }; return; } @@ -62,29 +59,17 @@ describe('shard', function() { cmd.shardcollection = db.name + '.' + collection; cmd.key = P.schema.options.shardkey; - P.db.db.executeDbAdminCommand(cmd, function(err, res) { + P.db.db.executeDbAdminCommand(cmd, function(err) { assert.ifError(err); - if (!(res && res.documents && res.documents[0] && res.documents[0].ok)) { - err = new Error('could not shard test collection ' - + collection + '\n' - + res.documents[0].errmsg + '\n' - + 'Make sure to use a different database than what ' - + 'is used for the MULTI_MONGOS_TEST'); - return done(err); - } - - db.db.admin(function(err, admin) { + db.db.admin().serverStatus(function(err, info) { + db.close(); assert.ifError(err); - admin.serverStatus(function(err, info) { - db.close(); - assert.ifError(err); - version = info.version.split('.').map(function(n) { - return parseInt(n, 10); - }); - greaterThan20x = version[0] > 2 || version[0] === 2 && version[0] > 0; - done(); + version = info.version.split('.').map(function(n) { + return parseInt(n, 10); }); + greaterThan20x = version[0] > 2 || version[0] === 2 && version[0] > 0; + done(); }); }); }); diff --git a/test/timestamps.test.js b/test/timestamps.test.js new file mode 100644 index 00000000000..f049ea95455 --- /dev/null +++ b/test/timestamps.test.js @@ -0,0 +1,104 @@ +'use strict'; + +var assert = require('assert'); +var start = require('./common'); + +var mongoose = start.mongoose; + +describe('timestamps', function() { + var db; + + before(function() { + db = start(); + }); + + after(function(done) { + db.close(done); + }); + + it('does not override timestamp params defined in schema (gh-4868)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + createdAt: { + type: Date, + select: false + }, + updatedAt: { + type: Date, + select: true + }, + name: String + }, { timestamps: true }); + var M = db.model('gh4868', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.createdAt); + assert.ok(doc.updatedAt); + assert.ok(doc.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); + + it('does not override nested timestamp params defined in schema (gh-4868)', function(done) { + var startTime = Date.now(); + var schema = new mongoose.Schema({ + ts: { + createdAt: { + type: Date, + select: false + }, + updatedAt: { + type: Date, + select: true + } + }, + name: String + }, { timestamps: { createdAt: 'ts.createdAt', updatedAt: 'ts.updatedAt' } }); + var M = db.model('gh4868_0', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.ts.createdAt); + assert.ok(doc.ts.updatedAt); + assert.ok(doc.ts.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); + + it('does not override timestamps in nested schema (gh-4868)', function(done) { + var startTime = Date.now(); + var tsSchema = new mongoose.Schema({ + createdAt: { + type: Date, + select: false + }, + updatedAt: { + type: Date, + select: true + } + }); + var schema = new mongoose.Schema({ + ts: tsSchema, + name: String + }, { timestamps: { createdAt: 'ts.createdAt', updatedAt: 'ts.updatedAt' } }); + var M = db.model('gh4868_1', schema); + + M.create({ name: 'Test' }, function(error) { + assert.ifError(error); + M.findOne({}, function(error, doc) { + assert.ifError(error); + assert.ok(!doc.ts.createdAt); + assert.ok(doc.ts.updatedAt); + assert.ok(doc.ts.updatedAt.valueOf() >= startTime); + done(); + }); + }); + }); +}); diff --git a/test/types.buffer.test.js b/test/types.buffer.test.js index d633d78795f..20944461077 100644 --- a/test/types.buffer.test.js +++ b/test/types.buffer.test.js @@ -69,7 +69,7 @@ describe('types.buffer', function() { }); t.validate(function(err) { - assert.equal(err.message, 'UserBuffer validation failed'); + assert.ok(err.message.indexOf('UserBuffer validation failed') === 0, err.message); assert.equal(err.errors.required.kind, 'required'); t.required = {x: [20]}; t.save(function(err) { @@ -83,11 +83,11 @@ describe('types.buffer', function() { t.sub.push({name: 'Friday Friday'}); t.save(function(err) { - assert.equal(err.message, 'UserBuffer validation failed'); + assert.ok(err.message.indexOf('UserBuffer validation failed') === 0, err.message); assert.equal(err.errors['sub.0.buf'].kind, 'required'); t.sub[0].buf = new Buffer('well well'); t.save(function(err) { - assert.equal(err.message, 'UserBuffer validation failed'); + assert.ok(err.message.indexOf('UserBuffer validation failed') === 0, err.message); assert.equal(err.errors['sub.0.buf'].kind, 'user defined'); assert.equal(err.errors['sub.0.buf'].message, 'valid failed'); diff --git a/tools/sharded.js b/tools/sharded.js new file mode 100644 index 00000000000..92ec2f1a899 --- /dev/null +++ b/tools/sharded.js @@ -0,0 +1,42 @@ +'use strict'; + +const co = require('co'); + +co(function*() { + var Sharded = require('mongodb-topology-manager').Sharded; + + // Create new instance + var topology = new Sharded({ + mongod: 'mongod', + mongos: 'mongos' + }); + + yield topology.addShard([{ + options: { + bind_ip: 'localhost', port: 31000, dbpath: `${__dirname}/31000` + } + }], { replSet: 'rs1' }); + + yield topology.addConfigurationServers([{ + options: { + bind_ip: 'localhost', port: 35000, dbpath: `${__dirname}/35000` + } + }], { replSet: 'rs0' }); + + yield topology.addProxies([{ + bind_ip: 'localhost', port: 51000, configdb: 'localhost:35000' + }], { + binary: 'mongos' + }); + + // Start up topology + yield topology.start(); + + // Shard db + yield topology.enableSharding('test'); + + console.log('done'); +}).catch(error => { + console.error(error); + process.exit(-1); +});