Source: core/cursor.js

/*! cursor.js */

var utils = require('./utils');

module.exports = Cursor;

function Proto() {}

/**
 * This returns the next object at the cursor.
 *
 * @method Cursor.prototype.nextObject
 * @param {Function} callback - function(err, item) {}
 * @returns {Cursor} instance itself for method chaining
 * @example
 * var cursor = collection.find();
 * cursor.nextObject(function(err, item){
 *   console.log(item);
 * });
 */

Proto.prototype.nextObject = function(callback) {
  if (!this.source) throw new Error('no source');
  this.source.nextObject(callback);
};

/**
 * This resets the cursor position to the original state.
 *
 * @method Cursor.prototype.rewind
 * @returns {Cursor} instance itself for method chaining
 * @example
 * var cursor = collection.find();
 * cursor.nextObject(function(err, item){
 *   console.log(item); // first item
 *   cursor.rewind();
 *   cursor.nextObject(function(err, item){
 *     console.log(item); // first item again
 *   });
 * });
 */

Proto.prototype.rewind = function() {
  if (!this.source) throw new Error('no source');
  if (this.source.rewind) this.source.rewind();
  return this;
};

/**
 * This is a Cursor instance constructor. find() and other some methods use this internally.
 *
 * @class Cursor
 * @param {KagoDB} collection - source collection
 * @param {Object|Function} [condition] - query parameters or function
 * @param {Object|Function} [projection] - mapping parameters or function
 */

function Cursor(collection, condition, projection) {
  this.collection = collection;
  this.source = new Source(this);
  this._source = this.source;
  this.obop = collection.obop();
  if (condition) {
    try {
      condition = this.obop.where(condition);
    } catch (e) {
      condition = null;
      this._error = e;
    }
    if (condition) {
      this.source = new Condition(this, condition);
    }
  }
  if (projection) {
    try {
      projection = this.obop.view(projection);
    } catch (e) {
      projection = null;
      this._error = e;
    }
    if (projection) {
      this.source = new Projection(this, projection);
    }
  }
}

utils.inherits(Cursor, Proto);

/**
 * This invokes a callback function with an index for all items of the collection whether a condition is given or not.
 *
 * @private
 * @param {Function} callback - function(err, list) {}
 * @returns {Cursor} instance itself for method chaining
 */

Cursor.prototype.index = function(callback) {
  var self = this;
  callback = callback || NOP;
  if (this._error) {
    callback(this._error);
    return this;
  }

  if (self._index) {
    var list = [].concat(self._index); // clone
    callback(null, list);
  } else {
    self.collection.index(function(err, list) {
      if (err) {
        callback(err);
      } else {
        self._index = list;
        list = [].concat(self._index); // clone
        callback(null, list);
      }
    });
  }
  return this;
};

/**
 * This invokes a callback function with a list of items found.
 *
 * @param {Function} callback - function(err, list) {}
 * @returns {Cursor} instance itself for method chaining
 * @example
 * collection.find().toArray(function(err, list) {
 *   if (err) {
 *     console.error(err);
 *   } else {
 *     list.forEach(function(item) {
 *       console.log(item);
 *     });
 *   }
 * });
 */

Cursor.prototype.toArray = function(callback) {
  var self = this;
  callback = callback || NOP;
  if (this._error) {
    callback(this._error);
    return this;
  }

  if (self._toArray) {
    var list = [].concat(self._toArray); // clone
    callback(null, list);
  } else {
    toArray(this.source, function(err, list) {
      if (err) {
        callback(err);
      } else {
        self._toArray = list;
        list = [].concat(self._toArray); // clone
        callback(null, list);
      }
    });
  }
  return this;
};

/**
 * This invokes a callback function with each items found.
 *
 * @param {Function} callback - function(err, item) {}
 * @returns {Cursor} instance itself for method chaining
 * @example
 * collection.find().each(function(err, item) {
 *   if (err) {
 *     console.error(err);
 *   } else if (!item) {
 *     // EOF
 *   } else {
 *     console.error(item);
 *   }
 * });
 */

Cursor.prototype.each = function(callback) {
  var self = this;
  callback = callback || NOP;
  if (this._error) {
    callback(this._error);
    return this;
  }

  this.nextObject(iterator);

  function iterator(err, item) {
    callback(err, item);
    if (!err && item) self.nextObject(iterator);
  }
};

/**
 * This invokes a callback function with the number of items found
 *
 * @param {Function} callback - function(err, count) {}
 * @returns {Cursor} instance itself for method chaining
 * @example
 * collection.find().count(function(err, count) {
 *   console.log(count);
 * });
 */

Cursor.prototype.count = function(callback) {
  callback = callback || NOP;
  if (this._error) {
    callback(this._error);
    return this;
  }

  var getlist = (this.source === this._source) ? this.index : this.toArray;
  getlist.call(this, function(err, list) {
    if (err) {
      callback(err);
    } else {
      callback(null, list.length);
    }
  });
  return this;
};

/**
 * This specifies a sort parameters
 *
 * @param {Function|Object} order - sort function or parameters
 * @returns {Cursor} instance itself for method chaining
 * @example
 * collection.find().sort(function(a, b){
 *   return a.price - b.price || b.stock - a.stock;
 * }).toArray();
 *
 * var sort = {price: 1, stock: -1});
 * collection.find().sort(sort).toArray(); // same order
 */

