Simple Chat Room in NodeJS, expressJS and mongoDB in Docker Swarm
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

'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;