Source: Document.js

"use strict";
// TODO: Remove the _update* methods because we are already mixing them
// TODO: in now via Mixin.Updating and update autobind to extend the _update*
// TODO: methods like we already do with collection
var Shared,
	Collection,
	Db;

Shared = require('./Shared');

/**
 * Creates a new Document instance. Documents allow you to create individual
 * objects that can have standard ForerunnerDB CRUD operations run against
 * them, as well as data-binding if the AutoBind module is included in your
 * project.
 * @name Document
 * @class Document
 * @constructor
 */
var FdbDocument = function () {
	this.init.apply(this, arguments);
};

FdbDocument.prototype.init = function (name) {
	this._name = name;
	this._data = {};
};

Shared.addModule('Document', FdbDocument);
Shared.mixin(FdbDocument.prototype, 'Mixin.Common');
Shared.mixin(FdbDocument.prototype, 'Mixin.Events');
Shared.mixin(FdbDocument.prototype, 'Mixin.ChainReactor');
Shared.mixin(FdbDocument.prototype, 'Mixin.Constants');
Shared.mixin(FdbDocument.prototype, 'Mixin.Triggers');
Shared.mixin(FdbDocument.prototype, 'Mixin.Matching');
Shared.mixin(FdbDocument.prototype, 'Mixin.Updating');

Collection = require('./Collection');
Db = Shared.modules.Db;

/**
 * Gets / sets the current state.
 * @func state
 * @memberof Document
 * @param {String=} val The name of the state to set.
 * @returns {*}
 */
Shared.synthesize(FdbDocument.prototype, 'state');

/**
 * Gets / sets the db instance this class instance belongs to.
 * @func db
 * @memberof Document
 * @param {Db=} db The db instance.
 * @returns {*}
 */
Shared.synthesize(FdbDocument.prototype, 'db');

/**
 * Gets / sets the document name.
 * @func name
 * @memberof Document
 * @param {String=} val The name to assign
 * @returns {*}
 */
Shared.synthesize(FdbDocument.prototype, 'name');

/**
 * Sets the data for the document.
 * @func setData
 * @memberof Document
 * @param data
 * @param options
 * @returns {Document}
 */
FdbDocument.prototype.setData = function (data, options) {
	var i,
		$unset;

	if (data) {
		options = options || {
			$decouple: true
		};

		if (options && options.$decouple === true) {
			data = this.decouple(data);
		}

		if (this._linked) {
			$unset = {};

			// Remove keys that don't exist in the new data from the current object
			for (i in this._data) {
				if (i.substr(0, 6) !== 'jQuery' && this._data.hasOwnProperty(i)) {
					// Check if existing data has key
					if (data[i] === undefined) {
						// Add property name to those to unset
						$unset[i] = 1;
					}
				}
			}

			data.$unset = $unset;

			// Now update the object with new data
			this.updateObject(this._data, data, {});
		} else {
			// Straight data assignment
			this._data = data;
		}

		this.deferEmit('change', {type: 'setData', data: this.decouple(this._data)});
	}

	return this;
};

/**
 * Gets the document's data returned as a single object.
 * @func find
 * @memberof Document
 * @param {Object} query The query object - currently unused, just
 * provide a blank object e.g. {}
 * @param {Object=} options An options object.
 * @returns {Object} The document's data object.
 */
FdbDocument.prototype.find = function (query, options) {
	var result;

	if (options && options.$decouple === false) {
		result = this._data;
	} else {
		result = this.decouple(this._data);
	}

	return result;
};

/**
 * Modifies the document. This will update the document with the data held in 'update'.
 * @func update
 * @memberof Document
 * @param {Object} query The query that must be matched for a document to be
 * operated on.
 * @param {Object} update The object containing updated key/values. Any keys that
 * match keys on the existing document will be overwritten with this data. Any
 * keys that do not currently exist on the document will be added to the document.
 * @param {Object=} options An options object.
 * @returns {Array} The items that were updated.
 */
