diff --git a/benchmarks/mongodb.js b/benchmarks/mongodb.js
index b18b64a..bafbd34 100644
--- a/benchmarks/mongodb.js
+++ b/benchmarks/mongodb.js
@@ -61,7 +61,7 @@ MongoClient.connect('mongodb://localhost/iridium_bench', function(err, mDB) {
function(done) {
console.log('Iridium 10000 Inserts { w: 1, wrap: false }');
var start = new Date();
- model.insert(objects, false, function(err, inserted) {
+ model.insert(objects, { wrap: false }, function(err, inserted) {
if(err) return done(err);
printTime(' => %s', start);
return done();
@@ -79,7 +79,7 @@ MongoClient.connect('mongodb://localhost/iridium_bench', function(err, mDB) {
function(done) {
console.log('Iridium find() { wrap: false }');
var start = new Date();
- model.find({}, false, function(err, results) {
+ model.find({}, { wrap: false }, function(err, results) {
if(err) return done(err);
printTime(' => %s', start);
return done();
diff --git a/lib/Instance.js b/lib/Instance.js
index e8bcedb..a3293ec 100644
--- a/lib/Instance.js
+++ b/lib/Instance.js
@@ -142,7 +142,7 @@ Instance.prototype.refresh = Instance.prototype.update = function(callback) {
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(latest, callback || function() { }, (function(value) {
this.__state.model.fromSource(value);
this.__state.original = _.cloneDeep(value);
@@ -160,7 +160,9 @@ Instance.prototype.remove = Instance.prototype.delete = function(callback) {
if(this.__state.isNew) return (callback || function() { })(null, 0);
var conditions = this.__state.model.uniqueConditions(this.__state.modified);
- this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, callback);
+ this.__state.model.cache.drop(conditions._id, function() {
+ this.__state.model.collection.remove(conditions, { w: callback ? 1 : 0 }, callback);
+ });
};
Instance.prototype.__extendSchema = function() {
diff --git a/lib/Model.js b/lib/Model.js
index 3c11ffb..7dd5d02 100644
--- a/lib/Model.js
+++ b/lib/Model.js
@@ -10,6 +10,7 @@ var Instance = require('./Instance');
var validate = require('./utils/validation');
var Concoction = require('concoction');
var ObjectID = require('mongodb').ObjectID;
+var NoOpCache = require('./caches/NoOpCache');
(require.modules || {}).Model = module.exports = Model;
@@ -76,6 +77,11 @@ function Model(database, collection, schema, options) {
enumerable: false
});
+ Object.defineProperty(this, 'cache', {
+ value: options.cache || new NoOpCache(),
+ enumerable: false
+ })
+
var extraValidators = [];
for(var i = 0; i < database.plugins.length; i++) {
@@ -167,7 +173,7 @@ Model.prototype.wrap = function (document, isNew) {
return new this.Instance(document, isNew);
};
-Model.prototype.onRetrieved = function(results, callback, wrapper) {
+Model.prototype.onRetrieved = function(results, callback, wrapper, options) {
///
///Handles any post-receive hooks and the wrapping of objects from the database
///The object retrieved from the database
@@ -190,9 +196,23 @@ Model.prototype.onRetrieved = function(results, callback, wrapper) {
///The function to be called once the objects have been wrapped
///A function which converts the retrieved objects prior to submission
///
+ ///
+ ///Handles any post-receive hooks and the wrapping of objects from the database
+ ///The objects retrieved from the database
+ ///The function to be called once the objects have been wrapped
+ ///A function which converts the retrieved objects prior to submission
+ ///A set of options determining how to handle the retrieved object
+ ///
var $ = this;
- wrapper = (wrapper !== true && wrapper) || this.wrap.bind(this);
+
+ wrapper = wrapper || this.wrap.bind(this);
+ options = options || {};
+
+ _.defaults(options, {
+ wrap: true,
+ cache: true
+ });
var returnArray = Array.isArray(results);
if(!returnArray) results = [results];
@@ -216,12 +236,19 @@ Model.prototype.onRetrieved = function(results, callback, wrapper) {
doHook(this.options.hooks.retrieved, target, (function(err) {
if(err) return done(err);
- var wrapped = wrapper(target);
+ var cacheDoc = _.cloneDeep(target);
+
+ var wrapped = options.wrap ? wrapper(target) : target;
- doHook(this.options.hooks.ready, wrapped, function(err) {
+ doHook(this.options.hooks.ready, wrapped, (function(err) {
if(err) return done(err);
- return done(null, wrapped);
- });
+ if(options.cache)
+ return this.cache.store(cacheDoc, function() {
+ return done(null, wrapped);
+ });
+ else
+ return done(null, wrapped);
+ }).bind(this));
}).bind(this));
}).bind(this);
}, this), function(err, output) {
@@ -269,12 +296,10 @@ Model.prototype.onSaving = function(instance, changes, callback) {
}
}
- var $ = this;
-
- doHook($.options.hooks.saving, instance, [changes], callback);
+ doHook(this.options.hooks.saving, instance, [changes], callback);
};
-Model.prototype.find = function (conditions, wrap, callback) {
+Model.prototype.find = function (conditions, options, callback) {
///
/// Gets all objects in the collection.
/// A function to be called with the results once they have been retrieved.
@@ -291,36 +316,42 @@ Model.prototype.find = function (conditions, wrap, callback) {
///
///
/// Gets all objects in the collection.
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called with the results once they have been retrieved.
///
///
/// Finds all occurences in the collection with an _id field matching the given conditions.
/// The _id field of the object to locate
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called with the results once they have been retrieved.
///
///
/// Finds all occurences in the collection which match the given conditions.
/// The conditions which will be used to select matches
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called with the results once they have been retrieved.
///
var args = Array.prototype.splice.call(arguments, 0);
- conditions = {};
- wrap = true;
+ conditions = null;
+ options = null;
for(var i = 0; i < args.length; i++) {
if('function' == typeof args[i])
callback = args[i];
- else if('boolean' == typeof args[i])
- wrap = args[i];
- else
+ else if(!conditions)
conditions = args[i];
+ else options = args[i];
}
+ conditions = conditions || {};
+ options = options || {};
+ _.defaults(options, {
+ wrap: true,
+ cache: true
+ });
+
var $ = this;
if (!_.isPlainObject(conditions)) conditions = this.downstreamID(conditions);
this.toSource(conditions);
@@ -328,11 +359,11 @@ Model.prototype.find = function (conditions, wrap, callback) {
this.collection.find(conditions).toArray(function (err, results) {
if (err) return callback(err);
if (!results) return callback(null, null);
- return $.onRetrieved(results, callback, wrap || function(value) { return value; });
+ return $.onRetrieved(results, callback, options.wrap || function(value) { return value; });
});
};
-Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, callback) {
+Model.prototype.findOne = Model.prototype.get = function (conditions, options, callback) {
///
/// Gets a single object from the collection.
/// A function to be called with the results once they have been retrieved.
@@ -349,51 +380,69 @@ Model.prototype.findOne = Model.prototype.get = function (conditions, wrap, call
///
///
/// Gets a single object from the collection.
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called with the results once they have been retrieved.
///
///
/// Finds the first occurence in the collection with an _id field matching the given conditions.
/// The _id field of the object to locate
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called with the results once they have been retrieved.
///
///
/// Finds the first occurence in the collection which matches the given conditions.
/// The conditions which will be used to select matches
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called with the results once they have been retrieved.
///
var args = Array.prototype.splice.call(arguments, 0);
- conditions = {};
- wrap = true;
+ conditions = null;
+ options = null;
for(var i = 0; i < args.length; i++) {
if('function' == typeof args[i])
callback = args[i];
- else if('boolean' == typeof args[i])
- wrap = args[i];
- else
+ else if(!conditions)
conditions = args[i];
+ else options = args[i];
}
+ conditions = conditions || {};
+ options = options || {};
+ _.defaults(options, {
+ wrap: true,
+ cache: true
+ });
- var $ = this;
- if (!_.isPlainObject(conditions)) conditions = this.downstreamID(conditions);
+ var isID = !_.isPlainObject(conditions);
+
+ if (isID) conditions = this.downstreamID(conditions);
this.toSource(conditions);
-
- this.collection.findOne(conditions, function (err, results) {
- if (err) return callback(err);
- if (!results) return callback(null, null);
-
- return $.onRetrieved(results, callback, wrap || function(value) { return value; });
- });
+
+ var fromDB = (function() {
+ this.collection.findOne(conditions, (function (err, results) {
+ if (err) return callback(err);
+ if (!results) return callback(null, null);
+
+ return this.onRetrieved(results, callback, null, { wrap: options.wrap, cache: options.cache });
+ }).bind(this));
+ }).bind(this);
+
+ if(isID && this.cache && options.cache)
+ this.cache.fetch(conditions._id, (function(err, doc) {
+ if(!err && doc)
+ return this.onRetrieved(doc, callback, null, { wrap: options.wrap, cache: false });
+ else
+ return fromDB();
+ }).bind(this));
+ else
+ return fromDB();
};
-Model.prototype.insert = Model.prototype.create = function (object, wrap, callback) {
+Model.prototype.insert = Model.prototype.create = function (object, options, callback) {
///
/// Inserts the given object into the database
/// The properties to set on the newly created object
@@ -414,13 +463,13 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
///
/// Inserts the given object into the database
/// The properties to set on the newly created object
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called once the object has been created
///
///
/// Inserts the given object into the database
/// An array of objects representing the properties to set on the newly created objects
- /// Whether or not to wrap results in an Instance object
+ /// Options dictating how Iridium handles this request
/// A function to be called once the objects have been created
///
@@ -429,10 +478,14 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
var returnArray = true;
if(!callback) {
- callback = wrap;
- wrap = true;
+ callback = options;
+ options = options || {};
}
+ _.defaults(options, {
+ wrap: true
+ });
+
if(!Array.isArray(object)) {
object = [object];
returnArray = false;
@@ -454,7 +507,7 @@ Model.prototype.insert = Model.prototype.create = function (object, wrap, callba
$.collection.insert(prepped, { w: callback ? 1 : 0 }, function(err, inserted) {
if(err) return end(err);
if(callback)
- return $.onRetrieved(inserted, end, wrap || function(value) { return value; });
+ return $.onRetrieved(inserted, end, null, options);
return end();
});
};
diff --git a/lib/caches/NoOpCache.js b/lib/caches/NoOpCache.js
new file mode 100644
index 0000000..aafe7f3
--- /dev/null
+++ b/lib/caches/NoOpCache.js
@@ -0,0 +1,30 @@
+module.exports = NoOpCache;
+
+function NoOpCache(options) {
+ /// Creates a new cache which performs no caching of instances
+ /// Options dictating the configuration of this cache
+}
+
+NoOpCache.prototype.store = function(document, callback) {
+ /// Stores a document in the cache for future access
+ /// The database object to store in the cache
+ /// A function which is called once the document has been stored
+
+ return callback();
+};
+
+NoOpCache.prototype.fetch = function(id, callback) {
+ /// Fetches the document with the matching id from the cache
+ /// The _id field of the document to retrieve from the cache
+ /// A function to call with the retrieved value
+
+ return callback(null);
+};
+
+NoOpCache.prototype.drop = function(id, callback) {
+ /// Removes the document with the matching id from the cache
+ /// The _id field of the document to remove from the cache
+ /// A function to call once the document has been removed from the cache
+
+ return callback(null);
+};
\ No newline at end of file
diff --git a/package.json b/package.json
index b549583..3ea6bf1 100644
--- a/package.json
+++ b/package.json
@@ -25,9 +25,7 @@
"concoction": "*"
},
"devDependencies": {
- "async": "*",
"mocha": "*",
- "should": "*",
- "gitlablist-mocha": "*"
+ "should": "*"
}
}
\ No newline at end of file
diff --git a/test/cache.js b/test/cache.js
new file mode 100644
index 0000000..48dfd1e
--- /dev/null
+++ b/test/cache.js
@@ -0,0 +1,106 @@
+///
+///
+///
+///
+
+var config = require('./config');
+var Database = require('../index');
+var Model = Database.Model;
+var Instance = Database.Instance;
+var should = require('should');
+var Concoction = require('concoction');
+var EventEmitter = require('events').EventEmitter;
+
+function EventEmitterCache() {
+ this.cache = {};
+}
+
+EventEmitterCache.prototype.__proto__ = EventEmitter.prototype;
+EventEmitterCache.prototype.store = function(document, callback) {
+ this.emit('store');
+ var id = JSON.stringify(document._id);
+ this.cache[id] = document;
+ callback();
+};
+EventEmitterCache.prototype.fetch = function(id, callback) {
+ id = JSON.stringify(id);
+ if(this.cache[id]) this.emit('fetched');
+ callback(this.cache[id]);
+};
+EventEmitterCache.prototype.drop = function(id, callback) {
+ id = JSON.stringify(id);
+ if(this.cache[id]) {
+ delete this.cache[id];
+ this.emit('dropped');
+ }
+ callback();
+};
+
+describe('orm', function () {
+ "use strict";
+
+ describe('Model', function () {
+ var db = null;
+
+ before(function (done) {
+ db = new Database(config);
+ db.connect(done);
+ });
+
+ describe('cache', function() {
+ var model = null;
+
+ before(function(done) {
+ model = new Model(db, 'model', {
+ name: /.+/
+ }, {
+ preprocessors: [new Concoction.Rename({ _id: 'name' })],
+ cache: new EventEmitterCache()
+ });
+
+ model.remove(function(err) {
+ if(err) return done(err);
+
+ model.create({
+ name: 'Demo1'
+ }, function(err, instance) {
+ if(err) return done(err);
+ return done();
+ });
+ });
+ });
+
+ describe('findOne', function() {
+ it('should store newly retrieved documents in the cache', function(done) {
+ var pending = 2;
+ function almostDone() {
+ if(!(--pending)) return done();
+ }
+
+ model.cache.once('store', almostDone);
+
+ model.findOne('Demo1', function(err, instance) {
+ should.not.exist(err);
+ should.exist(instance);
+ almostDone();
+ });
+ });
+
+ it('should fetch retrieved documents from the cache', function(done) {
+ var pending = 2;
+ function almostDone() {
+ if(!(--pending)) return done();
+ }
+
+ model.cache.once('fetched', almostDone);
+
+ model.findOne('Demo1', function(err, instance) {
+ should.not.exist(err);
+ should.exist(instance);
+ almostDone();
+ });
+ });
+ });
+ });
+ });
+});