Update Node_modules

This commit is contained in:
2019-07-02 16:05:15 +02:00
parent 95a0ff3913
commit 9dab5e2dbc
3495 changed files with 39728 additions and 465154 deletions
+6 -3
View File
@@ -6,6 +6,7 @@
const AggregationCursor = require('./cursor/AggregationCursor');
const Query = require('./query');
const applyGlobalMaxTimeMS = require('./helpers/query/applyGlobalMaxTimeMS');
const util = require('util');
const utils = require('./utils');
const read = Query.prototype.read;
@@ -373,7 +374,7 @@ Aggregate.prototype.unwind = function() {
res.push({ $unwind: arg });
} else if (typeof arg === 'string') {
res.push({
$unwind: (arg && arg.charAt(0) === '$') ? arg : '$' + arg
$unwind: (arg && arg.startsWith('$')) ? arg : '$' + arg
});
} else {
throw new Error('Invalid arg "' + arg + '" to unwind(), ' +
@@ -459,7 +460,7 @@ Aggregate.prototype.sortByCount = function(arg) {
return this.append({ $sortByCount: arg });
} else if (typeof arg === 'string') {
return this.append({
$sortByCount: (arg && arg.charAt(0) === '$') ? arg : '$' + arg
$sortByCount: (arg && arg.startsWith('$')) ? arg : '$' + arg
});
} else {
throw new TypeError('Invalid arg "' + arg + '" to sortByCount(), ' +
@@ -510,7 +511,7 @@ Aggregate.prototype.graphLookup = function(options) {
const startWith = cloneOptions.startWith;
if (startWith && typeof startWith === 'string') {
cloneOptions.startWith = cloneOptions.startWith.charAt(0) === '$' ?
cloneOptions.startWith = cloneOptions.startWith.startsWith('$') ?
cloneOptions.startWith :
'$' + cloneOptions.startWith;
}
@@ -932,6 +933,8 @@ Aggregate.prototype.exec = function(callback) {
const pipeline = this._pipeline;
const collection = this._model.collection;
applyGlobalMaxTimeMS(this.options, model);
if (this.options && this.options.cursor) {
return new AggregationCursor(this);
}
+4 -4
View File
@@ -237,7 +237,7 @@ module.exports = function cast(schema, obj, options, context) {
'schema, strict mode is `true`, and upsert is `true`.');
} else if (options.strictQuery === 'throw') {
throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
'schema and strictQuery is true.');
'schema and strictQuery is \'throw\'.');
} else if (options.strictQuery) {
delete obj[path];
}
@@ -245,7 +245,7 @@ module.exports = function cast(schema, obj, options, context) {
continue;
} else if (val.constructor.name === 'Object') {
any$conditionals = Object.keys(val).some(function(k) {
return k.charAt(0) === '$' && k !== '$id' && k !== '$ref';
return k.startsWith('$') && k !== '$id' && k !== '$ref';
});
if (!any$conditionals) {
@@ -266,7 +266,7 @@ module.exports = function cast(schema, obj, options, context) {
if ($cond === '$not') {
if (nested && schematype && !schematype.caster) {
_keys = Object.keys(nested);
if (_keys.length && _keys[0].charAt(0) === '$') {
if (_keys.length && _keys[0].startsWith('$')) {
for (const key in nested) {
nested[key] = schematype.castForQueryWrapper({
$conditional: key,
@@ -337,4 +337,4 @@ function _cast(val, numbertype, context) {
}
}
}
}
}
+50 -11
View File
@@ -40,10 +40,9 @@ const noPasswordAuthMechanisms = [
* @event `disconnecting`: Emitted when `connection.close()` was executed.
* @event `disconnected`: Emitted after getting disconnected from the db.
* @event `close`: Emitted after we `disconnected` and `onClose` executed on all of this connections models.
* @event `reconnected`: Emitted after we `connected` and subsequently `disconnected`, followed by successfully another successfull connection.
* @event `reconnected`: Emitted after we `connected` and subsequently `disconnected`, followed by successfully another successful connection.
* @event `error`: Emitted when an error occurs on this connection.
* @event `fullsetup`: Emitted in a replica-set scenario, when primary and at least one seconaries specified in the connection string are connected.
* @event `all`: Emitted in a replica-set scenario, when all nodes specified in the connection string are connected.
* @event `fullsetup`: Emitted after the driver has connected to primary and all secondaries if specified in the connection string.
* @api public
*/
@@ -61,9 +60,6 @@ function Connection(base) {
this._closeCalled = false;
this._hasOpened = false;
this.plugins = [];
this.$internalEmitter = new EventEmitter();
this.$internalEmitter.setMaxListeners(0);
}
/*!
@@ -123,6 +119,48 @@ Object.defineProperty(Connection.prototype, 'readyState', {
}
});
/**
* Gets the value of the option `key`. Equivalent to `conn.options[key]`
*
* ####Example:
*
* conn.get('test'); // returns the 'test' value
*
* @param {String} key
* @method get
* @api public
*/
Connection.prototype.get = function(key) {
return get(this.options, key);
};
/**
* Sets the value of the option `key`. Equivalent to `conn.options[key] = val`
*
* Supported options include:
*
* - `maxTimeMS`: Set [`maxTimeMS`](/docs/api.html#query_Query-maxTimeMS) for all queries on this connection.
* - `useFindAndModify`: Set to `false` to work around the [`findAndModify()` deprecation warning](/docs/deprecations.html#-findandmodify-)
*
* ####Example:
*
* conn.set('test', 'foo');
* conn.get('test'); // 'foo'
* conn.options.test; // 'foo'
*
* @param {String} key
* @param {Any} val
* @method set
* @api public
*/
Connection.prototype.set = function(key, val) {
this.options = this.options || {};
this.options[key] = val;
return val;
};
/**
* A hash of the collections associated with this connection
*
@@ -622,7 +660,6 @@ Connection.prototype.openUri = function(uri, options, callback) {
catch(err => {
if (this.listeners('error').length > 0) {
process.nextTick(() => this.emit('error', err));
return;
}
throw err;
});
@@ -689,11 +726,11 @@ Connection.prototype._close = function(force, callback) {
this._closeCalled = true;
switch (this.readyState) {
case 0: // disconnected
case STATES.disconnected:
callback();
break;
case 1: // connected
case STATES.connected:
this.readyState = STATES.disconnecting;
this.doClose(force, function(err) {
if (err) {
@@ -704,13 +741,13 @@ Connection.prototype._close = function(force, callback) {
});
break;
case 2: // connecting
case STATES.connecting:
this.once('open', function() {
_this.close(callback);
});
break;
case 3: // disconnecting
case STATES.disconnecting:
this.once('close', function() {
callback();
});
@@ -1011,6 +1048,8 @@ Connection.prototype.optionsProvideAuthenticationData = function(options) {
* @method useDb
* @memberOf Connection
* @param {String} name The database name
* @param {Object} [options]
* @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object.
* @return {Connection} New Connection Object
* @api public
*/
+63 -29
View File
@@ -34,7 +34,9 @@ const isMongooseObject = utils.isMongooseObject;
const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
const documentArrayParent = require('./helpers/symbols').documentArrayParent;
const documentSchemaSymbol = require('./helpers/symbols').documentSchemaSymbol;
const getSymbol = require('./helpers/symbols').getSymbol;
const populateModelSymbol = require('./helpers/symbols').populateModelSymbol;
let DocumentArray;
let MongooseArray;
@@ -718,6 +720,36 @@ Document.prototype.$session = function $session(session) {
return session;
};
/**
* Overwrite all values in this document with the values of `obj`, except
* for immutable properties. Behaves similarly to `set()`, except for it
* unsets all properties that aren't in `obj`.
*
* @param {Object} obj the object to overwrite this document with
* @method overwrite
* @name overwrite
* @memberOf Document
* @instance
* @api public
*/
Document.prototype.overwrite = function overwrite(obj) {
const keys = Array.from(new Set(Object.keys(this._doc).concat(Object.keys(obj))));
for (const key of keys) {
if (key === '_id') {
continue;
}
// Explicitly skip version key
if (this.schema.options.versionKey && key === this.schema.options.versionKey) {
continue;
}
this.$set(key, obj[key]);
}
return this;
};
/**
* Alias for `set()`, used internally to avoid conflicts
*
@@ -812,7 +844,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
}
if (typeof path[key] === 'object' &&
!Array.isArray(path[key]) &&
!utils.isNativeObject(path[key]) &&
path[key] != null &&
pathtype !== 'virtual' &&
pathtype !== 'real' &&
@@ -871,14 +903,14 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (pathType === 'nested' && val) {
if (typeof val === 'object' && val != null) {
if (!merge) {
this.setValue(path, null);
this.$__setValue(path, null);
cleanModifiedSubpaths(this, path);
} else {
return this.$set(val, path, constructing);
}
const keys = Object.keys(val);
this.setValue(path, {});
this.$__setValue(path, {});
for (const key of keys) {
this.$set(path + '.' + key, val[key], constructing);
}
@@ -958,7 +990,7 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (!this.isSelected(curPath)) {
this.unmarkModified(curPath);
}
cur = this.getValue(curPath);
cur = this.$__getValue(curPath);
}
}
@@ -986,12 +1018,12 @@ Document.prototype.$set = function $set(path, val, type, options) {
// if this doc is being constructed we should not trigger getters
const priorVal = (() => {
if (this.$__.$options.priorDoc != null) {
return this.$__.$options.priorDoc.getValue(path);
return this.$__.$options.priorDoc.$__getValue(path);
}
if (constructing) {
return void 0;
}
return this.getValue(path);
return this.$__getValue(path);
})();
if (!schema) {
@@ -1034,9 +1066,9 @@ Document.prototype.$set = function $set(path, val, type, options) {
if (refMatches && val instanceof Document) {
if (this.ownerDocument) {
this.ownerDocument().populated(this.$__fullPath(path),
val._id, {model: val.constructor});
val._id, { [populateModelSymbol]: val.constructor });
} else {
this.populated(path, val._id, {model: val.constructor});
this.populated(path, val._id, { [populateModelSymbol]: val.constructor });
}
didPopulate = true;
}
@@ -1052,11 +1084,11 @@ Document.prototype.$set = function $set(path, val, type, options) {
val[0].constructor.modelName &&
(schema.options[this.schema.options.typeKey][0].ref === val[0].constructor.baseModelName || schema.options[this.schema.options.typeKey][0].ref === val[0].constructor.modelName)) {
if (this.ownerDocument) {
popOpts = { model: val[0].constructor };
popOpts = { [populateModelSymbol]: val[0].constructor };
this.ownerDocument().populated(this.$__fullPath(path),
val.map(function(v) { return v._id; }), popOpts);
} else {
popOpts = { model: val[0].constructor };
popOpts = { [populateModelSymbol]: val[0].constructor };
this.populated(path, val.map(function(v) { return v._id; }), popOpts);
}
didPopulate = true;
@@ -1220,7 +1252,7 @@ Document.prototype.$__set = function(pathToMark, path, constructing, parts, sche
// 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) {
if (modifiedPath.indexOf(path + '.') === 0) {
if (modifiedPath.startsWith(path + '.')) {
_this.$__.activePaths.ignore(modifiedPath);
}
});
@@ -1270,7 +1302,7 @@ Document.prototype.$__set = function(pathToMark, path, constructing, parts, sche
* @api private
*/
Document.prototype.getValue = function(path) {
Document.prototype.$__getValue = function(path) {
return utils.getValue(path, this._doc);
};
@@ -1282,7 +1314,7 @@ Document.prototype.getValue = function(path) {
* @api private
*/
Document.prototype.setValue = function(path, val) {
Document.prototype.$__setValue = function(path, val) {
utils.setValue(path, val, this._doc);
return this;
};
@@ -1624,7 +1656,7 @@ Document.prototype.isModified = function(paths, modifiedPaths) {
});
return isModifiedChild || paths.some(function(path) {
return directModifiedPaths.some(function(mod) {
return mod === path || path.indexOf(mod + '.') === 0;
return mod === path || path.startsWith(mod + '.');
});
});
}
@@ -1772,11 +1804,11 @@ Document.prototype.isSelected = function isSelected(path) {
continue;
}
if (cur.indexOf(pathDot) === 0) {
if (cur.startsWith(pathDot)) {
return inclusive || cur !== pathDot;
}
if (pathDot.indexOf(cur + '.') === 0) {
if (pathDot.startsWith(cur + '.')) {
return inclusive;
}
}
@@ -1954,7 +1986,7 @@ function _getPathsToValidate(doc) {
continue;
}
const val = doc.getValue(path);
const val = doc.$__getValue(path);
if (val) {
const numElements = val.length;
for (let j = 0; j < numElements; ++j) {
@@ -1968,7 +2000,7 @@ function _getPathsToValidate(doc) {
for (i = 0; i < len; ++i) {
const pathToCheck = paths[i];
if (doc.schema.nested[pathToCheck]) {
let _v = doc.getValue(pathToCheck);
let _v = doc.$__getValue(pathToCheck);
if (isMongooseObject(_v)) {
_v = _v.toObject({ transform: false });
}
@@ -1988,7 +2020,7 @@ function _getPathsToValidate(doc) {
continue;
}
const val = doc.getValue(path);
const val = doc.$__getValue(path);
if (val == null) {
continue;
}
@@ -2093,7 +2125,7 @@ Document.prototype.$__validate = function(options, callback) {
return;
}
const val = _this.getValue(path);
const val = _this.$__getValue(path);
const scope = path in _this.$__.pathsToScopes ?
_this.$__.pathsToScopes[path] :
_this;
@@ -2191,7 +2223,7 @@ Document.prototype.validateSync = function(pathsToValidate, options) {
return;
}
const val = _this.getValue(path);
const val = _this.$__getValue(path);
const err = p.doValidateSync(val, _this, {
skipSchemaValidators: skipSchemaValidators[path],
path: path
@@ -2364,7 +2396,7 @@ Document.prototype.$__reset = function reset() {
this.$__.activePaths
.map('init', 'modify', function(i) {
return _this.getValue(i);
return _this.$__getValue(i);
})
.filter(function(val) {
return val && val instanceof Array && val.isMongooseDocumentArray && val.length;
@@ -2386,7 +2418,7 @@ Document.prototype.$__reset = function reset() {
this.$__.activePaths.
map('init', 'modify', function(i) {
return _this.getValue(i);
return _this.$__getValue(i);
}).
filter(function(val) {
return val && val.$isSingleNested;
@@ -2433,7 +2465,7 @@ Document.prototype.$__dirty = function() {
let all = this.$__.activePaths.map('modify', function(path) {
return {
path: path,
value: _this.getValue(path),
value: _this.$__getValue(path),
schema: _this.$__path(path)
};
});
@@ -2441,12 +2473,12 @@ Document.prototype.$__dirty = function() {
// gh-2558: if we had to set a default and the value is not undefined,
// we have to save as well
all = all.concat(this.$__.activePaths.map('default', function(path) {
if (path === '_id' || _this.getValue(path) == null) {
if (path === '_id' || _this.$__getValue(path) == null) {
return;
}
return {
path: path,
value: _this.getValue(path),
value: _this.$__getValue(path),
schema: _this.$__path(path)
};
}));
@@ -2506,6 +2538,7 @@ Document.prototype.$__setSchema = function(schema) {
}
this.schema = schema;
this[documentSchemaSymbol] = schema;
};
@@ -2524,7 +2557,7 @@ Document.prototype.$__getArrayPathsToValidate = function() {
// validate all document arrays.
return this.$__.activePaths
.map('init', 'modify', function(i) {
return this.getValue(i);
return this.$__getValue(i);
}.bind(this))
.filter(function(val) {
return val && val instanceof Array && val.isMongooseDocumentArray && val.length;
@@ -2553,7 +2586,7 @@ Document.prototype.$__getAllSubdocs = function() {
function docReducer(doc, seed, path) {
let val = doc;
if (path) {
if (doc instanceof Document && doc.schema.paths[path]) {
if (doc instanceof Document && doc[documentSchemaSymbol].paths[path]) {
val = doc._doc[path];
} else {
val = doc[path];
@@ -2852,7 +2885,7 @@ Document.prototype.$toObject = function(options, json) {
*
* Transforms are applied _only to the document and are not applied to sub-documents_.
*
* Transforms, like all of these options, are also available for `toJSON`.
* Transforms, like all of these options, are also available for `toJSON`. See [this guide to `JSON.stringify()`](https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html) to learn why `toJSON()` and `toObject()` are separate functions.
*
* See [schema options](/docs/guide.html#toObject) for some more details.
*
@@ -3014,6 +3047,7 @@ function applyGetters(self, json, type, options) {
* @param {Object} options
* @return {Object}
* @see Document#toObject #document_Document-toObject
* @see JSON.stringify() in JavaScript https://thecodebarbarian.com/the-80-20-guide-to-json-stringify-in-javascript.html
* @api public
* @memberOf Document
* @instance
+4 -1
View File
@@ -35,9 +35,12 @@ NativeConnection.prototype.__proto__ = MongooseConnection.prototype;
/**
* Switches to a different database using the same connection pool.
*
* Returns a new connection object, with the new db.
* Returns a new connection object, with the new db. If you set the `useCache`
* option, `useDb()` will cache connections by `name`.
*
* @param {String} name The database name
* @param {Object} [options]
* @param {Boolean} [options.useCache=false] If true, cache results so calling `useDb()` multiple times with the same name only creates 1 connection object.
* @return {Connection} New Connection Object
* @api public
*/
+1 -1
View File
@@ -19,7 +19,7 @@ const util = require('util');
function CastError(type, value, path, reason) {
let stringValue = util.inspect(value);
stringValue = stringValue.replace(/^'/, '"').replace(/'$/, '"');
if (stringValue.charAt(0) !== '"') {
if (!stringValue.startsWith('"')) {
stringValue = '"' + stringValue + '"';
}
MongooseError.call(this, 'Cast to ' + type + ' failed for value ' +
+62
View File
@@ -1,7 +1,49 @@
'use strict';
/**
* MongooseError constructor. MongooseError is the base class for all
* Mongoose-specific errors.
*
* ####Example:
* const Model = mongoose.model('Test', new Schema({ answer: Number }));
* const doc = new Model({ answer: 'not a number' });
* const err = doc.validateSync();
*
* err instanceof mongoose.Error; // true
*
* @constructor Error
* @param {String} msg Error message
* @inherits Error https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error
*/
const MongooseError = require('./mongooseError');
/**
* The name of the error. The name uniquely identifies this Mongoose error. The
* possible values are:
*
* - `MongooseError`: general Mongoose error
* - `CastError`: Mongoose could not convert a value to the type defined in the schema path. May be in a `ValidationError` class' `errors` property.
* - `DisconnectedError`: This [connection](connections.html) timed out in trying to reconnect to MongoDB and will not successfully reconnect to MongoDB unless you explicitly reconnect.
* - `DivergentArrayError`: You attempted to `save()` an array that was modified after you loaded it with a `$elemMatch` or similar projection
* - `MissingSchemaError`: You tried to access a model with [`mongoose.model()`](api.html#mongoose_Mongoose-model) that was not defined
* - `DocumentNotFoundError`: The document you tried to [`save()`](api.html#document_Document-save) was not found
* - `ValidatorError`: error from an individual schema path's validator
* - `ValidationError`: error returned from [`validate()`](api.html#document_Document-validate) or [`validateSync()`](api.html#document_Document-validateSync). Contains zero or more `ValidatorError` instances in `.errors` property.
* - `MissingSchemaError`: You called `mongoose.Document()` without a schema
* - `ObjectExpectedError`: Thrown when you set a nested path to a non-object value with [strict mode set](guide.html#strict).
* - `ObjectParameterError`: Thrown when you pass a non-object value to a function which expects an object as a paramter
* - `OverwriteModelError`: Thrown when you call [`mongoose.model()`](api.html#mongoose_Mongoose-model) to re-define a model that was already defined.
* - `ParallelSaveError`: Thrown when you call [`save()`](api.html#model_Model-save) on a document when the same document instance is already saving.
* - `StrictModeError`: Thrown when you set a path that isn't the schema and [strict mode](guide.html#strict) is set to `throw`.
* - `VersionError`: Thrown when the [document is out of sync](guide.html#versionKey)
*
* @api public
* @property {String} name
* @memberOf Error
* @instance
*/
/*!
* Module exports.
*/
@@ -13,6 +55,8 @@ module.exports = exports = MongooseError;
*
* @see Error.messages #error_messages_MongooseError-messages
* @api public
* @memberOf Error
* @static messages
*/
MongooseError.messages = require('./messages');
@@ -28,6 +72,8 @@ MongooseError.Messages = MongooseError.messages;
* the document.
*
* @api public
* @memberOf Error
* @static DocumentNotFoundError
*/
MongooseError.DocumentNotFoundError = require('./notFound');
@@ -37,6 +83,8 @@ MongooseError.DocumentNotFoundError = require('./notFound');
* cast a value.
*
* @api public
* @memberOf Error
* @static CastError
*/
MongooseError.CastError = require('./cast');
@@ -47,6 +95,8 @@ MongooseError.CastError = require('./cast');
* instances of CastError or ValidationError.
*
* @api public
* @memberOf Error
* @static ValidationError
*/
MongooseError.ValidationError = require('./validation');
@@ -55,6 +105,8 @@ MongooseError.ValidationError = require('./validation');
* A `ValidationError` has a hash of `errors` that contain individual `ValidatorError` instances
*
* @api public
* @memberOf Error
* @static ValidatorError
*/
MongooseError.ValidatorError = require('./validator');
@@ -65,6 +117,8 @@ MongooseError.ValidatorError = require('./validator');
* the [`versionKey` option](/docs/guide.html#versionKey) for more information.
*
* @api public
* @memberOf Error
* @static VersionError
*/
MongooseError.VersionError = require('./version');
@@ -75,6 +129,8 @@ MongooseError.VersionError = require('./version');
* information.
*
* @api public
* @memberOf Error
* @static ParallelSaveError
*/
MongooseError.ParallelSaveError = require('./parallelSave');
@@ -84,6 +140,8 @@ MongooseError.ParallelSaveError = require('./parallelSave');
* See [the FAQ about `OverwriteModelError`](/docs/faq.html#overwrite-model-error).
*
* @api public
* @memberOf Error
* @static OverwriteModelError
*/
MongooseError.OverwriteModelError = require('./overwriteModel');
@@ -92,6 +150,8 @@ MongooseError.OverwriteModelError = require('./overwriteModel');
* Thrown when you try to access a model that has not been registered yet
*
* @api public
* @memberOf Error
* @static MissingSchemaError
*/
MongooseError.MissingSchemaError = require('./missingSchema');
@@ -101,6 +161,8 @@ MongooseError.MissingSchemaError = require('./missingSchema');
* and then modified the array in an unsafe way.
*
* @api public
* @memberOf Error
* @static DivergentArrayError
*/
MongooseError.DivergentArrayError = require('./divergentArray');
+2 -32
View File
@@ -1,11 +1,7 @@
'use strict';
/**
* MongooseError constructor. MongooseError is the base class for all
* Mongoose-specific errors.
*
* @param {String} msg Error message
* @inherits Error https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error
/*!
* ignore
*/
function MongooseError(msg) {
@@ -26,30 +22,4 @@ function MongooseError(msg) {
MongooseError.prototype = Object.create(Error.prototype);
MongooseError.prototype.constructor = Error;
/**
* The name of the error. The name uniquely identifies this Mongoose error. The
* possible values are:
*
* - `MongooseError`: general Mongoose error
* - `CastError`: Mongoose could not convert a value to the type defined in the schema path. May be in a `ValidationError` class' `errors` property.
* - `DisconnectedError`: This [connection](connections.html) timed out in trying to reconnect to MongoDB and will not successfully reconnect to MongoDB unless you explicitly reconnect.
* - `DivergentArrayError`: You attempted to `save()` an array that was modified after you loaded it with a `$elemMatch` or similar projection
* - `MissingSchemaError`: You tried to access a model with [`mongoose.model()`](api.html#mongoose_Mongoose-model) that was not defined
* - `DocumentNotFoundError`: The document you tried to [`save()`](api.html#document_Document-save) was not found
* - `ValidatorError`: error from an individual schema path's validator
* - `ValidationError`: error returned from [`validate()`](api.html#document_Document-validate) or [`validateSync()`](api.html#document_Document-validateSync). Contains zero or more `ValidatorError` instances in `.errors` property.
* - `MissingSchemaError`: You called `mongoose.Document()` without a schema
* - `ObjectExpectedError`: Thrown when you set a nested path to a non-object value with [strict mode set](guide.html#strict).
* - `ObjectParameterError`: Thrown when you pass a non-object value to a function which expects an object as a paramter
* - `OverwriteModelError`: Thrown when you call [`mongoose.model()`](api.html#mongoose_Mongoose-model) to re-define a model that was already defined.
* - `ParallelSaveError`: Thrown when you call [`save()`](api.html#model_Model-save) on a document when the same document instance is already saving.
* - `StrictModeError`: Thrown when you set a path that isn't the schema and [strict mode](guide.html#strict) is set to `throw`.
* - `VersionError`: Thrown when the [document is out of sync](guide.html#versionKey)
*
* @api public
* @property {String} name
* @memberOf MongooseError
* @instance
*/
module.exports = MongooseError;
+6 -3
View File
@@ -13,20 +13,23 @@ const util = require('util');
* @inherits MongooseError
*/
function DocumentNotFoundError(filter) {
function DocumentNotFoundError(filter, model, numAffected, result) {
let msg;
const messages = MongooseError.messages;
if (messages.DocumentNotFoundError != null) {
msg = typeof messages.DocumentNotFoundError === 'function' ?
messages.DocumentNotFoundError(filter) :
messages.DocumentNotFoundError(filter, model) :
messages.DocumentNotFoundError;
} else {
msg = 'No document found for query "' + util.inspect(filter) + '"';
msg = 'No document found for query "' + util.inspect(filter) +
'" on model "' + model + '"';
}
MongooseError.call(this, msg);
this.name = 'DocumentNotFoundError';
this.result = result;
this.numAffected = numAffected;
if (Error.captureStackTrace) {
Error.captureStackTrace(this);
} else {
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const getDiscriminatorByValue = require('../../queryhelpers').getDiscriminatorByValue;
/*!
* Find the correct constructor, taking into account discriminators
*/
module.exports = function getConstructor(Constructor, value) {
const discriminatorKey = Constructor.schema.options.discriminatorKey;
if (value != null &&
Constructor.discriminators &&
value[discriminatorKey] != null) {
if (Constructor.discriminators[value[discriminatorKey]]) {
Constructor = Constructor.discriminators[value[discriminatorKey]];
} else {
const constructorByValue = getDiscriminatorByValue(Constructor, value[discriminatorKey]);
if (constructorByValue) {
Constructor = constructorByValue;
}
}
}
return Constructor;
};
+1 -1
View File
@@ -19,7 +19,7 @@ module.exports = function cleanModifiedSubpaths(doc, path, options) {
continue;
}
}
if (modifiedPath.indexOf(path + '.') === 0) {
if (modifiedPath.startsWith(path + '.')) {
delete doc.$__.activePaths.states.modify[modifiedPath];
++deleted;
}
+8
View File
@@ -1,5 +1,6 @@
'use strict';
const documentSchemaSymbol = require('../../helpers/symbols').documentSchemaSymbol;
const get = require('../../helpers/get');
const getSymbol = require('../../helpers/symbols').getSymbol;
const utils = require('../../utils');
@@ -71,6 +72,13 @@ function defineKey(prop, subprops, prototype, prefix, keys, options) {
value: prototype.schema
});
Object.defineProperty(nested, documentSchemaSymbol, {
enumerable: false,
configurable: true,
writable: false,
value: prototype.schema
});
Object.defineProperty(nested, 'toObject', {
enumerable: false,
configurable: true,
+12 -2
View File
@@ -1,5 +1,6 @@
'use strict';
const middlewareFunctions = require('../query/applyQueryMiddleware').middlewareFunctions;
const utils = require('../../utils');
module.exports = function applyStaticHooks(model, hooks, statics) {
@@ -8,7 +9,16 @@ module.exports = function applyStaticHooks(model, hooks, statics) {
numCallbackParams: 1
};
hooks = hooks.filter(hook => hook.model !== false);
hooks = hooks.filter(hook => {
// If the custom static overwrites an existing query middleware, don't apply
// middleware to it by default. This avoids a potential backwards breaking
// change with plugins like `mongoose-delete` that use statics to overwrite
// built-in Mongoose functions.
if (middlewareFunctions.indexOf(hook.name) !== -1) {
return !!hook.model;
}
return hook.model !== false;
});
model.$__insertMany = hooks.createWrapper('insertMany',
model.$__insertMany, model, kareemOptions);
@@ -58,4 +68,4 @@ module.exports = function applyStaticHooks(model, hooks, statics) {
};
}
}
};
};
+26 -10
View File
@@ -38,15 +38,23 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
const key = model.schema.options.discriminatorKey;
const baseSchemaAddition = {};
baseSchemaAddition[key] = {
default: void 0,
select: true,
$skipDiscriminatorCheck: true
};
baseSchemaAddition[key][model.schema.options.typeKey] = String;
model.schema.add(baseSchemaAddition);
defineKey(key, null, model.prototype, null, [key], model.schema.options);
const existingPath = model.schema.path(key);
if (existingPath != null) {
if (!utils.hasUserDefinedProperty(existingPath.options, 'select')) {
existingPath.options.select = true;
}
existingPath.options.$skipDiscriminatorCheck = true;
} else {
const baseSchemaAddition = {};
baseSchemaAddition[key] = {
default: void 0,
select: true,
$skipDiscriminatorCheck: true
};
baseSchemaAddition[key][model.schema.options.typeKey] = String;
model.schema.add(baseSchemaAddition);
defineKey(key, null, model.prototype, null, [key], model.schema.options);
}
if (schema.path(key) && schema.path(key).options.$skipDiscriminatorCheck !== true) {
throw new Error('Discriminator "' + name +
@@ -61,6 +69,7 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
function merge(schema, baseSchema) {
// Retain original schema before merging base schema
schema._originalSchema = schema.clone();
schema._baseSchema = baseSchema;
if (baseSchema.paths._id &&
baseSchema.paths._id.options &&
!baseSchema.paths._id.options.auto) {
@@ -94,6 +103,11 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
delete schema.paths[conflictingPaths[i]];
}
// Rebuild schema models because schemas may have been merged re: #7884
schema.childSchemas.forEach(obj => {
obj.model.prototype.$__setSchema(obj.schema);
});
const obj = {};
obj[key] = {
default: value,
@@ -106,7 +120,9 @@ module.exports = function discriminator(model, name, schema, tiedValue, applyPlu
},
$skipDiscriminatorCheck: true
};
obj[key][schema.options.typeKey] = String;
obj[key][schema.options.typeKey] = existingPath ?
existingPath.instance :
String;
schema.add(obj);
schema.discriminatorMapping = {key: key, value: value, isRoot: false};
+7 -2
View File
@@ -90,6 +90,11 @@ module.exports = function assignVals(o) {
const parts = o.path.split('.');
let cur = docs[i];
for (let j = 0; j < parts.length - 1; ++j) {
// If we get to an array with a dotted path, like `arr.foo`, don't set
// `foo` on the array.
if (Array.isArray(cur) && !utils.isArrayIndex(parts[j])) {
break;
}
if (cur[parts[j]] == null) {
cur[parts[j]] = {};
}
@@ -190,7 +195,7 @@ function valueFilter(val, assignmentOpts, populateOptions) {
function maybeRemoveId(subdoc, assignmentOpts) {
if (assignmentOpts.excludeId) {
if (typeof subdoc.setValue === 'function') {
if (typeof subdoc.$__setValue === 'function') {
delete subdoc._doc._id;
} else {
delete subdoc._id;
@@ -212,4 +217,4 @@ function isPopulatedObject(obj) {
obj.$isMongooseMap ||
obj.$__ != null ||
leanPopulateMap.has(obj);
}
}
+6 -4
View File
@@ -9,6 +9,8 @@ const get = require('../get');
const leanPopulateMap = require('./leanPopulateMap');
const mpath = require('mpath');
const populateModelSymbol = require('../symbols').populateModelSymbol;
/*!
* @param {Schema} schema
* @param {Object} doc POJO
@@ -138,18 +140,18 @@ module.exports = function getSchemaTypes(schema, doc, path) {
const fullPath = nestedPath.concat([trypath]).join('.');
if (topLevelDoc.$__ && topLevelDoc.populated(fullPath) && p < parts.length) {
const schema = get(doc.$__.populated[fullPath], 'options.model.schema');
if (schema != null) {
const model = doc.$__.populated[fullPath].options[populateModelSymbol];
if (model != null) {
const ret = search(
parts.slice(p),
schema,
model.schema,
subdoc ? mpath.get(trypath, subdoc) : null,
nestedPath.concat(parts.slice(0, p))
);
if (ret) {
ret.$isUnderneathDocArray = ret.$isUnderneathDocArray ||
!schema.$isSingleNested;
!model.schema.$isSingleNested;
}
return ret;
+1 -1
View File
@@ -20,7 +20,7 @@ module.exports = function isInclusive(projection) {
for (let i = 0; i < numProps; ++i) {
const prop = props[i];
// Plus paths can't define the projection (see gh-7050)
if (prop.charAt(0) === '+') {
if (prop.startsWith('+')) {
continue;
}
// If field is truthy (1, true, etc.) and not an object, then this
+15
View File
@@ -0,0 +1,15 @@
'use strict';
const utils = require('../../utils');
module.exports = function applyGlobalMaxTimeMS(options, model) {
if (utils.hasUserDefinedProperty(options, 'maxTimeMS')) {
return;
}
if (utils.hasUserDefinedProperty(model.db.options, 'maxTimeMS')) {
options.maxTimeMS = model.db.options.maxTimeMS;
} else if (utils.hasUserDefinedProperty(model.base.options, 'maxTimeMS')) {
options.maxTimeMS = model.base.options.maxTimeMS;
}
};
+3 -3
View File
@@ -3,7 +3,7 @@
module.exports = function castFilterPath(query, schematype, val) {
const ctx = query;
const any$conditionals = Object.keys(val).some(function(k) {
return k.charAt(0) === '$' && k !== '$id' && k !== '$ref';
return k.startsWith('$') && k !== '$id' && k !== '$ref';
});
if (!any$conditionals) {
@@ -24,7 +24,7 @@ module.exports = function castFilterPath(query, schematype, val) {
if ($cond === '$not') {
if (nested && schematype && !schematype.caster) {
const _keys = Object.keys(nested);
if (_keys.length && _keys[0].charAt(0) === '$') {
if (_keys.length && _keys[0].startsWith('$')) {
for (const key in nested) {
nested[key] = schematype.castForQueryWrapper({
$conditional: key,
@@ -52,4 +52,4 @@ module.exports = function castFilterPath(query, schematype, val) {
}
return val;
};
};
+19 -2
View File
@@ -5,6 +5,7 @@ const StrictModeError = require('../../error/strict');
const ValidationError = require('../../error/validation');
const castNumber = require('../../cast/number');
const getEmbeddedDiscriminatorPath = require('./getEmbeddedDiscriminatorPath');
const handleImmutable = require('./handleImmutable');
const utils = require('../../utils');
/*!
@@ -70,7 +71,7 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
while (i--) {
const op = ops[i];
val = ret[op];
hasDollarKey = hasDollarKey || op.charAt(0) === '$';
hasDollarKey = hasDollarKey || op.startsWith('$');
if (val &&
typeof val === 'object' &&
@@ -90,6 +91,10 @@ module.exports = function castUpdate(schema, obj, options, context, filter) {
+ 'Expected an object, received ' + typeof val;
throw new Error(msg);
}
if (op.startsWith('$') && Object.keys(val).length === 0) {
delete ret[op];
}
}
return hasKeys && ret;
@@ -137,6 +142,11 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
if (val && val.constructor.name === 'Object') {
// watch for embedded doc schemas
schematype = schema._getSchema(prefix + key);
if (handleImmutable(schematype, strict, obj, key, prefix + key)) {
continue;
}
if (schematype && schematype.caster && op in castOps) {
// embedded doc schema
if ('$each' in val) {
@@ -218,6 +228,13 @@ function walkUpdatePath(schema, obj, op, options, context, filter, pref) {
const checkPath = (key === '$each' || key === '$or' || key === '$and' || key === '$in') ?
pref : prefix + key;
schematype = schema._getSchema(checkPath);
// You can use `$setOnInsert` with immutable keys
if (op !== '$setOnInsert' &&
handleImmutable(schematype, strict, obj, key, prefix + key)) {
continue;
}
let pathDetails = schema._getPathType(checkPath);
// If no schema type, check for embedded discriminators
@@ -386,7 +403,7 @@ function castUpdateVal(schema, val, op, $conditional, context, path) {
++arrayDepth;
}
let tmp = schema.cast(Array.isArray(val) ? val : [val]);
let tmp = schema.applySetters(Array.isArray(val) ? val : [val], context);
for (let i = 0; i < additionalNesting; ++i) {
tmp = tmp[0];
+18
View File
@@ -0,0 +1,18 @@
'use strict';
const StrictModeError = require('../../error/strict');
module.exports = function handleImmutable(schematype, strict, obj, key, fullPath) {
if (schematype == null || !schematype.options.immutable) {
return false;
}
if (strict === false) {
return false;
}
if (strict === 'throw') {
throw new StrictModeError(null,
`Field ${fullPath} is immutable and strict = 'throw'`);
}
delete obj[key];
return true;
};
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = function(obj) {
const keys = Object.keys(obj);
const len = keys.length;
for (let i = 0; i < len; ++i) {
if (keys[i].charAt(0) === '$') {
if (keys[i].startsWith('$')) {
return true;
}
}
+9 -7
View File
@@ -15,13 +15,15 @@ module.exports = function applyPlugins(schema, plugins, options, cacheKey) {
options = Object.assign({}, options);
delete options.skipTopLevel;
for (const path of Object.keys(schema.paths)) {
const type = schema.paths[path];
if (type.schema != null) {
applyPlugins(type.schema, plugins, options, cacheKey);
if (options.applyPluginsToChildSchemas !== false) {
for (const path of Object.keys(schema.paths)) {
const type = schema.paths[path];
if (type.schema != null) {
applyPlugins(type.schema, plugins, options, cacheKey);
// Recompile schema because plugins may have changed it, see gh-7572
type.caster.prototype.$__setSchema(type.schema);
// Recompile schema because plugins may have changed it, see gh-7572
type.caster.prototype.$__setSchema(type.schema);
}
}
}
@@ -40,4 +42,4 @@ module.exports = function applyPlugins(schema, plugins, options, cacheKey) {
applyPlugins(discriminatorSchema, plugins,
{ skipTopLevel: !applyPluginsToDiscriminators }, cacheKey);
}
};
};
+1 -1
View File
@@ -1,7 +1,7 @@
'use strict';
module.exports = function merge(s1, s2) {
s1.add(s2.obj);
s1.add(s2.obj || {});
s1.callQueue = s1.callQueue.concat(s2.callQueue);
s1.method(s2.methods);
+28
View File
@@ -0,0 +1,28 @@
'use strict';
/*!
* ignore
*/
module.exports = function(schematype) {
if (schematype.$immutable) {
schematype.$immutableSetter = createImmutableSetter(schematype.path);
schematype.set(schematype.$immutableSetter);
} else if (schematype.$immutableSetter) {
schematype.setters = schematype.setters.
filter(fn => fn !== schematype.$immutableSetter);
delete schematype.$immutableSetter;
}
};
function createImmutableSetter(path) {
return function immutableSetter(v) {
if (this.$__ == null) {
return v;
}
if (this.isNew) {
return v;
}
return this[path];
};
}
+6 -3
View File
@@ -29,7 +29,7 @@ module.exports = function(filter, schema, castedDoc, options) {
}
for (let i = 0; i < numKeys; ++i) {
if (keys[i].charAt(0) === '$') {
if (keys[i].startsWith('$')) {
modifiedPaths(castedDoc[keys[i]], '', modified);
hasDollarUpdate = true;
}
@@ -49,7 +49,7 @@ module.exports = function(filter, schema, castedDoc, options) {
const numConditionKeys = conditionKeys.length;
let hasDollarKey = false;
for (let j = 0; j < numConditionKeys; ++j) {
if (conditionKeys[j].charAt(0) === '$') {
if (conditionKeys[j].startsWith('$')) {
hasDollarKey = true;
break;
}
@@ -69,7 +69,10 @@ module.exports = function(filter, schema, castedDoc, options) {
}
schema.eachPath(function(path, schemaType) {
if (schemaType.$isSingleNested) {
// Skip single nested paths if underneath a map
const isUnderneathMap = schemaType.path.endsWith('.$*') ||
schemaType.path.indexOf('.$*.') !== -1;
if (schemaType.$isSingleNested && !isUnderneathMap) {
// Only handle nested schemas 1-level deep to avoid infinite
// recursion re: https://github.com/mongodb-js/mongoose-autopopulate/issues/11
schemaType.schema.eachPath(function(_path, _schemaType) {
+3 -1
View File
@@ -5,8 +5,10 @@ exports.arrayParentSymbol = Symbol('mongoose#Array#_parent');
exports.arrayPathSymbol = Symbol('mongoose#Array#_path');
exports.arraySchemaSymbol = Symbol('mongoose#Array#_schema');
exports.documentArrayParent = Symbol('mongoose:documentArrayParent');
exports.documentSchemaSymbol = Symbol('mongoose#Document#schema');
exports.getSymbol = Symbol('mongoose#Document#get');
exports.modelSymbol = Symbol('mongoose#Model');
exports.objectIdSymbol = Symbol('mongoose#ObjectId');
exports.populateModelSymbol = Symbol('mongoose.PopulateOptions#Model');
exports.schemaTypeSymbol = Symbol('mongoose#schemaType');
exports.validatorErrorSymbol = Symbol('mongoose:validatorError');
exports.validatorErrorSymbol = Symbol('mongoose:validatorError');
+2 -2
View File
@@ -21,7 +21,7 @@ function applyTimestampsToChildren(now, update, schema) {
let timestamps;
let path;
const hasDollarKey = keys.length && keys[0].charAt(0) === '$';
const hasDollarKey = keys.length && keys[0].startsWith('$');
if (hasDollarKey) {
if (update.$push) {
@@ -173,4 +173,4 @@ function applyTimestampsToSingleNested(subdoc, schematype, now) {
if (createdAt != null) {
subdoc[createdAt] = now;
}
}
}
+2 -2
View File
@@ -38,7 +38,7 @@ module.exports = function(query, schema, castedDoc, options) {
let i;
for (i = 0; i < numKeys; ++i) {
if (keys[i].charAt(0) === '$') {
if (keys[i].startsWith('$')) {
hasDollarUpdate = true;
if (keys[i] === '$push' || keys[i] === '$addToSet') {
_keys = Object.keys(castedDoc[keys[i]]);
@@ -63,7 +63,7 @@ module.exports = function(query, schema, castedDoc, options) {
key = keys[i];
// With `$pull` we might flatten `$in`. Skip stuff nested under `$in`
// for the rest of the logic, it will get handled later.
if (updatedPath.indexOf('$') !== -1) {
if (updatedPath.includes('$')) {
continue;
}
if (key === '$set' || key === '$setOnInsert' ||
+7 -1
View File
@@ -140,12 +140,14 @@ Mongoose.prototype.driver = require('./driver');
* - 'useNewUrlParser': false by default. Set to `true` to make all connections set the `useNewUrlParser` option by default
* - 'cloneSchemas': false by default. Set to `true` to `clone()` all schemas before compiling into a model.
* - 'applyPluginsToDiscriminators': false by default. Set to true to apply global plugins to discriminator schemas. This typically isn't necessary because plugins are applied to the base schema and discriminators copy all middleware, methods, statics, and properties from the base schema.
* - 'applyPluginsToChildSchemas': true by default. Set to false to skip applying global plugins to child schemas
* - 'objectIdGetter': true by default. Mongoose adds a getter to MongoDB ObjectId's called `_id` that returns `this` for convenience with populate. Set this to false to remove the getter.
* - 'runValidators': false by default. Set to true to enable [update validators](/docs/validation.html#update-validators) for all validators by default.
* - 'toObject': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toObject()`](/docs/api.html#document_Document-toObject)
* - 'toJSON': `{ transform: true, flattenDecimals: true }` by default. Overwrites default objects to [`toJSON()`](/docs/api.html#document_Document-toJSON), for determining how Mongoose documents get serialized by `JSON.stringify()`
* - 'strict': true by default, may be `false`, `true`, or `'throw'`. Sets the default strict mode for schemas.
* - 'selectPopulatedPaths': true by default. Set to false to opt out of Mongoose adding all fields that you `populate()` to your `select()`. The schema-level option `selectPopulatedPaths` overwrites this one.
* - 'maxTimeMS': If set, attaches [maxTimeMS](https://docs.mongodb.com/manual/reference/operator/meta/maxTimeMS/) to every query
*
* @param {String} key
* @param {String|Function|Boolean} value
@@ -286,6 +288,9 @@ Mongoose.prototype.createConnection = function(uri, options, callback) {
Mongoose.prototype.connect = function() {
const _mongoose = this instanceof Mongoose ? this : mongoose;
const conn = _mongoose.connection;
if (conn.readyState !== STATES.disconnected) {
throw new _mongoose.Error('You can not `mongoose.connect()` multiple times while connected.');
}
return conn.openUri(arguments[0], arguments[1], arguments[2]).then(() => _mongoose);
};
@@ -406,7 +411,6 @@ Mongoose.prototype.pluralize = function(fn) {
Mongoose.prototype.model = function(name, schema, collection, skipInit) {
const _mongoose = this instanceof Mongoose ? this : mongoose;
let model;
if (typeof name === 'function') {
model = name;
@@ -574,6 +578,8 @@ Mongoose.prototype._applyPlugins = function(schema, options) {
options = options || {};
options.applyPluginsToDiscriminators = get(this,
'options.applyPluginsToDiscriminators', false);
options.applyPluginsToChildSchemas = get(this,
'options.applyPluginsToChildSchemas', true);
applyPlugins(schema, this.plugins, options, '$globalPluginsApplied');
};
+185 -51
View File
@@ -52,6 +52,7 @@ const VERSION_ALL = VERSION_WHERE | VERSION_INC;
const arrayAtomicsSymbol = require('./helpers/symbols').arrayAtomicsSymbol;
const modelCollectionSymbol = Symbol.for('mongoose#Model#collection');
const modelSymbol = require('./helpers/symbols').modelSymbol;
const populateModelSymbol = require('./helpers/symbols').populateModelSymbol;
const schemaMixedSymbol = require('./schema/symbols').schemaMixedSymbol;
const subclassedSymbol = Symbol('mongoose#Model#subclassed');
@@ -78,7 +79,7 @@ const subclassedSymbol = Symbol('mongoose#Model#subclassed');
* const userFromDb = await UserModel.findOne({ name: 'Foo' });
*
* @param {Object} doc values for initial set
* @param [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projetion](./api.html#query_Query-select).
* @param [fields] optional object containing the fields that were selected in the query which returned this document. You do **not** need to set this parameter to ensure Mongoose handles your [query projection](./api.html#query_Query-select).
* @inherits Document http://mongoosejs.com/docs/api.html#document-js
* @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.
@@ -326,9 +327,10 @@ Model.prototype.$__handleSave = function(options, callback) {
Model.prototype.$__save = function(options, callback) {
this.$__handleSave(options, (error, result) => {
const hooks = this.schema.s.hooks;
if (error) {
return this.schema.s.hooks.execPost('save:error', this, [this], { error: error }, function(error) {
callback(error);
return hooks.execPost('save:error', this, [this], { error: error }, (error) => {
callback(error, this);
});
}
@@ -359,7 +361,7 @@ Model.prototype.$__save = function(options, callback) {
this.$__.version = undefined;
const key = this.schema.options.versionKey;
const version = this.getValue(key) || 0;
const version = this.$__getValue(key) || 0;
if (numAffected <= 0) {
// the update failed. pass an error back
@@ -370,14 +372,15 @@ Model.prototype.$__save = function(options, callback) {
// increment version if was successful
if (doIncrement) {
this.setValue(key, version + 1);
this.$__setValue(key, version + 1);
}
}
if (result != null && numAffected <= 0) {
error = new DocumentNotFoundError(result.$where);
return this.schema.s.hooks.execPost('save:error', this, [this], { error: error }, function(error) {
callback(error);
error = new DocumentNotFoundError(result.$where,
this.constructor.modelName, numAffected, result);
return hooks.execPost('save:error', this, [this], { error: error }, (error) => {
callback(error, this);
});
}
}
@@ -397,7 +400,7 @@ function generateVersionError(doc, modifiedPaths) {
if (!key) {
return null;
}
const version = doc.getValue(key) || 0;
const version = doc.$__getValue(key) || 0;
return new VersionError(doc, version, modifiedPaths);
}
@@ -785,7 +788,7 @@ Model.prototype.$__version = function(where, delta) {
if (where === true) {
// this is an insert
if (key) this.setValue(key, delta[key] = 0);
if (key) this.$__setValue(key, delta[key] = 0);
return;
}
@@ -801,7 +804,7 @@ Model.prototype.$__version = function(where, delta) {
// $push $addToSet don't need the where clause set
if (VERSION_WHERE === (VERSION_WHERE & this.$__.version)) {
const value = this.getValue(key);
const value = this.$__getValue(key);
if (value != null) where[key] = value;
}
@@ -998,6 +1001,45 @@ Model.prototype.model = function model(name) {
return this.db.model(name);
};
/**
* Returns true if at least one document exists in the database that matches
* the given `filter`, and false otherwise.
*
* Under the hood, `MyModel.exists({ answer: 42 })` is equivalent to
* `MyModel.findOne({ answer: 42 }).select({ _id: 1 }).lean().then(doc => !!doc)`
*
* ####Example:
* await Character.deleteMany({});
* await Character.create({ name: 'Jean-Luc Picard' });
*
* await Character.exists({ name: /picard/i }); // true
* await Character.exists({ name: /riker/i }); // false
*
* This function triggers the following middleware.
*
* - `findOne()`
*
* @param {Object} filter
* @param {Function} [callback] callback
* @return {Promise}
*/
Model.exists = function exists(filter, callback) {
const query = this.findOne(filter).select({ _id: 1 }).lean();
if (typeof callback === 'function') {
query.exec(function(err, doc) {
if (err != null) {
return callback(err);
}
callback(null, !!doc);
});
return;
}
return query.then(doc => !!doc);
};
/**
* Adds a discriminator type.
*
@@ -1471,6 +1513,9 @@ function _ensureIndexes(model, options, callback) {
model.emit('index-single-start', fields, options);
};
const baseSchema = model.schema._baseSchema;
const baseSchemaIndexes = baseSchema ? baseSchema.indexes() : [];
const create = function() {
if (options._automatic) {
if (model.schema.options.autoIndex === false ||
@@ -1484,6 +1529,10 @@ function _ensureIndexes(model, options, callback) {
return done();
}
if (baseSchemaIndexes.find(i => utils.deepEqual(i, index))) {
return create();
}
const indexFields = utils.clone(index[0]);
const indexOptions = utils.clone(index[1]);
@@ -1751,21 +1800,30 @@ Model.remove = function remove(conditions, callback) {
* Like `Model.remove()`, this function does **not** trigger `pre('remove')` or `post('remove')` hooks.
*
* @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.deleteOne = function deleteOne(conditions, callback) {
Model.deleteOne = function deleteOne(conditions, options, callback) {
if (typeof conditions === 'function') {
callback = conditions;
conditions = {};
options = null;
}
else if (typeof options === 'function') {
callback = options;
options = null;
}
// get the mongodb collection object
const mq = new this.Query(conditions, {}, this, this.collection);
mq.setOptions(options);
callback = this.$wrapCallback(callback);
if (callback) {
callback = this.$wrapCallback(callback);
}
return mq.deleteOne(callback);
};
@@ -2284,6 +2342,7 @@ Model.$where = function $where() {
* @param {Boolean} [options.omitUndefined=false] If true, delete any properties whose value is `undefined` when casting an update. In other words, if this is set, Mongoose will delete `baz` from the update in `Model.updateOne({}, { foo: 'bar', baz: undefined })` before sending the update to the server.
* @param {Function} [callback]
* @return {Query}
* @see Tutorial /docs/tutorials/findoneandupdate.html
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @api public
*/
@@ -3112,6 +3171,7 @@ Model.$__insertMany = function(arr, options, callback) {
return;
}
for (let i = 0; i < docAttributes.length; ++i) {
docAttributes[i].$__reset();
docAttributes[i].isNew = false;
docAttributes[i].emit('isNew', false);
docAttributes[i].constructor.emit('isNew', false);
@@ -3282,13 +3342,15 @@ Model.hydrate = function(obj) {
*
* ####Valid options:
*
* - `safe` (boolean) safe mode (defaults to value set in schema (true))
* - `upsert` (boolean) whether to create the doc if it doesn't match (false)
* - `multi` (boolean) whether multiple documents should be updated (false)
* - `strict` (boolean): overrides the [schema-level `strict` option](/docs/guide.html#strict) for this update
* - `upsert` (boolean): whether to create the doc if it doesn't match (false)
* - `writeConcern` (object): sets the [write concern](https://docs.mongodb.com/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
* - `omitUndefined` (boolean): If true, delete any properties whose value is `undefined` when casting an update. In other words, if this is set, Mongoose will delete `baz` from the update in `Model.updateOne({}, { foo: 'bar', baz: undefined })` before sending the update to the server.
* - `multi` (boolean): whether multiple documents should be updated (false)
* - `runValidators`: if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
* - `setDefaultsOnInsert`: if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/).
* - `strict` (boolean) overrides the `strict` option for this update
* - `overwrite` (boolean) disables update-only mode, allowing you to overwrite the doc (false)
* - `setDefaultsOnInsert` (boolean): if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/).
* - `timestamps` (boolean): If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
* - `overwrite` (boolean): disables update-only mode, allowing you to overwrite the doc (false)
*
* All `update` values are cast to their appropriate SchemaTypes before being sent.
*
@@ -3318,14 +3380,15 @@ Model.hydrate = function(obj) {
*
* ####Note:
*
* Although values are casted to their appropriate types when using update, the following are *not* applied:
* Mongoose casts values and runs setters when using update. The following
* features are **not** applied by default.
*
* - defaults
* - setters
* - validators
* - [defaults](/docs/defaults.html#the-setdefaultsoninsert-option)
* - [validators](/docs/validation.html#update-validators)
* - middleware
*
* If you need those features, use the traditional approach of first retrieving the document.
* If you need document middleware and fully-featured validation, load the
* document first and then use [`save()`](/docs/api.html#model_Model-save).
*
* Model.findOne({ name: 'borne' }, function (err, doc) {
* if (err) ..
@@ -3335,14 +3398,18 @@ Model.hydrate = function(obj) {
*
* @see strict http://mongoosejs.com/docs/guide.html#strict
* @see response http://docs.mongodb.org/v2.6/reference/command/update/#output
* @param {Object} conditions
* @param {Object} filter
* @param {Object} doc
* @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict)
* @param {Boolean} [options.upsert=false] if true, and no documents found, insert a new document
* @param {Object} [options.writeConcern=null] sets the [write concern](https://docs.mongodb.com/manual/reference/write-concern/) for replica sets. Overrides the [schema-level write concern](/docs/guide.html#writeConcern)
* @param {Boolean} [options.omitUndefined=false] If true, delete any properties whose value is `undefined` when casting an update. In other words, if this is set, Mongoose will delete `baz` from the update in `Model.updateOne({}, { foo: 'bar', baz: undefined })` before sending the update to the server.
* @param {Boolean} [options.multi=false] whether multiple documents should be updated or just the first one that matches `filter`.
* @param {Boolean} [options.runValidators=false] if true, runs [update validators](/docs/validation.html#update-validators) on this command. Update validators validate the update operation against the model's schema.
* @param {Boolean} [options.setDefaultsOnInsert=false] if this and `upsert` are true, mongoose will apply the [defaults](http://mongoosejs.com/docs/defaults.html) specified in the model's schema if a new document is created. This option only works on MongoDB >= 2.4 because it relies on [MongoDB's `$setOnInsert` operator](https://docs.mongodb.org/v2.4/reference/operator/update/setOnInsert/).
* @param {Boolean} [options.timestamps=null] If set to `false` and [schema-level timestamps](/docs/guide.html#timestamps) are enabled, skip timestamps for this update. Does nothing if schema-level timestamps are not set.
* @param {Boolean} [options.overwrite=false] By default, if you don't include any [update operators](https://docs.mongodb.com/manual/reference/operator/update/) in `doc`, Mongoose will wrap `doc` in `$set` for you. This prevents you from accidentally overwriting the document. This option tells Mongoose to skip adding `$set`.
* @param {Function} [callback] params are (error, writeOpResult)
* @param {Function} [callback]
* @return {Query}
@@ -3358,7 +3425,7 @@ Model.update = function update(conditions, doc, options, callback) {
/**
* Same as `update()`, except MongoDB will update _all_ documents that match
* `criteria` (as opposed to just the first one) regardless of the value of
* `filter` (as opposed to just the first one) regardless of the value of
* the `multi` option.
*
* **Note** updateMany will _not_ fire update middleware. Use `pre('updateMany')`
@@ -3373,7 +3440,7 @@ Model.update = function update(conditions, doc, options, callback) {
*
* - `updateMany()`
*
* @param {Object} conditions
* @param {Object} filter
* @param {Object} doc
* @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict)
@@ -3397,7 +3464,7 @@ Model.updateMany = function updateMany(conditions, doc, options, callback) {
* Same as `update()`, except it does not support the `multi` or `overwrite`
* options.
*
* - MongoDB will update _only_ the first document that matches `criteria` regardless of the value of the `multi` option.
* - MongoDB will update _only_ the first document that matches `filter` regardless of the value of the `multi` option.
* - Use `replaceOne()` if you want to overwrite an entire document rather than using atomic operators like `$set`.
*
* ####Example:
@@ -3409,7 +3476,7 @@ Model.updateMany = function updateMany(conditions, doc, options, callback) {
*
* - `updateOne()`
*
* @param {Object} conditions
* @param {Object} filter
* @param {Object} doc
* @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict)
@@ -3442,7 +3509,7 @@ Model.updateOne = function updateOne(conditions, doc, options, callback) {
*
* - `replaceOne()`
*
* @param {Object} conditions
* @param {Object} filter
* @param {Object} doc
* @param {Object} [options] optional see [`Query.prototype.setOptions()`](http://mongoosejs.com/docs/api.html#query_Query-setOptions)
* @param {Boolean|String} [options.strict] overwrites the schema's [strict mode option](http://mongoosejs.com/docs/guide.html#strict)
@@ -3836,6 +3903,7 @@ Model.geoSearch = function(conditions, options, callback) {
* @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 {Object|Function} [options.match=null] Add an additional filter to the populate query. Can be a filter object containing [MongoDB query syntax](https://docs.mongodb.com/manual/tutorial/query-documents/), or a function that returns a filter object.
* @param {Boolean} [options.skipInvalidIds=false] By default, Mongoose throws a cast error if `localField` and `foreignField` schemas don't line up. If you enable this option, Mongoose will instead filter out any `localField` properties that cannot be casted to `foreignField`'s schema type.
* @param {Function} [callback(err,doc)] Optional callback, executed upon completion. Receives `err` and the `doc(s)`.
* @return {Promise}
* @api public
@@ -3870,18 +3938,16 @@ Model.populate = function(docs, paths, callback) {
*/
function _populate(model, docs, paths, cache, callback) {
const length = paths.length;
let pending = paths.length;
if (pending === 0) {
if (length === 0) {
return callback(null, docs);
}
// each path has its own query options and must be executed separately
let i = pending;
let path;
while (i--) {
path = paths[i];
populate(model, docs, path, next);
for (let i = 0; i < length; ++i) {
populate(model, docs, paths[i], next);
}
function next(err) {
@@ -3957,7 +4023,9 @@ function populate(model, docs, options, callback) {
hasOne = true;
if (mod.foreignField.size === 1) {
const foreignField = Array.from(mod.foreignField)[0];
const foreignSchemaType = mod.model.schema.path(foreignField);
if (foreignField !== '_id' || !match['_id']) {
ids = _filterInvalidIds(ids, foreignSchemaType, mod.options.skipInvalidIds);
match[foreignField] = { $in: ids };
}
} else {
@@ -3970,6 +4038,8 @@ function populate(model, docs, options, callback) {
}
for (const foreignField of mod.foreignField) {
if (foreignField !== '_id' || !match['_id']) {
const foreignSchemaType = mod.model.schema.path(foreignField);
ids = _filterInvalidIds(ids, foreignSchemaType, mod.options.skipInvalidIds);
$or.push({ [foreignField]: { $in: ids } });
}
}
@@ -4146,6 +4216,25 @@ function populate(model, docs, options, callback) {
}
}
/*!
* Optionally filter out invalid ids that don't conform to foreign field's schema
* to avoid cast errors (gh-7706)
*/
function _filterInvalidIds(ids, foreignSchemaType, skipInvalidIds) {
if (!skipInvalidIds) {
return ids;
}
return ids.filter(id => {
try {
foreignSchemaType.cast(id);
return true;
} catch (err) {
return false;
}
});
}
/*!
* Format `mod.match` given that it may be an array that we need to $or if
* the client has multiple docs with match functions
@@ -4174,7 +4263,6 @@ function getModelsMapForPopulate(model, docs, options) {
let currentOptions;
let modelNames;
let modelName;
let discriminatorKey;
let modelForFindSchema;
const originalModel = options.model;
@@ -4252,6 +4340,13 @@ function getModelsMapForPopulate(model, docs, options) {
let justOne = null;
if ('justOne' in options) {
justOne = options.justOne;
} else if (virtual && virtual.options && virtual.options.refPath) {
const normalizedRefPath =
normalizeRefPath(virtual.options.refPath, doc, options.path);
justOne = !!virtual.options.justOne;
isVirtual = true;
const refValue = utils.getValue(normalizedRefPath, doc);
modelNames = Array.isArray(refValue) ? refValue : [refValue];
} else if (virtual && virtual.options && virtual.options.ref) {
let normalizedRef;
if (typeof virtual.options.ref === 'function') {
@@ -4330,10 +4425,14 @@ function getModelsMapForPopulate(model, docs, options) {
continue;
}
// `PopulateOptions#connection`: if the model is passed as a string, the
// connection matters because different connections have different models.
const connection = options.connection != null ? options.connection : model.db;
try {
Model = originalModel && originalModel[modelSymbol] ?
originalModel :
modelName[modelSymbol] ? modelName : model.db.model(modelName);
modelName[modelSymbol] ? modelName : connection.model(modelName);
} catch (error) {
return error;
}
@@ -4353,10 +4452,10 @@ function getModelsMapForPopulate(model, docs, options) {
currentOptions.options = utils.clone(virtual.options.options);
}
utils.merge(currentOptions, options);
if (schema && !discriminatorKey) {
currentOptions.model = Model;
}
options.model = Model;
// Used internally for checking what model was used to populate this
// path.
options[populateModelSymbol] = Model;
available[modelName] = {
model: Model,
@@ -4370,7 +4469,8 @@ function getModelsMapForPopulate(model, docs, options) {
justOne: justOne,
isVirtual: isVirtual,
virtual: virtual,
count: count
count: count,
[populateModelSymbol]: Model
};
map.push(available[modelName]);
} else {
@@ -4447,11 +4547,10 @@ function getModelsMapForPopulate(model, docs, options) {
let ref;
if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
ref = handleRefFunction(ref, doc);
modelNames = [ref];
} else if ((ref = get(virtual, 'options.ref')) != null) {
if (typeof ref === 'function') {
ref = ref.call(doc, doc);
}
ref = handleRefFunction(ref, doc);
// When referencing nested arrays, the ref should be an Array
// of modelNames.
@@ -4483,6 +4582,17 @@ function getModelsMapForPopulate(model, docs, options) {
return map;
}
/*!
* ignore
*/
function handleRefFunction(ref, doc) {
if (typeof ref === 'function' && !ref[modelSymbol]) {
return ref.call(doc, doc);
}
return ref;
}
/*!
* Retrieve the _id of `val` if a Document or Array of Documents.
*
@@ -4559,12 +4669,16 @@ Model.compile = function compile(name, schema, collectionName, connection, base)
}
const discriminatorKey = model.schema.options.discriminatorKey;
if (model.discriminators == null || doc == null || doc[discriminatorKey] == null) {
Model.call(this, doc, fields, skipId);
return;
}
// If discriminator key is set, use the discriminator instead (gh-7586)
if (model.discriminators != null &&
doc != null &&
doc[discriminatorKey] != null &&
model.discriminators[doc[discriminatorKey]] != null) {
return new model.discriminators[doc[discriminatorKey]](doc, fields, skipId);
const Discriminator = model.discriminators[doc[discriminatorKey]] ||
getDiscriminatorByValue(model, doc[discriminatorKey]);
if (Discriminator != null) {
return new Discriminator(doc, fields, skipId);
}
// Otherwise, just use the top-level model
@@ -4736,6 +4850,26 @@ Model.$wrapCallback = function(callback) {
};
};
/**
* Helper for console.log
*
* @api public
* @method inspect
* @instance
*/
Model.inspect = function() {
return `Model { ${this.modelName} }`;
};
if (util.inspect.custom) {
/*!
* Avoid Node deprecation warning DEP0079
*/
Model[util.inspect.custom] = Model.inspect;
}
/*!
* Module exports.
*/
+30
View File
@@ -0,0 +1,30 @@
'use strict';
const utils = require('../utils');
class PopulateOptions {
constructor(obj) {
this._docs = {};
if (obj == null) {
return;
}
obj = utils.clone(obj);
Object.assign(this, obj);
if (typeof obj.subPopulate === 'object') {
this.populate = obj.subPopulate;
}
}
}
/**
* The connection used to look up models by name. If not specified, Mongoose
* will default to using the connection associated with the model in
* `PopulateOptions#model`.
*
* @memberOf PopulateOptions
* @property {Connection} connection
* @api public
*/
module.exports = PopulateOptions;
+1 -1
View File
@@ -67,7 +67,7 @@ function storeShard() {
let val;
for (let i = 0; i < len; ++i) {
val = this.getValue(paths[i]);
val = this.$__getValue(paths[i]);
if (val == null) {
orig[paths[i]] = val;
} else if (utils.isMongooseObject(val)) {
+78 -22
View File
@@ -11,6 +11,7 @@ const MongooseError = require('./error/mongooseError');
const ObjectParameterError = require('./error/objectParameter');
const QueryCursor = require('./cursor/QueryCursor');
const ReadPreference = require('./driver').get().ReadPreference;
const applyGlobalMaxTimeMS = require('./helpers/query/applyGlobalMaxTimeMS');
const applyWriteConcern = require('./helpers/schema/applyWriteConcern');
const cast = require('./cast');
const castArrayFilters = require('./helpers/update/castArrayFilters');
@@ -643,7 +644,7 @@ Query.prototype.mod = function() {
* @memberOf Query
* @instance
* @param {String|Object|Function} path
* @param {Object|Function} criteria
* @param {Object|Function} filter
* @return {Query} this
* @see $elemMatch http://docs.mongodb.org/manual/reference/operator/elemMatch/
* @api public
@@ -1411,7 +1412,27 @@ Query.prototype.maxTimeMS = function(ms) {
};
/**
* Returns the current query conditions as a JSON object.
* Returns the current query filter (also known as conditions) as a POJO.
*
* ####Example:
*
* const query = new Query();
* query.find({ a: 1 }).where('b').gt(2);
* query.getFilter(); // { a: 1, b: { $gt: 2 } }
*
* @return {Object} current query filter
* @api public
*/
Query.prototype.getFilter = function() {
return this._conditions;
};
/**
* Returns the current query filter. Equivalent to `getFilter()`.
*
* You should use `getFilter()` instead of `getQuery()` where possible. `getQuery()`
* will likely be deprecated in a future release.
*
* ####Example:
*
@@ -1419,7 +1440,7 @@ Query.prototype.maxTimeMS = function(ms) {
* query.find({ a: 1 }).where('b').gt(2);
* query.getQuery(); // { a: 1, b: { $gt: 2 } }
*
* @return {Object} current query conditions
* @return {Object} current query filter
* @api public
*/
@@ -1803,6 +1824,8 @@ Query.prototype._find = wrapThunk(function(callback) {
const _this = this;
const userProvidedFields = _this._userProvidedFields || {};
applyGlobalMaxTimeMS(this.options, this.model);
// Separate options to pass down to `completeMany()` in case we need to
// set a session on the document
const completeManyOptions = Object.assign({}, {
@@ -2022,6 +2045,8 @@ Query.prototype._findOne = wrapThunk(function(callback) {
this._applyPaths();
this._fields = this._castFields(this._fields);
applyGlobalMaxTimeMS(this.options, this.model);
// don't pass in the conditions because we already merged them in
Query.base.findOne.call(this, {}, (err, doc) => {
if (err) {
@@ -2629,7 +2654,7 @@ Query.prototype._deleteOne = wrapThunk(function(callback) {
/**
* Declare and/or execute this query as a `deleteMany()` operation. Works like
* remove, except it deletes _every_ document that matches `criteria` in the
* remove, except it deletes _every_ document that matches `filter` in the
* collection, regardless of the value of `single`.
*
* This function does not trigger any middleware
@@ -2811,7 +2836,7 @@ function prepareDiscriminatorCriteria(query) {
* @method findOneAndUpdate
* @memberOf Query
* @instance
* @param {Object|Query} [query]
* @param {Object|Query} [filter]
* @param {Object} [doc]
* @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)
@@ -2820,6 +2845,7 @@ function prepareDiscriminatorCriteria(query) {
* @param {Object} [options.lean] if truthy, mongoose will return the document as a plain JavaScript object rather than a mongoose document. See [`Query.lean()`](http://mongoosejs.com/docs/api.html#query_Query-lean).
* @param {Boolean} [options.omitUndefined=false] If true, delete any properties whose value is `undefined` when casting an update. In other words, if this is set, Mongoose will delete `baz` from the update in `Model.updateOne({}, { foo: 'bar', baz: undefined })` before sending the update to the server.
* @param {Function} [callback] optional params are (error, doc), _unless_ `rawResult` is used, in which case params are (error, writeOpResult)
* @see Tutorial /docs/tutorials/findoneandupdate.html
* @see mongodb http://www.mongodb.org/display/DOCS/findAndModify+Command
* @see writeOpResult http://mongodb.github.io/node-mongodb-native/2.2/api/Collection.html#~WriteOpResult
* @return {Query} this
@@ -3181,6 +3207,9 @@ Query.prototype.findOneAndReplace = function(filter, replacement, options, callb
}
if (replacement != null) {
if (hasDollarKeys(replacement)) {
throw new Error('The replacement document must not contain atomic operators.');
}
this._mergeUpdate(replacement);
}
@@ -3214,6 +3243,9 @@ Query.prototype._findOneAndReplace = wrapThunk(function(callback) {
const options = this._optionsForExec();
let fields = null;
let castedDoc = new this.model(this._update, null, true);
this._applyPaths();
if (this._fields != null) {
options.projection = this._castFields(utils.clone(this._fields));
fields = options.projection;
@@ -3223,14 +3255,24 @@ Query.prototype._findOneAndReplace = wrapThunk(function(callback) {
}
}
this._collection.collection.findOneAndReplace(filter, options, (err, res) => {
if (err) {
castedDoc.validate(err => {
if (err != null) {
return callback(err);
}
const doc = res.value;
if (castedDoc.toBSON) {
castedDoc = castedDoc.toBSON();
}
return this._completeOne(doc, res, callback);
this._collection.collection.findOneAndReplace(filter, castedDoc, options, (err, res) => {
if (err) {
return callback(err);
}
const doc = res.value;
return this._completeOne(doc, res, callback);
});
});
});
@@ -3733,17 +3775,17 @@ Query.prototype._replaceOne = wrapThunk(function(callback) {
*
* API summary
*
* update(criteria, doc, options, cb) // executes
* update(criteria, doc, options)
* update(criteria, doc, cb) // executes
* update(criteria, doc)
* update(filter, doc, options, cb) // executes
* update(filter, doc, options)
* update(filter, doc, cb) // executes
* update(filter, doc)
* update(doc, cb) // executes
* update(doc)
* update(cb) // executes
* update(true) // executes
* update()
*
* @param {Object} [criteria]
* @param {Object} [filter]
* @param {Object} [doc] the update command
* @param {Object} [options]
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
@@ -3794,7 +3836,7 @@ Query.prototype.update = function(conditions, doc, options, callback) {
/**
* Declare and/or execute this query as an updateMany() operation. Same as
* `update()`, except MongoDB will update _all_ documents that match
* `criteria` (as opposed to just the first one) regardless of the value of
* `filter` (as opposed to just the first one) regardless of the value of
* the `multi` option.
*
* **Note** updateMany will _not_ fire update middleware. Use `pre('updateMany')`
@@ -3809,7 +3851,7 @@ Query.prototype.update = function(conditions, doc, options, callback) {
*
* - `updateMany()`
*
* @param {Object} [criteria]
* @param {Object} [filter]
* @param {Object} [doc] the update command
* @param {Object} [options]
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
@@ -3861,7 +3903,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
* Declare and/or execute this query as an updateOne() operation. Same as
* `update()`, except it does not support the `multi` or `overwrite` options.
*
* - MongoDB will update _only_ the first document that matches `criteria` regardless of the value of the `multi` option.
* - MongoDB will update _only_ the first document that matches `filter` regardless of the value of the `multi` option.
* - Use `replaceOne()` if you want to overwrite an entire document rather than using [atomic](https://docs.mongodb.com/manual/tutorial/model-data-for-atomic-operations/#pattern) operators like `$set`.
*
* **Note** updateOne will _not_ fire update middleware. Use `pre('updateOne')`
@@ -3876,7 +3918,7 @@ Query.prototype.updateMany = function(conditions, doc, options, callback) {
*
* - `updateOne()`
*
* @param {Object} [criteria]
* @param {Object} [filter]
* @param {Object} [doc] the update command
* @param {Object} [options]
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
@@ -3941,7 +3983,7 @@ Query.prototype.updateOne = function(conditions, doc, options, callback) {
*
* - `replaceOne()`
*
* @param {Object} [criteria]
* @param {Object} [filter]
* @param {Object} [doc] the update command
* @param {Object} [options]
* @param {Boolean} [options.multipleCastError] by default, mongoose only returns the first error that occurred in casting the query. Turn on this option to aggregate all the cast errors.
@@ -4156,7 +4198,7 @@ function _orFailError(err, query) {
}
if (err == null) {
err = new DocumentNotFoundError(query.getQuery());
err = new DocumentNotFoundError(query.getQuery(), query.model.modelName);
}
return err;
@@ -4316,7 +4358,20 @@ Query.prototype._castUpdate = function _castUpdate(obj, overwrite) {
useNestedStrict = this.options.useNestedStrict;
}
return castUpdate(this.schema, obj, {
let schema = this.schema;
const filter = this._conditions;
if (schema != null &&
utils.hasUserDefinedProperty(filter, schema.options.discriminatorKey) &&
typeof filter[schema.options.discriminatorKey] !== 'object' &&
schema.discriminators != null) {
const discriminatorValue = filter[schema.options.discriminatorKey];
const byValue = helpers.getDiscriminatorByValue(this.model, discriminatorValue);
schema = schema.discriminators[discriminatorValue] ||
(byValue && byValue.schema) ||
schema;
}
return castUpdate(schema, obj, {
overwrite: overwrite,
strict: strict,
omitUndefined,
@@ -4396,7 +4451,8 @@ function castDoc(query, overwrite) {
*/
Query.prototype.populate = function() {
if (arguments.length === 0) {
// Bail when given no truthy arguments
if (!Array.from(arguments).some(Boolean)) {
return this;
}
+1 -1
View File
@@ -245,7 +245,7 @@ exports.applyPaths = function applyPaths(fields, schema) {
}
// Any leftover plus paths must in the schema, so delete them (gh-7017)
for (const key of Object.keys(fields || {})) {
if (key.charAt(0) === '+') {
if (key.startsWith('+')) {
delete fields[key];
}
}
+33 -23
View File
@@ -52,11 +52,12 @@ let id = 0;
* - [collection](/docs/guide.html#collection): string - no default
* - [id](/docs/guide.html#id): bool - defaults to true
* - [_id](/docs/guide.html#_id): bool - defaults to true
* - `minimize`: bool - controls [document#toObject](#document_Document-toObject) behavior when called manually - defaults to true
* - [minimize](/docs/guide.html#minimize): bool - controls [document#toObject](#document_Document-toObject) behavior when called manually - defaults to true
* - [read](/docs/guide.html#read): string
* - [writeConcern](/docs/guide.html#writeConcern): object - defaults to null, use to override [the MongoDB server's default write concern settings](https://docs.mongodb.com/manual/reference/write-concern/)
* - [shardKey](/docs/guide.html#shardKey): object - defaults to `null`
* - [strict](/docs/guide.html#strict): bool - defaults to true
* - [strictQuery](/docs/guide.html#strictQuery): bool - defaults to false
* - [toJSON](/docs/guide.html#toJSON) - object - no default
* - [toObject](/docs/guide.html#toObject) - object - no default
* - [typeKey](/docs/guide.html#typeKey) - string - defaults to 'type'
@@ -65,6 +66,9 @@ let id = 0;
* - [versionKey](/docs/guide.html#versionKey): string - defaults to "__v"
* - [collation](/docs/guide.html#collation): object - defaults to null (which means use no collation)
* - [selectPopulatedPaths](/docs/guide.html#selectPopulatedPaths): boolean - defaults to `true`
* - [skipVersioning](/docs/guide.html#skipVersioning): object - paths to exclude from versioning
* - [timestamps](/docs/guide.html#timestamps): object or boolean - defaults to `false`. If true, Mongoose adds `createdAt` and `updatedAt` properties to your schema and manages those properties for you.
* - [storeSubdocValidationError](/docs/guide.html#storeSubdocValidationError): boolean - Defaults to true. If false, Mongoose will wrap validation errors in single nested document subpaths into a single validation error on the single nested subdoc's path.
*
* ####Note:
*
@@ -312,7 +316,7 @@ Schema.prototype.clone = function() {
if (this.discriminatorMapping != null) {
s.discriminatorMapping = Object.assign({}, this.discriminatorMapping);
}
if (s.discriminators != null) {
if (this.discriminators != null) {
s.discriminators = Object.assign({}, this.discriminators);
}
@@ -393,7 +397,7 @@ Schema.prototype.defaultOptions = function(options) {
Schema.prototype.add = function add(obj, prefix) {
if (obj instanceof Schema) {
merge(this, obj);
return;
return this;
}
// Special case: setting top-level `_id` to false should convert to disabling
@@ -567,7 +571,7 @@ Schema.prototype.path = function(path, obj) {
console.log('WARN: ' + warnings[path]);
}
if (typeof obj === 'object' && 'ref' in obj) {
if (typeof obj === 'object' && utils.hasUserDefinedProperty(obj, 'ref')) {
validateRef(obj.ref, path);
}
@@ -601,9 +605,16 @@ Schema.prototype.path = function(path, obj) {
// The '$' is to imply this path should never be stored in MongoDB so we
// can easily build a regexp out of this path, and '*' to imply "any key."
const mapPath = path + '.$*';
this.paths[path + '.$*'] = this.interpretAsType(mapPath,
obj.of || { type: {} }, this.options);
schemaType.$__schemaType = this.paths[path + '.$*'];
let _mapType = { type: {} };
if (utils.hasUserDefinedProperty(obj, 'of')) {
const isInlineSchema = utils.isPOJO(obj.of) &&
Object.keys(obj.of).length > 0 &&
!utils.hasUserDefinedProperty(obj.of, this.options.typeKey);
_mapType = isInlineSchema ? new Schema(obj.of) : obj.of;
}
this.paths[mapPath] = this.interpretAsType(mapPath,
_mapType, this.options);
schemaType.$__schemaType = this.paths[mapPath];
}
if (schemaType.$isSingleNested) {
@@ -1026,7 +1037,7 @@ Schema.prototype.setupTimestamp = function(timestamps) {
let ts = defaultTimestamp;
if (this.isNew) {
if (createdAt != null) {
ts = this.getValue(createdAt);
ts = this.$__getValue(createdAt);
} else if (auto_id) {
ts = this._id.getTimestamp();
}
@@ -1183,10 +1194,10 @@ Schema.prototype.queue = function(name, args) {
*
* // Equivalent to calling `pre()` on `find`, `findOne`, `findOneAndUpdate`.
* toySchema.pre(/^find/, function(next) {
* console.log(this.getQuery());
* console.log(this.getFilter());
* });
*
* @param {String|RegExp} method or regular expression to match method name
* @param {String|RegExp} The method name or regular expression to match method name
* @param {Object} [options]
* @param {Boolean} [options.document] If `name` is a hook for both document and query middleware, set to `true` to run on document middleware.
* @param {Boolean} [options.query] If `name` is a hook for both document and query middleware, set to `true` to run on query middleware.
@@ -1236,7 +1247,7 @@ Schema.prototype.pre = function(name) {
* console.log('this fires after the post find hook');
* });
*
* @param {String|RegExp} method or regular expression to match method name
* @param {String|RegExp} The method name or regular expression to match method name
* @param {Object} [options]
* @param {Boolean} [options.document] If `name` is a hook for both document and query middleware, set to `true` to run on document middleware.
* @param {Boolean} [options.query] If `name` is a hook for both document and query middleware, set to `true` to run on query middleware.
@@ -1347,21 +1358,21 @@ Schema.prototype.method = function(name, fn, options) {
*
* ####Example
*
* var schema = new Schema(..);
* schema.static('findByName', function (name, callback) {
* return this.find({ name: name }, callback);
* const schema = new Schema(..);
* // Equivalent to `schema.statics.findByName = function(name) {}`;
* schema.static('findByName', function(name) {
* return this.find({ name: name });
* });
*
* var Drink = mongoose.model('Drink', schema);
* Drink.findByName('sanpellegrino', function (err, drinks) {
* //
* });
* const Drink = mongoose.model('Drink', schema);
* await Drink.findByName('LaCroix');
*
* If a hash of name/fn pairs is passed as the only argument, each name/fn pair will be added as statics.
*
* @param {String|Object} name
* @param {Function} [fn]
* @api public
* @see Statics /docs/guide.html#statics
*/
Schema.prototype.static = function(name, fn) {
@@ -1531,7 +1542,7 @@ Schema.prototype.indexes = function() {
*/
Schema.prototype.virtual = function(name, options) {
if (options && options.ref) {
if (utils.hasUserDefinedProperty(options, ['ref', 'refPath'])) {
if (!options.localField) {
throw new Error('Reference virtuals require `localField` option');
}
@@ -1568,10 +1579,10 @@ Schema.prototype.virtual = function(name, options) {
if (!this.$$populatedVirtuals) {
this.$$populatedVirtuals = {};
}
if (name in this.$$populatedVirtuals) {
if (this.$$populatedVirtuals.hasOwnProperty(name)) {
return this.$$populatedVirtuals[name];
}
return null;
return void 0;
}).
set(function(_v) {
if (!this.$$populatedVirtuals) {
@@ -1916,8 +1927,7 @@ Schema.prototype._getPathType = function(path) {
*/
function isArrayFilter(piece) {
return piece.indexOf('$[') === 0 &&
piece.lastIndexOf(']') === piece.length - 1;
return piece.startsWith('$[') && piece.endsWith(']');
}
/*!
+17 -3
View File
@@ -250,12 +250,26 @@ SchemaArray.prototype.cast = function(value, doc, init) {
if (!value.length && doc) {
const indexes = doc.schema.indexedPaths();
const arrayPath = this.path;
for (i = 0, l = indexes.length; i < l; ++i) {
const pathIndex = indexes[i][0][this.path];
const pathIndex = indexes[i][0][arrayPath];
if (pathIndex === '2dsphere' || pathIndex === '2d') {
return;
}
}
// Special case: if this index is on the parent of what looks like
// GeoJSON, skip setting the default to empty array re: #1668, #3233
const arrayGeojsonPath = this.path.endsWith('.coordinates') ?
this.path.substr(0, this.path.lastIndexOf('.')) : null;
if (arrayGeojsonPath != null) {
for (i = 0, l = indexes.length; i < l; ++i) {
const pathIndex = indexes[i][0][arrayGeojsonPath];
if (pathIndex === '2dsphere') {
return;
}
}
}
}
if (!(value && value.isMongooseArray)) {
@@ -417,7 +431,7 @@ function cast$elemMatch(val) {
for (let i = 0; i < numKeys; ++i) {
const key = keys[i];
const value = val[key];
if (key.indexOf('$') === 0 && value) {
if (key.startsWith('$') && value) {
val[key] = this.castForQuery(key, value);
}
}
@@ -474,11 +488,11 @@ handle.$gte =
handle.$lt =
handle.$lte =
handle.$ne =
handle.$nin =
handle.$regex = SchemaArray.prototype.castForQuery;
// `$in` is special because you can also include an empty array in the query
// like `$in: [1, []]`, see gh-5913
handle.$nin = SchemaType.prototype.$conditionalHandlers.$nin;
handle.$in = SchemaType.prototype.$conditionalHandlers.$in;
/*!
+3 -1
View File
@@ -7,6 +7,8 @@
const handleBitwiseOperator = require('./operators/bitwise');
const utils = require('../utils');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
const MongooseBuffer = require('../types/buffer');
const SchemaType = require('../schematype');
@@ -125,7 +127,7 @@ SchemaBuffer.prototype.cast = function(value, doc, init) {
const path = doc.$__fullPath(this.path);
const owner = doc.ownerDocument ? doc.ownerDocument() : doc;
const pop = owner.populated(path, true);
ret = new pop.options.model(value);
ret = new pop.options[populateModelSymbol](value);
ret.$__.wasPopulated = true;
return ret;
}
+3 -1
View File
@@ -10,6 +10,8 @@ const Decimal128Type = require('../types/decimal128');
const castDecimal128 = require('../cast/decimal128');
const utils = require('../utils');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
let Document;
/**
@@ -170,7 +172,7 @@ Decimal128.prototype.cast = function(value, doc, init) {
!doc.$__.populated[path].options ||
!doc.$__.populated[path].options.options ||
!doc.$__.populated[path].options.options.lean) {
ret = new pop.options.model(value);
ret = new pop.options[populateModelSymbol](value);
ret.$__.wasPopulated = true;
}
+14 -33
View File
@@ -11,7 +11,7 @@ const SchemaType = require('../schematype');
const discriminator = require('../helpers/model/discriminator');
const util = require('util');
const utils = require('../utils');
const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
const getConstructor = require('../helpers/discriminator/getConstructor');
const arrayParentSymbol = require('../helpers/symbols').arrayParentSymbol;
const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
@@ -210,7 +210,7 @@ DocumentArray.prototype.doValidate = function(array, fn, scope, options) {
// If you set the array index directly, the doc might not yet be
// a full fledged mongoose subdoc, so make it into one.
if (!(doc instanceof Subdocument)) {
const Constructor = getConstructor(_this, array[i]);
const Constructor = getConstructor(_this.casterConstructor, array[i]);
doc = array[i] = new Constructor(doc, array, undefined, undefined, i);
}
@@ -258,7 +258,7 @@ DocumentArray.prototype.doValidateSync = function(array, scope) {
// If you set the array index directly, the doc might not yet be
// a full fledged mongoose subdoc, so make it into one.
if (!(doc instanceof Subdocument)) {
const Constructor = getConstructor(this, array[i]);
const Constructor = getConstructor(this.casterConstructor, array[i]);
doc = array[i] = new Constructor(doc, array, undefined, undefined, i);
}
@@ -297,7 +297,7 @@ DocumentArray.prototype.getDefault = function(scope) {
ret[arrayParentSymbol] = null;
for (let i = 0; i < ret.length; ++i) {
const Constructor = getConstructor(this, ret[i]);
const Constructor = getConstructor(this.casterConstructor, ret[i]);
ret[i] = new Constructor(ret[i], ret, undefined,
undefined, i);
}
@@ -353,12 +353,17 @@ DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
continue;
}
const Constructor = getConstructor(this, value[i]);
const Constructor = getConstructor(this.casterConstructor, value[i]);
// Check if the document has a different schema (re gh-3701)
if ((value[i].$__) &&
!(value[i] instanceof Constructor)) {
value[i] = value[i].toObject({ transform: false, virtuals: false });
value[i] = value[i].toObject({
transform: false,
// Special case: if different model, but same schema, apply virtuals
// re: gh-7898
virtuals: value[i].schema === Constructor.schema
});
}
if (value[i] instanceof Subdocument) {
@@ -377,7 +382,7 @@ DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
subdoc = new Constructor(null, value, true, selected, i);
value[i] = subdoc.init(value[i]);
} else {
if (prev && (subdoc = prev.id(value[i]._id))) {
if (prev && typeof prev.id === 'function') {
subdoc = prev.id(value[i]._id);
}
@@ -410,30 +415,6 @@ DocumentArray.prototype.cast = function(value, doc, init, prev, options) {
return value;
};
/*!
* Find the correct subdoc constructor, taking into account discriminators
*/
function getConstructor(docArray, subdoc) {
let Constructor = docArray.casterConstructor;
const schema = Constructor.schema;
const discriminatorKey = schema.options.discriminatorKey;
const discriminatorValue = subdoc[discriminatorKey];
if (Constructor.discriminators && typeof discriminatorValue === 'string') {
if (Constructor.discriminators[discriminatorValue]) {
Constructor = Constructor.discriminators[discriminatorValue];
} else {
const constructorByValue = getDiscriminatorByValue(Constructor,
discriminatorValue);
if (constructorByValue) {
Constructor = constructorByValue;
}
}
}
return Constructor;
}
/*!
* ignore
*/
@@ -483,12 +464,12 @@ function scopePaths(array, fields, init) {
while (i--) {
key = keys[i];
if (key.indexOf(path) === 0) {
if (key.startsWith(path)) {
sub = key.substring(path.length);
if (sub === '$') {
continue;
}
if (sub.indexOf('$.') === 0) {
if (sub.startsWith('$.')) {
sub = sub.substr(2);
}
hasKeys || (hasKeys = true);
+5 -43
View File
@@ -13,7 +13,7 @@ const castToNumber = require('./operators/helpers').castToNumber;
const discriminator = require('../helpers/model/discriminator');
const geospatial = require('./operators/geospatial');
const get = require('../helpers/get');
const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
const getConstructor = require('../helpers/discriminator/getConstructor');
const internalToObjectOptions = require('../options').internalToObjectOptions;
let Subdocument;
@@ -149,20 +149,7 @@ Embedded.prototype.cast = function(val, doc, init, priorVal) {
throw new ObjectExpectedError(this.path, val);
}
let Constructor = this.caster;
const discriminatorKey = Constructor.schema.options.discriminatorKey;
if (val != null &&
Constructor.discriminators &&
typeof val[discriminatorKey] === 'string') {
if (Constructor.discriminators[val[discriminatorKey]]) {
Constructor = Constructor.discriminators[val[discriminatorKey]];
} else {
const constructorByValue = getDiscriminatorByValue(Constructor, val[discriminatorKey]);
if (constructorByValue) {
Constructor = constructorByValue;
}
}
}
const Constructor = getConstructor(this.caster, val);
let subdoc;
@@ -216,20 +203,7 @@ Embedded.prototype.castForQuery = function($conditional, val) {
val = this._applySetters(val);
}
let Constructor = this.caster;
const discriminatorKey = Constructor.schema.options.discriminatorKey;
if (val != null &&
Constructor.discriminators &&
typeof val[discriminatorKey] === 'string') {
if (Constructor.discriminators[val[discriminatorKey]]) {
Constructor = Constructor.discriminators[val[discriminatorKey]];
} else {
const constructorByValue = getDiscriminatorByValue(Constructor, val[discriminatorKey]);
if (constructorByValue) {
Constructor = constructorByValue;
}
}
}
const Constructor = getConstructor(this.caster, val);
try {
val = new Constructor(val);
@@ -250,20 +224,7 @@ Embedded.prototype.castForQuery = function($conditional, val) {
*/
Embedded.prototype.doValidate = function(value, fn, scope, options) {
let Constructor = this.caster;
const discriminatorKey = Constructor.schema.options.discriminatorKey;
if (value != null &&
Constructor.discriminators &&
typeof value[discriminatorKey] === 'string') {
if (Constructor.discriminators[value[discriminatorKey]]) {
Constructor = Constructor.discriminators[value[discriminatorKey]];
} else {
const constructorByValue = getDiscriminatorByValue(Constructor, value[discriminatorKey]);
if (constructorByValue) {
Constructor = constructorByValue;
}
}
}
const Constructor = getConstructor(this.caster, value);
if (options && options.skipSchemaValidators) {
if (!(value instanceof Constructor)) {
@@ -328,5 +289,6 @@ Embedded.prototype.clone = function() {
const options = Object.assign({}, this.options);
const schematype = new this.constructor(this.schema, this.path, options);
schematype.validators = this.validators.slice();
schematype.caster.discriminators = Object.assign({}, this.caster.discriminators);
return schematype;
};
+3 -1
View File
@@ -10,6 +10,8 @@ const castNumber = require('../cast/number');
const handleBitwiseOperator = require('./operators/bitwise');
const utils = require('../utils');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
const CastError = SchemaType.CastError;
let Document;
@@ -293,7 +295,7 @@ SchemaNumber.prototype.cast = function(value, doc, init) {
const path = doc.$__fullPath(this.path);
const owner = doc.ownerDocument ? doc.ownerDocument() : doc;
const pop = owner.populated(path, true);
const ret = new pop.options.model(value);
const ret = new pop.options[populateModelSymbol](value);
ret.$__.wasPopulated = true;
return ret;
}
+3 -1
View File
@@ -9,6 +9,8 @@ const SchemaType = require('../schematype');
const oid = require('../types/objectid');
const utils = require('../utils');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
const CastError = SchemaType.CastError;
let Document;
@@ -226,7 +228,7 @@ ObjectId.prototype.cast = function(value, doc, init) {
!doc.$__.populated[path].options ||
!doc.$__.populated[path].options.options ||
!doc.$__.populated[path].options.options.lean) {
ret = new pop.options.model(value);
ret = new pop.options[populateModelSymbol](value);
ret.$__.wasPopulated = true;
}
+4 -2
View File
@@ -10,6 +10,8 @@ const MongooseError = require('../error');
const castString = require('../cast/string');
const utils = require('../utils');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
let Document;
/**
@@ -537,7 +539,7 @@ SchemaString.prototype.cast = function(value, doc, init) {
const path = doc.$__fullPath(this.path);
const owner = doc.ownerDocument ? doc.ownerDocument() : doc;
const pop = owner.populated(path, true);
const ret = new pop.options.model(value);
const ret = new pop.options[populateModelSymbol](value);
ret.$__.wasPopulated = true;
return ret;
}
@@ -577,7 +579,7 @@ SchemaString.prototype.$conditionalHandlers =
$gte: handleSingle,
$lt: handleSingle,
$lte: handleSingle,
$options: handleSingle,
$options: String,
$regex: handleSingle,
$not: handleSingle
});
+60 -2
View File
@@ -8,6 +8,7 @@ const MongooseError = require('./error');
const $exists = require('./schema/operators/exists');
const $type = require('./schema/operators/type');
const get = require('./helpers/get');
const handleImmutable = require('./helpers/schematype/handleImmutable');
const immediate = require('./helpers/immediate');
const schemaTypeSymbol = require('./helpers/symbols').schemaTypeSymbol;
const util = require('util');
@@ -44,6 +45,11 @@ function SchemaType(path, options, instance) {
this.options = options;
this._index = null;
this.selected;
if (utils.hasUserDefinedProperty(options, 'immutable')) {
this.$immutable = options.immutable;
handleImmutable(this);
}
for (const prop in options) {
if (this[prop] && typeof this[prop] === 'function') {
@@ -300,7 +306,7 @@ SchemaType.prototype.text = function(bool) {
*
* ####Example:
*
* var s = new Schema({ name: { type: String, sparse: true })
* var s = new Schema({ name: { type: String, sparse: true } });
* Schema.path('name').index({ sparse: true });
*
* @param {Boolean} bool
@@ -327,6 +333,56 @@ SchemaType.prototype.sparse = function(bool) {
return this;
};
/**
* Defines this path as immutable. Mongoose prevents you from changing
* immutable paths unless the parent document has [`isNew: true`](/docs/api.html#document_Document-isNew).
*
* ####Example:
*
* const schema = new Schema({
* name: { type: String, immutable: true },
* age: Number
* });
* const Model = mongoose.model('Test', schema);
*
* await Model.create({ name: 'test' });
* const doc = await Model.findOne();
*
* doc.isNew; // false
* doc.name = 'new name';
* doc.name; // 'test', because `name` is immutable
*
* Mongoose also prevents changing immutable properties using `updateOne()`
* and `updateMany()` based on [strict mode](/docs/guide.html#strict).
*
* ####Example:
*
* // Mongoose will strip out the `name` update, because `name` is immutable
* Model.updateOne({}, { $set: { name: 'test2' }, $inc: { age: 1 } });
*
* // If `strict` is set to 'throw', Mongoose will throw an error if you
* // update `name`
* const err = await Model.updateOne({}, { name: 'test2' }, { strict: 'throw' }).
* then(() => null, err => err);
* err.name; // StrictModeError
*
* // If `strict` is `false`, Mongoose allows updating `name` even though
* // the property is immutable.
* Model.updateOne({}, { name: 'test2' }, { strict: false });
*
* @param {Boolean} bool
* @return {SchemaType} this
* @see isNew /docs/api.html#document_Document-isNew
* @api public
*/
SchemaType.prototype.immutable = function(bool) {
this.$immutable = bool;
handleImmutable(this);
return this;
};
/**
* Adds a setter to this schematype.
*
@@ -1024,6 +1080,7 @@ SchemaType.prototype.doValidate = function(value, fn, scope, options) {
function(ok) { validate(ok, validatorProperties); },
function(error) {
validatorProperties.reason = error;
validatorProperties.message = error.message;
ok = false;
validate(ok, validatorProperties);
});
@@ -1071,6 +1128,7 @@ function asyncValidate(validator, scope, value, props, cb) {
called = true;
props.reason = error;
props.message = error.message;
cb(false, props);
});
}
@@ -1253,7 +1311,7 @@ SchemaType.prototype.$conditionalHandlers = {
$eq: handleSingle,
$in: handle$in,
$ne: handleSingle,
$nin: handleArray,
$nin: handle$in,
$exists: $exists,
$type: $type
};
+8 -827
View File
@@ -5,20 +5,14 @@
'use strict';
const CoreMongooseArray = require('./core_array');
const EmbeddedDocument = require('./embedded');
const Document = require('../document');
const ObjectId = require('./objectid');
const cleanModifiedSubpaths = require('../helpers/document/cleanModifiedSubpaths');
const get = require('../helpers/get');
const internalToObjectOptions = require('../options').internalToObjectOptions;
const utils = require('../utils');
const util = require('util');
const arrayAtomicsSymbol = require('../helpers/symbols').arrayAtomicsSymbol;
const arrayParentSymbol = require('../helpers/symbols').arrayParentSymbol;
const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../helpers/symbols').arraySchemaSymbol;
const isMongooseObject = utils.isMongooseObject;
const _basePush = Array.prototype.push;
/**
* Mongoose Array constructor.
@@ -39,24 +33,20 @@ function MongooseArray(values, path, doc) {
// TODO: replace this with `new CoreMongooseArray().concat()` when we remove
// support for node 4.x and 5.x, see https://i.imgur.com/UAAHk4S.png
const arr = new CoreMongooseArray();
arr[arrayAtomicsSymbol] = {};
if (Array.isArray(values)) {
values.forEach(v => { arr.push(v); });
}
const len = values.length;
for (let i = 0; i < len; ++i) {
_basePush.call(arr, values[i]);
}
const keysMA = Object.keys(MongooseArray.mixin);
const numKeys = keysMA.length;
for (let i = 0; i < numKeys; ++i) {
arr[keysMA[i]] = MongooseArray.mixin[keysMA[i]];
arr[arrayAtomicsSymbol] = values[arrayAtomicsSymbol] || {};
}
arr[arrayPathSymbol] = path;
arr.validators = [];
arr[arrayAtomicsSymbol] = {};
arr[arraySchemaSymbol] = void 0;
if (util.inspect.custom) {
arr[util.inspect.custom] = arr.inspect;
}
// Because doc comes from the context of another function, doc === global
// can happen if there was a null somewhere up the chain (see #3020)
@@ -70,815 +60,6 @@ function MongooseArray(values, path, doc) {
return arr;
}
MongooseArray.mixin = {
/*!
* ignore
*/
toBSON: function() {
return this.toObject(internalToObjectOptions);
},
/*!
* ignore
*/
$parent: function() {
return this[arrayParentSymbol];
},
/*!
* ignore
*/
$schema: function() {
return this[arraySchemaSymbol];
},
/*!
* ignore
*/
$path: function() {
return this[arrayPathSymbol];
},
/*!
* ignore
*/
$atomics: function() {
return this[arrayAtomicsSymbol];
},
/**
* Casts a member based on this arrays schema.
*
* @param {any} value
* @return value the casted value
* @method _cast
* @api private
* @memberOf MongooseArray
*/
_cast: function(value) {
let populated = false;
let Model;
if (this[arrayParentSymbol]) {
populated = this[arrayParentSymbol].populated(this[arrayPathSymbol], true);
}
if (populated && value !== null && value !== undefined) {
// cast to the populated Models schema
Model = populated.options.model || populated.options.Model;
// only objects are permitted so we can safely assume that
// non-objects are to be interpreted as _id
if (Buffer.isBuffer(value) ||
value instanceof ObjectId || !utils.isObject(value)) {
value = {_id: value};
}
// gh-2399
// we should cast model only when it's not a discriminator
const isDisc = value.schema && value.schema.discriminatorMapping &&
value.schema.discriminatorMapping.key !== undefined;
if (!isDisc) {
value = new Model(value);
}
return this[arraySchemaSymbol].caster.applySetters(value, this[arrayParentSymbol], true);
}
return this[arraySchemaSymbol].caster.applySetters(value, this[arrayParentSymbol], false);
},
/**
* Marks this array as modified.
*
* If it bubbles up from an embedded document change, then it takes the following arguments (otherwise, takes 0 arguments)
*
* @param {EmbeddedDocument} embeddedDoc the embedded doc that invoked this method on the Array
* @param {String} embeddedPath the path which changed in the embeddedDoc
* @method _markModified
* @api private
* @memberOf MongooseArray
*/
_markModified: function(elem, embeddedPath) {
const parent = this[arrayParentSymbol];
let dirtyPath;
if (parent) {
dirtyPath = this[arrayPathSymbol];
if (arguments.length) {
if (embeddedPath != null) {
// an embedded doc bubbled up the change
dirtyPath = dirtyPath + '.' + this.indexOf(elem) + '.' + embeddedPath;
} else {
// directly set an index
dirtyPath = dirtyPath + '.' + elem;
}
}
parent.markModified(dirtyPath, arguments.length > 0 ? elem : parent);
}
return this;
},
/**
* Register an atomic operation with the parent.
*
* @param {Array} op operation
* @param {any} val
* @method _registerAtomic
* @api private
* @memberOf MongooseArray
*/
_registerAtomic: function(op, val) {
if (op === '$set') {
// $set takes precedence over all other ops.
// mark entire array modified.
this[arrayAtomicsSymbol] = {$set: val};
cleanModifiedSubpaths(this[arrayParentSymbol], this[arrayPathSymbol]);
this._markModified();
return this;
}
const atomics = this[arrayAtomicsSymbol];
// reset pop/shift after save
if (op === '$pop' && !('$pop' in atomics)) {
const _this = this;
this[arrayParentSymbol].once('save', function() {
_this._popped = _this._shifted = null;
});
}
// check for impossible $atomic combos (Mongo denies more than one
// $atomic op on a single path
if (this[arrayAtomicsSymbol].$set ||
Object.keys(atomics).length && !(op in atomics)) {
// a different op was previously registered.
// save the entire thing.
this[arrayAtomicsSymbol] = {$set: this};
return this;
}
let selector;
if (op === '$pullAll' || op === '$addToSet') {
atomics[op] || (atomics[op] = []);
atomics[op] = atomics[op].concat(val);
} else if (op === '$pullDocs') {
const pullOp = atomics['$pull'] || (atomics['$pull'] = {});
if (val[0] instanceof EmbeddedDocument) {
selector = pullOp['$or'] || (pullOp['$or'] = []);
Array.prototype.push.apply(selector, val.map(function(v) {
return v.toObject({transform: false, virtuals: false});
}));
} else {
selector = pullOp['_id'] || (pullOp['_id'] = {$in: []});
selector['$in'] = selector['$in'].concat(val);
}
} else if (op === '$push') {
atomics.$push = atomics.$push || { $each: [] };
atomics.$push.$each = atomics.$push.$each.concat(val);
} else {
atomics[op] = val;
}
return this;
},
/**
* Depopulates stored atomic operation values as necessary for direct insertion to MongoDB.
*
* If no atomics exist, we return all array values after conversion.
*
* @return {Array}
* @method $__getAtomics
* @memberOf MongooseArray
* @instance
* @api private
*/
$__getAtomics: function() {
const ret = [];
const keys = Object.keys(this[arrayAtomicsSymbol]);
let i = keys.length;
const opts = Object.assign({}, internalToObjectOptions, { _isNested: true });
if (i === 0) {
ret[0] = ['$set', this.toObject(opts)];
return ret;
}
while (i--) {
const op = keys[i];
let val = this[arrayAtomicsSymbol][op];
// the atomic values which are arrays are not MongooseArrays. we
// need to convert their elements as if they were MongooseArrays
// to handle populated arrays versus DocumentArrays properly.
if (isMongooseObject(val)) {
val = val.toObject(opts);
} else if (Array.isArray(val)) {
val = this.toObject.call(val, opts);
} else if (val != null && Array.isArray(val.$each)) {
val.$each = this.toObject.call(val.$each, opts);
} else if (val != null && typeof val.valueOf === 'function') {
val = val.valueOf();
}
if (op === '$addToSet') {
val = {$each: val};
}
ret.push([op, val]);
}
return ret;
},
/**
* Returns the number of pending atomic operations to send to the db for this array.
*
* @api private
* @return {Number}
* @method hasAtomics
* @memberOf MongooseArray
*/
hasAtomics: function hasAtomics() {
if (!(this[arrayAtomicsSymbol] && this[arrayAtomicsSymbol].constructor.name === 'Object')) {
return 0;
}
return Object.keys(this[arrayAtomicsSymbol]).length;
},
/**
* Internal helper for .map()
*
* @api private
* @return {Number}
* @method _mapCast
* @memberOf MongooseArray
*/
_mapCast: function(val, index) {
return this._cast(val, this.length + index);
},
/**
* Wraps [`Array#push`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
*
* @param {Object} [args...]
* @api public
* @method push
* @memberOf MongooseArray
*/
push: function() {
_checkManualPopulation(this, arguments);
let values = [].map.call(arguments, this._mapCast, this);
values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol], undefined,
undefined, { skipDocumentArrayCast: true });
const ret = [].push.apply(this, values);
this._registerAtomic('$push', values);
this._markModified();
return ret;
},
/**
* Pushes items to the array non-atomically.
*
* ####NOTE:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @param {any} [args...]
* @api public
* @method nonAtomicPush
* @memberOf MongooseArray
*/
nonAtomicPush: function() {
const values = [].map.call(arguments, this._mapCast, this);
const ret = [].push.apply(this, values);
this._registerAtomic('$set', this);
this._markModified();
return ret;
},
/**
* Pops the array atomically at most one time per document `save()`.
*
* #### NOTE:
*
* _Calling this mulitple times on an array before saving sends the same command as calling it once._
* _This update is implemented using the MongoDB [$pop](http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
*
* doc.array = [1,2,3];
*
* var popped = doc.array.$pop();
* console.log(popped); // 3
* console.log(doc.array); // [1,2]
*
* // no affect
* popped = doc.array.$pop();
* console.log(doc.array); // [1,2]
*
* doc.save(function (err) {
* if (err) return handleError(err);
*
* // we saved, now $pop works again
* popped = doc.array.$pop();
* console.log(popped); // 2
* console.log(doc.array); // [1]
* })
*
* @api public
* @method $pop
* @memberOf MongooseArray
* @instance
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
* @method $pop
* @memberOf MongooseArray
*/
$pop: function() {
this._registerAtomic('$pop', 1);
this._markModified();
// only allow popping once
if (this._popped) {
return;
}
this._popped = true;
return [].pop.call(this);
},
/**
* Wraps [`Array#pop`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/pop) with proper change tracking.
*
* ####Note:
*
* _marks the entire array as modified which will pass the entire thing to $set potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @see MongooseArray#$pop #types_array_MongooseArray-%24pop
* @api public
* @method pop
* @memberOf MongooseArray
*/
pop: function() {
const ret = [].pop.call(this);
this._registerAtomic('$set', this);
this._markModified();
return ret;
},
/**
* Atomically shifts the array at most one time per document `save()`.
*
* ####NOTE:
*
* _Calling this mulitple times on an array before saving sends the same command as calling it once._
* _This update is implemented using the MongoDB [$pop](http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
*
* doc.array = [1,2,3];
*
* var shifted = doc.array.$shift();
* console.log(shifted); // 1
* console.log(doc.array); // [2,3]
*
* // no affect
* shifted = doc.array.$shift();
* console.log(doc.array); // [2,3]
*
* doc.save(function (err) {
* if (err) return handleError(err);
*
* // we saved, now $shift works again
* shifted = doc.array.$shift();
* console.log(shifted ); // 2
* console.log(doc.array); // [3]
* })
*
* @api public
* @memberOf MongooseArray
* @instance
* @method $shift
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
*/
$shift: function $shift() {
this._registerAtomic('$pop', -1);
this._markModified();
// only allow shifting once
if (this._shifted) {
return;
}
this._shifted = true;
return [].shift.call(this);
},
/**
* Wraps [`Array#shift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
*
* ####Example:
*
* doc.array = [2,3];
* var res = doc.array.shift();
* console.log(res) // 2
* console.log(doc.array) // [3]
*
* ####Note:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method shift
* @memberOf MongooseArray
*/
shift: function() {
const ret = [].shift.call(this);
this._registerAtomic('$set', this);
this._markModified();
return ret;
},
/**
* Pulls items from the array atomically. Equality is determined by casting
* the provided value to an embedded document and comparing using
* [the `Document.equals()` function.](./api.html#document_Document-equals)
*
* ####Examples:
*
* doc.array.pull(ObjectId)
* doc.array.pull({ _id: 'someId' })
* doc.array.pull(36)
* doc.array.pull('tag 1', 'tag 2')
*
* To remove a document from a subdocument array we may pass an object with a matching `_id`.
*
* doc.subdocs.push({ _id: 4815162342 })
* doc.subdocs.pull({ _id: 4815162342 }) // removed
*
* Or we may passing the _id directly and let mongoose take care of it.
*
* doc.subdocs.push({ _id: 4815162342 })
* doc.subdocs.pull(4815162342); // works
*
* The first pull call will result in a atomic operation on the database, if pull is called repeatedly without saving the document, a $set operation is used on the complete array instead, overwriting possible changes that happened on the database in the meantime.
*
* @param {any} [args...]
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
* @api public
* @method pull
* @memberOf MongooseArray
*/
pull: function() {
const values = [].map.call(arguments, this._cast, this);
const cur = this[arrayParentSymbol].get(this[arrayPathSymbol]);
let i = cur.length;
let mem;
while (i--) {
mem = cur[i];
if (mem instanceof Document) {
const some = values.some(function(v) {
return mem.equals(v);
});
if (some) {
[].splice.call(cur, i, 1);
}
} else if (~cur.indexOf.call(values, mem)) {
[].splice.call(cur, i, 1);
}
}
if (values[0] instanceof EmbeddedDocument) {
this._registerAtomic('$pullDocs', values.map(function(v) {
return v._id || v;
}));
} else {
this._registerAtomic('$pullAll', values);
}
this._markModified();
// Might have modified child paths and then pulled, like
// `doc.children[1].name = 'test';` followed by
// `doc.children.remove(doc.children[0]);`. In this case we fall back
// to a `$set` on the whole array. See #3511
if (cleanModifiedSubpaths(this[arrayParentSymbol], this[arrayPathSymbol]) > 0) {
this._registerAtomic('$set', this);
}
return this;
},
/**
* Wraps [`Array#splice`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
*
* ####Note:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method splice
* @memberOf MongooseArray
*/
splice: function splice() {
let ret;
_checkManualPopulation(this, Array.prototype.slice.call(arguments, 2));
if (arguments.length) {
const vals = [];
for (let i = 0; i < arguments.length; ++i) {
vals[i] = i < 2 ?
arguments[i] :
this._cast(arguments[i], arguments[0] + (i - 2));
}
ret = [].splice.apply(this, vals);
this._registerAtomic('$set', this);
}
return ret;
},
/**
* Wraps [`Array#unshift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
*
* ####Note:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method unshift
* @memberOf MongooseArray
*/
unshift: function() {
_checkManualPopulation(this, arguments);
let values = [].map.call(arguments, this._cast, this);
values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
[].unshift.apply(this, values);
this._registerAtomic('$set', this);
this._markModified();
return this.length;
},
/**
* Wraps [`Array#sort`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/sort) with proper change tracking.
*
* ####NOTE:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method sort
* @memberOf MongooseArray
*/
sort: function() {
const ret = [].sort.apply(this, arguments);
this._registerAtomic('$set', this);
return ret;
},
/**
* Adds values to the array if not already present.
*
* ####Example:
*
* console.log(doc.array) // [2,3,4]
* var added = doc.array.addToSet(4,5);
* console.log(doc.array) // [2,3,4,5]
* console.log(added) // [5]
*
* @param {any} [args...]
* @return {Array} the values that were added
* @memberOf MongooseArray
* @api public
* @method addToSet
*/
addToSet: function addToSet() {
_checkManualPopulation(this, arguments);
let values = [].map.call(arguments, this._mapCast, this);
values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
const added = [];
let type = '';
if (values[0] instanceof EmbeddedDocument) {
type = 'doc';
} else if (values[0] instanceof Date) {
type = 'date';
}
values.forEach(function(v) {
let found;
const val = +v;
switch (type) {
case 'doc':
found = this.some(function(doc) {
return doc.equals(v);
});
break;
case 'date':
found = this.some(function(d) {
return +d === val;
});
break;
default:
found = ~this.indexOf(v);
}
if (!found) {
[].push.call(this, v);
this._registerAtomic('$addToSet', v);
this._markModified();
[].push.call(added, v);
}
}, this);
return added;
},
/**
* Sets the casted `val` at index `i` and marks the array modified.
*
* ####Example:
*
* // given documents based on the following
* var Doc = mongoose.model('Doc', new Schema({ array: [Number] }));
*
* var doc = new Doc({ array: [2,3,4] })
*
* console.log(doc.array) // [2,3,4]
*
* doc.array.set(1,"5");
* console.log(doc.array); // [2,5,4] // properly cast to number
* doc.save() // the change is saved
*
* // VS not using array#set
* doc.array[1] = "5";
* console.log(doc.array); // [2,"5",4] // no casting
* doc.save() // change is not saved
*
* @return {Array} this
* @api public
* @method set
* @memberOf MongooseArray
*/
set: function set(i, val) {
const value = this._cast(val, i);
this[i] = value;
this._markModified(i);
return this;
},
/**
* Returns a native js Array.
*
* @param {Object} options
* @return {Array}
* @api public
* @method toObject
* @memberOf MongooseArray
*/
toObject: function(options) {
if (options && options.depopulate) {
options = utils.clone(options);
options._isNested = true;
return this.map(function(doc) {
return doc instanceof Document
? doc.toObject(options)
: doc;
});
}
return this.slice();
},
/**
* Helper for console.log
*
* @api public
* @method inspect
* @memberOf MongooseArray
*/
inspect: function() {
return JSON.stringify(this);
},
/**
* Return the index of `obj` or `-1` if not found.
*
* @param {Object} obj the item to look for
* @return {Number}
* @api public
* @method indexOf
* @memberOf MongooseArray
*/
indexOf: function indexOf(obj) {
if (obj instanceof ObjectId) {
obj = obj.toString();
}
for (let i = 0, len = this.length; i < len; ++i) {
if (obj == this[i]) {
return i;
}
}
return -1;
},
/**
* Return whether or not the `obj` is included in the array.
*
* @param {Object} obj the item to check
* @return {Boolean}
* @api public
* @method includes
* @memberOf MongooseArray
*/
includes: function includes(obj) {
return this.indexOf(obj) !== -1;
}
};
/**
* Alias of [pull](#types_array_MongooseArray-pull)
*
* @see MongooseArray#pull #types_array_MongooseArray-pull
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
* @api public
* @memberOf MongooseArray
* @instance
* @method remove
*/
MongooseArray.mixin.remove = MongooseArray.mixin.pull;
/*!
* ignore
*/
function _isAllSubdocs(docs, ref) {
if (!ref) {
return false;
}
for (let i = 0; i < docs.length; ++i) {
const arg = docs[i];
if (arg == null) {
return false;
}
const model = arg.constructor;
if (!(arg instanceof Document) ||
(model.modelName !== ref && model.baseModelName !== ref)) {
return false;
}
}
return true;
}
/*!
* ignore
*/
function _checkManualPopulation(arr, docs) {
const ref = arr == null ?
null :
get(arr[arraySchemaSymbol], 'caster.options.ref', null);
if (arr.length === 0 &&
docs.length > 0) {
if (_isAllSubdocs(docs, ref)) {
arr[arrayParentSymbol].populated(arr[arrayPathSymbol], [], { model: docs[0].constructor });
}
}
}
/*!
* Module exports.
*/
+840 -2
View File
@@ -1,5 +1,22 @@
'use strict';
const Document = require('../document');
const EmbeddedDocument = require('./embedded');
const ObjectId = require('./objectid');
const cleanModifiedSubpaths = require('../helpers/document/cleanModifiedSubpaths');
const get = require('../helpers/get');
const internalToObjectOptions = require('../options').internalToObjectOptions;
const utils = require('../utils');
const util = require('util');
const arrayAtomicsSymbol = require('../helpers/symbols').arrayAtomicsSymbol;
const arrayParentSymbol = require('../helpers/symbols').arrayParentSymbol;
const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../helpers/symbols').arraySchemaSymbol;
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
const _basePush = Array.prototype.push;
/*!
* ignore
*/
@@ -9,7 +26,828 @@ class CoreMongooseArray extends Array {
return true;
}
remove() {}
/**
* Depopulates stored atomic operation values as necessary for direct insertion to MongoDB.
*
* If no atomics exist, we return all array values after conversion.
*
* @return {Array}
* @method $__getAtomics
* @memberOf MongooseArray
* @instance
* @api private
*/
$__getAtomics() {
const ret = [];
const keys = Object.keys(this[arrayAtomicsSymbol]);
let i = keys.length;
const opts = Object.assign({}, internalToObjectOptions, { _isNested: true });
if (i === 0) {
ret[0] = ['$set', this.toObject(opts)];
return ret;
}
while (i--) {
const op = keys[i];
let val = this[arrayAtomicsSymbol][op];
// the atomic values which are arrays are not MongooseArrays. we
// need to convert their elements as if they were MongooseArrays
// to handle populated arrays versus DocumentArrays properly.
if (utils.isMongooseObject(val)) {
val = val.toObject(opts);
} else if (Array.isArray(val)) {
val = this.toObject.call(val, opts);
} else if (val != null && Array.isArray(val.$each)) {
val.$each = this.toObject.call(val.$each, opts);
} else if (val != null && typeof val.valueOf === 'function') {
val = val.valueOf();
}
if (op === '$addToSet') {
val = {$each: val};
}
ret.push([op, val]);
}
return ret;
}
/*!
* ignore
*/
$atomics() {
return this[arrayAtomicsSymbol];
}
/*!
* ignore
*/
$parent() {
return this[arrayParentSymbol];
}
/*!
* ignore
*/
$path() {
return this[arrayPathSymbol];
}
/**
* Atomically shifts the array at most one time per document `save()`.
*
* ####NOTE:
*
* _Calling this multiple times on an array before saving sends the same command as calling it once._
* _This update is implemented using the MongoDB [$pop](http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
*
* doc.array = [1,2,3];
*
* var shifted = doc.array.$shift();
* console.log(shifted); // 1
* console.log(doc.array); // [2,3]
*
* // no affect
* shifted = doc.array.$shift();
* console.log(doc.array); // [2,3]
*
* doc.save(function (err) {
* if (err) return handleError(err);
*
* // we saved, now $shift works again
* shifted = doc.array.$shift();
* console.log(shifted ); // 2
* console.log(doc.array); // [3]
* })
*
* @api public
* @memberOf MongooseArray
* @instance
* @method $shift
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
*/
$shift() {
this._registerAtomic('$pop', -1);
this._markModified();
// only allow shifting once
if (this._shifted) {
return;
}
this._shifted = true;
return [].shift.call(this);
}
/**
* Pops the array atomically at most one time per document `save()`.
*
* #### NOTE:
*
* _Calling this mulitple times on an array before saving sends the same command as calling it once._
* _This update is implemented using the MongoDB [$pop](http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop) method which enforces this restriction._
*
* doc.array = [1,2,3];
*
* var popped = doc.array.$pop();
* console.log(popped); // 3
* console.log(doc.array); // [1,2]
*
* // no affect
* popped = doc.array.$pop();
* console.log(doc.array); // [1,2]
*
* doc.save(function (err) {
* if (err) return handleError(err);
*
* // we saved, now $pop works again
* popped = doc.array.$pop();
* console.log(popped); // 2
* console.log(doc.array); // [1]
* })
*
* @api public
* @method $pop
* @memberOf MongooseArray
* @instance
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pop
* @method $pop
* @memberOf MongooseArray
*/
$pop() {
this._registerAtomic('$pop', 1);
this._markModified();
// only allow popping once
if (this._popped) {
return;
}
this._popped = true;
return [].pop.call(this);
}
/*!
* ignore
*/
$schema() {
return this[arraySchemaSymbol];
}
/**
* Casts a member based on this arrays schema.
*
* @param {any} value
* @return value the casted value
* @method _cast
* @api private
* @memberOf MongooseArray
*/
_cast(value) {
let populated = false;
let Model;
if (this[arrayParentSymbol]) {
populated = this[arrayParentSymbol].populated(this[arrayPathSymbol], true);
}
if (populated && value !== null && value !== undefined) {
// cast to the populated Models schema
Model = populated.options[populateModelSymbol];
// only objects are permitted so we can safely assume that
// non-objects are to be interpreted as _id
if (Buffer.isBuffer(value) ||
value instanceof ObjectId || !utils.isObject(value)) {
value = {_id: value};
}
// gh-2399
// we should cast model only when it's not a discriminator
const isDisc = value.schema && value.schema.discriminatorMapping &&
value.schema.discriminatorMapping.key !== undefined;
if (!isDisc) {
value = new Model(value);
}
return this[arraySchemaSymbol].caster.applySetters(value, this[arrayParentSymbol], true);
}
return this[arraySchemaSymbol].caster.applySetters(value, this[arrayParentSymbol], false);
}
/**
* Internal helper for .map()
*
* @api private
* @return {Number}
* @method _mapCast
* @memberOf MongooseArray
*/
_mapCast(val, index) {
return this._cast(val, this.length + index);
}
/**
* Marks this array as modified.
*
* If it bubbles up from an embedded document change, then it takes the following arguments (otherwise, takes 0 arguments)
*
* @param {EmbeddedDocument} embeddedDoc the embedded doc that invoked this method on the Array
* @param {String} embeddedPath the path which changed in the embeddedDoc
* @method _markModified
* @api private
* @memberOf MongooseArray
*/
_markModified(elem, embeddedPath) {
const parent = this[arrayParentSymbol];
let dirtyPath;
if (parent) {
dirtyPath = this[arrayPathSymbol];
if (arguments.length) {
if (embeddedPath != null) {
// an embedded doc bubbled up the change
dirtyPath = dirtyPath + '.' + this.indexOf(elem) + '.' + embeddedPath;
} else {
// directly set an index
dirtyPath = dirtyPath + '.' + elem;
}
}
parent.markModified(dirtyPath, arguments.length > 0 ? elem : parent);
}
return this;
}
/**
* Register an atomic operation with the parent.
*
* @param {Array} op operation
* @param {any} val
* @method _registerAtomic
* @api private
* @memberOf MongooseArray
*/
_registerAtomic(op, val) {
if (op === '$set') {
// $set takes precedence over all other ops.
// mark entire array modified.
this[arrayAtomicsSymbol] = {$set: val};
cleanModifiedSubpaths(this[arrayParentSymbol], this[arrayPathSymbol]);
this._markModified();
return this;
}
const atomics = this[arrayAtomicsSymbol];
// reset pop/shift after save
if (op === '$pop' && !('$pop' in atomics)) {
const _this = this;
this[arrayParentSymbol].once('save', function() {
_this._popped = _this._shifted = null;
});
}
// check for impossible $atomic combos (Mongo denies more than one
// $atomic op on a single path
if (this[arrayAtomicsSymbol].$set ||
Object.keys(atomics).length && !(op in atomics)) {
// a different op was previously registered.
// save the entire thing.
this[arrayAtomicsSymbol] = {$set: this};
return this;
}
let selector;
if (op === '$pullAll' || op === '$addToSet') {
atomics[op] || (atomics[op] = []);
atomics[op] = atomics[op].concat(val);
} else if (op === '$pullDocs') {
const pullOp = atomics['$pull'] || (atomics['$pull'] = {});
if (val[0] instanceof EmbeddedDocument) {
selector = pullOp['$or'] || (pullOp['$or'] = []);
Array.prototype.push.apply(selector, val.map(function(v) {
return v.toObject({transform: false, virtuals: false});
}));
} else {
selector = pullOp['_id'] || (pullOp['_id'] = {$in: []});
selector['$in'] = selector['$in'].concat(val);
}
} else if (op === '$push') {
atomics.$push = atomics.$push || { $each: [] };
atomics.$push.$each = atomics.$push.$each.concat(val);
} else {
atomics[op] = val;
}
return this;
}
/**
* Adds values to the array if not already present.
*
* ####Example:
*
* console.log(doc.array) // [2,3,4]
* var added = doc.array.addToSet(4,5);
* console.log(doc.array) // [2,3,4,5]
* console.log(added) // [5]
*
* @param {any} [args...]
* @return {Array} the values that were added
* @memberOf MongooseArray
* @api public
* @method addToSet
*/
addToSet() {
_checkManualPopulation(this, arguments);
let values = [].map.call(arguments, this._mapCast, this);
values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
const added = [];
let type = '';
if (values[0] instanceof EmbeddedDocument) {
type = 'doc';
} else if (values[0] instanceof Date) {
type = 'date';
}
values.forEach(function(v) {
let found;
const val = +v;
switch (type) {
case 'doc':
found = this.some(function(doc) {
return doc.equals(v);
});
break;
case 'date':
found = this.some(function(d) {
return +d === val;
});
break;
default:
found = ~this.indexOf(v);
}
if (!found) {
[].push.call(this, v);
this._registerAtomic('$addToSet', v);
this._markModified();
[].push.call(added, v);
}
}, this);
return added;
}
/**
* Returns the number of pending atomic operations to send to the db for this array.
*
* @api private
* @return {Number}
* @method hasAtomics
* @memberOf MongooseArray
*/
hasAtomics() {
if (!utils.isPOJO(this[arrayAtomicsSymbol])) {
return 0;
}
return Object.keys(this[arrayAtomicsSymbol]).length;
}
/**
* Return whether or not the `obj` is included in the array.
*
* @param {Object} obj the item to check
* @return {Boolean}
* @api public
* @method includes
* @memberOf MongooseArray
*/
includes(obj) {
return this.indexOf(obj) !== -1;
}
/**
* Return the index of `obj` or `-1` if not found.
*
* @param {Object} obj the item to look for
* @return {Number}
* @api public
* @method indexOf
* @memberOf MongooseArray
*/
indexOf(obj) {
if (obj instanceof ObjectId) {
obj = obj.toString();
}
for (let i = 0, len = this.length; i < len; ++i) {
if (obj == this[i]) {
return i;
}
}
return -1;
}
/**
* Helper for console.log
*
* @api public
* @method inspect
* @memberOf MongooseArray
*/
inspect() {
return JSON.stringify(this);
}
/**
* Pushes items to the array non-atomically.
*
* ####NOTE:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @param {any} [args...]
* @api public
* @method nonAtomicPush
* @memberOf MongooseArray
*/
nonAtomicPush() {
const values = [].map.call(arguments, this._mapCast, this);
const ret = [].push.apply(this, values);
this._registerAtomic('$set', this);
this._markModified();
return ret;
}
/**
* Wraps [`Array#pop`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/pop) with proper change tracking.
*
* ####Note:
*
* _marks the entire array as modified which will pass the entire thing to $set potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @see MongooseArray#$pop #types_array_MongooseArray-%24pop
* @api public
* @method pop
* @memberOf MongooseArray
*/
pop() {
const ret = [].pop.call(this);
this._registerAtomic('$set', this);
this._markModified();
return ret;
}
/**
* Pulls items from the array atomically. Equality is determined by casting
* the provided value to an embedded document and comparing using
* [the `Document.equals()` function.](./api.html#document_Document-equals)
*
* ####Examples:
*
* doc.array.pull(ObjectId)
* doc.array.pull({ _id: 'someId' })
* doc.array.pull(36)
* doc.array.pull('tag 1', 'tag 2')
*
* To remove a document from a subdocument array we may pass an object with a matching `_id`.
*
* doc.subdocs.push({ _id: 4815162342 })
* doc.subdocs.pull({ _id: 4815162342 }) // removed
*
* Or we may passing the _id directly and let mongoose take care of it.
*
* doc.subdocs.push({ _id: 4815162342 })
* doc.subdocs.pull(4815162342); // works
*
* The first pull call will result in a atomic operation on the database, if pull is called repeatedly without saving the document, a $set operation is used on the complete array instead, overwriting possible changes that happened on the database in the meantime.
*
* @param {any} [args...]
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
* @api public
* @method pull
* @memberOf MongooseArray
*/
pull() {
const values = [].map.call(arguments, this._cast, this);
const cur = this[arrayParentSymbol].get(this[arrayPathSymbol]);
let i = cur.length;
let mem;
while (i--) {
mem = cur[i];
if (mem instanceof Document) {
const some = values.some(function(v) {
return mem.equals(v);
});
if (some) {
[].splice.call(cur, i, 1);
}
} else if (~cur.indexOf.call(values, mem)) {
[].splice.call(cur, i, 1);
}
}
if (values[0] instanceof EmbeddedDocument) {
this._registerAtomic('$pullDocs', values.map(function(v) {
return v._id || v;
}));
} else {
this._registerAtomic('$pullAll', values);
}
this._markModified();
// Might have modified child paths and then pulled, like
// `doc.children[1].name = 'test';` followed by
// `doc.children.remove(doc.children[0]);`. In this case we fall back
// to a `$set` on the whole array. See #3511
if (cleanModifiedSubpaths(this[arrayParentSymbol], this[arrayPathSymbol]) > 0) {
this._registerAtomic('$set', this);
}
return this;
}
/**
* Wraps [`Array#push`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/push) with proper change tracking.
*
* @param {Object} [args...]
* @api public
* @method push
* @memberOf MongooseArray
*/
push() {
if (this[arraySchemaSymbol] == null) {
return _basePush.apply(this, arguments);
}
_checkManualPopulation(this, arguments);
let values = [].map.call(arguments, this._mapCast, this);
values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol], undefined,
undefined, { skipDocumentArrayCast: true });
const ret = [].push.apply(this, values);
this._registerAtomic('$push', values);
this._markModified();
return ret;
}
/**
* Alias of [pull](#types_array_MongooseArray-pull)
*
* @see MongooseArray#pull #types_array_MongooseArray-pull
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
* @api public
* @memberOf MongooseArray
* @instance
* @method remove
*/
remove() {
return this.pull.apply(this, arguments);
}
/**
* Sets the casted `val` at index `i` and marks the array modified.
*
* ####Example:
*
* // given documents based on the following
* var Doc = mongoose.model('Doc', new Schema({ array: [Number] }));
*
* var doc = new Doc({ array: [2,3,4] })
*
* console.log(doc.array) // [2,3,4]
*
* doc.array.set(1,"5");
* console.log(doc.array); // [2,5,4] // properly cast to number
* doc.save() // the change is saved
*
* // VS not using array#set
* doc.array[1] = "5";
* console.log(doc.array); // [2,"5",4] // no casting
* doc.save() // change is not saved
*
* @return {Array} this
* @api public
* @method set
* @memberOf MongooseArray
*/
set(i, val) {
const value = this._cast(val, i);
this[i] = value;
this._markModified(i);
return this;
}
/**
* Wraps [`Array#shift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
*
* ####Example:
*
* doc.array = [2,3];
* var res = doc.array.shift();
* console.log(res) // 2
* console.log(doc.array) // [3]
*
* ####Note:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method shift
* @memberOf MongooseArray
*/
shift() {
const ret = [].shift.call(this);
this._registerAtomic('$set', this);
this._markModified();
return ret;
}
/**
* Wraps [`Array#sort`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/sort) with proper change tracking.
*
* ####NOTE:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method sort
* @memberOf MongooseArray
*/
sort() {
const ret = [].sort.apply(this, arguments);
this._registerAtomic('$set', this);
return ret;
}
/**
* Wraps [`Array#splice`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/splice) with proper change tracking and casting.
*
* ####Note:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method splice
* @memberOf MongooseArray
*/
splice() {
let ret;
_checkManualPopulation(this, Array.prototype.slice.call(arguments, 2));
if (arguments.length) {
const vals = [];
for (let i = 0; i < arguments.length; ++i) {
vals[i] = i < 2 ?
arguments[i] :
this._cast(arguments[i], arguments[0] + (i - 2));
}
ret = [].splice.apply(this, vals);
this._registerAtomic('$set', this);
}
return ret;
}
/*!
* ignore
*/
toBSON() {
return this.toObject(internalToObjectOptions);
}
/**
* Returns a native js Array.
*
* @param {Object} options
* @return {Array}
* @api public
* @method toObject
* @memberOf MongooseArray
*/
toObject(options) {
if (options && options.depopulate) {
options = utils.clone(options);
options._isNested = true;
return this.map(function(doc) {
return doc instanceof Document
? doc.toObject(options)
: doc;
});
}
return this.slice();
}
/**
* Wraps [`Array#unshift`](https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/unshift) with proper change tracking.
*
* ####Note:
*
* _marks the entire array as modified, which if saved, will store it as a `$set` operation, potentially overwritting any changes that happen between when you retrieved the object and when you save it._
*
* @api public
* @method unshift
* @memberOf MongooseArray
*/
unshift() {
_checkManualPopulation(this, arguments);
let values = [].map.call(arguments, this._cast, this);
values = this[arraySchemaSymbol].applySetters(values, this[arrayParentSymbol]);
[].unshift.apply(this, values);
this._registerAtomic('$set', this);
this._markModified();
return this.length;
}
}
module.exports = CoreMongooseArray;
if (util.inspect.custom) {
CoreMongooseArray.prototype[util.inspect.custom] =
CoreMongooseArray.prototype.inspect;
}
/*!
* ignore
*/
function _isAllSubdocs(docs, ref) {
if (!ref) {
return false;
}
for (let i = 0; i < docs.length; ++i) {
const arg = docs[i];
if (arg == null) {
return false;
}
const model = arg.constructor;
if (!(arg instanceof Document) ||
(model.modelName !== ref && model.baseModelName !== ref)) {
return false;
}
}
return true;
}
/*!
* ignore
*/
function _checkManualPopulation(arr, docs) {
const ref = arr == null ?
null :
get(arr[arraySchemaSymbol], 'caster.options.ref', null);
if (arr.length === 0 &&
docs.length > 0) {
if (_isAllSubdocs(docs, ref)) {
arr[arrayParentSymbol].populated(arr[arrayPathSymbol], [], {
[populateModelSymbol]: docs[0].constructor
});
}
}
}
module.exports = CoreMongooseArray;
+4 -11
View File
@@ -6,7 +6,6 @@
const CoreMongooseArray = require('./core_array');
const Document = require('../document');
const MongooseArray = require('./array');
const ObjectId = require('./objectid');
const castObjectId = require('../cast/objectid');
const getDiscriminatorByValue = require('../queryhelpers').getDiscriminatorByValue;
@@ -20,6 +19,8 @@ const arrayPathSymbol = require('../helpers/symbols').arrayPathSymbol;
const arraySchemaSymbol = require('../helpers/symbols').arraySchemaSymbol;
const documentArrayParent = require('../helpers/symbols').documentArrayParent;
const _basePush = Array.prototype.push;
/**
* DocumentArray constructor
*
@@ -52,21 +53,13 @@ function MongooseDocumentArray(values, path, doc) {
arr[arrayAtomicsSymbol] = Object.assign({}, values[arrayAtomicsSymbol]);
}
values.forEach(v => {
arr.push(v);
_basePush.call(arr, v);
});
}
arr[arrayPathSymbol] = path;
// Values always have to be passed to the constructor to initialize, since
// otherwise MongooseArray#push will mark the array as modified to the parent.
const keysMA = Object.keys(MongooseArray.mixin);
let numKeys = keysMA.length;
for (let j = 0; j < numKeys; ++j) {
arr[keysMA[j]] = MongooseArray.mixin[keysMA[j]];
}
const keysMDA = Object.keys(MongooseDocumentArray.mixin);
numKeys = keysMDA.length;
let numKeys = keysMDA.length;
for (let i = 0; i < numKeys; ++i) {
arr[keysMDA[i]] = MongooseDocumentArray.mixin[keysMDA[i]];
}
+3 -1
View File
@@ -5,6 +5,8 @@ const get = require('../helpers/get');
const util = require('util');
const utils = require('../utils');
const populateModelSymbol = require('../helpers/symbols').populateModelSymbol;
/*!
* ignore
*/
@@ -57,7 +59,7 @@ class MongooseMap extends Map {
if (populated != null) {
if (value.$__ == null) {
value = new populated.options.model(value);
value = new populated.options[populateModelSymbol](value);
}
value.$__.wasPopulated = true;
} else {
+147 -85
View File
@@ -6,6 +6,7 @@
const Decimal = require('./types/decimal128');
const ObjectId = require('./types/objectid');
const PopulateOptions = require('./options/PopulateOptions');
const PromiseProvider = require('./promise_provider');
const cloneRegExp = require('regexp-clone');
const get = require('./helpers/get');
@@ -391,7 +392,11 @@ exports.merge = function merge(to, from, options, path) {
}
if (from[key] != null) {
if (from[key].instanceOfSchema) {
to[key] = from[key].clone();
if (to[key].instanceOfSchema) {
to[key].add(from[key].clone());
} else {
to[key] = from[key].clone();
}
continue;
} else if (from[key] instanceof ObjectId) {
to[key] = new ObjectId(from[key]);
@@ -478,7 +483,7 @@ exports.isObject = function(arg) {
* @return {Boolean}
*/
exports.isPOJO = function(arg) {
exports.isPOJO = function isPOJO(arg) {
if (arg == null || typeof arg !== 'object') {
return false;
}
@@ -489,6 +494,37 @@ exports.isPOJO = function(arg) {
return !proto || proto.constructor.name === 'Object';
};
/*!
* Determines if `obj` is a built-in object like an array, date, boolean,
* etc.
*/
exports.isNativeObject = function(arg) {
return Array.isArray(arg) ||
arg instanceof Date ||
arg instanceof Boolean ||
arg instanceof Number ||
arg instanceof String;
};
/*!
* Search if `obj` or any POJOs nested underneath `obj` has a property named
* `key`
*/
exports.hasKey = function hasKey(obj, key) {
const props = Object.keys(obj);
for (const prop of props) {
if (prop === key) {
return true;
}
if (exports.isPOJO(obj[prop]) && exports.hasKey(obj[prop], key)) {
return true;
}
}
return false;
};
/*!
* A faster Array.prototype.slice.call(arguments) alternative
* @api private
@@ -575,39 +611,54 @@ exports.expires = function expires(object) {
delete object.expires;
};
/*!
* Populate options constructor
*/
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 (obj.count != null) {
this.count = obj.count;
}
this._docs = {};
}
// make it compatible with utils.clone
PopulateOptions.prototype.constructor = Object;
// expose
exports.PopulateOptions = PopulateOptions;
/*!
* populate helper
*/
exports.populate = function populate(path, select, model, match, options, subPopulate, justOne, count) {
// might have passed an object specifying all arguments
let obj = null;
if (arguments.length === 1) {
if (path instanceof PopulateOptions) {
return [path];
}
if (Array.isArray(path)) {
const singles = makeSingles(path);
return singles.map(o => exports.populate(o)[0]);
}
if (exports.isObject(path)) {
obj = Object.assign({}, path);
} else {
obj = { path: path };
}
} else if (typeof model === 'object') {
obj = {
path: path,
select: select,
match: model,
options: match
};
} else {
obj = {
path: path,
select: select,
model: model,
match: match,
options: options,
populate: subPopulate,
justOne: justOne,
count: count
};
}
if (typeof obj.path !== 'string') {
throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`');
}
return _populateObj(obj);
// 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).
@@ -628,47 +679,12 @@ exports.populate = function populate(path, select, model, match, options, subPop
return ret;
}
};
// might have passed an object specifying all arguments
if (arguments.length === 1) {
if (path instanceof PopulateOptions) {
return [path];
}
if (Array.isArray(path)) {
const singles = makeSingles(path);
return singles.map(function(o) {
if (o.populate && !(o.match || o.options)) {
return exports.populate(o)[0];
} else {
return exports.populate(o)[0];
}
});
}
if (exports.isObject(path)) {
match = path.match;
options = path.options;
select = path.select;
model = path.model;
subPopulate = path.populate;
justOne = path.justOne;
path = path.path;
count = path.count;
}
} else if (typeof model === 'object') {
options = match;
match = model;
model = undefined;
}
if (typeof path !== 'string') {
throw new TypeError('utils.populate: invalid path. Expected string. Got typeof `' + typeof path + '`');
}
if (Array.isArray(subPopulate)) {
function _populateObj(obj) {
if (Array.isArray(obj.populate)) {
const ret = [];
subPopulate.forEach(function(obj) {
obj.populate.forEach(function(obj) {
if (/[\s]/.test(obj.path)) {
const copy = Object.assign({}, obj);
const paths = copy.path.split(' ');
@@ -680,29 +696,23 @@ exports.populate = function populate(path, select, model, match, options, subPop
ret.push(exports.populate(obj)[0]);
}
});
subPopulate = exports.populate(ret);
} else if (typeof subPopulate === 'object') {
subPopulate = exports.populate(subPopulate);
obj.populate = exports.populate(ret);
} else if (obj.populate != null && typeof obj.populate === 'object') {
obj.populate = exports.populate(obj.populate);
}
const ret = [];
const paths = path.split(' ');
options = exports.clone(options);
const paths = obj.path.split(' ');
if (obj.options != null) {
obj.options = exports.clone(obj.options);
}
for (let i = 0; i < paths.length; ++i) {
ret.push(new PopulateOptions({
path: paths[i],
select: select,
match: match,
options: options,
model: model,
subPopulate: subPopulate,
justOne: justOne,
count: count
}));
ret.push(new PopulateOptions(Object.assign({}, obj, { path: paths[i] })));
}
return ret;
};
}
/*!
* Return the value of `obj` at the given `path`.
@@ -809,6 +819,58 @@ exports.array.flatten = function flatten(arr, filter, ret) {
return ret;
};
/*!
* ignore
*/
const _hasOwnProperty = Object.prototype.hasOwnProperty;
exports.hasUserDefinedProperty = function(obj, key) {
if (obj == null) {
return false;
}
if (Array.isArray(key)) {
for (const k of key) {
if (exports.hasUserDefinedProperty(obj, k)) {
return true;
}
}
return false;
}
if (_hasOwnProperty.call(obj, key)) {
return true;
}
if (key in obj) {
const v = obj[key];
return v !== Object.prototype[key] && v !== Array.prototype[key];
}
return false;
};
/*!
* ignore
*/
const MAX_ARRAY_INDEX = Math.pow(2, 32) - 1;
exports.isArrayIndex = function(val) {
if (typeof val === 'number') {
return val >= 0 && val <= MAX_ARRAY_INDEX;
}
if (typeof val === 'string') {
if (!/^\d+$/.test(val)) {
return false;
}
val = +val;
return val >= 0 && val <= MAX_ARRAY_INDEX;
}
return false;
};
/*!
* Removes duplicate values from an array
*
+35 -16
View File
@@ -62,16 +62,22 @@ VirtualType.prototype.clone = function() {
};
/**
* Defines a getter.
* Adds a custom getter to this virtual.
*
* Mongoose calls the getter function with 3 parameters:
*
* - `value`: the value returned by the previous getter. If there is only one getter, `value` will be `undefined`.
* - `virtual`: the virtual object you called `.get()` on
* - `doc`: the document this virtual is attached to. Equivalent to `this`.
*
* ####Example:
*
* var virtual = schema.virtual('fullname');
* virtual.get(function () {
* virtual.get(function(value, virtual, doc) {
* return this.name.first + ' ' + this.name.last;
* });
*
* @param {Function} fn
* @param {Function(Any, VirtualType, Document)} fn
* @return {VirtualType} this
* @api public
*/
@@ -82,18 +88,31 @@ VirtualType.prototype.get = function(fn) {
};
/**
* Defines a setter.
* Adds a custom setter to this virtual.
*
* Mongoose calls the setter function with 3 parameters:
*
* - `value`: the value being set
* - `virtual`: the virtual object you're calling `.set()` on
* - `doc`: the document this virtual is attached to. Equivalent to `this`.
*
* ####Example:
*
* var virtual = schema.virtual('fullname');
* virtual.set(function (v) {
* var parts = v.split(' ');
* const virtual = schema.virtual('fullname');
* virtual.set(function(value, virtual, doc) {
* var parts = value.split(' ');
* this.name.first = parts[0];
* this.name.last = parts[1];
* });
*
* @param {Function} fn
* const Model = mongoose.model('Test', schema);
* const doc = new Model();
* // Calls the setter with `value = 'Jean-Luc Picard'`
* doc.fullname = 'Jean-Luc Picard';
* doc.name.first; // 'Jean-Luc'
* doc.name.last; // 'Picard'
*
* @param {Function(Any, VirtualType, Document)} fn
* @return {VirtualType} this
* @api public
*/
@@ -104,35 +123,35 @@ VirtualType.prototype.set = function(fn) {
};
/**
* Applies getters to `value` using optional `scope`.
* Applies getters to `value`.
*
* @param {Object} value
* @param {Object} scope
* @param {Document} doc The document this virtual is attached to
* @return {any} the value after applying all getters
* @api public
*/
VirtualType.prototype.applyGetters = function(value, scope) {
VirtualType.prototype.applyGetters = function(value, doc) {
let v = value;
for (let l = this.getters.length - 1; l >= 0; l--) {
v = this.getters[l].call(scope, v, this);
v = this.getters[l].call(doc, v, this, doc);
}
return v;
};
/**
* Applies setters to `value` using optional `scope`.
* Applies setters to `value`.
*
* @param {Object} value
* @param {Object} scope
* @param {Document} doc
* @return {any} the value after applying all setters
* @api public
*/
VirtualType.prototype.applySetters = function(value, scope) {
VirtualType.prototype.applySetters = function(value, doc) {
let v = value;
for (let l = this.setters.length - 1; l >= 0; l--) {
v = this.setters[l].call(scope, v, this);
v = this.setters[l].call(doc, v, this, doc);
}
return v;
};