Source: NodePersist.js

"use strict";

// Import external names locally
var Shared = require('./Shared'),
	fs = require('fs'),
	async = require('async'),
	FdbCompress = require('./PersistCompress'),// jshint ignore:line
	FdbCrypto = require('./PersistCrypto'),// jshint ignore:line
	Db,
	Collection,
	CollectionDrop,
	CollectionGroup,
	CollectionInit,
	DbInit,
	DbDrop,
	NodePersist,
	Overload;

NodePersist = function () {
	this.init.apply(this, arguments);
};

/**
 * The init method that can be overridden or extended.
 * @param {Db} db The ForerunnerDB database instance.
 */
NodePersist.prototype.init = function (db) {
	var self = this;

	// Default to saving data via files
	this.mode('file');

	this._encodeSteps = [
		function () { return self._encode.apply(self, arguments); }
	];
	this._decodeSteps = [
		function () { return self._decode.apply(self, arguments); }
	];
};

Shared.addModule('NodePersist', NodePersist);
Shared.mixin(NodePersist.prototype, 'Mixin.Common');
Shared.mixin(NodePersist.prototype, 'Mixin.ChainReactor');

Db = Shared.modules.Db;
Collection = require('./Collection');
CollectionDrop = Collection.prototype.drop;
CollectionGroup = require('./CollectionGroup');
CollectionInit = Collection.prototype.init;
DbInit = Db.prototype.init;
DbDrop = Db.prototype.drop;
Overload = Shared.overload;

/**
 * Gets / sets the persistent storage mode.
 * @param {String} type The library to use for storage. Defaults
 * to file.
 * @returns {*}
 */
NodePersist.prototype.mode = function (type) {
	if (type !== undefined) {
		this._mode = type;
		return this;
	}

	return this._mode;
};

/**
 * Starts a decode waterfall process.
 * @param {*} val The data to be decoded.
 * @param {Function} finished The callback to pass final data to.
 */
NodePersist.prototype.decode = function (val, finished) {
	async.waterfall([function (callback) {
		callback(false, val, {});
	}].concat(this._decodeSteps), finished);
};

/**
 * Starts an encode waterfall process.
 * @param {*} val The data to be encoded.
 * @param {Function} finished The callback to pass final data to.
 */
NodePersist.prototype.encode = function (val, finished) {
	async.waterfall([function (callback) {
		callback(false, val, {});
	}].concat(this._encodeSteps), finished);
};

Shared.synthesize(NodePersist.prototype, 'encodeSteps');
Shared.synthesize(NodePersist.prototype, 'decodeSteps');

/**
 * Adds an encode/decode step to the persistent storage system so
 * that you can add custom functionality.
 * @param {Function} encode The encode method called with the data from the
 * previous encode step. When your method is complete it MUST call the
 * callback method. If you provide anything other than false to the err
 * parameter the encoder will fail and throw an error.
 * @param {Function} decode The decode method called with the data from the
 * previous decode step. When your method is complete it MUST call the
 * callback method. If you provide anything other than false to the err
 * parameter the decoder will fail and throw an error.
 * @param {Number=} index Optional index to add the encoder step to. This
 * allows you to place a step before or after other existing steps. If not
 * provided your step is placed last in the list of steps. For instance if
 * you are providing an encryption step it makes sense to place this last
 * since all previous steps will then have their data encrypted by your
 * final step.
 */
NodePersist.prototype.addStep = new Overload({
	'object': function (obj) {
		this.$main.call(this, function objEncode () { obj.encode.apply(obj, arguments); }, function objDecode () { obj.decode.apply(obj, arguments); }, 0);
	},

	'function, function': function (encode, decode) {
		this.$main.call(this, encode, decode, 0);
	},

	'function, function, number': function (encode, decode, index) {
		this.$main.call(this, encode, decode, index);
	},

	$main: function (encode, decode, index) {
		if (index === 0 || index === undefined) {
			this._encodeSteps.push(encode);
			this._decodeSteps.unshift(decode);
		} else {
			// Place encoder step at index then work out correct
			// index to place decoder step
			this._encodeSteps.splice(index, 0, encode);
			this._decodeSteps.splice(this._decodeSteps.length - index, 0, decode);
		}
	}
});

NodePersist.prototype.unwrap = function (dataStr) {
	var parts = dataStr.split('::fdb::'),
		data;

	switch (parts[0]) {
		case 'json':
			data = this.jParse(parts[1]);
			break;

		case 'raw':
			data = parts[1];
			break;

		default:
			break;
	}
};

/**
 * Takes encoded data and decodes it for use as JS native objects and arrays.
 * @param {String} val The currently encoded string data.
 * @param {Object} meta Meta data object that can be used to pass back useful
 * supplementary data.
 * @param {Function} finished The callback method to call when decoding is
 * completed.
 * @private
 */
