Revision 121df320d337fd860fed17f9bbddeb8e605427e2 authored by Aaron Heckmann on 09 February 2012, 13:20:07 UTC, committed by Aaron Heckmann on 09 February 2012, 13:20:07 UTC
1 parent 627745b
Raw File
model.js

/**
 * Module dependencies.
 */

var Document = require('./document')
  , MongooseArray = require('./types/array')
  , MongooseBuffer = require('./types/buffer')
  , MongooseError = require('./error')
  , Query = require('./query')
  , utils = require('./utils')
  , isMongooseObject = utils.isMongooseObject
  , EventEmitter = utils.EventEmitter
  , merge = utils.merge
  , Promise = require('./promise')
  , tick = utils.tick

/**
 * Model constructor
 *
 * @param {Object} values to set
 * @api public
 */

function Model (doc, fields) {
  Document.call(this, doc, fields);
};

/**
 * Inherits from Document.
 */

Model.prototype.__proto__ = Document.prototype;

/**
 * Connection the model uses. Set by the Connection or if absent set to the
 * default mongoose connection;
 *
 * @api public
 */

Model.prototype.db;

/**
 * Collection the model uses. Set by Mongoose instance
 *
 * @api public
 */

Model.prototype.collection;

/**
 * Model name.
 *
 * @api public
 */

Model.prototype.modelName;

/**
 * Returns what paths can be populated
 *
 * @param {query} query object
 * @return {Object] population paths
 * @api private
 */

Model.prototype._getPopulationKeys = function getPopulationKeys (query) {
  if (!(query && query.options.populate)) return;

  var names = Object.keys(query.options.populate)
    , n = names.length
    , name
    , paths = {}
    , hasKeys
    , schema

  while (n--) {
    name = names[n];
    schema = this.schema.path(name);
    hasKeys = true;

    if (!schema) {
      // if the path is not recognized, it's potentially embedded docs
      // walk path atoms from right to left to find a matching path
      var pieces = name.split('.')
        , i = pieces.length;

      while (i--) {
        var path = pieces.slice(0, i).join('.')
          , pathSchema = this.schema.path(path);

        // loop until we find an array schema
        if (pathSchema && pathSchema.caster) {
          if (!paths[path]) {
            paths[path] = { sub: {} };
          }

          paths[path].sub[pieces.slice(i).join('.')] = query.options.populate[name];
          hasKeys || (hasKeys = true);
          break;
        }
      }
    } else {
      paths[name] = query.options.populate[name];
      hasKeys || (hasKeys = true);
    }
  }

  return hasKeys && paths;
};

/**
 * Populates an object
 *
 * @param {SchemaType} schema type for the oid
 * @param {Object} object id or array of object ids
 * @param {Object} object specifying query conditions, fields, and options
 * @param {Function} callback
 * @api private
 */

Model.prototype._populate = function populate (schema, oid, query, fn) {
  if (!Array.isArray(oid)) {
    var conditions = query.conditions || {};
    conditions._id = oid;

    return this
    .model(schema.options.ref)
    .findOne(conditions, query.fields, query.options, fn);
  }

  if (!oid.length) {
    return fn(null, oid);
  }

  var model = this.model(schema.caster.options.ref)
    , conditions = query && query.conditions || {};
  conditions._id || (conditions._id = { $in: oid });

  model.find(conditions, query.fields, query.options, function (err, docs) {
    if (err) return fn(err);

    // user specified sort order?
    if (query.options && query.options.sort) {
      return fn(null, docs);
    }

    // put back in original id order (using a hash reduces complexity from n*n to 2n)
    var docHash = {};
    docs.forEach(function (doc) {
      docHash[doc._id] = doc;
    });

    var arr = [];
    oid.forEach(function (id) {
      if (id in docHash) arr.push(docHash[id]);
    });

    fn(null, arr);
  });
};

/**
 * Performs auto-population of relations.
 *
 * @param {Object} document returned by mongo
 * @param {Query} query that originated the initialization
 * @param {Function} callback
 * @api private
 */

