/* jshint -W098 */
"use strict";
var Strings = require("./strings"),
Err = require("./utils/error"),
object = require("./utils/object"),
_ = require("lodash"),
uuid = require("node-uuid");
/**
* Constructor for the data provider base class. This class cannot be used directly.
*
* Options
*
* - `connStr` {String} <required>, connection string.
*
* @class Represents a base class for storage specific data providers.
* All provider implementations should derive from this class.
*
* In EntreeJS every data collection is an instance of Provider class.
*
* @param {object} opts - Additional options. The only required option is `connStr`.
* @param {object=} schema - Defines the database schema such as fields, indexes, types and structure.
*/
function Provider(options, schema) {
if (!options || !options.connStr || !_.isString(options.connStr)) {
throw new Error(Strings.MISSING_PROV_ARGS);
}
this.options = options;
this.schema = schema || {};
this._stack = [];
this._uuid = uuid.v1();
}
/**
* This callback will be called after executing the method.
* The first parameter will contain an error object if an error occurred during execution,
* while the second parameter will contain the result from the execution if it was successful.
*
* For insert, upsert and update operations the result should be an object containing the same set of fields
* that were originally passed plus any fields that were automatically updated, such as timestamps or identity fields.
*
* NOTE: the returned result on insert, update and delete may differ slightly for the different providers, but if no
* error is returned the operation is considered successful.
*
* @callback Provider~actionCallback
* @param {object} err - the error object upon unsuccessful execution.
* @param {object} res - the result object upon successful execution.
*/
/**
* Inserts new item in the data collecton.
* If an item with the same identity is already present in the data collection,
* the operation is aborted and an error is returned.
*
* @param {object=} context - Provides context for the current execution.
* Usually this is information about the user making the call.
* Although this parameter is optional, it might be required by some interceptors, such as authorization.
*
* @param {(object|object[])} items - An item or an array of items to be inserted in the data store.
* @param {Provider~actionCallback} callback - The callback that handles the result of this action.
* @return {null}
*/
Provider.prototype.insert = function () {
var args = object.validateArgs(arguments, "items");
if (args) {
this._do("_insert", args.context, args.item, args.callback);
}
};
/**
* Inserts new item in the data collection if the item is not present, otherwise updates it.
*
* @param {Object=} context - provides context for the current execution.
* Usually this is information about the user making the call.
* Although this parameter is optional, it might be required by some interceptors, such as authorization.
*
* @param {Object} item - the item to be added or updated.
* @param {Provider~actionCallback} callback - The callback that handles the result of this action.
* @return {null}
*/
Provider.prototype.upsert = function () {
var args = object.validateArgs(arguments, "item");
if (args) {
this._do("_upsert", args.context, args.item, args.callback);
}
};
/**
* Updates the specified item with the provided fields.
* If an item with the specified identity could not be found an error is returned.
*
* @param {Object=} context - provides context for the current execution.
* Usually this is information about the user making the call.
* Although this parameter is optional, it might be required by some interceptors, such as authorization.
*
* @param {Object} item - the item to be updated.
* @param {Provider~actionCallback} callback - The callback that handles the result of this action.
* @return {null}
*/
Provider.prototype.update = function () {
var args = object.validateArgs(arguments, "item");
if (args) {
this._do("_update", args.context, args.item, args.callback);
}
};
/**
* Removes the specified item from the data collection.
* If an item with the specified identity is not found an error is returned.
*
* @param {Object=} context - provides context for the current execution.
* Usually this is information about the user making the call.
* Although this parameter is optional, it might be required by some interceptors, such as authorization.
*
* @param {Object|String|Number} item|identity - the item or the identity of an item to be deleted.
* @param {Provider~actionCallback} callback - The callback that handles the result of this action.
* @return {null}
*/
Provider.prototype.delete = function () {
var args = object.validateArgs(arguments, "itemOrID");
if (args) {
this._do("_delete", args.context, args.item, args.callback);
}
};
/**
* Retrieves the specified item from the data collection.
* If an item with the specified identity is not found an error is returned.
*
* @param {Object=} context - provides context for the current execution.
* Usually this is information about the user making the call.
* Although this parameter is optional, it might be required by some interceptors, such as authorization.
*
* @param {(Object|String|Number)} item|identity - the item or the identity of an item to be retrieved.
* @param {Provider~actionCallback} callback - The callback that handles the result of this action.
* @return {null}
*/
Provider.prototype.get = function () {
var args = object.validateArgs(arguments, "itemOrID");
if (args) {
this._do("_get", args.context, args.item, args.callback);
}
};
/**
* Selects items in the data collection and returns a {@link Cursor} to the selected items.
*
* This method can return a Cursor either directly or with a callback. In most cases a cursor is created and returned synchronously, but some interceptors like {@link module:cache} require a callback. If you plan to use asynchronous interceptors you have to use the callback form.
*
* Select uses MongoDB query [syntax](http://docs.mongodb.org/manual/reference/operator/query). All Entree data providers should support the full specification.
*
* Options
*
* - `skip` {Number}, the number of items to skip.
* - `limit` {Number}, limit the number of returned items.
* - `sort` {Object}, set to sort the documents coming back from the query.
*
* @example
*
* // Select at most 10 teenagers starting from the 21th that matches the criteria:
* entree.users.select({ age: { $gte: 13, $lte: 19 }})
* .skip(20)
* .limit(10)
* .sort({ age: 1 })
* .each(function (err, user)) {
* if (err) {
* return console.log(err);
* }
* console.log(JSON.stringify(user));
* });
*
* // Do the same but with a callback - the recommended way if interceptors are used on the select method:
* entree.users.select({ age: { $gte: 13, $lte: 19 }}, function (err, cursor) {
* if (err) {
* return console.log(err);
* }
* cursor
* .skip(20)
* .limit(10)
* .sort({ age: 1 })
* .each(function (err, user) {
* if (err) {
* return console.log(err);
* }
* console.log(JSON.stringify(user));
* });
* });
*
* // Again the same but limitations set as options:
* entree.users.select({ age: { $gte: 13, $lte: 19 }}, { skip: 20, limit: 10, sort: { age: 1 }}, function (err, cursor) {
* if (err) {
* return console.log(err);
* }
* cursor.each(function (err, user) {
* if (err) {
* return console.log(err);
* }
* onsole.log(JSON.stringify(user));
* });
* });
*
* // Get the total number of all users in the collection:
* entree.users.select(function (err, currsor) {
* cursor.count(err, count) {
* if (err) {
* return console.log(err);
* }
* console.log(count);
* });
* });
*
* @param {object=} context - Provides context for the current execution.
* @param {object=} query - Specifies selection criteria. To return all items in a collection, omit this parameter or pass an empty object.
* @param {object=} options - Additional options for the cursor.
* @param {function=} callback - Optional callback function that will be called when the {@link Cursor} is constructed.
* @return {(null|Cursor)} - The method returns null if a callback is provided otherwise it returns a {@link Cursor}.
*/
Provider.prototype.select = function () {
var args = object.validateSelectArgs(arguments);
if (args) {
return this._do("_select", args.context, { query: args.query, options: args.options }, args.callback);
}
};
/**
* This method is the same as `select`, but instead of cursor it returns the first item that
* matches the select and sort criteria. If there is no match, null is returned.
*
* @param {object=} context - Provides context for the current execution.
* @param {object=} query - Specifies selection criteria.
* @param {object=} options - Additional options for the cursor.
* @param {Provider~actionCallback} callback - The callback that handles the result of this action.
* @return {null}
*/
Provider.prototype.selectOne = function (context, query, options, callback) {
if (!callback) {
if (options) {
callback = options;
options = null;
} else if (query) {
callback = query;
query = null;
} else if (context) {
callback = context;
context = null;
}
}
if (!_.isFunction(callback)) {
throw new Error(Strings.REQUIRED_CALLBACK);
}
this.select(context, query, options).first(callback);
};
/**
* Adds an interception module to this provider instance.
* See {@tutorial interceptors} tutorial for more details.
*
* @param {function} fn - The function that handles the interception.
* @return {null}
*/
Provider.prototype.use = function (fn) {
this._stack.push(fn);
};
/**
* @protected
*/
Provider.prototype._getId = function (item) {
if (typeof item === "object" && !(item instanceof Array)) {
return item[this._getIdKey()] || (item.$set ? item.$set[this._getIdKey()] : undefined);
} else {
return item;
}
};
/**
* @protected
*/
Provider.prototype._setId = function (item, id) {
if (typeof item === "object" && !(item instanceof Array)) {
item[this._getIdKey()] = id;
}
};
/**
* @protected
*/
Provider.prototype._getIdKey = function () {
return this.schema.__identifier__ || "_id";
};
/**
* @protected
*/
Provider.prototype._do = function (action, context, item, callback) {
var idx = 0,
that = this;
function next(itm, out) {
var layer = that._stack[idx++];
if (layer) {
return layer.call(that, action, context, itm, next, out);
}
return that[action](itm, out);
}
return next(item, callback);
};
/**
* This is utility method intended mainly for data provider implementers.
*
* @param {function} - Optional callback that will be called when the provider is initialized.
* @return {null}
*/
Provider.prototype.init = function (callback) {
// NOTE: Override if you need to initialize database connection.
callback();
};
/**
* Releases its database connection, clears current state and any cached data.
*
* This is utility method intended mainly for data provider implementers.
*
* @param {function} - Optional callback that will be called when the provider is disposed.
* @return {null}
*/
Provider.prototype.dispose = function (callback) {
// NOTE: Override if you need to dispose database connection.
callback();
};
/**
* Creates new error object with the specified error message or error code.
*
* This is utility method intended mainly for data provider implementers.
*
* @param {(string|Error)} - This argument should be either error message, EntreeJS error code or error object.
* @param {function=} - Error handler. If omitted the error will be thrown immediately.
* @return {null}
*/
Provider.prototype.handleError = function (err, callback) {
if (typeof err === "string") {
err = new Err(err);
}
if (callback) {
callback(err);
} else {
throw err;
}
};
/**
* @abstract
*/
Provider.prototype._insert = function (items, callback) {
callback(new Error(Strings.NOT_IMPLEMENTED));
};
/** @abstract */
Provider.prototype._upsert = function (item, callback) {
callback(new Error(Strings.NOT_IMPLEMENTED));
};
/**
* @abstract
*/
Provider.prototype._update = function (item, callback) {
callback(new Error(Strings.NOT_IMPLEMENTED));
};
Provider.prototype._delete = function (item, callback) {
callback(new Error(Strings.NOT_IMPLEMENTED));
};
/**
* @abstract
*/
Provider.prototype._get = function (item, callback) {
callback(new Error(Strings.NOT_IMPLEMENTED));
};
/**
* @abstract
*/
Provider.prototype._select = function (args, callback) {
throw new Error(Strings.NOT_IMPLEMENTED);
};
module.exports = Provider;