NodePersist.prototype._decode = function (val, meta, finished) {
	var parts,
		data;

	if (val) {
		parts = val.split('::fdb::');

		switch (parts[0]) {
			case 'json':
				data = this.jParse(parts[1]);
				break;

			case 'raw':
				data = parts[1];
				break;

			default:
				break;
		}

		if (data) {
			meta.foundData = true;
			meta.rowCount = data.length;
		} else {
			meta.foundData = false;
		}

		if (finished) {
			finished(false, data, meta);
		}
	} else {
		meta.foundData = false;
		meta.rowCount = 0;

		if (finished) {
			finished(false, val, meta);
		}
	}
};

/**
 * Takes native JS data and encodes it for for storage as a string.
 * @param {Object} val The current un-encoded data.
 * @param {Object} meta Meta data object that can be used to pass back useful
 * supplementary data.
 * @param {Function} finished The callback method to call when encoding is
 * completed.
 * @private
 */
NodePersist.prototype._encode = function (val, meta, finished) {
	var data = val;

	if (typeof val === 'object') {
		val = 'json::fdb::' + this.jStringify(val);
	} else {
		val = 'raw::fdb::' + val;
	}

	if (data) {
		meta.foundData = true;
		meta.rowCount = data.length;
	} else {
		meta.foundData = false;
	}

	if (finished) {
		finished(false, val, meta);
	}
};

/**
 * Encodes passed data and then stores it in the browser's persistent
 * storage layer.
 * @param {String} key The key to store the data under in the persistent
 * storage.
 * @param {Object} data The data to store under the key.
 * @param {Function=} callback The method to call when the save process
 * has completed.
 */
NodePersist.prototype.save = function (key, data, callback) {
	var self = this;

	switch (this.mode()) {
		case 'file':
			this.encode(data, function (err, data, tableStats) {
				self.saveDataFile(key, data, function (err, data) {
					if (!err) {
						if (callback) {
							callback(false, data, tableStats);
						}
					} else {
						if (callback) { callback(err); }
					}
				});
			});
			break;

		default:
			if (callback) { callback('No data handler.'); }
			break;
	}
};

/**
 * Loads and decodes data from the passed key.
 * @param {String} key The key to retrieve data from in the persistent
 * storage.
 * @param {Function=} callback The method to call when the load process
 * has completed.
 */
NodePersist.prototype.load = function (key, callback) {
	var self = this;

	switch (this.mode()) {
		case 'file':
			self.loadDataFile(key, function (err, val) {
				if (!err) {
					self.decode(val, callback);
				} else {
					if (callback) { callback(err); }
				}
			});
			break;

		default:
			if (callback) { callback('No data handler or unrecognised data type.');	}
			break;
	}
};

/**
 * Deletes data in persistent storage stored under the passed key.
 * @param {String} key The key to drop data for in the storage.
 * @param {Function=} callback The method to call when the data is dropped.
 */
NodePersist.prototype.drop = function (key, callback) {
	var self = this;

	switch (this.mode()) {
		case 'file':
			self.removeDataFile(key, function (err) {
				if (callback) { callback(err); }
			});
			break;

		default:
			if (callback) {
				callback('No data handler or unrecognised data type.');
			}
			break;
	}

};

Shared.synthesize(NodePersist.prototype, 'dataDir', function (val) {
	if (val !== undefined) {
		// Ensure the folder exists
		fs.stat(val, function (err, stats) {
			if (!err) {
				if (!stats.isDirectory() && !stats.isFile()) {
					fs.mkdir(val);
				}
			} else {
				try {
					fs.mkdir(val);
				} catch (e) {

				}
			}
		});
	}

	return this.$super.call(this, val);
});

NodePersist.prototype.saveDataFile = function (key, data, callback) {
	fs.writeFile(this.dataDir() + "/" + key + '.fdb', data, callback);
};

NodePersist.prototype.loadDataFile = function (key, callback) {
	fs.readFile(this.dataDir() + "/" + key + '.fdb', 'utf8', callback);
};

NodePersist.prototype.removeDataFile = function (key, callback) {
	fs.unlinkFile(this.dataDir() + "/" + key + '.fdb', callback);
};