Model.prototype.init = function init (doc, query, fn) {
  if ('function' == typeof query) {
    fn = query;
    query = null;
  }

  var populate = this._getPopulationKeys(query);

  if (!populate) {
    return Document.prototype.init.call(this, doc, fn);
  }

  // population from other models is necessary
  var self = this;

  init(doc, '', function (err) {
    if (err) return fn(err);
    Document.prototype.init.call(self, doc, fn);
  });

  return this;

  function init (obj, prefix, fn) {
    prefix = prefix || '';

    var keys = Object.keys(obj)
      , len = keys.length;

    function next () {
      if (--len < 0) return fn();

      var i = keys[len]
        , path = prefix + i
        , schema = self.schema.path(path)
        , total = 0
        , poppath

      if (!schema && obj[i] && 'Object' === obj[i].constructor.name) {
        // assume nested object
        return init(obj[i], path + '.', next);
      }

      if (!(obj[i] && schema && populate[path])) return next();

      // this query object is re-used and passed around. we clone
      // it to prevent query condition contamination between
      // one populate call to the next.
      poppath = utils.clone(populate[path]);

      if (poppath.sub) {
        obj[i].forEach(function (subobj) {
          var pkeys = Object.keys(poppath.sub)
            , pi = pkeys.length
            , key

          while (pi--) {
            key = pkeys[pi];

            if (subobj[key]) (function (key) {

              total++;
              self._populate(schema.schema.path(key), subobj[key], poppath.sub[key], done);
              function done (err, doc) {
                if (err) return error(err);
                subobj[key] = doc;
                --total || next();
              }
            })(key);
          }
        });

        if (0 === total) return next();

      } else {
        self._populate(schema, obj[i], poppath, function (err, doc) {
          if (err) return error(err);
          obj[i] = doc;
          next();
        });
      }
    };

    next();
  };

  function error (err) {
    if (error.err) return;
    fn(error.err = err);
  }
};

function handleSave (promise, self) {
  return tick(function handleSave (err, result) {
    if (err) return promise.error(err);

    self._storeShard();

    var numAffected;
    if (result) {
      numAffected = result.length
        ? result.length
        : result;
    } else {
      numAffected = 0;
    }

    self.emit('save', self, numAffected);
    promise.complete(self, numAffected);
    promise = null;
    self = null;
  });
}

/**
 * Saves this document.
 *
 * @see Model#registerHooks
 * @param {Function} fn
 * @api public
 */

Model.prototype.save = function save (fn) {
  var promise = new Promise(fn)
    , complete = handleSave(promise, this)
    , options = {}

  if (this.options.safe) {
    options.safe = this.options.safe;
  }

  if (this.isNew) {
    // send entire doc
    this.collection.insert(this.toObject({ depopulate: 1 }), options, complete);
    this._reset();
    this.isNew = false;
    this.emit('isNew', false);

  } else {
    var delta = this._delta();
    this._reset();

    if (delta) {
      var where = this._where();
      this.collection.update(where, delta, options, complete);
    } else {
      complete(null);
    }

    this.emit('isNew', false);
  }
};

/**
 * Produces a special query document of the modified properties.
 * @api private
 */

