"use strict"; var Err = require("./utils/error"), util = require("util"), timers = require("timers"), Strings = require("./strings"), async = require("async"), object = require("./utils/object"), _ = require("lodash"); function uprem(cursor, data, callback) { function updateItems(items) { async.each( items, function (item, cb) { if (data) { if (object.validateKeys(data) !== "opers") { return cb(new Err("OPERS_ONLY_ALLOWED")); } try { item = object.update(data, item); } catch (err) { return cb(err); } cursor.provider._update(item, cb); } else { cursor.provider._delete(item, cb); } }, function (err) { if (err) { return callback(err); } callback(null, items.length); } ); } cursor.exec(function (err) { if (err) { callback(err); } else { if (cursor.items) { updateItems(cursor.items); } else { cursor.toArray(function (err, array) { if (err) { return callback(err); } updateItems(array); }); } } }); } /** * Constructor for a cursor object that handles all the operations on query result using select. * This cursor object is unidirectional and cannot traverse backwards. * Clients should not be creating a cursor directly, but use select to acquire a cursor. * * Options * * - `skip` {number}, The number of items to skip from the beginning of the matching result. * - `limit` {number}, Limit the number of items to return. * - `sort` {object}, Controls the order that the query returns matching items. * - `map` {object}, Specifies which fields from the item should be returned. * * @class Represents a Cursor. * @param {object} provider - the provider that created this cursor. * @param {object} query - an object containing the conditions for an item to be included in the result set. * @param {object=} options - additional options for the cursor. */ function Cursor(provider, query, options) { this.provider = provider; this.query = query; var opt = options || {}; this.mapping = opt.map; this.skipValue = opt.skip || 0; this.limitValue = opt.limit || 0; this.queryRun = false; this.state = Cursor.INIT; this._crtKeys = null; if (_.isFunction(opt.sort)) { this.comparer = opt.sort; } else { this.criteria = opt.sort; } } /** * Sets the limit parameter of this cursor to the given value. * * @param {number} limit the new limit. * @param {function} [callback] An optional callback that will be called after executing this method. The first parameter will contain an error object when the limit given is not a valid number or when the cursor is already closed while the second parameter will contain a reference to this object upon successful execution. * @return {Cursor} an instance of this object. * @api public */ Cursor.prototype.limit = function (limit, callback) { if (this.queryRun === true || this.state === Cursor.CLOSED) { var err = new Error(Strings.CURSOR_CLOSED); if (callback) { callback(err); } else { throw new Error(err); } } else { if (limit !== null && limit.constructor !== Number) { var msg = util.format(Strings.REQUIRES_INT, "limit"), errInt = new Error(msg); if (callback) { callback(errInt); } else { throw errInt; } } else { this.limitValue = limit; this.reset(); if (callback) { return callback(null, this); } } } return this; }; /** * This method is just an alias to Cursor.limit() method. */ Cursor.prototype.take = function (limit, callback) { return this.limit(limit, callback); }; /** * Sets the skip parameter of this cursor to the given value. * * @param {number} skip the new skip value. * @param {function} [callback] this optional callback will be called after executing this method. The first parameter will contain an error object when the skip value given is not a valid number or when the cursor is already closed while the second parameter will contain a reference to this object upon successful execution. * @return {Cursor} an instance of this object. * @api public */ Cursor.prototype.skip = function (skip, callback) { if (this.queryRun === true || this.state === Cursor.CLOSED) { var err = new Error(Strings.CURSOR_CLOSED); if (callback) { callback(err); } else { throw err; } } else { if (skip !== null && skip.constructor !== Number) { var msg = util.format(Strings.REQUIRES_INT, "skip"), errInt = new Error(msg); if (callback) { callback(errInt); } else { throw errInt; } } else { this.skipValue = skip; this.reset(); if (callback) { return callback(null, this); } } } return this; }; /** * Sets the sort parameter of this cursor to the given value. * * @param {object} criteria - the criteria by which the result set will be sorted. * @param {function} callback - Will be called after executing this method. The first parameter will contain an error object when the cursor is already closed while the second parameter will contain a reference to this object upon successful execution. * @return {Cursor} an instance of this object. * @api public */ Cursor.prototype.sort = function (criteria, callback) { if (this.queryRun === true || this.state === Cursor.CLOSED) { var err = new Error(Strings.CURSOR_CLOSED); if (callback) { callback(err); } else { throw new Error(err); } } else { this._crtKeys = null; this.criteria = criteria; this.reset(); if (callback) { callback(null, this); } } return this; }; Cursor.prototype.comparer = function (a, b) { function getValue(obj, paths) { var current = obj, i; for (i = 0; i < paths.length; ++i) { if (current[paths[i]] === undefined) { return undefined; } else { current = current[paths[i]]; } } return current; } function compare(_key, _a, _b) { var va = getValue(_a, _key.paths), vb = getValue(_b, _key.paths); if (!va && !vb) { return 0; } if (!va) { return -1; } if (!vb) { return 1; } if (_key.isStr) { return va.localeCompare(vb); } if (va > vb) { return 1; } if (va < vb) { return -1; } return 0; } var key, val, i; if (!this._crtKeys) { this._crtKeys = []; for (key in this.criteria) { if (this.criteria.hasOwnProperty(key)) { this._crtKeys.push({ name: key, paths: key.split("."), isStr: typeof a[key] === "string" }); } } } for (i = 0; i < this._crtKeys.length; i++) { key = this._crtKeys[i]; val = this.criteria[key.name] < 0 ? compare(key, b, a) : compare(key, a, b); if (val !== 0) { break; } } return val; }; /** * Specifies which fields from the item should be returned. * * @example * provider * .select() * .map(["name", "age"]) * .toArray(function (err, arr) { * // -> The items in the array will have only name and age fields. * }); * * @example * provider * .select() * .map({ name: 1, age: 1 }) * .toArray(function (err, arr) { * // -> The items in the array will have only name and age fields. * }); * * @param {(object|string[])} mapping - Either, an object specifying which fields to include or exclude (not both) or an array of field names to include. * @param {function} callback - Will be called after executing this method. * The first parameter will contain an error object when the cursor is already closed * while the second parameter will contain a reference to this object upon successful execution. * @return {Cursor} - An instance of this object. */ Cursor.prototype.map = function (mapping, callback) { if (this.queryRun === true || this.state === Cursor.CLOSED) { var err = new Error(Strings.CURSOR_CLOSED); if (callback) { callback(err); } else { throw new Error(err); } } else { this.mapping = mapping; this.reset(); if (callback) { callback(null, this); } } return this; }; /** * Returns an array of items. The caller is responsible for making sure that there * is enough memory to store the results. * * @param {function} callback - Will be called after executing this method successfully. The first parameter will contain the Error object if an error occurred, or null otherwise. The second parameter will contain an array of objects as a result of the query. * @return {null} * @api public */ Cursor.prototype.toArray = function (callback) { if (!callback) { throw new Error(util.format(Strings.MISSING_ARG, "callback")); } var that = this, list = []; this.exec(function (err) { if (err) { callback(err); } else { if (that.items) { return callback(null, that.items); } that.each(function (err, item) { if (err) { return callback(err); } if (item) { list.push(item); } else { that.items = list; callback(null, that.items); } }); } }); }; /** * Iterates over all the items for this cursor. As with **{cursor.toArray}**, * not all of the elements will be iterated if this cursor had been previouly accessed. * * @param {function} callback - Will be called for while iterating every item of the query result. * The first parameter will contain the Error object if an error occured, or null otherwise. * While the second parameter will contain the item. * @return {null} * @api public */ Cursor.prototype.each = function (callback) { var i, that = this; if (!callback) { throw new Error(util.format(Strings.MISSING_ARG, "callback")); } function visit() { that.next(function (err, item) { callback(err, item); if (item) { timers.setImmediate(function () { visit(); }); } }); } this.exec(function (err) { if (err) { callback(err); } else { if (that.items) { for (i = 0; i < that.items.length; i++) { callback(null, that.items[i]); } callback(null, null); } else { visit(); } } }); }; /** * Gets the next object from the cursor. * * @param {function} callback - Will be called after executing this method. The first parameter will contain an error object on error while the second parameter will contain a object from the returned result or null if there are no more results. * @api public */ Cursor.prototype.next = function (callback) { var that = this; if (!callback) { throw new Error(util.format(Strings.MISSING_ARG, "callback")); } this.exec(function (err) { if (err) { return callback(err); } that._nextObject(function (err, item) { if (err) { that.state = Cursor.CLOSED; } callback(err, item); }); }); }; /** * Counts the number of items matching the select query. * * @example * provider * .select({"publisher.name": "TLC"}) * .count(function (err, count) { * if (err) { * console.log(err); * } else { * console.log(count); * } * }); * * @param {function} callback - Will be called after executing this method. The first parameter will contain an error object on error while the second parameter will contain the number of matching items. * @return {null} * @api public */ Cursor.prototype.count = function (callback) { var that = this; if (!callback) { throw new Error(util.format(Strings.MISSING_ARG, "callback")); } this.exec(function (err) { if (err) { callback(err); } else { if (that.items) { callback(null, that.items.length); } else { that.toArray(function (err, array) { callback(err, array.length); }); } } }); }; /** * Gets the first item that matches the select query and sorting criteria. * * @param {function} callback - Will be called after executing this method. The first parameter will contain an error object on error while the second parameter will contain the first item that matches the query. * @return {null} * @api public */ Cursor.prototype.first = function (callback) { this.limitValue = 1; this.next(callback); }; /** * Updates all items that match the select query. * * @example * provider * .select({"publisher.name": "TLC"}) * .update({$set: { age: 555 }}); * * @param {object} data - An object containing the fields and values to update. * @param {function} callback - Will be called after executing this method. * The first parameter will contain an error object on error while the second parameter will return the number of updated items. * @return {null} * @api public */ Cursor.prototype.update = function (data, callback) { uprem(this, data, callback); }; /** * Deletes all items that match the select query. * * @param {function} callback - Will be called after executing this method. The first parameter will contain an error object on error while the second parameter will contain the number of deleted items. * @return {null} * @api public */ Cursor.prototype.delete = function (callback) { uprem(this, null, callback); }; Cursor.prototype.exec = function (callback) { var that = this; if (this.queryRun) { if (this.state === Cursor.CLOSED) { callback(new Error(Strings.CURSOR_CLOSED)); } else { callback(null); } } else { this.queryRun = true; this.state = Cursor.OPEN; this._exec(function (err) { if (err) { that.state = Cursor.CLOSED; } callback(err); }); } }; Cursor.prototype.wrapCallback = function (funcName, wrapper) { var that = this, func = this[funcName]; this[funcName] = function () { var args = Array.prototype.slice.call(arguments), callback = args.shift(); function handleCallback() { var args = Array.prototype.slice.call(arguments); args.push(callback); wrapper.apply(that, args); } args.unshift(handleCallback); func.apply(that, args); }; }; Cursor.prototype.reset = function () { // abstract throw new Error(Strings.NOT_IMPLEMENTED); }; Cursor.prototype._nextObject = function (callback) { // abstract callback(new Error(Strings.NOT_IMPLEMENTED)); }; Cursor.prototype._exec = function (callback) { // abstract callback(new Error(Strings.NOT_IMPLEMENTED)); }; /** * Init state * * @classconstant INIT **/ Cursor.INIT = 0; /** * Cursor open * * @classconstant OPEN **/ Cursor.OPEN = 1; /** * Cursor closed * * @classconstant CLOSED **/ Cursor.CLOSED = 2; module.exports = Cursor;