You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
958 lines
25 KiB
958 lines
25 KiB
4 years ago
|
'use strict';
|
||
|
|
||
|
const Document = require('../document');
|
||
|
const EmbeddedDocument = require('./embedded');
|
||
|
const MongooseError = require('../error/mongooseError');
|
||
|
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 slicedSymbol = Symbol('mongoose#Array#sliced');
|
||
|
|
||
|
const _basePush = Array.prototype.push;
|
||
|
|
||
|
const validatorsSymbol = Symbol('mongoose#MongooseCoreArray#validators');
|
||
|
|
||
|
/*!
|
||
|
* ignore
|
||
|
*/
|
||
|
|
||
|
class CoreMongooseArray extends Array {
|
||
|
get isMongooseArray() {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
get validators() {
|
||
|
return this[validatorsSymbol];
|
||
|
}
|
||
|
|
||
|
set validators(v) {
|
||
|
this[validatorsSymbol] = v;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (dirtyPath != null && dirtyPath.endsWith('.$')) {
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
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 (this[slicedSymbol]) {
|
||
|
return;
|
||
|
}
|
||
|
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: [] };
|
||
|
if (val != null && utils.hasUserDefinedProperty(val, '$each')) {
|
||
|
atomics.$push = val;
|
||
|
} else {
|
||
|
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, fromIndex) {
|
||
|
const ret = this.indexOf(obj, fromIndex);
|
||
|
return ret !== -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, fromIndex) {
|
||
|
if (obj instanceof ObjectId) {
|
||
|
obj = obj.toString();
|
||
|
}
|
||
|
|
||
|
fromIndex = fromIndex == null ? 0 : fromIndex;
|
||
|
const len = this.length;
|
||
|
for (let i = fromIndex; 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.
|
||
|
*
|
||
|
* ####Example:
|
||
|
*
|
||
|
* const schema = Schema({ nums: [Number] });
|
||
|
* const Model = mongoose.model('Test', schema);
|
||
|
*
|
||
|
* const doc = await Model.create({ nums: [3, 4] });
|
||
|
* doc.nums.push(5); // Add 5 to the end of the array
|
||
|
* await doc.save();
|
||
|
*
|
||
|
* // You can also pass an object with `$each` as the
|
||
|
* // first parameter to use MongoDB's `$position`
|
||
|
* doc.nums.push({
|
||
|
* $each: [1, 2],
|
||
|
* $position: 0
|
||
|
* });
|
||
|
* doc.nums; // [1, 2, 3, 4, 5]
|
||
|
*
|
||
|
* @param {Object} [args...]
|
||
|
* @api public
|
||
|
* @method push
|
||
|
* @memberOf MongooseArray
|
||
|
*/
|
||
|
|
||
|
push() {
|
||
|
let values = arguments;
|
||
|
let atomic = values;
|
||
|
const isOverwrite = values[0] != null &&
|
||
|
utils.hasUserDefinedProperty(values[0], '$each');
|
||
|
if (isOverwrite) {
|
||
|
atomic = values[0];
|
||
|
values = values[0].$each;
|
||
|
}
|
||
|
|
||
|
if (this[arraySchemaSymbol] == null) {
|
||
|
return _basePush.apply(this, values);
|
||
|
}
|
||
|
|
||
|
_checkManualPopulation(this, values);
|
||
|
|
||
|
const parent = this[arrayParentSymbol];
|
||
|
values = [].map.call(values, this._mapCast, this);
|
||
|
values = this[arraySchemaSymbol].applySetters(values, parent, undefined,
|
||
|
undefined, { skipDocumentArrayCast: true });
|
||
|
let ret;
|
||
|
const atomics = this[arrayAtomicsSymbol];
|
||
|
|
||
|
if (isOverwrite) {
|
||
|
atomic.$each = values;
|
||
|
|
||
|
if (get(atomics, '$push.$each.length', 0) > 0 &&
|
||
|
atomics.$push.$position != atomics.$position) {
|
||
|
throw new MongooseError('Cannot call `Array#push()` multiple times ' +
|
||
|
'with different `$position`');
|
||
|
}
|
||
|
|
||
|
if (atomic.$position != null) {
|
||
|
[].splice.apply(this, [atomic.$position, 0].concat(values));
|
||
|
ret = this.length;
|
||
|
} else {
|
||
|
ret = [].push.apply(this, values);
|
||
|
}
|
||
|
} else {
|
||
|
if (get(atomics, '$push.$each.length', 0) > 0 &&
|
||
|
atomics.$push.$position != null) {
|
||
|
throw new MongooseError('Cannot call `Array#push()` multiple times ' +
|
||
|
'with different `$position`');
|
||
|
}
|
||
|
atomic = values;
|
||
|
ret = [].push.apply(this, values);
|
||
|
}
|
||
|
this._registerAtomic('$push', atomic);
|
||
|
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
|
||
|
* @see https://masteringjs.io/tutorials/fundamentals/array-sort
|
||
|
*/
|
||
|
|
||
|
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
|
||
|
* @see https://masteringjs.io/tutorials/fundamentals/array-splice
|
||
|
*/
|
||
|
|
||
|
splice() {
|
||
|
let ret;
|
||
|
|
||
|
_checkManualPopulation(this, Array.prototype.slice.call(arguments, 2));
|
||
|
|
||
|
if (arguments.length) {
|
||
|
let vals;
|
||
|
if (this[arraySchemaSymbol] == null) {
|
||
|
vals = arguments;
|
||
|
} else {
|
||
|
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
|
||
|
*/
|
||
|
|
||
|
slice() {
|
||
|
const ret = super.slice.apply(this, arguments);
|
||
|
ret[arrayParentSymbol] = this[arrayParentSymbol];
|
||
|
ret[arraySchemaSymbol] = this[arraySchemaSymbol];
|
||
|
ret[arrayAtomicsSymbol] = this[arrayAtomicsSymbol];
|
||
|
ret[slicedSymbol] = true;
|
||
|
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 overwriting 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;
|
||
|
if (this[arraySchemaSymbol] == null) {
|
||
|
values = arguments;
|
||
|
} else {
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (util.inspect.custom) {
|
||
|
CoreMongooseArray.prototype[util.inspect.custom] =
|
||
|
CoreMongooseArray.prototype.inspect;
|
||
|
}
|
||
|
|
||
|
/*!
|
||
|
* ignore
|
||
|
*/
|
||
|
|
||
|
function _isAllSubdocs(docs, ref) {
|
||
|
if (!ref) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
for (const arg of docs) {
|
||
|
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;
|