Model.prototype._delta = function _delta () {
  var dirty = this._dirty();

  if (!dirty.length) return;

  var self = this
    , useSet = this.options['use$SetOnSave'];

  return dirty.reduce(function (delta, data) {
    var type = data.value
      , schema = data.schema
      , atomics
      , val
      , obj

    if (type === undefined) {
      if (!delta.$unset) delta.$unset = {};
      delta.$unset[data.path] = 1;

    } else if (type === null) {
      if (!delta.$set) delta.$set = {};
      delta.$set[data.path] = type;

    } else if (type._path && type.doAtomics) {
      // a MongooseArray or MongooseNumber
      atomics = type._atomics;

      var ops = Object.keys(atomics)
        , i = ops.length
        , op;

      while (i--) {
        op = ops[i]

        if (op === '$pushAll' || op === '$pullAll') {
          if (atomics[op].length === 1) {
            val = atomics[op][0];
            delete atomics[op];
            op = op.replace('All', '');
            atomics[op] = val;
          }
        }

        val = atomics[op];
        obj = delta[op] = delta[op] || {};

        if (op === '$pull' || op === '$push') {
          if ('Object' !== val.constructor.name) {
            if (Array.isArray(val)) val = [val];
            // TODO Should we place pull and push casting into the pull and push methods?
            val = schema.cast(val)[0];
          }
        }

        obj[data.path] = isMongooseObject(val)
          ? val.toObject({ depopulate: 1 }) // MongooseArray
          : Array.isArray(val)
            ? val.map(function (mem) {
                return isMongooseObject(mem)
                  ? mem.toObject({ depopulate: 1 })
                  : mem.valueOf
                    ? mem.valueOf()
                    : mem;
              })
            : val.valueOf
              ? val.valueOf() // Numbers
              : val;

        if ('$addToSet' === op) {
          if (val.length > 1) {
            obj[data.path] = { $each: obj[data.path] };
          } else {
            obj[data.path] = obj[data.path][0];
          }
        }
      }
    } else {
      if (type instanceof MongooseArray ||
          type instanceof MongooseBuffer) {
        type = type.toObject({ depopulate: 1 });
      } else if (type._path) {
        type = type.valueOf();
      } else {
        // nested object literal
        type = utils.clone(type);
      }

      if (useSet) {
        if (!('$set' in delta))
          delta['$set'] = {};

        delta['$set'][data.path] = type;
      } else
        delta[data.path] = type;
    }

    return delta;
  }, {});
}

/**
 * _where
 *
 * Returns a query object which applies shardkeys if
 * they exist.
 *
 * @private
 */

Model.prototype._where = function _where () {
  var where = {};

  if (this._shardval) {
    var paths = Object.keys(this._shardval)
      , len = paths.length

    for (var i = 0; i < len; ++i) {
      where[paths[i]] = this._shardval[paths[i]];
    }
  }

  var id = this._doc._id.valueOf // MongooseNumber
    ? this._doc._id.valueOf()
    : this._doc._id;

  where._id = id;
  return where;
}

/**
 * Remove the document
 *
 * @param {Function} callback
 * @api public
 */

Model.prototype.remove = function remove (fn) {
  if (this._removing) return this;

  var promise = this._removing = new Promise(fn)
    , where = this._where()
    , self = this;

  this.collection.remove(where, tick(function (err) {
    if (err) {
      this._removing = null;
      return promise.error(err);
    }
    promise.complete();
    self.emit('remove');
  }));

  return this;
};

/**
 * Register hooks override
 *
 * @api private
 */

Model.prototype._registerHooks = function registerHooks () {
  Document.prototype._registerHooks.call(this);
};

/**
 * Shortcut to access another model.
 *
 * @param {String} model name
 * @api public
 */

Model.prototype.model = function model (name) {
  return this.db.model(name);
};

/**
 * Access the options defined in the schema
 *
 * @api private
 */

Model.prototype.__defineGetter__('options', function () {
  return this.schema ? this.schema.options : {};
});

/**
 * Give the constructor the ability to emit events.
 */

for (var i in EventEmitter.prototype)
  Model[i] = EventEmitter.prototype[i];

/**
 * Called when the model compiles
 *
 * @api private
 */

Model.init = function init () {
  // build indexes
  var self = this
    , indexes = this.schema.indexes
    , count = indexes.length;

  indexes.forEach(function (index) {
    self.collection.ensureIndex(index[0], index[1], tick(function (err) {
      if (err) return self.db.emit('error', err);
      --count || self.emit('index');
    }));
  });

  this.schema.emit('init', this);
};

/**
 * Document schema
 *
 * @api public
 */

Model.schema;

/**
 * Database instance the model uses.
 *
 * @api public
 */

Model.db;

/**
 * Collection the model uses.
 *
 * @api public
 */

Model.collection;

/**
 * Define properties that access the prototype.
 */

['db', 'collection', 'schema', 'options', 'model'].forEach(function(prop){
  Model.__defineGetter__(prop, function(){
    return this.prototype[prop];
  });
});

/**
 * Module exports.
 */

module.exports = exports = Model;