FdbDocument.prototype.update = function (query, update, options) {
	var result = this.updateObject(this._data, update, query, options);

	if (result) {
		this.deferEmit('change', {type: 'update', data: this.decouple(this._data)});
	}
};

/**
 * Internal method for document updating.
 * @func updateObject
 * @memberof Document
 * @param {Object} doc The document to update.
 * @param {Object} update The object with key/value pairs to update the document with.
 * @param {Object} query The query object that we need to match to perform an update.
 * @param {Object} options An options object.
 * @param {String} path The current recursive path.
 * @param {String} opType The type of update operation to perform, if none is specified
 * default is to set new data against matching fields.
 * @returns {Boolean} True if the document was updated with new / changed data or
 * false if it was not updated because the data was the same.
 * @private
 */
FdbDocument.prototype.updateObject = Collection.prototype.updateObject;

/**
 * Determines if the passed key has an array positional mark (a dollar at the end
 * of its name).
 * @func _isPositionalKey
 * @memberof Document
 * @param {String} key The key to check.
 * @returns {Boolean} True if it is a positional or false if not.
 * @private
 */
FdbDocument.prototype._isPositionalKey = function (key) {
	return key.substr(key.length - 2, 2) === '.$';
};

/**
 * Updates a property on an object depending on if the collection is
 * currently running data-binding or not.
 * @func _updateProperty
 * @memberof Document
 * @param {Object} doc The object whose property is to be updated.
 * @param {String} prop The property to update.
 * @param {*} val The new value of the property.
 * @private
 */
FdbDocument.prototype._updateProperty = function (doc, prop, val) {
	if (this._linked) {
		window.jQuery.observable(doc).setProperty(prop, val);

		if (this.debug()) {
			console.log(this.logIdentifier() + ' Setting data-bound document property "' + prop + '"');
		}
	} else {
		doc[prop] = val;

		if (this.debug()) {
			console.log(this.logIdentifier() + ' Setting non-data-bound document property "' + prop + '"');
		}
	}
};

/**
 * Increments a value for a property on a document by the passed number.
 * @func _updateIncrement
 * @memberof Document
 * @param {Object} doc The document to modify.
 * @param {String} prop The property to modify.
 * @param {Number} val The amount to increment by.
 * @private
 */
FdbDocument.prototype._updateIncrement = function (doc, prop, val) {
	if (this._linked) {
		window.jQuery.observable(doc).setProperty(prop, doc[prop] + val);
	} else {
		doc[prop] += val;
	}
};

/**
 * Changes the index of an item in the passed array.
 * @func _updateSpliceMove
 * @memberof Document
 * @param {Array} arr The array to modify.
 * @param {Number} indexFrom The index to move the item from.
 * @param {Number} indexTo The index to move the item to.
 * @private
 */
FdbDocument.prototype._updateSpliceMove = function (arr, indexFrom, indexTo) {
	if (this._linked) {
		window.jQuery.observable(arr).move(indexFrom, indexTo);

		if (this.debug()) {
			console.log(this.logIdentifier() + ' Moving data-bound document array index from "' + indexFrom + '" to "' + indexTo + '"');
		}
	} else {
		arr.splice(indexTo, 0, arr.splice(indexFrom, 1)[0]);

		if (this.debug()) {
			console.log(this.logIdentifier() + ' Moving non-data-bound document array index from "' + indexFrom + '" to "' + indexTo + '"');
		}
	}
};

/**
 * Inserts an item into the passed array at the specified index.
 * @func _updateSplicePush
 * @memberof Document
 * @param {Array} arr The array to insert into.
 * @param {Number} index The index to insert at.
 * @param {Object} doc The document to insert.
 * @private
 */
FdbDocument.prototype._updateSplicePush = function (arr, index, doc) {
	if (arr.length > index) {
		if (this._linked) {
			window.jQuery.observable(arr).insert(index, doc);
		} else {
			arr.splice(index, 0, doc);
		}
	} else {
		if (this._linked) {
			window.jQuery.observable(arr).insert(doc);
		} else {
			arr.push(doc);
		}
	}
};