Cursor.prototype.sort = function(order) {
  this.source = new Sort(this, order);
  return this;
};

/**
 * This specifies a offset parameters
 *
 * @param {Number} offset - offset parameter
 * @returns {Cursor} instance itself for method chaining
 * @example
 * collection.find().offset(100).toArray();
 */

Cursor.prototype.offset = function(offset) {
  this.source = new Offset(this, offset);
  return this;
};

/**
 * This specifies a limit parameters
 *
 * @param {Number} limit - limit parameter
 * @returns {Cursor} instance itself for method chaining
 * @example
 * collection.find().limit(100).toArray();
 */

Cursor.prototype.limit = function(limit) {
  this.source = new Limit(this, limit);
  return this;
};

function Source(cursor) {
  this.collection = cursor.collection;
}

utils.inherits(Source, Proto);

Source.prototype.nextObject = function(callback) {
  var self = this;
  callback = callback || NOP;

  if (this.list) {
    if (!this.list.length) {
      callback(); // EOF
      return;
    } else {
      var id = this.list.shift();
      this.collection.read(id, callback);
      return;
    }
  }

  // read all keys at first
  this.collection.index(function(err, list) {
    if (err) return callback(err);
    self.list = list || [];
    self.nextObject(callback);
  });

  return this;
};

Source.prototype.rewind = function(callback) {
  delete this.list;
};

function Condition(cursor, condition) {
  this.source = cursor.source;
  this.condition = condition;
}

utils.inherits(Condition, Proto);

Condition.prototype.nextObject = function(callback) {
  var self = this;
  var source = this.source;
  var condition = this.condition;
  if ('function' != typeof condition) {
    var err = new Error('invalid condition: ' + condition);
    callback(err);
    return;
  }

  source.nextObject(next);

  function next(err, item) {
    if (err) {
      callback(err);
    } else if (!item) {
      callback(); // EOF
    } else if (condition(item)) {
      callback(null, item); // OK
    } else {
      source.nextObject(next); // NG
    }
  }
};

function Sort(cursor, order) {
  var sorter;
  this.source = cursor.source;
  try {
    this.sorter = cursor.obop.order(order);
  } catch (e) {
    this._error = e;
  }
}

utils.inherits(Sort, Proto);

Sort.prototype.nextObject = function(callback) {
  var self = this;
  callback = callback || NOP;
  if (this._error) {
    callback(this._error);
    return;
  }

  if (this.list) {
    var item = this.list.shift();
    callback(null, item);
    return;
  }

  toArray(this.source, function(err, list) {
    if (err) return callback(err);
    list = list || [];
    self.list = list.sort(self.sorter);
    self.nextObject(callback);
  });
};

Sort.prototype.rewind = function() {
  delete this.list;
  if (this.source.rewind) this.source.rewind();
};

function Offset(cursor, offset) {
  this.source = cursor.source;
  this.offset = offset;
}

utils.inherits(Offset, Proto);

Offset.prototype.nextObject = function(callback) {
  var self = this;
  var rest = this.offset;
  var source = this.source;
  callback = callback || NOP;

  if (this.ready || rest <= 0) {
    source.nextObject(callback);
  } else {
    source.nextObject(iterator);
  }

  function iterator(err, item) {
    if (err) {
      callback(err); // error on read
    } else if (!item) {
      callback(); // EOF
    } else if (--rest > 0) {
      source.nextObject(iterator); // next
    } else {
      self.ready = true;
      source.nextObject(callback);
    }
  }
};

Offset.prototype.rewind = function() {
  delete this.ready;
  Proto.prototype.rewind.call(this);
};

function Limit(cursor, limit) {
  this.source = cursor.source;
  this.limit = limit;
  this.rest = limit;
}

utils.inherits(Limit, Proto);

Limit.prototype.nextObject = function(callback) {
  var source = this.source;
  callback = callback || NOP;

  if (this.rest-- > 0) {
    source.nextObject(callback);
  } else {
    callback();
  }
};

Limit.prototype.rewind = function() {
  this.rest = this.limit;
  Proto.prototype.rewind.call(this);
};

function Projection(cursor, projection) {
  this.source = cursor.source;
  this.projection = projection;
}

utils.inherits(Projection, Proto);

Projection.prototype.nextObject = function(callback) {
  var self = this;
  var projection = this.projection;

  if ('function' != typeof projection) {
    var err = new Error('invalid projection: ' + projection);
    callback(err);
    return;
  }

  this.source.nextObject(function(err, item) {
    if (err) {
      callback(err);
    } else if (!item) {
      callback();
    } else {
      item = projection(item);
      callback(null, item);
    }
  });
};

function toArray(source, callback) {
  var buf = [];
  callback = callback || NOP;
  source.nextObject(iterator);

  function iterator(err, item) {
    // error on read
    if (err) {
      callback(err);
      return;
    }

    // last item
    if (!item) {
      callback(null, buf);
      return;
    }

    buf.push(item);

    source.nextObject(iterator);
  }
}

function NOP() {}