Model.remove = function remove (conditions, callback) {
  if ('function' === typeof conditions) {
    callback = conditions;
    conditions = {};
  }

  var query = new Query(conditions).bind(this, 'remove');

  if ('undefined' === typeof callback)
    return query;

  this._applyNamedScope(query);
  return query.remove(callback);
};

/**
 * Finds documents
 *
 * Examples:
 *    // retrieve only certain keys
 *    MyModel.find({ name: /john/i }, ['name', 'friends'], function () { })
 *
 *    // pass options
 *    MyModel.find({ name: /john/i }, [], { skip: 10 } )
 *
 * @param {Object} conditions
 * @param {Object/Function} (optional) fields to hydrate or callback
 * @param {Function} callback
 * @api public
 */

Model.find = function find (conditions, fields, options, callback) {
  if ('function' == typeof conditions) {
    callback = conditions;
    conditions = {};
    fields = null;
    options = null;
  } else if ('function' == typeof fields) {
    callback = fields;
    fields = null;
    options = null;
  } else if ('function' == typeof options) {
    callback = options;
    options = null;
  }

  var query = new Query(conditions, options).select(fields).bind(this, 'find');

  if ('undefined' === typeof callback)
    return query;

  this._applyNamedScope(query);
  return query.find(callback);
};

/**
 * Merges the current named scope query into `query`.
 *
 * @param {Query} query
 * @api private
 */

Model._applyNamedScope = function _applyNamedScope (query) {
  var cQuery = this._cumulativeQuery;

  if (cQuery) {
    merge(query._conditions, cQuery._conditions);
    if (query._fields && cQuery._fields)
      merge(query._fields, cQuery._fields);
    if (query.options && cQuery.options)
      merge(query.options, cQuery.options);
    delete this._cumulativeQuery;
  }

  return query;
}

/**
 * Finds by id
 *
 * @param {ObjectId/Object} objectid, or a value that can be casted to it
 * @api public
 */

Model.findById = function findById (id, fields, options, callback) {
  return this.findOne({ _id: id }, fields, options, callback);
};

/**
 * Finds one document
 *
 * @param {Object} conditions
 * @param {Object/Function} (optional) fields to hydrate or callback
 * @param {Function} callback
 * @api public
 */

Model.findOne = function findOne (conditions, fields, options, callback) {
  if ('function' == typeof options) {
    // TODO Handle all 3 of the following scenarios
    // Hint: Only some of these scenarios are possible if cQuery is present
    // Scenario: findOne(conditions, fields, callback);
    // Scenario: findOne(fields, options, callback);
    // Scenario: findOne(conditions, options, callback);
    callback = options;
    options = null;
  } else if ('function' == typeof fields) {
    // TODO Handle all 2 of the following scenarios
    // Scenario: findOne(conditions, callback)
    // Scenario: findOne(fields, callback)
    // Scenario: findOne(options, callback);
    callback = fields;
    fields = null;
    options = null;
  } else if ('function' == typeof conditions) {
    callback = conditions;
    conditions = {};
    fields = null;
    options = null;
  }

  var query = new Query(conditions, options).select(fields).bind(this, 'findOne');

  if ('undefined' == typeof callback)
    return query;

  this._applyNamedScope(query);
  return query.findOne(callback);
};

/**
 * Counts documents
 *
 * @param {Object} conditions
 * @param {Function} optional callback
 * @api public
 */

Model.count = function count (conditions, callback) {
  if ('function' === typeof conditions)
    callback = conditions, conditions = {};

  var query = new Query(conditions).bind(this, 'count');
  if ('undefined' == typeof callback)
    return query;

  this._applyNamedScope(query);
  return query.count(callback);
};

Model.distinct = function distinct (field, conditions, callback) {
  var query = new Query(conditions).bind(this, 'distinct');
  if ('undefined' == typeof callback) {
    query._distinctArg = field;
    return query;
  }

  this._applyNamedScope(query);
  return query.distinct(field, callback);
};