// Extend the Collection prototype with persist methods
Collection.prototype.drop = new Overload({
	/**
	 * Drop collection and persistent storage.
	 */
	'': function () {
		if (!this.isDropped()) {
			this.drop(true);
		}
	},

	/**
	 * Drop collection and persistent storage with callback.
	 * @param {Function} callback Callback method.
	 */
	'function': function (callback) {
		if (!this.isDropped()) {
			this.drop(true, callback);
		}
	},

	/**
	 * Drop collection and optionally drop persistent storage.
	 * @param {Boolean} removePersistent True to drop persistent storage, false to keep it.
	 */
	'boolean': function (removePersistent) {
		if (!this.isDropped()) {
			// Remove persistent storage
			if (removePersistent) {
				if (this._name) {
					if (this._db) {
						// Drop the collection data from storage
						this._db.persist.drop(this._db._name + '::' + this._name);
						this._db.persist.drop(this._db._name + '::' + this._name + '::metaData');
					}
				} else {
					throw('ForerunnerDB.NodePersist: Cannot drop a collection\'s persistent storage when no name assigned to collection!');
				}
			}

			// Call the original method
			CollectionDrop.apply(this);
		}
	},

	/**
	 * Drop collections and optionally drop persistent storage with callback.
	 * @param {Boolean} removePersistent True to drop persistent storage, false to keep it.
	 * @param {Function} callback Callback method.
	 */
	'boolean, function': function (removePersistent, callback) {
		var self = this;

		if (!this.isDropped()) {
			// Remove persistent storage
			if (removePersistent) {
				if (this._name) {
					if (this._db) {
						// Drop the collection data from storage
						this._db.persist.drop(this._db._name + '::' + this._name, function () {
							self._db.persist.drop(self._db._name + '::' + self._name + '::metaData', callback);
						});
					} else {
						if (callback) {
							callback('Cannot drop a collection\'s persistent storage when the collection is not attached to a database!');
						}
					}
				} else {
					if (callback) {
						callback('Cannot drop a collection\'s persistent storage when no name assigned to collection!');
					}
				}
			}

			// Call the original method
			CollectionDrop.apply(this, callback);
		}
	}
});

/**
 * Saves an entire collection's data to persistent storage.
 * @param {Function=} callback The method to call when the save function
 * has completed.
 */
Collection.prototype.save = function (callback) {
	var self = this,
		processSave;

	if (self._name) {
		if (self._db) {
			processSave = function () {
				// Save the collection data
				self._db.persist.save(self._db._name + '::' + self._name, self._data, function (err, data, tableStats) {
					if (!err) {
						self._db.persist.save(self._db._name + '::' + self._name + '::metaData', self.metaData(), function (err, data, metaStats) {
							if (callback) {
								callback(err, data, tableStats, metaStats);
							}
						});
					} else {
						if (callback) {
							callback(err);
						}
					}
				});
			};

			// Check for processing queues
			if (self.isProcessingQueue()) {
				// Hook queue complete to process save
				self.on('queuesComplete', function () {
					processSave();
				});
			} else {
				// Process save immediately
				processSave();
			}
		} else {
			if (callback) {
				callback('Cannot save a collection that is not attached to a database!');
			}
		}
	} else {
		if (callback) {
			callback('Cannot save a collection with no assigned name!');
		}
	}
};

/**
 * Loads an entire collection's data from persistent storage.
 * @param {Function=} callback The method to call when the load function
 * has completed.
 */
Collection.prototype.load = function (callback) {
	var self = this;

	if (self._name) {
		if (self._db) {
			// Load the collection data
			self._db.persist.load(self._db._name + '::' + self._name, function (err, data, tableStats) {
				if (!err) {
					if (data) {
						self.setData(data);
					}

					// Now load the collection's metadata
					self._db.persist.load(self._db._name + '::' + self._name + '::metaData', function (err, data, metaStats) {
						if (!err) {
							if (data) {
								self.metaData(data);
							}
						}

						if (callback) {
							callback(err, tableStats, metaStats);
						}
					});
				} else {
					if (callback) {
						callback(err);
					}
				}
			});
		} else {
			if (callback) {
				callback('Cannot load a collection that is not attached to a database!');
			}
		}
	} else {
		if (callback) {
			callback('Cannot load a collection with no assigned name!');
		}
	}
};

// Override the DB init to instantiate the plugin
Db.prototype.init = function () {
	DbInit.apply(this, arguments);
	this.persist = new NodePersist(this);
};

/**
 * Loads an entire database's data from persistent storage.
 * @param {Function=} callback The method to call when the load function
 * has completed.
 */
Db.prototype.load = function (callback) {
	// Loop the collections in the database
	var obj = this._collection,
		keys = obj.keys(),
		keyCount = keys.length,
		loadCallback,
		index;

	loadCallback = function (err) {
		if (!err) {
			keyCount--;

			if (keyCount === 0) {
				if (callback) { callback(false); }
			}
		} else {
			if (callback) { callback(err); }
		}
	};

	for (index in obj) {
		if (obj.hasOwnProperty(index)) {
			// Call the collection load method
			obj[index].load(loadCallback);
		}
	}
};

/**
 * Saves an entire database's data to persistent storage.
 * @param {Function=} callback The method to call when the save function
 * has completed.
 */
Db.prototype.save = function (callback) {
	// Loop the collections in the database
	var obj = this._collection,
		keys = obj.keys(),
		keyCount = keys.length,
		saveCallback,
		index;

	saveCallback = function (err) {
		if (!err) {
			keyCount--;

			if (keyCount === 0) {
				if (callback) { callback(false); }
			}
		} else {
			if (callback) { callback(err); }
		}
	};

	for (index in obj) {
		if (obj.hasOwnProperty(index)) {
			// Call the collection save method
			obj[index].save(saveCallback);
		}
	}
};

Shared.finishModule('NodePersist');

module.exports = NodePersist;