lib/types/array.js
'use strict';
//TODO: почистить код
/*!
* Module dependencies.
*/
var EmbeddedDocument = require('./embedded');
var Document = require('../document');
var ObjectId = require('./objectid');
/**
* Storage Array constructor.
*
* ####NOTE:
*
* _Values always have to be passed to the constructor to initialize, otherwise `StorageArray#push` will mark the array as modified._
*
* @param {Array} values
* @param {String} path
* @param {Document} doc parent document
* @api private
* @inherits Array
*/
function StorageArray (values, path, doc) {
var arr = [].concat(values);
_.mixin( arr, StorageArray.mixin );
arr.validators = [];
arr._path = path;
arr.isStorageArray = true;
if (doc) {
arr._parent = doc;
arr._schema = doc.schema.path(path);
}
return arr;
}
StorageArray.mixin = {
/**
* Parent owner document
*
* @property _parent
* @api private
*/
_parent: undefined,
/**
* Casts a member based on this arrays schema.
*
* @param {*} value
* @return value the casted value
* @api private
*/
_cast: function ( value ) {
var owner = this._owner;
var populated = false;
if (this._parent) {
// if a populated array, we must cast to the same model
// instance as specified in the original query.
if (!owner) {
owner = this._owner = this._parent.ownerDocument
? this._parent.ownerDocument()
: this._parent;
}
populated = owner.populated(this._path, true);
}
if (populated && null != value) {
// cast to the populated Models schema
var Model = populated.options.model;
// only objects are permitted so we can safely assume that
// non-objects are to be interpreted as _id
if ( value instanceof ObjectId || !_.isObject(value) ) {
value = { _id: value };
}
// gh-2399
// we should cast model only when it's not a discriminator
var isDisc = value.schema && value.schema.discriminatorMapping &&
value.schema.discriminatorMapping.key !== undefined;
if (!isDisc) {
value = new Model(value);
}
return this._schema.caster.cast(value, this._parent, true);
}
return this._schema.caster.cast(value, this._parent, 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
* @api private
*/
_markModified: function (elem, embeddedPath) {
var parent = this._parent
, dirtyPath;
if (parent) {
dirtyPath = this._path;
if (arguments.length) {
if (null != embeddedPath) {
// an embedded doc bubbled up the change
dirtyPath = dirtyPath + '.' + this.indexOf(elem) + '.' + embeddedPath;
} else {
// directly set an index
dirtyPath = dirtyPath + '.' + elem;
}
}
parent.markModified(dirtyPath);
}
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
*/
push: function () {
var values = [].map.call(arguments, this._cast, this)
, ret = [].push.apply(this, values);
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 StorageArray#$pop #types_array_StorageArray-%24pop
* @api public
*/
pop: function () {
var ret = [].pop.call(this);
this._markModified();
return ret;
},
/**
* 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
*/
shift: function () {
var ret = [].shift.call(this);
this._markModified();
return ret;
},
/**
* Pulls items from the array atomically.
*
* ####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 storage take care of it.
*
* doc.subdocs.push({ _id: 4815162342 })
* doc.subdocs.pull(4815162342); // works
*
* @param {*} arguments
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
* @api public
*/
pull: function () {
var values = [].map.call(arguments, this._cast, this)
, cur = this._parent.get(this._path)
, i = cur.length
, mem;
while (i--) {
mem = cur[i];
if (mem instanceof EmbeddedDocument) {
if (values.some(function (v) { return v.equals(mem); } )) {
[].splice.call(cur, i, 1);
}
} else if (~cur.indexOf.call(values, mem)) {
[].splice.call(cur, i, 1);
}
}
this._markModified();
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
*/
splice: function splice () {
var ret, vals, i;
if (arguments.length) {
vals = [];
for (i = 0; i < arguments.length; ++i) {
vals[i] = i < 2
? arguments[i]
: this._cast(arguments[i]);
}
ret = [].splice.apply(this, vals);
this._markModified();
}
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
*/
unshift: function () {
var values = [].map.call(arguments, this._cast, this);
[].unshift.apply(this, values);
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
*/
sort: function () {
var ret = [].sort.apply(this, arguments);
this._markModified();
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 {*} arguments
* @return {Array} the values that were added
* @api public
*/
addToSet: function addToSet () {
var values = [].map.call(arguments, this._cast, this)
, added = []
, type = values[0] instanceof EmbeddedDocument ? 'doc' :
values[0] instanceof Date ? 'date' :
'';
values.forEach(function (v) {
var found;
switch (type) {
case 'doc':
found = this.some(function(doc){ return doc.equals(v); });
break;
case 'date':
var val = +v;
found = this.some(function(d){ return +d === val; });
break;
default:
found = ~this.indexOf(v);
}
if (!found) {
[].push.call(this, 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 docs = storage.createCollection('Doc', new Schema({ array: [Number] }));
*
* var doc = docs.add({ 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
*/
set: function (i, val) {
this[i] = this._cast(val);
this._markModified(i);
return this;
},
/**
* Returns a native js Array.
*
* @param {Object} options
* @return {Array}
* @api public
*/
toObject: function (options) {
if (options && options.depopulate) {
return this.map(function (doc) {
return doc instanceof Document
? doc.toObject(options)
: doc;
});
}
return this.slice();
},
/**
* Return the index of `obj` or `-1` if not found.
*
* @param {Object} obj the item to look for
* @return {Number}
* @api public
*/
indexOf: function indexOf (obj) {
if (obj instanceof ObjectId) obj = obj.toString();
for (var i = 0, len = this.length; i < len; ++i) {
if (obj == this[i])
return i;
}
return -1;
}
};
/**
* Alias of [pull](#types_array_StorageArray-pull)
*
* @see StorageArray#pull #types_array_StorageArray-pull
* @see mongodb http://www.mongodb.org/display/DOCS/Updating/#Updating-%24pull
* @api public
* @memberOf StorageArray
* @method remove
*/
StorageArray.mixin.remove = StorageArray.mixin.pull;
/*!
* Module exports.
*/
module.exports = StorageArray;