diff --git a/docs/populate.jade b/docs/populate.jade
index fb181e5bcb1..295a64ea23e 100644
--- a/docs/populate.jade
+++ b/docs/populate.jade
@@ -444,8 +444,6 @@ block content
- _New in 4.5.0_
-
So far you've only populated based on the `_id` field. However, that's
sometimes not the right choice.
In particular, [arrays that grow without bound are a MongoDB anti-pattern](https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/).
@@ -453,12 +451,12 @@ block content
between documents.
```javascript
- var PersonSchema = new Schema({
+ const PersonSchema = new Schema({
name: String,
band: String
});
- var BandSchema = new Schema({
+ const BandSchema = new Schema({
name: String
});
BandSchema.virtual('members', {
@@ -471,8 +469,8 @@ block content
options: { sort: { name: -1 }, limit: 5 } // Query options, see http://bit.ly/mongoose-query-options
});
- var Person = mongoose.model('Person', PersonSchema);
- var Band = mongoose.model('Band', BandSchema);
+ const Person = mongoose.model('Person', PersonSchema);
+ const Band = mongoose.model('Band', BandSchema);
/**
* Suppose you have 2 bands: "Guns N' Roses" and "Motley Crue"
@@ -491,7 +489,7 @@ block content
```javascript
// Set `virtuals: true` so `res.json()` works
- var BandSchema = new Schema({
+ const BandSchema = new Schema({
name: String
}, { toJSON: { virtuals: true } });
```
@@ -515,6 +513,34 @@ block content
});
```
+
+
+ Populate virtuals also support counting the number of documents with
+ matching `foreignField` as opposed to the documents themselves. Set the
+ `count` option on your virtual:
+
+ ```javascript
+ const PersonSchema = new Schema({
+ name: String,
+ band: String
+ });
+
+ const BandSchema = new Schema({
+ name: String
+ });
+ BandSchema.virtual('numMembers', {
+ ref: 'Person', // The model to use
+ localField: 'name', // Find people where `localField`
+ foreignField: 'band', // is equal to `foreignField`
+ count: true // And only get the number of docs
+ });
+
+ // Later
+ const doc = await Band.findOne({ name: 'Motley Crue' }).
+ populate('numMembers');
+ doc.numMembers; // 2
+ ```
+
You can populate in either pre or post [hooks](http://mongoosejs.com/docs/middleware.html). If you want to
diff --git a/lib/aggregate.js b/lib/aggregate.js
index a506cf20cf0..28bea5c4ea6 100644
--- a/lib/aggregate.js
+++ b/lib/aggregate.js
@@ -700,7 +700,7 @@ Aggregate.prototype.explain = function(callback) {
}
cb(null, result);
});
- });
+ }, this._model.events);
};
/**
@@ -823,16 +823,16 @@ Aggregate.prototype.cursor = function(options) {
* @param {Boolean} value
* @return {Aggregate} this
* @api public
- * @see mongodb http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#addCursorFlag
+ * @deprecated Use [`.option()`](api.html#aggregate_Aggregate-option) instead. Note that MongoDB aggregations do **not** support a `noCursorTimeout` option.
*/
-Aggregate.prototype.addCursorFlag = function(flag, value) {
+Aggregate.prototype.addCursorFlag = util.deprecate(function(flag, value) {
if (!this.options) {
this.options = {};
}
this.options[flag] = value;
return this;
-};
+}, 'Mongoose: `Aggregate#addCursorFlag()` is deprecated, use `option()` instead');
/**
* Adds a collation
@@ -963,7 +963,7 @@ Aggregate.prototype.exec = function(callback) {
});
});
});
- });
+ }, model.events);
};
/**
@@ -982,6 +982,20 @@ Aggregate.prototype.then = function(resolve, reject) {
return this.exec().then(resolve, reject);
};
+/**
+ * Executes the query returning a `Promise` which will be
+ * resolved with either the doc(s) or rejected with the error.
+ * Like [`.then()`](#query_Query-then), but only takes a rejection handler.
+ *
+ * @param {Function} [reject]
+ * @return {Promise}
+ * @api public
+ */
+
+Aggregate.prototype.catch = function(reject) {
+ return this.exec().then(null, reject);
+};
+
/**
* Returns an asyncIterator for use with [`for/await/of` loops](http://bit.ly/async-iterators)
* This function *only* works for `find()` queries.
diff --git a/lib/browserDocument.js b/lib/browserDocument.js
index 80976ca268f..b1c803d7243 100644
--- a/lib/browserDocument.js
+++ b/lib/browserDocument.js
@@ -73,6 +73,12 @@ function Document(obj, schema, fields, skipId, skipInit) {
Document.prototype = Object.create(NodeJSDocument.prototype);
Document.prototype.constructor = Document;
+/*!
+ * ignore
+ */
+
+Document.events = new EventEmitter();
+
/*!
* Browser doc exposes the event emitter API
*/
diff --git a/lib/cast/date.js b/lib/cast/date.js
new file mode 100644
index 00000000000..ac17006df70
--- /dev/null
+++ b/lib/cast/date.js
@@ -0,0 +1,41 @@
+'use strict';
+
+const assert = require('assert');
+
+module.exports = function castDate(value) {
+ // Support empty string because of empty form values. Originally introduced
+ // in https://github.com/Automattic/mongoose/commit/efc72a1898fc3c33a319d915b8c5463a22938dfe
+ if (value == null || value === '') {
+ return null;
+ }
+
+ if (value instanceof Date) {
+ assert.ok(!isNaN(value.valueOf()));
+
+ return value;
+ }
+
+ let date;
+
+ assert.ok(typeof value !== 'boolean');
+
+ if (value instanceof Number || typeof value === 'number') {
+ date = new Date(value);
+ } else if (typeof value === 'string' && !isNaN(Number(value)) && (Number(value) >= 275761 || Number(value) < -271820)) {
+ // string representation of milliseconds take this path
+ date = new Date(Number(value));
+ } else if (typeof value.valueOf === 'function') {
+ // support for moment.js. This is also the path strings will take because
+ // strings have a `valueOf()`
+ date = new Date(value.valueOf());
+ } else {
+ // fallback
+ date = new Date(value);
+ }
+
+ if (!isNaN(date.valueOf())) {
+ return date;
+ }
+
+ assert.ok(false);
+};
\ No newline at end of file
diff --git a/lib/cast/decimal128.js b/lib/cast/decimal128.js
new file mode 100644
index 00000000000..bfb1578c406
--- /dev/null
+++ b/lib/cast/decimal128.js
@@ -0,0 +1,36 @@
+'use strict';
+
+const Decimal128Type = require('../types/decimal128');
+const assert = require('assert');
+
+module.exports = function castDecimal128(value) {
+ if (value == null) {
+ return value;
+ }
+
+ if (typeof value === 'object' && typeof value.$numberDecimal === 'string') {
+ return Decimal128Type.fromString(value.$numberDecimal);
+ }
+
+ if (value instanceof Decimal128Type) {
+ return value;
+ }
+
+ if (typeof value === 'string') {
+ return Decimal128Type.fromString(value);
+ }
+
+ if (Buffer.isBuffer(value)) {
+ return new Decimal128Type(value);
+ }
+
+ if (typeof value === 'number') {
+ return Decimal128Type.fromString(String(value));
+ }
+
+ if (typeof value.valueOf === 'function' && typeof value.valueOf() === 'string') {
+ return Decimal128Type.fromString(value.valueOf());
+ }
+
+ assert.ok(false);
+};
\ No newline at end of file
diff --git a/lib/cast/number.js b/lib/cast/number.js
index 180973547f3..abc22f65cb9 100644
--- a/lib/cast/number.js
+++ b/lib/cast/number.js
@@ -1,6 +1,6 @@
'use strict';
-const CastError = require('../error/cast');
+const assert = require('assert');
/*!
* Given a value, cast it to a number, or throw a `CastError` if the value
@@ -9,14 +9,12 @@ const CastError = require('../error/cast');
* @param {Any} value
* @param {String} [path] optional the path to set on the CastError
* @return {Boolean|null|undefined}
- * @throws {CastError} if `value` is not one of the allowed values
+ * @throws {Error} if `value` is not one of the allowed values
* @api private
*/
-module.exports = function castNumber(val, path) {
- if (isNaN(val)) {
- throw new CastError('number', val, path);
- }
+module.exports = function castNumber(val) {
+ assert.ok(!isNaN(val));
if (val == null) {
return val;
@@ -29,9 +27,7 @@ module.exports = function castNumber(val, path) {
val = Number(val);
}
- if (isNaN(val)) {
- throw new CastError('number', val, path);
- }
+ assert.ok(!isNaN(val));
if (val instanceof Number) {
return val;
}
@@ -45,5 +41,5 @@ module.exports = function castNumber(val, path) {
return new Number(val);
}
- throw new CastError('number', val, path);
+ assert.ok(false);
};
diff --git a/lib/cast/objectid.js b/lib/cast/objectid.js
new file mode 100644
index 00000000000..67cffb54304
--- /dev/null
+++ b/lib/cast/objectid.js
@@ -0,0 +1,29 @@
+'use strict';
+
+const ObjectId = require('../driver').get().ObjectId;
+const assert = require('assert');
+
+module.exports = function castObjectId(value) {
+ if (value == null) {
+ return value;
+ }
+
+ if (value instanceof ObjectId) {
+ return value;
+ }
+
+ if (value._id) {
+ if (value._id instanceof ObjectId) {
+ return value._id;
+ }
+ if (value._id.toString instanceof Function) {
+ return new ObjectId(value._id.toString());
+ }
+ }
+
+ if (value.toString instanceof Function) {
+ return new ObjectId(value.toString());
+ }
+
+ assert.ok(false);
+};
\ No newline at end of file
diff --git a/lib/collection.js b/lib/collection.js
index 7794d092ea2..1f797058b44 100644
--- a/lib/collection.js
+++ b/lib/collection.js
@@ -174,6 +174,14 @@ Collection.prototype.findOneAndDelete = function() {
throw new Error('Collection#findOneAndDelete unimplemented by driver');
};
+/**
+ * Abstract method that drivers must implement.
+ */
+
+Collection.prototype.findOneAndReplace = function() {
+ throw new Error('Collection#findOneAndReplace unimplemented by driver');
+};
+
/**
* Abstract method that drivers must implement.
*/
diff --git a/lib/cursor/QueryCursor.js b/lib/cursor/QueryCursor.js
index f7032a9216d..d0f2f2c5530 100644
--- a/lib/cursor/QueryCursor.js
+++ b/lib/cursor/QueryCursor.js
@@ -41,6 +41,7 @@ function QueryCursor(query, options) {
const model = query.model;
this._mongooseOptions = {};
this._transforms = [];
+ this.model = model;
model.hooks.execPre('find', query, () => {
this._transforms = this._transforms.concat(query._transforms.slice());
if (options.transform) {
@@ -157,7 +158,7 @@ QueryCursor.prototype.close = function(callback) {
this.emit('close');
cb(null);
});
- });
+ }, this.model.events);
};
/**
@@ -178,7 +179,7 @@ QueryCursor.prototype.next = function(callback) {
}
cb(null, doc);
});
- });
+ }, this.model.events);
};
/**
diff --git a/lib/document.js b/lib/document.js
index 6cf5fba9c11..d03b64b38d0 100644
--- a/lib/document.js
+++ b/lib/document.js
@@ -594,10 +594,17 @@ Document.prototype.update = function update() {
* @instance
*/
-Document.prototype.updateOne = function updateOne() {
- const args = utils.args(arguments);
- args.unshift({_id: this._id});
- return this.constructor.updateOne.apply(this.constructor, args);
+Document.prototype.updateOne = function updateOne(doc, options, callback) {
+ return utils.promiseOrCallback(callback,
+ cb => this.$__updateOne(doc, options, cb), this.constructor.events);
+};
+
+/*!
+ * ignore
+ */
+
+Document.prototype.$__updateOne = function $__updateOne(doc, options, callback) {
+ return this.constructor.updateOne({ _id: this._id }, doc, options, callback);
};
/**
@@ -1705,7 +1712,7 @@ Document.prototype.validate = function(options, callback) {
return utils.promiseOrCallback(callback, cb => this.$__validate(function(error) {
cb(error);
- }));
+ }), this.constructor.events);
};
/*!
@@ -3012,7 +3019,7 @@ Document.prototype.populate = function populate() {
Document.prototype.execPopulate = function(callback) {
return utils.promiseOrCallback(callback, cb => {
this.populate(cb);
- });
+ }, this.constructor.events);
};
/**
diff --git a/lib/helpers/model/applyHooks.js b/lib/helpers/model/applyHooks.js
index f8beeceaf10..fff81eac106 100644
--- a/lib/helpers/model/applyHooks.js
+++ b/lib/helpers/model/applyHooks.js
@@ -16,6 +16,7 @@ applyHooks.middlewareFunctions = [
'save',
'validate',
'remove',
+ 'updateOne',
'init'
];
@@ -58,10 +59,13 @@ function applyHooks(model, schema, options) {
// information.
const middleware = schema.s.hooks.filter(hook => {
- if (hook.name !== 'remove') {
- return true;
+ if (hook.name === 'updateOne') {
+ return !!hook['document'];
}
- return hook['document'] == null || !!hook['document'];
+ if (hook.name === 'remove') {
+ return hook['document'] == null || !!hook['document'];
+ }
+ return true;
});
objToDecorate.$__save = middleware.
@@ -70,6 +74,8 @@ function applyHooks(model, schema, options) {
createWrapper('validate', objToDecorate.$__validate, null, kareemOptions);
objToDecorate.$__remove = middleware.
createWrapper('remove', objToDecorate.$__remove, null, kareemOptions);
+ objToDecorate.$__updateOne = middleware.
+ createWrapper('updateOne', objToDecorate.$__updateOne, null, kareemOptions);
objToDecorate.$__init = middleware.
createWrapperSync('init', objToDecorate.$__init, null, kareemOptions);
diff --git a/lib/helpers/populate/assignRawDocsToIdStructure.js b/lib/helpers/populate/assignRawDocsToIdStructure.js
index ad37ec1a50c..1de37c6e8d4 100644
--- a/lib/helpers/populate/assignRawDocsToIdStructure.js
+++ b/lib/helpers/populate/assignRawDocsToIdStructure.js
@@ -49,11 +49,13 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re
sid = String(id);
- if (recursed) {
- // apply find behavior
+ doc = resultDocs[sid];
+ // If user wants separate copies of same doc, use this option
+ if (options.clone) {
+ doc = doc.constructor.hydrate(doc._doc);
+ }
- // assign matching documents in original order unless sorting
- doc = resultDocs[sid];
+ if (recursed) {
if (doc) {
if (sorting) {
newOrder[resultOrder[sid]] = doc;
@@ -65,7 +67,7 @@ function assignRawDocsToIdStructure(rawIds, resultDocs, resultOrder, options, re
}
} else {
// apply findOne behavior - if document in results, assign, else assign null
- newOrder[i] = doc = resultDocs[sid] || null;
+ newOrder[i] = doc || null;
}
}
diff --git a/lib/helpers/query/applyQueryMiddleware.js b/lib/helpers/query/applyQueryMiddleware.js
index f1ce4311066..3a987c40392 100644
--- a/lib/helpers/query/applyQueryMiddleware.js
+++ b/lib/helpers/query/applyQueryMiddleware.js
@@ -13,11 +13,14 @@ module.exports = applyQueryMiddleware;
applyQueryMiddleware.middlewareFunctions = [
'count',
'countDocuments',
+ 'deleteMany',
+ 'deleteOne',
'estimatedDocumentCount',
'find',
'findOne',
'findOneAndDelete',
'findOneAndRemove',
+ 'findOneAndReplace',
'findOneAndUpdate',
'remove',
'replaceOne',
@@ -41,10 +44,13 @@ function applyQueryMiddleware(Query, model) {
};
const middleware = model.hooks.filter(hook => {
- if (hook.name !== 'remove') {
- return true;
+ if (hook.name === 'updateOne') {
+ return hook.query == null || !!hook.query;
}
- return !!hook.query;
+ if (hook.name === 'remove') {
+ return !!hook.query;
+ }
+ return true;
});
// `update()` thunk has a different name because `_update` was already taken
diff --git a/lib/helpers/query/castUpdate.js b/lib/helpers/query/castUpdate.js
index 7cd9c5aa5ad..47cf6d4c40d 100644
--- a/lib/helpers/query/castUpdate.js
+++ b/lib/helpers/query/castUpdate.js
@@ -352,9 +352,14 @@ const overwriteOps = {
function castUpdateVal(schema, val, op, $conditional, context, path) {
if (!schema) {
// non-existing schema path
- return op in numberOps ?
- castNumber(val, path) :
- val;
+ if (op in numberOps) {
+ try {
+ return castNumber(val);
+ } catch (err) {
+ throw new CastError('number', val, path);
+ }
+ }
+ return val;
}
const cond = schema.caster && op in castOps &&
@@ -390,7 +395,11 @@ function castUpdateVal(schema, val, op, $conditional, context, path) {
context: context
});
}
- return castNumber(val, schema.path);
+ try {
+ return castNumber(val);
+ } catch (error) {
+ throw new CastError('number', val, schema.path);
+ }
}
if (op === '$currentDate') {
if (typeof val === 'object') {
diff --git a/lib/index.js b/lib/index.js
index 37efff4eced..cd5614a9768 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -863,6 +863,22 @@ Mongoose.prototype.Decimal128 = SchemaTypes.Decimal128;
Mongoose.prototype.Mixed = SchemaTypes.Mixed;
+/**
+ * The Mongoose Number [SchemaType](/docs/schematypes.html). Used for
+ * declaring paths in your schema that Mongoose should cast to numbers.
+ *
+ * ####Example:
+ *
+ * const schema = new Schema({ num: mongoose.Number });
+ * // Equivalent to:
+ * const schema = new Schema({ num: 'number' });
+ *
+ * @property Number
+ * @api public
+ */
+
+Mongoose.prototype.Number = SchemaTypes.Number;
+
/**
* The [MongooseError](#error_MongooseError) constructor.
*
diff --git a/lib/model.js b/lib/model.js
index 98e5fadbf43..afeb9bc5e5c 100644
--- a/lib/model.js
+++ b/lib/model.js
@@ -162,6 +162,26 @@ Model.prototype.$where;
Model.prototype.baseModelName;
+/**
+ * Event emitter that reports any errors that occurred. Useful for global error
+ * handling.
+ *
+ * ####Example:
+ *
+ * MyModel.events.on('error', err => console.log(err.message));
+ *
+ * // Prints a 'CastError' because of the above handler
+ * await MyModel.findOne({ _id: 'notanid' }).catch({} => {});
+ *
+ * @api public
+ * @fires error whenever any query or model function errors
+ * @property events
+ * @memberOf Model
+ * @static
+ */
+
+Model.events;
+
/*!
* ignore
*/
@@ -436,7 +456,7 @@ Model.prototype.save = function(options, fn) {
}
cb(null, this);
});
- });
+ }, this.constructor.events);
};
/*!
@@ -861,7 +881,7 @@ Model.prototype.remove = function remove(options, fn) {
return utils.promiseOrCallback(fn, cb => {
this.$__remove(options, cb);
- });
+ }, this.constructor.events);
};
/**
@@ -1134,7 +1154,7 @@ Model.createCollection = function createCollection(options, callback) {
this.collection = this.db.collection(this.collection.collectionName, options);
cb(null, this.collection);
}));
- });
+ }, this.events);
};
/**
@@ -1241,7 +1261,7 @@ Model.syncIndexes = function syncIndexes(options, callback) {
cb(null, dropped);
});
});
- });
+ }, this.events);
};
/**
@@ -1269,7 +1289,7 @@ Model.listIndexes = function init(callback) {
} else {
_listIndexes(cb);
}
- });
+ }, this.events);
};
/**
@@ -1318,7 +1338,7 @@ Model.ensureIndexes = function ensureIndexes(options, callback) {
}
cb(null);
});
- });
+ }, this.events);
};
/**
@@ -2419,6 +2439,76 @@ Model.findByIdAndDelete = function(id, options, callback) {
return this.findOneAndDelete({_id: id}, options, callback);
};
+/**
+ * Issue a MongoDB `findOneAndReplace()` command.
+ *
+ * Finds a matching document, replaces it with the provided doc, and passes the
+ * returned doc to the callback.
+ *
+ * Executes immediately if `callback` is passed else a Query object is returned.
+ *
+ * This function triggers the following query middleware.
+ *
+ * - `findOneAndReplace()`
+ *
+ * ####Options:
+ *
+ * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
+ * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
+ * - `select`: sets the document fields to return
+ * - `projection`: like select, it determines which fields to return, ex. `{ projection: { _id: 0 } }`
+ * - `rawResult`: if true, returns the [raw result from the MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify)
+ * - `strict`: overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict) for this update
+ *
+ * ####Examples:
+ *
+ * A.findOneAndReplace(conditions, options, callback) // executes
+ * A.findOneAndReplace(conditions, options) // return Query
+ * A.findOneAndReplace(conditions, callback) // executes
+ * A.findOneAndReplace(conditions) // returns Query
+ * A.findOneAndReplace() // returns Query
+ *
+ * Values are cast to their appropriate types when using the findAndModify helpers.
+ * However, the below are not executed by default.
+ *
+ * - defaults. Use the `setDefaultsOnInsert` option to override.
+ *
+ * @param {Object} conditions
+ * @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
+ * @param {Function} [callback]
+ * @return {Query}
+ * @api public
+ */
+
+Model.findOneAndReplace = function(conditions, options, callback) {
+ if (arguments.length === 1 && typeof conditions === 'function') {
+ const msg = 'Model.findOneAndDelete(): First argument must not be a function.\n\n'
+ + ' ' + this.modelName + '.findOneAndDelete(conditions, callback)\n'
+ + ' ' + this.modelName + '.findOneAndDelete(conditions)\n'
+ + ' ' + this.modelName + '.findOneAndDelete()\n';
+ throw new TypeError(msg);
+ }
+
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ if (callback) {
+ callback = this.$wrapCallback(callback);
+ }
+
+ let fields;
+ if (options) {
+ fields = options.select;
+ options.select = undefined;
+ }
+
+ const mq = new this.Query({}, {}, this, this.collection);
+ mq.select(fields);
+
+ return mq.findOneAndReplace(conditions, options, callback);
+};
+
/**
* Issue a mongodb findAndModify remove command.
*
@@ -2673,7 +2763,7 @@ Model.create = function create(doc, options, callback) {
cb.apply(this, [null].concat(savedDocs));
}
});
- });
+ }, this.events);
};
/**
@@ -2781,7 +2871,7 @@ Model.insertMany = function(arr, options, callback) {
}
return utils.promiseOrCallback(callback, cb => {
this.$__insertMany(arr, options, cb);
- });
+ }, this.events);
};
/*!
@@ -2957,7 +3047,7 @@ Model.bulkWrite = function(ops, options, callback) {
cb(null, res);
});
});
- });
+ }, this.events);
};
/**
@@ -3277,7 +3367,7 @@ Model.mapReduce = function mapReduce(o, callback) {
cb(null, res);
});
- });
+ }, this.events);
};
/**
@@ -3433,7 +3523,7 @@ Model.geoSearch = function(conditions, options, callback) {
res.results[i].init(temp, {}, init);
}
});
- });
+ }, this.events);
};
/**
@@ -3501,6 +3591,7 @@ Model.geoSearch = function(conditions, options, callback) {
* @param {Object} options A hash of key/val (path, options) used for population.
* @param {boolean} [options.retainNullValues=false] by default, Mongoose removes null and undefined values from populated arrays. Use this option to make `populate()` retain `null` and `undefined` array entries.
* @param {boolean} [options.getters=false] if true, Mongoose will call any getters defined on the `localField`. By default, Mongoose gets the raw value of `localField`. For example, you would need to set this option to `true` if you wanted to [add a `lowercase` getter to your `localField`](/docs/schematypes.html#schematype-options).
+ * @param {boolean} [options.clone=false] When you do `BlogPost.find().populate('author')`, blog posts with the same author will share 1 copy of an `author` doc. Enable this option to make Mongoose clone populated docs before assigning them.
* @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
* @return {Promise}
* @api public
@@ -3520,7 +3611,7 @@ Model.populate = function(docs, paths, callback) {
return utils.promiseOrCallback(callback, cb => {
_populate(_this, docs, paths, cache, cb);
- });
+ }, this.events);
};
/*!
@@ -3650,12 +3741,33 @@ function populate(model, docs, options, callback) {
}
}
+ // If just setting count, skip everything else
+ if (mod.count) {
+ mod.model.countDocuments(match, function(err, count) {
+ if (err != null) {
+ return callback(err);
+ }
+
+ for (const doc of docs) {
+ try {
+ utils.setValue(mod.options.path, count, doc);
+ } catch (err) {
+ return callback(err);
+ }
+ }
+
+ callback(null);
+ });
+ continue;
+ }
+
if (mod.options.options && mod.options.options.limit) {
assignmentOpts.originalLimit = mod.options.options.limit;
mod.options.options.limit = mod.options.options.limit * ids.length;
}
const subPopulate = utils.clone(mod.options.populate);
+
const query = mod.model.find(match, select, mod.options.options);
// If we're doing virtual populate and projection is inclusive and foreign
@@ -3802,11 +3914,12 @@ function populate(model, docs, options, callback) {
*/
function assignVals(o) {
- // Glob all options together because `populateOptions` is confusing
- const retainNullValues = get(o, 'allOptions.options.options.retainNullValues', false);
- const populateOptions = Object.assign({}, o.options, {
- justOne: o.justOne,
- retainNullValues: retainNullValues
+ // Options that aren't explicitly listed in `populateOptions`
+ const userOptions = get(o, 'allOptions.options.options');
+ // `o.options` contains options explicitly listed in `populateOptions`, like
+ // `match` and `limit`.
+ const populateOptions = Object.assign({}, o.options, userOptions, {
+ justOne: o.justOne
});
// replace the original ids in our intermediate _ids structure
@@ -3958,6 +4071,7 @@ function getModelsMapForPopulate(model, docs, options) {
const virtual = getVirtual(model.schema, options.path);
let localField;
+ let count = false;
if (virtual && virtual.options) {
const virtualPrefix = virtual.$nestedSchemaPath ?
virtual.$nestedSchemaPath + '.' : '';
@@ -3966,6 +4080,7 @@ function getModelsMapForPopulate(model, docs, options) {
} else {
localField = virtualPrefix + virtual.options.localField;
}
+ count = virtual.options.count;
} else {
localField = options.path;
}
@@ -4078,7 +4193,8 @@ function getModelsMapForPopulate(model, docs, options) {
foreignField: new Set([foreignField]),
justOne: justOne,
isVirtual: isVirtual,
- virtual: virtual
+ virtual: virtual,
+ count: count
};
map.push(available[modelName]);
} else {
@@ -4386,6 +4502,7 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
model.db = model.prototype.db = connection;
model.discriminators = model.prototype.discriminators = undefined;
model[modelSymbol] = true;
+ model.events = new EventEmitter();
model.prototype.$__setSchema(schema);
diff --git a/lib/query.js b/lib/query.js
index 0deef281142..917b93a9ffe 100644
--- a/lib/query.js
+++ b/lib/query.js
@@ -1039,6 +1039,7 @@ Query.prototype.session = function session(v) {
* - `deleteOne()`
* - `deleteMany()`
* - `findOneAndDelete()`
+ * - `findOneAndReplace()`
* - `findOneAndUpdate()`
* - `remove()`
* - `update()`
@@ -1080,6 +1081,7 @@ Query.prototype.w = function w(val) {
* - `deleteOne()`
* - `deleteMany()`
* - `findOneAndDelete()`
+ * - `findOneAndReplace()`
* - `findOneAndUpdate()`
* - `remove()`
* - `update()`
@@ -1119,6 +1121,7 @@ Query.prototype.j = function j(val) {
* - `deleteOne()`
* - `deleteMany()`
* - `findOneAndDelete()`
+ * - `findOneAndReplace()`
* - `findOneAndUpdate()`
* - `remove()`
* - `update()`
@@ -1344,6 +1347,30 @@ Query.prototype.explain = function(verbose) {
return this;
};
+/**
+ * Sets the [maxTimeMS](https://docs.mongodb.com/manual/reference/method/cursor.maxTimeMS/)
+ * option. This will tell the MongoDB server to abort if the query or write op
+ * has been running for more than `ms` milliseconds.
+ *
+ * Calling `query.maxTimeMS(v)` is equivalent to `query.setOption({ maxTimeMS: v })`
+ *
+ * ####Example:
+ *
+ * const query = new Query();
+ * // Throws an error 'operation exceeded time limit' as long as there's
+ * // >= 1 doc in the queried collection
+ * const res = await query.find({ $where: 'sleep(1000) || true' }).maxTimeMS(100);
+ *
+ * @param {Number} [ms] The number of milliseconds
+ * @return {Query} this
+ * @api public
+ */
+
+Query.prototype.maxTimeMS = function(ms) {
+ this.options.maxTimeMS = ms;
+ return this;
+};
+
/**
* Returns the current query conditions as a JSON object.
*
@@ -2943,6 +2970,121 @@ Query.prototype._findOneAndDelete = function(callback) {
}));
};
+/**
+ * Issues a MongoDB [findOneAndReplace](https://docs.mongodb.com/manual/reference/method/db.collection.findOneAndReplace/) command.
+ *
+ * Finds a matching document, removes it, and passes the found document (if any) to the callback. Executes immediately if `callback` is passed.
+ *
+ * This function triggers the following middleware.
+ *
+ * - `findOneAndReplace()`
+ *
+ * ####Available options
+ *
+ * - `sort`: if multiple docs are found by the conditions, sets the sort order to choose which doc to update
+ * - `maxTimeMS`: puts a time limit on the query - requires mongodb >= 2.6.0
+ * - `rawResult`: if true, resolves to the [raw result from the MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify)
+ *
+ * ####Callback Signature
+ * function(error, doc) {
+ * // error: any errors that occurred
+ * // doc: the document before updates are applied if `new: false`, or after updates if `new = true`
+ * }
+ *
+ * ####Examples
+ *
+ * A.where().findOneAndReplace(conditions, options, callback) // executes
+ * A.where().findOneAndReplace(conditions, options) // return Query
+ * A.where().findOneAndReplace(conditions, callback) // executes
+ * A.where().findOneAndReplace(conditions) // returns Query
+ * A.where().findOneAndReplace(callback) // executes
+ * A.where().findOneAndReplace() // returns Query
+ *
+ * @method findOneAndReplace
+ * @memberOf Query
+ * @param {Object} [conditions]
+ * @param {Object} [options]
+ * @param {Boolean} [options.rawResult] if true, returns the [raw result from the MongoDB driver](http://mongodb.github.io/node-mongodb-native/2.0/api/Collection.html#findAndModify)
+ * @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict)
+ * @param {Function} [callback] optional params are (error, document)
+ * @return {Query} this
+ * @api public
+ */
+
+Query.prototype.findOneAndReplace = function(conditions, options, callback) {
+ this.op = 'findOneAndReplace';
+ this._validate();
+
+ switch (arguments.length) {
+ case 2:
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ }
+ break;
+ case 1:
+ if (typeof conditions === 'function') {
+ callback = conditions;
+ conditions = undefined;
+ options = undefined;
+ }
+ break;
+ }
+
+ if (mquery.canMerge(conditions)) {
+ this.merge(conditions);
+ }
+
+ options && this.setOptions(options);
+
+ if (!callback) {
+ return this;
+ }
+
+ this._findOneAndReplace(callback);
+
+ return this;
+};
+
+/*!
+ * Thunk around findOneAndReplace()
+ *
+ * @param {Function} [callback]
+ * @return {Query} this
+ * @api private
+ */
+Query.prototype._findOneAndReplace = function(callback) {
+ this._castConditions();
+
+ if (this.error() != null) {
+ callback(this.error());
+ return null;
+ }
+
+ const filter = this._conditions;
+ const options = this._optionsForExec();
+ let fields = null;
+
+ if (this._fields != null) {
+ options.projection = this._castFields(utils.clone(this._fields));
+ fields = options.projection;
+ if (fields instanceof Error) {
+ callback(fields);
+ return null;
+ }
+ }
+
+ this._collection.collection.findOneAndReplace(filter, options, (err, res) => {
+ if (err) {
+ return callback(err);
+ }
+
+ const doc = res.value;
+
+ return this._completeOne(doc, res, callback);
+ });
+};
+
/*!
* Thunk around findOneAndRemove()
*
@@ -3687,11 +3829,31 @@ function _update(query, op, filter, doc, options, callback) {
}
/**
+ * Runs a function `fn` and treats the return value of `fn` as the new value
+ * for the query to resolve to.
+ *
+ * Any functions you pass to `map()` will run **after** any post hooks.
+ *
+ * ####Example:
*
+ * const res = await MyModel.findOne().map(res => {
+ * // Sets a `loadedAt` property on the doc that tells you the time the
+ * // document was loaded.
+ * return res == null ?
+ * res :
+ * Object.assign(res, { loadedAt: new Date() });
+ * });
+ *
+ * @method map
+ * @memberOf Query
+ * @instance
+ * @param {Function} fn function to run to transform the query result
+ * @return {Query} this
*/
Query.prototype.map = function(fn) {
this._transforms.push(fn);
+ return this;
};
/**
@@ -3818,7 +3980,7 @@ Query.prototype.exec = function exec(op, callback) {
cb(null, res);
});
- });
+ }, this.model.events);
};
/*!
diff --git a/lib/schema.js b/lib/schema.js
index d44a9b7ba3c..e8ab068acce 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -67,7 +67,7 @@ let id = 0;
*
* _When nesting schemas, (`children` in the example above), always declare the child schema first before passing it into its parent._
*
- * @param {Object} definition
+ * @param {Object|Schema|Array} [definition] Can be one of: object describing schema paths, or schema to copy, or array of objects and schemas
* @param {Object} [options]
* @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
* @event `init`: Emitted after the schema is compiled into a `Model`.
@@ -106,7 +106,11 @@ function Schema(obj, options) {
this.options = this.defaultOptions(options);
// build paths
- if (obj) {
+ if (Array.isArray(obj)) {
+ for (const definition of obj) {
+ this.add(definition);
+ }
+ } else if (obj) {
this.add(obj);
}
@@ -1356,6 +1360,11 @@ Schema.prototype.indexes = function() {
*
* @param {String} name
* @param {Object} [options]
+ * @param {String|Model} [options.ref] model name or model instance. Marks this as a [populate virtual](populate.html#populate-virtuals).
+ * @param {String|Function} [options.localField] Required for populate virtuals. See [populate virtual docs](populate.html#populate-virtuals) for more information.
+ * @param {String|Function} [options.foreignField] Required for populate virtuals. See [populate virtual docs](populate.html#populate-virtuals) for more information.
+ * @param {Boolean|Function} [options.justOne=false] Only works with populate virtuals. If truthy, will be a single doc or `null`. Otherwise, the populate virtual will be an array.
+ * @param {Boolean} [options.count=false] Only works with populate virtuals. If truthy, this populate virtual will contain the number of documents rather than the documents themselves when you `populate()`.
* @return {VirtualType}
*/
@@ -1376,7 +1385,7 @@ Schema.prototype.virtual = function(name, options) {
this.$$populatedVirtuals = {};
}
- if (options.justOne) {
+ if (options.justOne || options.count) {
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
_v[0] :
_v;
@@ -1407,13 +1416,13 @@ Schema.prototype.virtual = function(name, options) {
this.$$populatedVirtuals = {};
}
- if (options.justOne) {
+ if (options.justOne || options.count) {
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
_v[0] :
_v;
if (typeof this.$$populatedVirtuals[name] !== 'object') {
- this.$$populatedVirtuals[name] = null;
+ this.$$populatedVirtuals[name] = options.count ? _v : null;
}
} else {
this.$$populatedVirtuals[name] = Array.isArray(_v) ?
diff --git a/lib/schema/boolean.js b/lib/schema/boolean.js
index 47c5be901ca..f25d6e66532 100644
--- a/lib/schema/boolean.js
+++ b/lib/schema/boolean.js
@@ -4,6 +4,7 @@
* Module dependencies.
*/
+const CastError = require('../error/cast');
const SchemaType = require('../schematype');
const castBoolean = require('../cast/boolean');
const utils = require('../utils');
@@ -35,6 +36,70 @@ SchemaBoolean.schemaName = 'Boolean';
SchemaBoolean.prototype = Object.create(SchemaType.prototype);
SchemaBoolean.prototype.constructor = SchemaBoolean;
+/*!
+ * ignore
+ */
+
+SchemaBoolean._cast = castBoolean;
+
+/**
+ * Get/set the function used to cast arbitrary values to objectids.
+ *
+ * ####Example:
+ *
+ * // Make Mongoose only try to cast strings
+ * const original = mongoose.ObjectId.cast();
+ * mongoose.ObjectId.cast(v => {
+ * assert.ok(v == null || typeof v === 'string');
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.ObjectId.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaBoolean.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = v => {
+ if (v != null && typeof v !== 'boolean') {
+ throw new Error();
+ }
+ return v;
+ };
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
+/*!
+ * ignore
+ */
+
+SchemaBoolean._checkRequired = v => v === true || v === false;
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaBoolean.checkRequired = SchemaType.checkRequired;
+
/**
* Check if the given value satisfies a required validator. For a boolean
* to satisfy a required validator, it must be strictly equal to true or to
@@ -46,7 +111,7 @@ SchemaBoolean.prototype.constructor = SchemaBoolean;
*/
SchemaBoolean.prototype.checkRequired = function(value) {
- return value === true || value === false;
+ return this.constructor._checkRequired(value);
};
/**
@@ -98,7 +163,11 @@ Object.defineProperty(SchemaBoolean, 'convertToFalse', {
*/
SchemaBoolean.prototype.cast = function(value) {
- return castBoolean(value, this.path);
+ try {
+ return this.constructor.cast()(value);
+ } catch (error) {
+ throw new CastError('Boolean', value, this.path);
+ }
};
SchemaBoolean.$conditionalHandlers =
diff --git a/lib/schema/buffer.js b/lib/schema/buffer.js
index c0697392c91..8bf6bc52bf2 100644
--- a/lib/schema/buffer.js
+++ b/lib/schema/buffer.js
@@ -41,6 +41,33 @@ SchemaBuffer.schemaName = 'Buffer';
SchemaBuffer.prototype = Object.create(SchemaType.prototype);
SchemaBuffer.prototype.constructor = SchemaBuffer;
+/*!
+ * ignore
+ */
+
+SchemaBuffer._checkRequired = v => !!(v && v.length);
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * ####Example:
+ *
+ * // Allow empty strings to pass `required` check
+ * mongoose.Schema.Types.String.checkRequired(v => v != null);
+ *
+ * const M = mongoose.model({ buf: { type: Buffer, required: true } });
+ * new M({ buf: Buffer.from('') }).validateSync(); // validation passes!
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaBuffer.checkRequired = SchemaType.checkRequired;
+
/**
* Check if the given value satisfies a required validator. To satisfy a
* required validator, a buffer must not be null or undefined and have
@@ -56,7 +83,7 @@ SchemaBuffer.prototype.checkRequired = function(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
- return !!(value && value.length);
+ return this.constructor._checkRequired(value);
};
/**
diff --git a/lib/schema/date.js b/lib/schema/date.js
index e96a9ee5ec7..6e06cb7e4cd 100644
--- a/lib/schema/date.js
+++ b/lib/schema/date.js
@@ -5,6 +5,7 @@
'use strict';
const MongooseError = require('../error');
+const castDate = require('../cast/date');
const utils = require('../utils');
const SchemaType = require('../schematype');
@@ -38,6 +39,52 @@ SchemaDate.schemaName = 'Date';
SchemaDate.prototype = Object.create(SchemaType.prototype);
SchemaDate.prototype.constructor = SchemaDate;
+/*!
+ * ignore
+ */
+
+SchemaDate._cast = castDate;
+
+/**
+ * Get/set the function used to cast arbitrary values to dates.
+ *
+ * ####Example:
+ *
+ * // Mongoose converts empty string '' into `null` for date types. You
+ * // can create a custom caster to disable it.
+ * const original = mongoose.Schema.Types.Date.cast();
+ * mongoose.Schema.Types.Date.cast(v => {
+ * assert.ok(v !== '');
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.Schema.Types.Date.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaDate.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = v => {
+ if (v != null && !(v instanceof Date)) {
+ throw new Error();
+ }
+ return v;
+ };
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
/**
* Declares a TTL index (rounded to the nearest second) for _Date_ types only.
*
@@ -79,6 +126,33 @@ SchemaDate.prototype.expires = function(when) {
return this;
};
+/*!
+ * ignore
+ */
+
+SchemaDate._checkRequired = v => v instanceof Date;
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * ####Example:
+ *
+ * // Allow empty strings to pass `required` check
+ * mongoose.Schema.Types.String.checkRequired(v => v != null);
+ *
+ * const M = mongoose.model({ str: { type: String, required: true } });
+ * new M({ str: '' }).validateSync(); // `null`, validation passes!
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaDate.checkRequired = SchemaType.checkRequired;
+
/**
* Check if the given value satisfies a required validator. To satisfy
* a required validator, the given value must be an instance of `Date`.
@@ -89,8 +163,11 @@ SchemaDate.prototype.expires = function(when) {
* @api public
*/
-SchemaDate.prototype.checkRequired = function(value) {
- return value instanceof Date;
+SchemaDate.prototype.checkRequired = function(value, doc) {
+ if (SchemaType._isRef(this, value, doc, true)) {
+ return !!value;
+ }
+ return this.constructor._checkRequired(value);
};
/**
@@ -213,44 +290,12 @@ SchemaDate.prototype.max = function(value, message) {
*/
SchemaDate.prototype.cast = function(value) {
- // If null or undefined
- if (value === null || value === void 0 || value === '') {
- return null;
- }
-
- if (value instanceof Date) {
- if (isNaN(value.valueOf())) {
- throw new CastError('date', value, this.path);
- }
-
- return value;
- }
-
- let date;
-
- if (typeof value === 'boolean') {
+ const _castDate = this.constructor.cast();
+ try {
+ return _castDate(value);
+ } catch (error) {
throw new CastError('date', value, this.path);
}
-
- if (value instanceof Number || typeof value === 'number') {
- date = new Date(value);
- } else if (typeof value === 'string' && !isNaN(Number(value)) && (Number(value) >= 275761 || Number(value) < -271820)) {
- // string representation of milliseconds take this path
- date = new Date(Number(value));
- } else if (typeof value.valueOf === 'function') {
- // support for moment.js. This is also the path strings will take because
- // strings have a `valueOf()`
- date = new Date(value.valueOf());
- } else {
- // fallback
- date = new Date(value);
- }
-
- if (!isNaN(date.valueOf())) {
- return date;
- }
-
- throw new CastError('date', value, this.path);
};
/*!
diff --git a/lib/schema/decimal128.js b/lib/schema/decimal128.js
index bc7795a1436..0338905ffe8 100644
--- a/lib/schema/decimal128.js
+++ b/lib/schema/decimal128.js
@@ -7,7 +7,9 @@
const SchemaType = require('../schematype');
const CastError = SchemaType.CastError;
const Decimal128Type = require('../types/decimal128');
+const castDecimal128 = require('../cast/decimal128');
const utils = require('../utils');
+
let Document;
/**
@@ -37,6 +39,70 @@ Decimal128.schemaName = 'Decimal128';
Decimal128.prototype = Object.create(SchemaType.prototype);
Decimal128.prototype.constructor = Decimal128;
+/*!
+ * ignore
+ */
+
+Decimal128._cast = castDecimal128;
+
+/**
+ * Get/set the function used to cast arbitrary values to decimals.
+ *
+ * ####Example:
+ *
+ * // Make Mongoose only refuse to cast numbers as decimal128
+ * const original = mongoose.Schema.Types.Decimal128.cast();
+ * mongoose.Decimal128.cast(v => {
+ * assert.ok(typeof v !== 'number');
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.Decimal128.cast(false);
+ *
+ * @param {Function} [caster]
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+Decimal128.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = v => {
+ if (v != null && !(v instanceof Decimal128Type)) {
+ throw new Error();
+ }
+ return v;
+ };
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
+/*!
+ * ignore
+ */
+
+Decimal128._checkRequired = v => v instanceof Decimal128Type;
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+Decimal128.checkRequired = SchemaType.checkRequired;
+
/**
* Check if the given value satisfies a required validator.
*
@@ -50,7 +116,7 @@ Decimal128.prototype.checkRequired = function checkRequired(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
- return value instanceof Decimal128Type;
+ return this.constructor._checkRequired(value);
};
/**
@@ -104,35 +170,12 @@ Decimal128.prototype.cast = function(value, doc, init) {
return ret;
}
- if (value == null) {
- return value;
+ const _castDecimal128 = this.constructor.cast();
+ try {
+ return _castDecimal128(value);
+ } catch (error) {
+ throw new CastError('Decimal128', value, this.path);
}
-
- if (typeof value === 'object' && typeof value.$numberDecimal === 'string') {
- return Decimal128Type.fromString(value.$numberDecimal);
- }
-
- if (value instanceof Decimal128Type) {
- return value;
- }
-
- if (typeof value === 'string') {
- return Decimal128Type.fromString(value);
- }
-
- if (Buffer.isBuffer(value)) {
- return new Decimal128Type(value);
- }
-
- if (typeof value === 'number') {
- return Decimal128Type.fromString(String(value));
- }
-
- if (typeof value.valueOf === 'function' && typeof value.valueOf() === 'string') {
- return Decimal128Type.fromString(value.valueOf());
- }
-
- throw new CastError('Decimal128', value, this.path);
};
/*!
diff --git a/lib/schema/documentarray.js b/lib/schema/documentarray.js
index 1a074f737e9..39e8f7447b1 100644
--- a/lib/schema/documentarray.js
+++ b/lib/schema/documentarray.js
@@ -82,6 +82,7 @@ function _createConstructor(schema, options) {
EmbeddedDocument.schema = schema;
EmbeddedDocument.prototype.constructor = EmbeddedDocument;
EmbeddedDocument.$isArraySubdocument = true;
+ EmbeddedDocument.events = new EventEmitter();
// apply methods
for (const i in schema.methods) {
diff --git a/lib/schema/embedded.js b/lib/schema/embedded.js
index 537ca444a17..ba837c98af4 100644
--- a/lib/schema/embedded.js
+++ b/lib/schema/embedded.js
@@ -78,6 +78,7 @@ function _createConstructor(schema) {
_embedded.prototype.constructor = _embedded;
_embedded.schema = schema;
_embedded.$isSingleNested = true;
+ _embedded.events = new EventEmitter();
_embedded.prototype.toBSON = function() {
return this.toObject(internalToObjectOptions);
};
diff --git a/lib/schema/mixed.js b/lib/schema/mixed.js
index 6f461d98790..672cc519167 100644
--- a/lib/schema/mixed.js
+++ b/lib/schema/mixed.js
@@ -50,6 +50,26 @@ Mixed.schemaName = 'Mixed';
Mixed.prototype = Object.create(SchemaType.prototype);
Mixed.prototype.constructor = Mixed;
+/**
+ * Attaches a getter for all Mixed paths.
+ *
+ * ####Example:
+ *
+ * // Hide the 'hidden' path
+ * mongoose.Schema.Mixed.get(v => Object.assign({}, v, { hidden: null }));
+ *
+ * const Model = mongoose.model('Test', new Schema({ test: {} }));
+ * new Model({ test: { hidden: 'Secret!' } }).test.hidden; // null
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+Mixed.get = SchemaType.get;
+
/**
* Casts `val` for Mixed.
*
diff --git a/lib/schema/number.js b/lib/schema/number.js
index ada26f31b6b..49dc63119e4 100644
--- a/lib/schema/number.js
+++ b/lib/schema/number.js
@@ -26,6 +26,71 @@ function SchemaNumber(key, options) {
SchemaType.call(this, key, options, 'Number');
}
+/**
+ * Attaches a getter for all Number instances.
+ *
+ * ####Example:
+ *
+ * // Make all numbers round down
+ * mongoose.Number.get(function(v) { return Math.floor(v); });
+ *
+ * const Model = mongoose.model('Test', new Schema({ test: Number }));
+ * new Model({ test: 3.14 }).test; // 3
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaNumber.get = SchemaType.get;
+
+/*!
+ * ignore
+ */
+
+SchemaNumber._cast = castNumber;
+
+/**
+ * Get/set the function used to cast arbitrary values to numbers.
+ *
+ * ####Example:
+ *
+ * // Make Mongoose cast empty strings '' to 0 for paths declared as numbers
+ * const original = mongoose.Number.cast();
+ * mongoose.Number.cast(v => {
+ * if (v === '') { return 0; }
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.Number.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaNumber.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = v => {
+ if (typeof v !== 'number') {
+ throw new Error();
+ }
+ return v;
+ };
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
/**
* This schema type's name, to defend against minifiers that mangle
* function names.
@@ -40,6 +105,25 @@ SchemaNumber.schemaName = 'Number';
SchemaNumber.prototype = Object.create(SchemaType.prototype);
SchemaNumber.prototype.constructor = SchemaNumber;
+/*!
+ * ignore
+ */
+
+SchemaNumber._checkRequired = v => typeof v === 'number' || v instanceof Number;
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaNumber.checkRequired = SchemaType.checkRequired;
+
/**
* Check if the given value satisfies a required validator.
*
@@ -53,7 +137,7 @@ SchemaNumber.prototype.checkRequired = function checkRequired(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
- return typeof value === 'number' || value instanceof Number;
+ return this.constructor._checkRequired(value);
};
/**
@@ -211,7 +295,11 @@ SchemaNumber.prototype.cast = function(value, doc, init) {
value._id : // documents
value;
- return castNumber(val, this.path);
+ try {
+ return this.constructor.cast()(val);
+ } catch (err) {
+ throw new CastError('number', val, this.path);
+ }
};
/*!
diff --git a/lib/schema/objectid.js b/lib/schema/objectid.js
index 3637572c3aa..efe1eee7f71 100644
--- a/lib/schema/objectid.js
+++ b/lib/schema/objectid.js
@@ -4,6 +4,7 @@
'use strict';
+const castObjectId = require('../cast/objectid');
const SchemaType = require('../schematype');
const oid = require('../types/objectid');
const utils = require('../utils');
@@ -47,6 +48,26 @@ ObjectId.schemaName = 'ObjectId';
ObjectId.prototype = Object.create(SchemaType.prototype);
ObjectId.prototype.constructor = ObjectId;
+/**
+ * Attaches a getter for all ObjectId instances
+ *
+ * ####Example:
+ *
+ * // Always convert to string when getting an ObjectId
+ * mongoose.ObjectId.get(v => v.toString());
+ *
+ * const Model = mongoose.model('Test', new Schema({}));
+ * typeof (new Model({})._id); // 'string'
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+ObjectId.get = SchemaType.get;
+
/**
* Adds an auto-generated ObjectId default if turnOn is true.
* @param {Boolean} turnOn auto generated ObjectId defaults
@@ -63,6 +84,79 @@ ObjectId.prototype.auto = function(turnOn) {
return this;
};
+/*!
+ * ignore
+ */
+
+ObjectId._checkRequired = v => v instanceof oid;
+
+/*!
+ * ignore
+ */
+
+ObjectId._cast = castObjectId;
+
+/**
+ * Get/set the function used to cast arbitrary values to objectids.
+ *
+ * ####Example:
+ *
+ * // Make Mongoose only try to cast length 24 strings. By default, any 12
+ * // char string is a valid ObjectId.
+ * const original = mongoose.ObjectId.cast();
+ * mongoose.ObjectId.cast(v => {
+ * assert.ok(typeof v !== 'string' || v.length === 24);
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.ObjectId.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+ObjectId.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = v => {
+ if (!(v instanceof oid)) {
+ throw new Error();
+ }
+ return v;
+ };
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * ####Example:
+ *
+ * // Allow empty strings to pass `required` check
+ * mongoose.Schema.Types.String.checkRequired(v => v != null);
+ *
+ * const M = mongoose.model({ str: { type: String, required: true } });
+ * new M({ str: '' }).validateSync(); // `null`, validation passes!
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+ObjectId.checkRequired = SchemaType.checkRequired;
+
/**
* Check if the given value satisfies a required validator.
*
@@ -76,7 +170,7 @@ ObjectId.prototype.checkRequired = function checkRequired(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
- return value instanceof oid;
+ return this.constructor._checkRequired(value);
};
/**
@@ -132,32 +226,11 @@ ObjectId.prototype.cast = function(value, doc, init) {
return ret;
}
- if (value === null || value === undefined) {
- return value;
- }
-
- if (value instanceof oid) {
- return value;
+ try {
+ return this.constructor.cast()(value);
+ } catch (error) {
+ throw new CastError('ObjectId', value, this.path);
}
-
- if (value._id) {
- if (value._id instanceof oid) {
- return value._id;
- }
- if (value._id.toString instanceof Function) {
- return new oid(value._id.toString());
- }
- }
-
- if (value.toString instanceof Function) {
- try {
- return new oid(value.toString());
- } catch (err) {
- throw new CastError('ObjectId', value, this.path);
- }
- }
-
- throw new CastError('ObjectId', value, this.path);
};
/*!
diff --git a/lib/schema/operators/helpers.js b/lib/schema/operators/helpers.js
index 755f157e51f..a17951cd72e 100644
--- a/lib/schema/operators/helpers.js
+++ b/lib/schema/operators/helpers.js
@@ -4,9 +4,7 @@
* Module requirements.
*/
-const Types = {
- Number: require('../number')
-};
+const SchemaNumber = require('../number');
/*!
* @ignore
@@ -20,7 +18,7 @@ exports.castArraysOfNumbers = castArraysOfNumbers;
*/
function castToNumber(val) {
- return Types.Number.prototype.cast.call(this, val);
+ return SchemaNumber.cast()(val);
}
function castArraysOfNumbers(arr, self) {
diff --git a/lib/schema/string.js b/lib/schema/string.js
index 91097d8782a..c2e48e4f8e1 100644
--- a/lib/schema/string.js
+++ b/lib/schema/string.js
@@ -41,6 +41,98 @@ SchemaString.schemaName = 'String';
SchemaString.prototype = Object.create(SchemaType.prototype);
SchemaString.prototype.constructor = SchemaString;
+/*!
+ * ignore
+ */
+
+SchemaString._cast = castString;
+
+/**
+ * Get/set the function used to cast arbitrary values to objectids.
+ *
+ * ####Example:
+ *
+ * // Make Mongoose only try to cast strings
+ * const original = mongoose.ObjectId.cast();
+ * mongoose.ObjectId.cast(v => {
+ * assert.ok(v == null || typeof v === 'string');
+ * return original(v);
+ * });
+ *
+ * // Or disable casting entirely
+ * mongoose.ObjectId.cast(false);
+ *
+ * @param {Function} caster
+ * @return {Function}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaString.cast = function cast(caster) {
+ if (arguments.length === 0) {
+ return this._cast;
+ }
+ if (caster === false) {
+ caster = v => {
+ if (v != null && typeof v !== 'string') {
+ throw new Error();
+ }
+ return v;
+ };
+ }
+ this._cast = caster;
+
+ return this._cast;
+};
+
+/**
+ * Attaches a getter for all String instances.
+ *
+ * ####Example:
+ *
+ * // Make all numbers round down
+ * mongoose.Schema.String.get(v => v.toLowerCase());
+ *
+ * const Model = mongoose.model('Test', new Schema({ test: String }));
+ * new Model({ test: 'FOO' }).test; // 'foo'
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaString.get = SchemaType.get;
+
+/*!
+ * ignore
+ */
+
+SchemaString._checkRequired = v => (v instanceof String || typeof v === 'string') && v.length;
+
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * ####Example:
+ *
+ * // Allow empty strings to pass `required` check
+ * mongoose.Schema.Types.String.checkRequired(v => v != null);
+ *
+ * const M = mongoose.model({ str: { type: String, required: true } });
+ * new M({ str: '' }).validateSync(); // `null`, validation passes!
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaString.checkRequired = SchemaType.checkRequired;
+
/**
* Adds an enum validator
*
@@ -399,7 +491,7 @@ SchemaString.prototype.checkRequired = function checkRequired(value, doc) {
if (SchemaType._isRef(this, value, doc, true)) {
return !!value;
}
- return (value instanceof String || typeof value === 'string') && value.length;
+ return this.constructor._checkRequired(value, doc);
};
/**
@@ -442,7 +534,11 @@ SchemaString.prototype.cast = function(value, doc, init) {
return ret;
}
- return castString(value, this.path);
+ try {
+ return this.constructor.cast()(value);
+ } catch (error) {
+ throw new CastError('string', value, this.path);
+ }
};
/*!
diff --git a/lib/schematype.js b/lib/schematype.js
index 29ad9043e64..66cb82e4833 100644
--- a/lib/schematype.js
+++ b/lib/schematype.js
@@ -34,8 +34,10 @@ function SchemaType(path, options, instance) {
this.path = path;
this.instance = instance;
this.validators = [];
+ this.getters = this.constructor.hasOwnProperty('getters') ?
+ this.constructor.getters.slice() :
+ [];
this.setters = [];
- this.getters = [];
this.options = options;
this._index = null;
this.selected;
@@ -68,6 +70,26 @@ function SchemaType(path, options, instance) {
});
}
+/**
+ * Attaches a getter for all instances of this schema type.
+ *
+ * ####Example:
+ *
+ * // Make all numbers round down
+ * mongoose.Number.get(function(v) { return Math.floor(v); });
+ *
+ * @param {Function} getter
+ * @return {this}
+ * @function get
+ * @static
+ * @api public
+ */
+
+SchemaType.get = function(getter) {
+ this.getters = this.hasOwnProperty('getters') ? this.getters : [];
+ this.getters.push(getter);
+};
+
/**
* Sets a default value for this SchemaType.
*
@@ -1193,6 +1215,25 @@ SchemaType.prototype._castForQuery = function(val) {
return this.applySetters(val, this.$$context);
};
+/**
+ * Override the function the required validator uses to check whether a string
+ * passes the `required` check.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ * @function checkRequired
+ * @static
+ * @api public
+ */
+
+SchemaType.checkRequired = function(fn) {
+ if (arguments.length > 0) {
+ this._checkRequired = fn;
+ }
+
+ return this._checkRequired;
+};
+
/**
* Default check for if this path satisfies the `required` validator.
*
diff --git a/lib/types/documentarray.js b/lib/types/documentarray.js
index 09b227c1b76..3c90b64e8b1 100644
--- a/lib/types/documentarray.js
+++ b/lib/types/documentarray.js
@@ -4,15 +4,15 @@
* Module dependencies.
*/
+const Document = require('../document');
const MongooseArray = require('./array');
const ObjectId = require('./objectid');
-const ObjectIdSchema = require('../schema/objectid');
+const castObjectId = require('../cast/objectid');
const get = require('../helpers/get');
-const internalToObjectOptions = require('../options').internalToObjectOptions;
-const utils = require('../utils');
-const Document = require('../document');
const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
+const internalToObjectOptions = require('../options').internalToObjectOptions;
const util = require('util');
+const utils = require('../utils');
const documentArrayParent = require('../helpers/symbols').documentArrayParent;
@@ -200,15 +200,12 @@ MongooseDocumentArray.mixin = {
*/
id: function(id) {
- let casted,
- sid,
- _id;
+ let casted;
+ let sid;
+ let _id;
try {
- const casted_ = ObjectIdSchema.prototype.cast.call({}, id);
- if (casted_) {
- casted = String(casted_);
- }
+ casted = castObjectId(id).toString();
} catch (e) {
casted = null;
}
diff --git a/lib/utils.js b/lib/utils.js
index b951522804b..d0ba37a15e0 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -14,6 +14,8 @@ const mpath = require('mpath');
const ms = require('ms');
const Buffer = require('safe-buffer').Buffer;
+const emittedSymbol = Symbol.for('mongoose:emitted');
+
let MongooseBuffer;
let MongooseArray;
let Document;
@@ -233,15 +235,25 @@ const clone = exports.clone;
* ignore
*/
-exports.promiseOrCallback = function promiseOrCallback(callback, fn) {
+exports.promiseOrCallback = function promiseOrCallback(callback, fn, ee) {
if (typeof callback === 'function') {
- try {
- return fn(callback);
- } catch (error) {
- return process.nextTick(() => {
- throw error;
- });
- }
+ return fn(function(error) {
+ if (error != null) {
+ if (ee != null && ee.listeners('error').length > 0 && !error[emittedSymbol]) {
+ error[emittedSymbol] = true;
+ ee.emit('error', error);
+ }
+ try {
+ callback(error);
+ } catch (error) {
+ return process.nextTick(() => {
+ throw error;
+ });
+ }
+ return;
+ }
+ callback.apply(this, arguments);
+ });
}
const Promise = PromiseProvider.get();
@@ -249,6 +261,10 @@ exports.promiseOrCallback = function promiseOrCallback(callback, fn) {
return new Promise((resolve, reject) => {
fn(function(error, res) {
if (error != null) {
+ if (ee.listeners('error').length > 0 && !error[emittedSymbol]) {
+ error[emittedSymbol] = true;
+ ee.emit('error', error);
+ }
return reject(error);
}
if (arguments.length > 2) {
@@ -541,17 +557,20 @@ exports.expires = function expires(object) {
* Populate options constructor
*/
-function PopulateOptions(path, select, match, options, model, subPopulate, justOne) {
- this.path = path;
- this.match = match;
- this.select = select;
- this.options = options;
- this.model = model;
- if (typeof subPopulate === 'object') {
- this.populate = subPopulate;
+function PopulateOptions(obj) {
+ this.path = obj.path;
+ this.match = obj.match;
+ this.select = obj.select;
+ this.options = obj.options;
+ this.model = obj.model;
+ if (typeof obj.subPopulate === 'object') {
+ this.populate = obj.subPopulate;
+ }
+ if (obj.justOne != null) {
+ this.justOne = obj.justOne;
}
- if (justOne != null) {
- this.justOne = justOne;
+ if (obj.count != null) {
+ this.count = obj.count;
}
this._docs = {};
}
@@ -566,7 +585,7 @@ exports.PopulateOptions = PopulateOptions;
* populate helper
*/
-exports.populate = function populate(path, select, model, match, options, subPopulate, justOne) {
+exports.populate = function populate(path, select, model, match, options, subPopulate, justOne, count) {
// The order of select/conditions args is opposite Model.find but
// necessary to keep backward compatibility (select could be
// an array, string, or object literal).
@@ -598,14 +617,7 @@ exports.populate = function populate(path, select, model, match, options, subPop
const singles = makeSingles(path);
return singles.map(function(o) {
if (o.populate && !(o.match || o.options)) {
- return exports.populate(
- o.path,
- o.select,
- o.model,
- o.match,
- o.options,
- o.populate,
- o.justOne)[0];
+ return exports.populate(o)[0];
} else {
return exports.populate(o)[0];
}
@@ -620,6 +632,7 @@ exports.populate = function populate(path, select, model, match, options, subPop
subPopulate = path.populate;
justOne = path.justOne;
path = path.path;
+ count = path.count;
}
} else if (typeof model === 'object') {
options = match;
@@ -654,7 +667,16 @@ exports.populate = function populate(path, select, model, match, options, subPop
const paths = path.split(' ');
options = exports.clone(options);
for (let i = 0; i < paths.length; ++i) {
- ret.push(new PopulateOptions(paths[i], select, match, options, model, subPopulate, justOne));
+ ret.push(new PopulateOptions({
+ path: paths[i],
+ select: select,
+ match: match,
+ options: options,
+ model: model,
+ subPopulate: subPopulate,
+ justOne: justOne,
+ count: count
+ }));
}
return ret;
diff --git a/test/aggregate.test.js b/test/aggregate.test.js
index a59f920f171..7dd9bde10a6 100644
--- a/test/aggregate.test.js
+++ b/test/aggregate.test.js
@@ -1132,6 +1132,17 @@ describe('aggregate: ', function() {
});
});
+ it('catch() (gh-7267)', function() {
+ const MyModel = db.model('gh7267', {});
+
+ return co(function * () {
+ const err = yield MyModel.aggregate([{ $group: { foo: 'bar' } }]).
+ catch(err => err);
+ assert.ok(err instanceof Error);
+ assert.equal(err.name, 'MongoError');
+ });
+ });
+
it('cursor() without options (gh-3855)', function(done) {
const db = start();
diff --git a/test/document.test.js b/test/document.test.js
index e67c6033087..4073d7c2464 100644
--- a/test/document.test.js
+++ b/test/document.test.js
@@ -6485,6 +6485,44 @@ describe('document', function() {
});
});
+ it('updateOne() hooks (gh-7133)', function() {
+ const schema = new mongoose.Schema({ name: String });
+
+ let queryCount = 0;
+ let docCount = 0;
+
+ schema.pre('updateOne', () => ++queryCount);
+ schema.pre('updateOne', { document: true, query: false }, () => ++docCount);
+
+ let removeCount1 = 0;
+ let removeCount2 = 0;
+ schema.pre('remove', () => ++removeCount1);
+ schema.pre('remove', { document: true, query: false }, () => ++removeCount2);
+
+ const Model = db.model('gh7133', schema);
+
+ return co(function*() {
+ const doc = new Model({ name: 'test' });
+ yield doc.save();
+
+ assert.equal(queryCount, 0);
+ assert.equal(docCount, 0);
+
+ yield doc.updateOne({ name: 'test2' });
+
+ assert.equal(queryCount, 1);
+ assert.equal(docCount, 1);
+
+ assert.equal(removeCount1, 0);
+ assert.equal(removeCount2, 0);
+
+ yield doc.remove();
+
+ assert.equal(removeCount1, 1);
+ assert.equal(removeCount2, 1);
+ });
+ });
+
it('doesnt mark single nested doc date as modified if setting with string (gh-7264)', function() {
const subSchema = new mongoose.Schema({
date2: Date
diff --git a/test/model.findOneAndReplace.test.js b/test/model.findOneAndReplace.test.js
new file mode 100644
index 00000000000..6b775d13e8d
--- /dev/null
+++ b/test/model.findOneAndReplace.test.js
@@ -0,0 +1,423 @@
+'use strict';
+
+/**
+ * Test dependencies.
+ */
+
+const assert = require('assert');
+const co = require('co');
+const start = require('./common');
+const random = require('../lib/utils').random;
+
+const mongoose = start.mongoose;
+const Schema = mongoose.Schema;
+const ObjectId = Schema.Types.ObjectId;
+const DocumentObjectId = mongoose.Types.ObjectId;
+
+describe('model: findOneAndReplace:', function() {
+ let Comments;
+ let BlogPost;
+ let modelname;
+ let collection;
+ let strictSchema;
+ let db;
+
+ before(function() {
+ Comments = new Schema;
+
+ Comments.add({
+ title: String,
+ date: Date,
+ body: String,
+ comments: [Comments]
+ });
+
+ BlogPost = new Schema({
+ title: String,
+ author: String,
+ slug: String,
+ date: Date,
+ meta: {
+ date: Date,
+ visitors: Number
+ },
+ published: Boolean,
+ mixed: {},
+ numbers: [Number],
+ owners: [ObjectId],
+ comments: [Comments]
+ });
+
+ BlogPost.virtual('titleWithAuthor')
+ .get(function() {
+ return this.get('title') + ' by ' + this.get('author');
+ })
+ .set(function(val) {
+ const split = val.split(' by ');
+ this.set('title', split[0]);
+ this.set('author', split[1]);
+ });
+
+ BlogPost.method('cool', function() {
+ return this;
+ });
+
+ BlogPost.static('woot', function() {
+ return this;
+ });
+
+ modelname = 'ReplaceOneBlogPost';
+ mongoose.model(modelname, BlogPost);
+
+ collection = 'replaceoneblogposts';
+
+ strictSchema = new Schema({name: String}, {strict: true});
+ mongoose.model('ReplaceOneStrictSchema', strictSchema);
+
+ db = start();
+ });
+
+ after(function(done) {
+ db.close(done);
+ });
+
+ it('returns the original document', function() {
+ const M = db.model(modelname, collection);
+ const title = 'remove muah';
+
+ const post = new M({title: title});
+
+ return co(function*() {
+ yield post.save();
+
+ const doc = yield M.findOneAndReplace({title: title});
+
+ assert.equal(post.id, doc.id);
+ });
+ });
+
+ it('options/conditions/doc are merged when no callback is passed', function(done) {
+ const M = db.model(modelname, collection);
+
+ const now = new Date;
+ let query;
+
+ // Model.findOneAndReplace
+ query = M.findOneAndReplace({author: 'aaron'}, {select: 'author'});
+ assert.equal(query._fields.author, 1);
+ assert.equal(query._conditions.author, 'aaron');
+
+ query = M.findOneAndReplace({author: 'aaron'});
+ assert.equal(query._fields, undefined);
+ assert.equal(query._conditions.author, 'aaron');
+
+ query = M.findOneAndReplace();
+ assert.equal(query.options.new, undefined);
+ assert.equal(query._fields, undefined);
+ assert.equal(query._conditions.author, undefined);
+
+ // Query.findOneAndReplace
+ query = M.where('author', 'aaron').findOneAndReplace({date: now});
+ assert.equal(query._fields, undefined);
+ assert.equal(query._conditions.date, now);
+ assert.equal(query._conditions.author, 'aaron');
+
+ query = M.find().findOneAndReplace({author: 'aaron'}, {select: 'author'});
+ assert.equal(query._fields.author, 1);
+ assert.equal(query._conditions.author, 'aaron');
+
+ query = M.find().findOneAndReplace();
+ assert.equal(query._fields, undefined);
+ assert.equal(query._conditions.author, undefined);
+ done();
+ });
+
+ it('executes when a callback is passed', function(done) {
+ const M = db.model(modelname, collection + random());
+ let pending = 5;
+
+ M.findOneAndReplace({name: 'aaron1'}, {select: 'name'}, cb);
+ M.findOneAndReplace({name: 'aaron1'}, cb);
+ M.where().findOneAndReplace({name: 'aaron1'}, {select: 'name'}, cb);
+ M.where().findOneAndReplace({name: 'aaron1'}, cb);
+ M.where('name', 'aaron1').findOneAndReplace(cb);
+
+ function cb(err, doc) {
+ assert.ifError(err);
+ assert.equal(doc, null); // no previously existing doc
+ if (--pending) return;
+ done();
+ }
+ });
+
+ it('executed with only a callback throws', function(done) {
+ const M = db.model(modelname, collection);
+ let err;
+
+ try {
+ M.findOneAndReplace(function() {});
+ } catch (e) {
+ err = e;
+ }
+
+ assert.ok(/First argument must not be a function/.test(err));
+ done();
+ });
+
+ it('executed with only a callback throws', function(done) {
+ const M = db.model(modelname, collection);
+ let err;
+
+ try {
+ M.findByIdAndDelete(function() {});
+ } catch (e) {
+ err = e;
+ }
+
+ assert.ok(/First argument must not be a function/.test(err));
+ done();
+ });
+
+ it('executes when a callback is passed', function(done) {
+ const M = db.model(modelname, collection + random());
+ const _id = new DocumentObjectId;
+ let pending = 2;
+
+ M.findByIdAndDelete(_id, {select: 'name'}, cb);
+ M.findByIdAndDelete(_id, cb);
+
+ function cb(err, doc) {
+ assert.ifError(err);
+ assert.equal(doc, null); // no previously existing doc
+ if (--pending) return;
+ done();
+ }
+ });
+
+ it('returns the original document', function(done) {
+ const M = db.model(modelname, collection);
+ const title = 'remove muah pleez';
+
+ const post = new M({title: title});
+ post.save(function(err) {
+ assert.ifError(err);
+ M.findByIdAndDelete(post.id, function(err, doc) {
+ assert.ifError(err);
+ assert.equal(post.id, doc.id);
+ M.findById(post.id, function(err, gone) {
+ assert.ifError(err);
+ assert.equal(gone, null);
+ done();
+ });
+ });
+ });
+ });
+
+ it('options/conditions/doc are merged when no callback is passed', function(done) {
+ const M = db.model(modelname, collection);
+ const _id = new DocumentObjectId();
+
+ let query;
+
+ // Model.findByIdAndDelete
+ query = M.findByIdAndDelete(_id, {select: 'author'});
+ assert.equal(query._fields.author, 1);
+ assert.equal(query._conditions._id.toString(), _id.toString());
+
+ query = M.findByIdAndDelete(_id.toString());
+ assert.equal(query._fields, undefined);
+ assert.equal(query._conditions._id, _id.toString());
+
+ query = M.findByIdAndDelete();
+ assert.equal(query.options.new, undefined);
+ assert.equal(query._fields, undefined);
+ assert.equal(query._conditions._id, undefined);
+ done();
+ });
+
+ it('supports v3 select string syntax', function(done) {
+ const M = db.model(modelname, collection);
+ const _id = new DocumentObjectId();
+
+ let query;
+
+ query = M.findByIdAndDelete(_id, {select: 'author -title'});
+ assert.strictEqual(1, query._fields.author);
+ assert.strictEqual(0, query._fields.title);
+
+ query = M.findOneAndReplace({}, {select: 'author -title'});
+ assert.strictEqual(1, query._fields.author);
+ assert.strictEqual(0, query._fields.title);
+ done();
+ });
+
+ it('supports v3 select object syntax', function(done) {
+ const M = db.model(modelname, collection);
+ const _id = new DocumentObjectId;
+
+ let query;
+
+ query = M.findByIdAndDelete(_id, {select: {author: 1, title: 0}});
+ assert.strictEqual(1, query._fields.author);
+ assert.strictEqual(0, query._fields.title);
+
+ query = M.findOneAndReplace({}, {select: {author: 1, title: 0}});
+ assert.strictEqual(1, query._fields.author);
+ assert.strictEqual(0, query._fields.title);
+ done();
+ });
+
+ it('supports v3 sort string syntax', function(done) {
+ const M = db.model(modelname, collection);
+ const _id = new DocumentObjectId();
+
+ let query;
+
+ query = M.findByIdAndDelete(_id, {sort: 'author -title'});
+ assert.equal(Object.keys(query.options.sort).length, 2);
+ assert.equal(query.options.sort.author, 1);
+ assert.equal(query.options.sort.title, -1);
+
+ query = M.findOneAndReplace({}, {sort: 'author -title'});
+ assert.equal(Object.keys(query.options.sort).length, 2);
+ assert.equal(query.options.sort.author, 1);
+ assert.equal(query.options.sort.title, -1);
+ done();
+ });
+
+ it('supports v3 sort object syntax', function(done) {
+ const M = db.model(modelname, collection);
+ const _id = new DocumentObjectId();
+
+ let query;
+
+ query = M.findByIdAndDelete(_id, {sort: {author: 1, title: -1}});
+ assert.equal(Object.keys(query.options.sort).length, 2);
+ assert.equal(query.options.sort.author, 1);
+ assert.equal(query.options.sort.title, -1);
+
+ query = M.findOneAndReplace(_id, {sort: {author: 1, title: -1}});
+ assert.equal(Object.keys(query.options.sort).length, 2);
+ assert.equal(query.options.sort.author, 1);
+ assert.equal(query.options.sort.title, -1);
+ done();
+ });
+
+ it('supports population (gh-1395)', function(done) {
+ const M = db.model('A', {name: String});
+ const N = db.model('B', {a: {type: Schema.ObjectId, ref: 'A'}, i: Number});
+
+ M.create({name: 'i am an A'}, function(err, a) {
+ if (err) return done(err);
+ N.create({a: a._id, i: 10}, function(err, b) {
+ if (err) return done(err);
+
+ N.findOneAndReplace({_id: b._id}, {a: a._id})
+ .populate('a')
+ .exec(function(err, doc) {
+ if (err) return done(err);
+ assert.ok(doc);
+ assert.ok(doc.a);
+ assert.equal(doc.a.name, 'i am an A');
+ done();
+ });
+ });
+ });
+ });
+
+ it('only calls setters once (gh-6203)', function() {
+ return co(function*() {
+ const calls = [];
+ const userSchema = new mongoose.Schema({
+ name: String,
+ foo: {
+ type: String,
+ set: function(val) {
+ calls.push(val);
+ return val + val;
+ }
+ }
+ });
+ const Model = db.model('gh6203', userSchema);
+
+ yield Model.findOneAndReplace({ foo: '123' }, { name: 'bar' });
+
+ assert.deepEqual(calls, ['123']);
+ });
+ });
+
+ describe('middleware', function() {
+ it('works', function(done) {
+ const s = new Schema({
+ topping: {type: String, default: 'bacon'},
+ base: String
+ });
+
+ let preCount = 0;
+ s.pre('findOneAndReplace', function() {
+ ++preCount;
+ });
+
+ let postCount = 0;
+ s.post('findOneAndReplace', function() {
+ ++postCount;
+ });
+
+ const Breakfast = db.model('gh-439', s);
+ const breakfast = new Breakfast({
+ base: 'eggs'
+ });
+
+ breakfast.save(function(error) {
+ assert.ifError(error);
+
+ Breakfast.findOneAndReplace(
+ {base: 'eggs'},
+ {},
+ function(error, breakfast) {
+ assert.ifError(error);
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
+ done();
+ });
+ });
+ });
+
+ it('works with exec() (gh-439)', function(done) {
+ const s = new Schema({
+ topping: {type: String, default: 'bacon'},
+ base: String
+ });
+
+ let preCount = 0;
+ s.pre('findOneAndReplace', function() {
+ ++preCount;
+ });
+
+ let postCount = 0;
+ s.post('findOneAndReplace', function() {
+ ++postCount;
+ });
+
+ const Breakfast = db.model('Breakfast', s);
+ const breakfast = new Breakfast({
+ base: 'eggs'
+ });
+
+ breakfast.save(function(error) {
+ assert.ifError(error);
+
+ Breakfast.
+ findOneAndReplace({base: 'eggs'}, {}).
+ exec(function(error, breakfast) {
+ assert.ifError(error);
+ assert.equal(breakfast.base, 'eggs');
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/test/model.populate.test.js b/test/model.populate.test.js
index c77b1532e9f..207ae14f919 100644
--- a/test/model.populate.test.js
+++ b/test/model.populate.test.js
@@ -7940,7 +7940,47 @@ describe('model: populate:', function() {
});
});
- it('explicit option overrides refPath (gh-7273)', function() {
+ it('count option (gh-4469)', function() {
+ const childSchema = new Schema({ parentId: mongoose.ObjectId });
+
+ const parentSchema = new Schema({ name: String });
+ parentSchema.virtual('childCount', {
+ ref: 'gh4469_Child',
+ localField: '_id',
+ foreignField: 'parentId',
+ count: true
+ });
+
+ parentSchema.virtual('children', {
+ ref: 'gh4469_Child',
+ localField: '_id',
+ foreignField: 'parentId',
+ count: false
+ });
+
+ const Child = db.model('gh4469_Child', childSchema);
+ const Parent = db.model('gh4469_Parent', parentSchema);
+
+ return co(function*() {
+ const p = yield Parent.create({ name: 'test' });
+
+ yield Child.create([{ parentId: p._id }, { parentId: p._id }, {}]);
+
+ let doc = yield Parent.findOne().populate('children childCount');
+ assert.equal(doc.childCount, 2);
+ assert.equal(doc.children.length, 2);
+
+ doc = yield Parent.find().populate('children childCount').then(res => res[0]);
+ assert.equal(doc.childCount, 2);
+ assert.equal(doc.children.length, 2);
+
+ doc = yield Parent.find().populate('childCount').then(res => res[0]);
+ assert.equal(doc.childCount, 2);
+ assert.equal(doc.children, null);
+ });
+ });
+
+ it('explicit model option overrides refPath (gh-7273)', function() {
const userSchema = new Schema({ name: String });
const User1 = db.model('gh7273_User_1', userSchema);
db.model('gh7273_User_2', userSchema);
@@ -7967,6 +8007,37 @@ describe('model: populate:', function() {
});
});
+ it('clone option means identical ids get separate copies of doc (gh-3258)', function() {
+ const userSchema = new Schema({ name: String });
+ const User = db.model('gh3258_User', userSchema);
+
+ const postSchema = new Schema({
+ user: {
+ type: mongoose.ObjectId,
+ ref: User
+ },
+ title: String
+ });
+
+ const Post = db.model('gh3258_Post', postSchema);
+
+ return co(function*() {
+ const user = yield User.create({ name: 'val' });
+ yield Post.create([
+ { title: 'test1', user: user },
+ { title: 'test2', user: user }
+ ]);
+
+ const posts = yield Post.find().populate({
+ path: 'user',
+ options: { clone: true }
+ });
+
+ posts[0].user.name = 'val2';
+ assert.equal(posts[1].user.name, 'val');
+ });
+ });
+
it('populate single path with numeric path underneath doc array (gh-7273)', function() {
const schema = new Schema({
arr1: [{
diff --git a/test/model.test.js b/test/model.test.js
index 000b4c2c6b2..0484b624bab 100644
--- a/test/model.test.js
+++ b/test/model.test.js
@@ -6143,4 +6143,32 @@ describe('Model', function() {
const _schema = JSON.parse(JSON.stringify(Model.schema));
assert.ok(_schema.obj.nested);
});
+
+ it('Model.events() (gh-7125)', function() {
+ const Model = db.model('gh7125', Schema({
+ name: { type: String, validate: () => false }
+ }));
+
+ let called = [];
+ Model.events.on('error', err => { called.push(err); });
+
+ return co(function*() {
+ yield Model.findOne({ _id: 'notanid' }).catch(() => {});
+ assert.equal(called.length, 1);
+ assert.equal(called[0].name, 'CastError');
+
+ called = [];
+
+ const doc = new Model({ name: 'fail' });
+ yield doc.save().catch(() => {});
+ assert.equal(called.length, 1);
+ assert.equal(called[0].name, 'ValidationError');
+
+ called = [];
+
+ yield Model.aggregate([{ $group: { fail: true } }]).exec().catch(() => {});
+ assert.equal(called.length, 1);
+ assert.equal(called[0].name, 'MongoError');
+ });
+ });
});
diff --git a/test/query.middleware.test.js b/test/query.middleware.test.js
index f232451902a..60e994130a4 100644
--- a/test/query.middleware.test.js
+++ b/test/query.middleware.test.js
@@ -1,6 +1,9 @@
'use strict';
+
const start = require('./common');
const assert = require('power-assert');
+const co = require('co');
+
const mongoose = start.mongoose;
const Schema = mongoose.Schema;
@@ -371,6 +374,58 @@ describe('query middleware', function() {
});
});
+ it('deleteOne() (gh-7195)', function() {
+ let preCount = 0;
+ let postCount = 0;
+
+ schema.pre('deleteOne', function() {
+ ++preCount;
+ });
+
+ schema.post('deleteOne', function() {
+ ++postCount;
+ });
+
+ return co(function*() {
+ const Model = db.model('gh7195_deleteOne', schema);
+ yield Model.create([{ title: 'foo' }, { title: 'bar' }]);
+
+ yield Model.deleteOne();
+
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
+
+ const count = yield Model.countDocuments();
+ assert.equal(count, 1);
+ });
+ });
+
+ it('deleteMany() (gh-7195)', function() {
+ let preCount = 0;
+ let postCount = 0;
+
+ schema.pre('deleteMany', function() {
+ ++preCount;
+ });
+
+ schema.post('deleteMany', function() {
+ ++postCount;
+ });
+
+ return co(function*() {
+ const Model = db.model('gh7195_deleteMany', schema);
+ yield Model.create([{ title: 'foo' }, { title: 'bar' }]);
+
+ yield Model.deleteMany();
+
+ assert.equal(preCount, 1);
+ assert.equal(postCount, 1);
+
+ const count = yield Model.countDocuments();
+ assert.equal(count, 0);
+ });
+ });
+
it('error handlers (gh-2284)', function(done) {
const testSchema = new Schema({ title: { type: String, unique: true } });
diff --git a/test/query.test.js b/test/query.test.js
index bd1563d8a1a..a509f89b732 100644
--- a/test/query.test.js
+++ b/test/query.test.js
@@ -2773,6 +2773,21 @@ describe('Query', function() {
});
});
+ it('map (gh-7142)', function() {
+ const Model = db.model('gh7142', new Schema({ name: String }));
+
+ return co(function*() {
+ yield Model.create({ name: 'test' });
+ const now = new Date();
+ const res = yield Model.findOne().map(res => {
+ res.loadedAt = now;
+ return res;
+ });
+
+ assert.equal(res.loadedAt, now);
+ });
+ });
+
describe('orFail (gh-6841)', function() {
let Model;
@@ -3282,4 +3297,18 @@ describe('Query', function() {
assert.equal(doc.hasDefault, 'success');
});
});
+
+ it('maxTimeMS() (gh-7254)', function() {
+ const Model = db.model('gh7254', new Schema({}));
+
+ return co(function*() {
+ yield Model.create({});
+
+ const res = yield Model.find({ $where: 'sleep(1000) || true' }).
+ maxTimeMS(10).
+ then(() => null, err => err);
+ assert.ok(res);
+ assert.ok(res.message.indexOf('time limit') !== -1, res.message);
+ });
+ });
});
diff --git a/test/schema.test.js b/test/schema.test.js
index 353d4cdb793..5d5f85ef1aa 100644
--- a/test/schema.test.js
+++ b/test/schema.test.js
@@ -6,7 +6,7 @@
const start = require('./common');
const mongoose = start.mongoose;
-const assert = require('power-assert');
+const assert = require('assert');
const Schema = mongoose.Schema;
const Document = mongoose.Document;
const VirtualType = mongoose.VirtualType;
@@ -1368,6 +1368,17 @@ describe('schema', function() {
});
});
});
+
+ it('array of of schemas and objects (gh-7218)', function(done) {
+ const baseSchema = new Schema({ created: Date }, { id: true });
+ const s = new Schema([baseSchema, { name: String }], { id: false });
+
+ assert.ok(s.path('created'));
+ assert.ok(s.path('name'));
+ assert.ok(!s.options.id);
+
+ done();
+ });
});
describe('property names', function() {
diff --git a/test/schematype.cast.test.js b/test/schematype.cast.test.js
new file mode 100644
index 00000000000..2007b52739a
--- /dev/null
+++ b/test/schematype.cast.test.js
@@ -0,0 +1,194 @@
+'use strict';
+
+const ObjectId = require('bson').ObjectId;
+const Schema = require('../lib/schema');
+const assert = require('assert');
+
+describe('SchemaType.cast() (gh-7045)', function() {
+ const original = {};
+
+ beforeEach(function() {
+ original.objectid = Schema.ObjectId.cast();
+ original.boolean = Schema.Types.Boolean.cast();
+ original.string = Schema.Types.String.cast();
+ original.date = Schema.Types.Date.cast();
+ original.decimal128 = Schema.Types.Decimal128.cast();
+ });
+
+ afterEach(function() {
+ Schema.ObjectId.cast(original.objectid);
+ Schema.Types.Boolean.cast(original.boolean);
+ Schema.Types.String.cast(original.string);
+ Schema.Types.Date.cast(original.date);
+ Schema.Types.Decimal128.cast(original.decimal128);
+ });
+
+ it('with inheritance', function() {
+ class CustomObjectId extends Schema.ObjectId {}
+
+ CustomObjectId.cast(v => {
+ assert.ok(v == null || (typeof v === 'string' && v.length === 24));
+ return original.objectid(v);
+ });
+
+ const objectid = new CustomObjectId('test', { suppressWarning: true });
+ const baseObjectId = new Schema.ObjectId('test', { suppressWarning: true });
+
+ let threw = false;
+ try {
+ objectid.cast('12charstring');
+ } catch (error) {
+ threw = true;
+ assert.equal(error.name, 'CastError');
+ }
+
+ objectid.cast('000000000000000000000000'); // Should not throw
+
+ // Base objectid shouldn't throw
+ baseObjectId.cast('12charstring');
+ baseObjectId.cast('000000000000000000000000');
+
+ assert.ok(threw);
+ });
+
+ it('handles objectid', function() {
+ Schema.ObjectId.cast(v => {
+ assert.ok(v == null || typeof v === 'string');
+ return original.objectid(v);
+ });
+
+ const objectid = new Schema.ObjectId('test', { suppressWarning: true });
+
+ let threw = false;
+ try {
+ objectid.cast({ toString: () => '000000000000000000000000' });
+ } catch (error) {
+ threw = true;
+ assert.equal(error.name, 'CastError');
+ }
+ assert.ok(threw);
+ });
+
+ it('handles disabling casting', function() {
+ Schema.ObjectId.cast(false);
+
+ const objectid = new Schema.ObjectId('test', { suppressWarning: true });
+
+ let threw = false;
+ try {
+ objectid.cast('000000000000000000000000');
+ } catch (error) {
+ threw = true;
+ assert.equal(error.name, 'CastError');
+ }
+ assert.ok(threw);
+
+ objectid.cast(new ObjectId()); // Should not throw
+ });
+
+ it('handles boolean', function() {
+ Schema.ObjectId.cast(v => {
+ assert.ok(v == null || typeof v === 'string');
+ return original.objectid(v);
+ });
+
+ const objectid = new Schema.ObjectId('test', { suppressWarning: true });
+
+ let threw = false;
+ try {
+ objectid.cast(123);
+ } catch (error) {
+ threw = true;
+ assert.equal(error.name, 'CastError');
+ }
+ assert.ok(threw);
+ });
+
+ it('handles disabling casting', function() {
+ Schema.Types.Boolean.cast(false);
+
+ const b = new Schema.Types.Boolean();
+
+ let threw = false;
+ try {
+ b.cast(1);
+ } catch (error) {
+ threw = true;
+ assert.equal(error.name, 'CastError');
+ }
+ assert.ok(threw);
+
+ b.cast(true); // Should not throw
+ });
+
+ describe('string', function() {
+ it('supports custom cast functions', function() {
+ Schema.Types.String.cast(v => {
+ assert.ok(v.length < 10);
+ return original.string(v);
+ });
+
+ const s = new Schema.Types.String();
+ s.cast('short'); // Should not throw
+
+ assert.throws(() => s.cast('wayyyy too long'), /CastError/);
+ });
+
+ it('supports disabling casting', function() {
+ Schema.Types.String.cast(false);
+
+ const s = new Schema.Types.String();
+ s.cast('short'); // Should not throw
+
+ assert.throws(() => s.cast(123), /CastError/);
+ });
+ });
+
+ describe('date', function() {
+ it('supports custom cast functions', function() {
+ Schema.Types.Date.cast(v => {
+ assert.ok(v !== '');
+ return original.date(v);
+ });
+
+ const d = new Schema.Types.Date();
+ d.cast('2018-06-01'); // Should not throw
+ d.cast(new Date()); // Should not throw
+
+ assert.throws(() => d.cast(''), /CastError/);
+ });
+
+ it('supports disabling casting', function() {
+ Schema.Types.Date.cast(false);
+
+ const d = new Schema.Types.Date();
+ d.cast(new Date()); // Should not throw
+
+ assert.throws(() => d.cast('2018-06-01'), /CastError/);
+ });
+ });
+
+ describe('decimal128', function() {
+ it('supports custom cast functions', function() {
+ Schema.Types.Decimal128.cast(v => {
+ assert.ok(typeof v !== 'number');
+ return original.date(v);
+ });
+
+ const d = new Schema.Types.Decimal128();
+ d.cast('1000'); // Should not throw
+
+ assert.throws(() => d.cast(1000), /CastError/);
+ });
+
+ it('supports disabling casting', function() {
+ Schema.Types.Decimal128.cast(false);
+
+ const d = new Schema.Types.Decimal128();
+ assert.throws(() => d.cast('1000'), /CastError/);
+ assert.throws(() => d.cast(1000), /CastError/);
+
+ d.cast(original.decimal128('1000')); // Should not throw
+ });
+ });
+});
\ No newline at end of file
diff --git a/test/types.number.test.js b/test/types.number.test.js
index 49f33177822..3ade9579897 100644
--- a/test/types.number.test.js
+++ b/test/types.number.test.js
@@ -121,4 +121,51 @@ describe('types.number', function() {
}
assert.ok(/CastError/.test(err));
});
+
+ describe('custom caster (gh-7045)', function() {
+ let original = {};
+
+ beforeEach(function() {
+ original = SchemaNumber.cast();
+ });
+
+ afterEach(function() {
+ SchemaNumber.cast(original);
+ });
+
+ it('disallow empty string', function() {
+ SchemaNumber.cast(v => {
+ assert.ok(v !== '');
+ return original(v);
+ });
+
+ const num = new SchemaNumber();
+
+ let err;
+ try {
+ num.cast('');
+ } catch (e) {
+ err = e;
+ }
+ assert.ok(/CastError/.test(err));
+
+ num.cast('123'); // Should be ok
+ });
+
+ it('disable casting', function() {
+ SchemaNumber.cast(false);
+
+ const num = new SchemaNumber();
+
+ let err;
+ try {
+ num.cast('123');
+ } catch (e) {
+ err = e;
+ }
+ assert.ok(/CastError/.test(err));
+
+ num.cast(123); // Should be ok
+ });
+ });
});