iridium
Version:
A custom lightweight ORM for MongoDB designed for power-users
338 lines (279 loc) • 12.1 kB
JavaScript
/// <reference path="../nodelib/node.js"/>
/// <reference path="utils/transforms.js"/>
/// <reference path="utils/validation.js"/>
var ObjectID = require('mongodb').ObjectID,
_ = require('lodash'),
EventEmitter = require('events').EventEmitter,
debug = require('debug')('iridium:Instance'),
validate = require('./utils/validation');
(require.modules || {}).Instance = module.exports = Instance;
function Instance(model, doc, isNew) {
/// <signature>
/// <summary>Creates a new wrapper around a database document</summary>
/// <param name="model" type="Model">The model for which the instance should be created</param>
/// <param name="doc" type="Object">The document from the database which is to be wrapped</param>
/// </signature>
/// <signature>
/// <summary>Creates a new wrapper around a database document</summary>
/// <param name="model" type="Model">The model for which the instance should be created</param>
/// <param name="doc" type="Object">The document from the database which is to be wrapped</param>
/// <param name="isNew" type="Boolean>Should be true if this instance is not present in the database</param>
/// </signature>
"use strict";
EventEmitter.call(this);
if(!isNew) model.fromSource(doc);
this.__state = {
model: model,
isNew: isNew,
original: _.cloneDeep(doc),
modified: _.cloneDeep(doc)
}
this.__extendSchema();
for(var i = 0; i < model.database.plugins.length; i++)
if(model.database.plugins[i].newInstance)
model.database.plugins[i].newInstance.call(this, model, doc, isNew);
this.on('error', function(err) {
debug('encountered an error %s', err.message);
});
this.emit('ready', this);
}
Instance.prototype.__proto__ = EventEmitter.prototype;
Instance.prototype.save = function(conditions, changes, callback) {
/// <signature>
/// <summary>Saves changes made to the current instance to the database without waiting for a response</summary>
/// </signature>
/// <signature>
/// <summary>Saves changes made to the current instance to the database without waiting for a response</summary>
/// <param name="changes" type="Object">MongoDB changes query to be used instead of differential patching</param>
/// </signature>
/// <signature>
/// <summary>Saves changes made to the current instance to the database</summary>
/// <param name="callback" type="Function">A function which is called when the save has been completed</param>
/// </signature>
/// <signature>
/// <summary>Saves changes made to the current instance to the database</summary>
/// <param name="changes" type="Object">MongoDB changes query to be used instead of differential patching</param>
/// <param name="callback" type="Function">A function which is called when the save has been completed</param>
/// </signature>
/// <signature>
/// <summary>Saves changes made to the current instance to the database</summary>
/// <param name="conditions" type="Object">A set of conditions used to determine aspects of the document to update, merged with _id: ...</param>
/// <param name="changes" type="Object">MongoDB changes query to be used instead of differential patching</param>
/// </signature>
/// <signature>
/// <summary>Saves changes made to the current instance to the database</summary>
/// <param name="conditions" type="Object">A set of conditions used to determine aspects of the document to update, merged with _id: ...</param>
/// <param name="changes" type="Object">MongoDB changes query to be used instead of differential patching</param>
/// <param name="callback" type="Function">A function which is called when the save has been completed</param>
/// </signature>
var args = Array.prototype.splice.call(arguments, 0);
conditions = null;
changes = null;
callback = null;
for(var i = 0; i < args.length; i++) {
if('function' == typeof args[i]) callback = args[i];
else if(!conditions) conditions = args[i];
else changes = args[i];
}
conditions = conditions || {};
var onError = (function (err) {
this.emit('error', err);
if(callback) return callback(err);
else throw err;
}).bind(this);
if(this.__state.isNew) {
var toCreate = _.cloneDeep(this.__state.modified);
this.__state.model.onCreating(toCreate, (function(err) {
if(err) return onError(err);
this.emit('creating', toCreate);
this.__state.model.toSource(toCreate);
this.__state.model.collection.insert(toCreate, { w: 1 }, (function(err, created) {
if(err) return onError(err);
this.__state.isNew = false;
this.__state.model.onRetrieved(conditions, created[0], callback || function() { }, (function(value) {
this.__state.model.fromSource(value);
this.__state.original = _.cloneDeep(value);
this.__state.modified = _.cloneDeep(value);
this.__extendSchema();
this.emit('retrieved', this);
return this;
}).bind(this));
}).bind(this));
}).bind(this));
}
if(!changes) {
var original = _.cloneDeep(this.__state.original);
var modified = _.cloneDeep(this.__state.modified);
this.__state.model.toSource(original);
this.__state.model.toSource(modified);
changes = Instance.diff(original, modified);
}
if(Object.keys(changes).length === 0) return (callback || function() { })(null, this);
this.__state.model.onSaving(this, changes, (function(err) {
if(err) return onError(err);
this.emit('saving', this, changes);
this.__state.model.toSource(conditions);
_.merge(conditions, this.__state.model.uniqueConditions(this.__state.modified));
this.__state.model.collection.update(conditions, changes, { w : 1 }, (function(err, changed) {
if(err) return onError(err);
if(!changed) return (callback || function() { })(null, this);
this.__state.model.collection.findOne(conditions, (function(err, latest) {
if(err) return onError(err);
this.__state.model.onRetrieved(conditions, latest, callback || function() { }, (function(value) {
this.__state.model.fromSource(value);
this.__state.original = _.cloneDeep(value);
this.__state.modified = _.cloneDeep(value);
this.__extendSchema();
this.emit('retrieved', this);
return this;
}).bind(this));
}).bind(this));
}).bind(this))
}).bind(this));
};
Instance.prototype.refresh = Instance.prototype.update = function(callback) {
/// <signature>
/// <summary>Updates this object from the database, bringing it up to date</summary>
/// </signature>
/// <signature>
/// <summary>Updates this object from the database, bringing it up to date</summary>
/// <param name="callback" type="Function">A function to be called once the update is complete</param>
/// </signature>
var onError = (function (err) {
this.emit('error', err);
if(callback) return callback(err);
else throw err;
}).bind(this);
var conditions = this.__state.model.uniqueConditions(this.__state.original);
this.__state.model.collection.findOne(conditions, (function(err, latest) {
if(err) return onError(err);
this.__state.model.onRetrieved(conditions, latest, callback || function() { }, (function(value) {
this.__state.model.fromSource(value);
this.__state.original = _.cloneDeep(value);
this.__state.modified = _.cloneDeep(value);
this.__extendSchema();
this.emit('retrieved', this);
return this;
}).bind(this));
}).bind(this));
};
Instance.prototype.remove = Instance.prototype.delete = function(callback) {
/// <summary>Removes this object from the database collection</summary>
/// <param name="callback" type="Function">A function to be called when the object has been removed</param>
if(this.__state.isNew) return (callback || function() { })(null, 0);
var conditions = this.__state.model.uniqueConditions(this.__state.modified);
this.__state.model.cache.drop(conditions, function() {
this.emit('removing', this);
this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, (function(err, removed) {
if(err) this.emit('error', err);
else this.emit('removed', this);
return callback(err, removed);
}).bind(this));
});
};
Instance.prototype.__extendSchema = function() {
var $ = this;
var schema = {};
for(var property in this.__state.modified)
schema[property] = false;
for(var property in this.__state.model.schema)
if(schema[property]) delete schema[property];
for(var targetProperty in schema) {
if(!$[targetProperty] && !$.hasOwnProperty(targetProperty))
(function(targetProperty) {
Object.defineProperty($, targetProperty, {
get: function() {
/// <value type="Object">Get the most recent value for this field</value>
return $.__state.modified[targetProperty];
},
set: function(value) {
/// <value type="Object">Set the value of this field. Changes may be committed by calling save() on this instance.</value>
var validation = validate(schema[targetProperty], value, targetProperty, this.__state.model.extraValidators);
if (!validation.passed) throw validation.toError();
$.__state.modified[targetProperty] = value;
},
enumerable: true
});
})(targetProperty);
}
};
Instance.forModel = function(model) {
/// <summary>Creates an instance wrapper for the specified model</summary>
/// <param name="model" type="Model">The model which the instance wraps</param>
/// <return type="Function"/>
function ModelInstance(doc, isNew) {
/// <signature>
/// <summary>Creates a new model instance for the specified document</summary>
/// <param name="doc" type="Object">The document which the instance should wrap</param>
/// </signature>
/// <signature>
/// <summary>Creates a new model instance for the specified document</summary>
/// <param name="doc" type="Object">The document which the instance should wrap</param>
/// <param name="isNew" type="Boolean">Whether or not the document was sourced from the database</param>
/// </signature>
Instance.call(this, model, doc, isNew);
}
var proto = {};
_.each(model.schema, function(validator, name) {
Object.defineProperty(proto, name, {
get: function() {
return this.__state.modified[name] === undefined ? null : this.__state.modified[name];
},
set: function(value) {
var validation = validate(validator, value, name, model.extraValidators);
if(!validation.passed) throw validation.toError();
this.__state.modified[name] = value;
},
enumerable: true
});
});
_.each(model.options.virtuals, function(property, name) {
if('function' == typeof property)
Object.defineProperty(proto, name, {
get: property,
enumerable: true
});
else
Object.defineProperty(proto, name, {
get: property.get,
set: property.set,
enumerable: true
});
});
proto.__proto__ = Instance.prototype;
_.each(model.options.methods, function(method, name) {
proto[name] = method;
});
ModelInstance.prototype = proto;
return ModelInstance;
};
var diffPatch = Instance.diff = function (oldDoc, newDoc, path) {
/// <signature>
/// <summary>Creates a differential update query for use by MongoDB</summary>
/// <param name="oldDoc" type="Object">The original document prior to any changes</param>
/// <param name="newDoc" type="Object">The document containing the changes made to the original document</param>
/// </signature>
"use strict";
var changes = {};
for (var k in newDoc) {
if (Array.isArray(newDoc[k]) && Array.isArray(oldDoc[k])) {
var different = newDoc.length !== oldDoc.length;
for (var i = 0; i < newDoc[k].length && !different; i++) {
if (oldDoc[k][i] !== newDoc[k][i]) different = true;
}
if (!different) continue;
changes.$set = changes.$set || {};
changes.$set[(path ? (path + '.') : '') + k] = newDoc[k];
}
else if (_.isPlainObject(newDoc[k]) && _.isPlainObject(oldDoc[k])) {
// Make recursive diff update
_.merge(changes, diffPatch(oldDoc[k], newDoc[k], (path ? (path + '.') : '') + k));
}
else {
if (oldDoc[k] === newDoc[k]) continue;
changes.$set = changes.$set || {};
changes.$set[(path ? (path + '.') : '') + k] = newDoc[k];
}
}
return changes;
};