/**
 * `where` enables a very nice sugary api for doing your queries.
 * For example, instead of writing:
 *     User.find({age: {$gte: 21, $lte: 65}}, callback);
 * we can instead write more readably:
 *     User.where('age').gte(21).lte(65);
 * Moreover, you can also chain a bunch of these together like:
 *     User
 *       .where('age').gte(21).lte(65)
 *       .where('name', /^b/i)        // All names that begin where b or B
 *       .where('friends').slice(10);
 * @param {String} path
 * @param {Object} val (optional)
 * @return {Query}
 * @api public
 */

Model.where = function where (path, val) {
  var q = new Query().bind(this, 'find');
  return q.where.apply(q, arguments);
};

/**
 * Sometimes you need to query for things in mongodb using a JavaScript
 * expression. You can do so via find({$where: javascript}), or you can
 * use the mongoose shortcut method $where via a Query chain or from
 * your mongoose Model.
 *
 * @param {String|Function} js is a javascript string or anonymous function
 * @return {Query}
 * @api public
 */

Model.$where = function $where () {
  var q = new Query().bind(this, 'find');
  return q.$where.apply(q, arguments);
};

/**
 * Shortcut for creating a new Document that is automatically saved
 * to the db if valid.
 *
 * @param {Object} doc
 * @param {Function} callback
 * @api public
 */

Model.create = function create (doc, fn) {
  if (1 === arguments.length) {
    return 'function' === typeof doc && doc(null);
  }

  var self = this
    , docs = [null]
    , promise
    , count
    , args

  if (Array.isArray(doc)) {
    args = doc;
  } else {
    args = utils.args(arguments, 0, arguments.length - 1);
    fn = arguments[arguments.length - 1];
  }

  if (0 === args.length) return fn(null);

  promise = new Promise(fn);
  count = args.length;

  args.forEach(function (arg, i) {
    var doc = new self(arg);
    docs[i+1] = doc;
    doc.save(function (err) {
      if (err) return promise.error(err);
      --count || fn.apply(null, docs);
    });
  });

  // TODO
  // utilize collection.insertAll for batch processing?
};

/**
 * Updates documents.
 *
 * Examples:
 *
 *     MyModel.update({ age: { $gt: 18 } }, { oldEnough: true }, fn);
 *     MyModel.update({ name: 'Tobi' }, { ferret: true }, { multi: true }, fn);
 *
 * Valid options:
 *
 *  - safe (boolean) safe mode (defaults to value set in schema (true))
 *  - upsert (boolean) whether to create the doc if it doesn't match (false)
 *  - multi (boolean) whether multiple documents should be updated (false)
 *
 * @param {Object} conditions
 * @param {Object} doc
 * @param {Object} options
 * @param {Function} callback
 * @return {Query}
 * @api public
 */

Model.update = function update (conditions, doc, options, callback) {
  if (arguments.length < 4) {
    if ('function' === typeof options) {
      // Scenario: update(conditions, doc, callback)
      callback = options;
      options = null;
    } else if ('function' === typeof doc) {
      // Scenario: update(doc, callback);
      callback = doc;
      doc = conditions;
      conditions = {};
      options = null;
    }
  }

  var query = new Query(conditions, options).bind(this, 'update', doc);

  if ('undefined' == typeof callback)
    return query;

  this._applyNamedScope(query);
  return query.update(doc, callback);
};

/**
 * Compiler utility.
 *
 * @param {String} model name
 * @param {Schema} schema object
 * @param {String} collection name
 * @param {Connection} connection to use
 * @param {Mongoose} mongoose instance
 * @api private
 */

Model.compile = function compile (name, schema, collectionName, connection, base) {
  // generate new class
  function model () {
    Model.apply(this, arguments);
  };

  model.modelName = name;
  model.__proto__ = Model;
  model.prototype.__proto__ = Model.prototype;
  model.prototype.base = base;
  model.prototype.schema = schema;
  model.prototype.db = connection;
  model.prototype.collection = connection.collection(collectionName);

  // apply methods
  for (var i in schema.methods)
    model.prototype[i] = schema.methods[i];

  // apply statics
  for (var i in schema.statics)
    model[i] = schema.statics[i];

  // apply named scopes
  if (schema.namedScopes) schema.namedScopes.compile(model);

  return model;
};
back to top