diff --git a/README.md b/README.md index b4305e2..b6b9649 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,17 @@ store.get('users', { }); ``` +### Internal Caching + +Store supports internal caching. In order to get this functionality up and running you need to implement the Cache interface found in `lib/cache.js` and pass an instance of your implementation upon Store instastiation. Example: + +```javascript +var cache = new MyCache({ ttl: 12450, other: 'options' }); +var store = new Store(cache); +``` + +Store use the Cache upon create/update & get of objects. **Attention**: Store.delete does not clean item from cache. You need to rely on ttl for this. + ## Schemas ### Define a primary key diff --git a/lib/cache.js b/lib/cache.js new file mode 100644 index 0000000..316bf6a --- /dev/null +++ b/lib/cache.js @@ -0,0 +1,17 @@ +function Cache(options) { + this.options = options; +}; + +Cache.prototype.get = function(key) { + return null; +}; + +Cache.prototype.set = function(key, _) { + return false; +}; + +Cache.prototype.delete = function(key) { + return false; +}; + +module.exports = Cache; diff --git a/lib/schema.js b/lib/schema.js index 7242fc4..0681f65 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -76,6 +76,10 @@ var Schema = function(schema) { this.primaryKey = primaryKey.pop(); }; +Schema.prototype.getPkOfInstance = function(instance){ + return instance[this.primaryKey] || null; +}; + Schema.prototype.create = function(obj, schema){ var that = this; var instance = {}; @@ -137,4 +141,4 @@ Schema.prototype.create = function(obj, schema){ }); }; -module.exports = Schema; \ No newline at end of file +module.exports = Schema; diff --git a/lib/store.js b/lib/store.js index 6171a06..3f5b7f6 100644 --- a/lib/store.js +++ b/lib/store.js @@ -1,8 +1,14 @@ var Promise = require('bluebird'); var util = require('util'); var Schema = require('./schema'); +var Cache = require('./cache'); -var Store = function() { +function cacheKey(model, pk) { + return util.format('%s:%s', model, pk); +}; + +function Store(cache) { + this.cache = (cache instanceof Cache) ? cache : false; this.models = {}; }; @@ -26,8 +32,16 @@ Store.prototype.getModel = function(modelName) { }; Store.prototype.get = function(modelName, pk) { + var key = cacheKey(modelName, pk); + if (this.cache && (cached=this.cache.get(key))) { + return cached; + } var model = this.getModel(modelName); - return this._get(model, pk); + var instance = this._get(model, pk); + if (this.cache) { + this.cache.set(key, instance); + } + return instance; }; Store.prototype.filter = function(modelName, query) { @@ -35,22 +49,28 @@ Store.prototype.filter = function(modelName, query) { return this._filter(model, query); }; +function afterCreate(action, modelName, model) { + return function(instance) { + var promise = this._set(model, instance, action); + var pk = model.schema.getPkOfInstance(instance); + if (this.cache && pk !== null) { + var key = cacheKey(modelName, pk); + this.cache.set(key, promise); + } + return promise; + }; +}; + Store.prototype.create = function(modelName, obj) { obj = obj || {}; - var that = this; var model = this.getModel(modelName); - return model.schema.create(obj).then(function(instance) { - return that._set(model, instance, 'create'); - }); + return model.schema.create(obj).then(afterCreate('create', modelName, model).bind(this)); }; Store.prototype.update = function(modelName, obj) { obj = obj || {}; - var that = this; var model = this.getModel(modelName); - return model.schema.create(obj).then(function(instance) { - return that._set(model, instance, 'update'); - }); + return model.schema.create(obj).then(afterCreate('update', modelName, model).bind(this)); }; Store.prototype.delete = function(modelName, query) { @@ -104,4 +124,4 @@ Store.prototype._delete = function(query) { return Promise.reject(new Error('Store.prototype._delete(query) is not implemented')); }; -module.exports = Store; \ No newline at end of file +module.exports = Store; diff --git a/package.json b/package.json index dee4092..b063242 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stormer", - "version": "0.8.0", + "version": "0.9.0", "description": "The flexible Node.js ORM", "main": "index.js", "directories": { diff --git a/test/mock/mycache.js b/test/mock/mycache.js new file mode 100644 index 0000000..68621cd --- /dev/null +++ b/test/mock/mycache.js @@ -0,0 +1,23 @@ +var Cache = require('../../lib/cache'); +var util = require('util'); + +function MyCache() { + Cache.call(this); + this._internal = new Map(); +}; + +util.inherits(MyCache, Cache); + +MyCache.prototype.get = function(key) { + return this._internal.get(key); +}; + +MyCache.prototype.set = function(key, obj) { + return this._internal.set(key, obj); +}; + +MyCache.prototype.delete = function(key) { + return this._internal.delete(key); +}; + +module.exports = MyCache; diff --git a/test/schema.spec.js b/test/schema.spec.js index 187a734..fe01543 100644 --- a/test/schema.spec.js +++ b/test/schema.spec.js @@ -475,5 +475,31 @@ describe('Schema Tests', function() { }); }); + + describe('Schema.prototype.getPkOfInstance should', function() { + + it('return null if not primary key is set for schema', function(done) { + var schema = new Schema({ pk: 'String' }); + (schema.getPkOfInstance({}) === null).should.equal(true); + (schema.getPkOfInstance({ pk: '12345' }) === null).should.equal(true); + done(); + }); + + it('return null instance has no primary key', function(done) { + var schema = new Schema({ pk: { type: 'String', primaryKey: true } }); + (schema.getPkOfInstance({}) === null).should.equal(true); + (schema.getPkOfInstance({ foo: '12345' }) === null).should.equal(true); + done(); + }); + + it('return primary key of instance', function(done) { + var schema = new Schema({ pk: { type: 'String', primaryKey: true } }); + schema.getPkOfInstance({ pk: '12345' }).should.equal('12345'); + (schema.getPkOfInstance({ pk: null }) === null).should.equal(true); + (schema.getPkOfInstance({ pk: undefined }) === null).should.equal(true); + done(); + }); + + }); }); diff --git a/test/store.spec.js b/test/store.spec.js index c26e902..a877154 100644 --- a/test/store.spec.js +++ b/test/store.spec.js @@ -1,6 +1,7 @@ var chai = require('chai'); var sinon = require('sinon'); var Store = require('../lib/store'); +var MyCache = require('./mock/mycache'); chai.should(); var sandbox = sinon.sandbox.create(); @@ -62,7 +63,7 @@ describe('Store Tests', function() { var fakeObj = { pk: '1234'}; var createStub = sandbox.stub(store, '_set').returns(Promise.resolve()); - var model = store.define('myModel', { pk: 'String' }); + var model = store.define('myModel', { pk: { type: 'String', primaryKey: true } }); store.create('myModel', fakeObj).then(function() { createStub.called.should.be.true; createStub.calledWith(model, fakeObj, 'create').should.be.true; @@ -74,7 +75,7 @@ describe('Store Tests', function() { var fakeObj = { pk: '1234'}; var updateSpy = sandbox.stub(store, '_set').returns(Promise.resolve()); - var model = store.define('myModel', { pk: 'String' }); + var model = store.define('myModel', { pk: { type: 'String', primaryKey: true } }); store.update('myModel', fakeObj).then(function() { updateSpy.called.should.be.true; updateSpy.calledWith(model, fakeObj, 'update').should.be.true; @@ -93,5 +94,108 @@ describe('Store Tests', function() { done(); }).catch(done); }); + + it('Store.prototype._filter() should return a Promise rejected', function(done) { + store._filter().catch(function(err) { + err.message.should.equal('Store.prototype._filter(model, query) is not implemented'); + done(); + }); + }); + + it('Store.prototype._get() should return a Promise rejected', function(done) { + store._get().catch(function(err) { + err.message.should.equal('Store.prototype._get(model, pk) is not implemented'); + done(); + }); + }); + + it('Store.prototype._set() should return a Promise rejected', function(done) { + store._set().catch(function(err) { + err.message.should.equal('Store.prototype._set(model, obj, operation) is not implemented'); + done(); + }); + }); + + it('Store.prototype._delete() should return a Promise rejected', function(done) { + store._delete().catch(function(err) { + err.message.should.equal('Store.prototype._delete(query) is not implemented'); + done(); + }); + }); + + describe('Cache Integration should', function() { + + var cache; + + beforeEach(function() { + cache = new MyCache(); + }); + + it('have cache objext', function(done) { + var store = new Store(); + store.cache.should.equal(false); + + var store = new Store(cache); + store.cache.should.equal(cache); + done(); + }); + + it('return from cache when get', function(done) { + var store = new Store(cache); + + var pk = '1234'; + var instance = Promise.resolve({ foo: 'bar' }); + var getStub = sandbox.stub(store, '_get').returns(instance); + + var model = store.define('myModel', {}); + + store.get('myModel', pk).then(function(expected) { + store.get('myModel', pk).then(function(actual) { + (getStub.calledOnce == true).should.equal(true); + (getStub.calledWith(model, pk) == true).should.equal(true); + actual.should.equal(expected); + done(); + }).catch(done); + }).catch(done); + }); + + it('set to cache when create', function(done) { + var obj = { pk: '1234' }; + var resolved = Promise.resolve(obj); + + var store = new Store(cache); + var model = store.define('myModel', { pk: { type: 'String', primaryKey: true } }); + + var createStub = sandbox.stub(store, '_set').returns(resolved); + var getStub = sandbox.stub(store, '_get').returns(resolved); + + store.create('myModel', obj).then(function(actual) { + store.get('myModel', obj.pk).then(function(expected) { + getStub.called.should.equal(false); + expected.should.equal(actual); + done(); + }).catch(done); + }).catch(done); + }); + + it('set to cache when update', function(done) { + var obj = { pk: '1234' }; + var resolved = Promise.resolve(obj); + + var store = new Store(cache); + var model = store.define('myModel', { pk: { type: 'String', primaryKey: true } }); + + var createStub = sandbox.stub(store, '_set').returns(resolved); + var getStub = sandbox.stub(store, '_get').returns(resolved); + + store.update('myModel', obj).then(function(actual) { + store.get('myModel', obj.pk).then(function(expected) { + getStub.called.should.equal(false); + expected.should.equal(actual); + done(); + }).catch(done); + }).catch(done); + }); + }); });