UNPKG

iridium

Version:

A custom lightweight ORM for MongoDB designed for power-users

365 lines (299 loc) 13.6 kB
/// <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'), diff = require('./utils/diff'); (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; Object.defineProperty(Instance.prototype, 'document', { get: function() { return this.__state.modified; }, enumerable: false }); 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 = args.length - 1; i >= 0; i--) { if('function' == typeof args[i]) callback = args[i]; else if(!changes) changes = args[i]; else conditions = 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 validation = validate(this.__state.model.schema, this.__state.modified, undefined, this.__state.model.extraValidators); if(!validation.passed) return callback(validation.toError()); var original = _.cloneDeep(this.__state.original); var modified = _.cloneDeep(this.__state.modified); this.__state.model.toSource(original); this.__state.model.toSource(modified); if(modified._id != original._id) return onError(new Error('Cannot change _id, if you want to create a new document please use Model.create')); 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); var conditions = this.__state.model.uniqueConditions(this.__state.modified); 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); this.__state.isNew = true; return callback(err, removed); }).bind(this)); }).bind(this)); }; Instance.prototype.select = function(collection, filter) { /// <signature> /// <summary>Finds elements in the array for which the filter function returns truey</summary> /// <param name="collection" type="Object">The array to search through for matches</param> /// <param name="filter" type="Function">A function called with the array's element and its index/key for filtering pursposes</param> /// <returns type="Object"/> /// </signature> /// <signature> /// <summary>Finds elements in the array for which the filter function returns truey</summary> /// <param name="collection" type="Array">The array to search through for matches</param> /// <param name="filter" type="Function">A function called with the array's element and its index/key for filtering pursposes</param> /// <returns type="Array"/> /// </signature> var isArray = Array.isArray(collection); var results = isArray ? [] : {}; _.each(collection, function(value, key) { if(filter.call(this, value, key)) { if(isArray) results.push(value); else results[key] = value; } }, this); return results; }; Instance.prototype.first = function (collection, filter) { /// <signature> /// <summary>Finds the first element in the object for which the filter function returns truey</summary> /// <param name="collection" type="Object">The array to search through for matches</param> /// <param name="filter" type="Function">A function called with the array's element and its index/key for filtering pursposes</param> /// <returns type="Mixed"/> /// </signature> /// <signature> /// <summary>Finds the first element in the array for which the filter function returns truey</summary> /// <param name="collection" type="Array">The array to search through for matches</param> /// <param name="filter" type="Function">A function called with the array's element and its index/key for filtering pursposes</param> /// <returns type="Mixed"/> /// </signature> var result; _.each(collection, function (value, key) { if (filter.call(this, value, key)) { result = value; return false; } }, this); return result; }; Instance.prototype.__extendSchema = function() { var $ = this; var schema = {}, property; for(property in this.__state.modified) schema[property] = false; for(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> $.__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> /// <returns 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) { 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; }; Instance.diff = diff;