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
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;
};
Computing file changes ...