/**
 * Inserts an item at the end of an array.
 * @func _updatePush
 * @memberof Document
 * @param {Array} arr The array to insert the item into.
 * @param {Object} doc The document to insert.
 * @private
 */
FdbDocument.prototype._updatePush = function (arr, doc) {
	if (this._linked) {
		window.jQuery.observable(arr).insert(doc);
	} else {
		arr.push(doc);
	}
};

/**
 * Removes an item from the passed array.
 * @func _updatePull
 * @memberof Document
 * @param {Array} arr The array to modify.
 * @param {Number} index The index of the item in the array to remove.
 * @private
 */
FdbDocument.prototype._updatePull = function (arr, index) {
	if (this._linked) {
		window.jQuery.observable(arr).remove(index);
	} else {
		arr.splice(index, 1);
	}
};

/**
 * Multiplies a value for a property on a document by the passed number.
 * @func _updateMultiply
 * @memberof Document
 * @param {Object} doc The document to modify.
 * @param {String} prop The property to modify.
 * @param {Number} val The amount to multiply by.
 * @private
 */
FdbDocument.prototype._updateMultiply = function (doc, prop, val) {
	if (this._linked) {
		window.jQuery.observable(doc).setProperty(prop, doc[prop] * val);
	} else {
		doc[prop] *= val;
	}
};

/**
 * Renames a property on a document to the passed property.
 * @func _updateRename
 * @memberof Document
 * @param {Object} doc The document to modify.
 * @param {String} prop The property to rename.
 * @param {Number} val The new property name.
 * @private
 */
FdbDocument.prototype._updateRename = function (doc, prop, val) {
	var existingVal = doc[prop];
	if (this._linked) {
		window.jQuery.observable(doc).setProperty(val, existingVal);
		window.jQuery.observable(doc).removeProperty(prop);
	} else {
		doc[val] = existingVal;
		delete doc[prop];
	}
};

/**
 * Deletes a property on a document.
 * @func _updateUnset
 * @memberof Document
 * @param {Object} doc The document to modify.
 * @param {String} prop The property to delete.
 * @private
 */
FdbDocument.prototype._updateUnset = function (doc, prop) {
	if (this._linked) {
		window.jQuery.observable(doc).removeProperty(prop);
	} else {
		delete doc[prop];
	}
};

/**
 * Drops the document.
 * @func drop
 * @memberof Document
 * @returns {boolean} True if successful, false if not.
 */
FdbDocument.prototype.drop = function () {
	if (!this.isDropped()) {
		if (this._db && this._name) {
			if (this._db && this._db._document && this._db._document[this._name]) {
				this._state = 'dropped';

				delete this._db._document[this._name];
				delete this._data;

				this.emit('drop', this);

				return true;
			}
		}
	} else {
		return true;
	}

	return false;
};

/**
 * Creates a new document instance.
 * @func document
 * @memberof Db
 * @param {String} documentName The name of the document to create.
 * @returns {*}
 */
Db.prototype.document = function (documentName) {
	if (documentName) {
		// Handle being passed an instance
		if (documentName instanceof FdbDocument) {
			if (documentName.state() !== 'droppped') {
				return documentName;
			} else {
				documentName = documentName.name();
			}
		}

		this._document = this._document || {};
		this._document[documentName] = this._document[documentName] || new FdbDocument(documentName).db(this);
		return this._document[documentName];
	} else {
		// Return an object of document data
		return this._document;
	}
};

/**
 * Returns an array of documents the DB currently has.
 * @func documents
 * @memberof Db
 * @returns {Array} An array of objects containing details of each document
 * the database is currently managing.
 */
Db.prototype.documents = function () {
	var arr = [],
		item,
		i;

	for (i in this._document) {
		if (this._document.hasOwnProperty(i)) {
			item = this._document[i];

			arr.push({
				name: i,
				linked: item.isLinked !== undefined ? item.isLinked() : false
			});
		}
	}

	return arr;
};

Shared.finishModule('Document');
module.exports = FdbDocument;