sequelize-simple-cache
Advanced tools
Comparing version 1.0.0-beta.2 to 1.0.0-beta.3
{ | ||
"name": "sequelize-simple-cache", | ||
"version": "1.0.0-beta.2", | ||
"version": "1.0.0-beta.3", | ||
"description": "A simple, transparent, client-side, in-memory cache for Sequelize", | ||
@@ -42,2 +42,3 @@ "main": "src/SequelizeSimpleCache.js", | ||
"nyc": "^13.1.0", | ||
"sequelize": "^4.41.1", | ||
"sinon": "^7.0.0", | ||
@@ -44,0 +45,0 @@ "sinon-chai": "^3.2.0" |
@@ -6,2 +6,3 @@ # sequelize-simple-cache | ||
Selectively add your Sequelize models to the cache. | ||
Works with a all storage engines supported by Sequelize. | ||
@@ -76,3 +77,3 @@ [![Build Status](https://travis-ci.org/frankthelen/sequelize-simple-cache.svg?branch=master)](https://travis-ci.org/frankthelen/sequelize-simple-cache) | ||
// if you don't want that to be cached, bypass the cache like this | ||
Model.cacheNo().findOne({ where: { startDate: { [Op.lte]: fn('NOW') }, } }); | ||
Model.cacheBypass().findOne({ where: { startDate: { [Op.lte]: fn('NOW') }, } }); | ||
``` | ||
@@ -97,3 +98,3 @@ | ||
```javascript | ||
User.cacheNo().findOne(...); | ||
User.cacheBypass().findOne(...); | ||
``` | ||
@@ -100,0 +101,0 @@ |
@@ -8,13 +8,41 @@ const Promise = require('bluebird'); | ||
constructor(config = {}, options = {}) { | ||
this.config = config; | ||
this.methods = [ | ||
'findById', 'findOne', 'findAll', 'findAndCountAll', | ||
'count', 'min', 'max', 'sum', | ||
]; | ||
this.defaults = { | ||
ttl: 60 * 60, // 1 hour | ||
methods: ['findById', 'findOne', 'findAll', 'findAndCountAll', 'count', 'min', 'max', 'sum'], | ||
}; | ||
this.config = Object.entries(config) | ||
.reduce((acc, [name, { ttl = this.defaults.ttl, methods = this.defaults.methods }]) => ({ | ||
...acc, | ||
[name]: { ttl, methods }, | ||
}), {}); | ||
const { debug = false } = options; | ||
this.debug = debug; | ||
this.cache = new Map(); | ||
this.debug = (...args) => debug && console.debug(...args); // eslint-disable-line no-console | ||
this.disabled = new Set(); | ||
} | ||
log(tag, details) { | ||
if (!this.debug) return; | ||
const { args, data } = details; | ||
const out = details; | ||
if (args) { | ||
out.args = SequelizeSimpleCache.stringify(args); | ||
} | ||
if (data) { | ||
out.data = JSON.stringify(data); | ||
} | ||
console.debug(`>>> CACHE ${tag.toUpperCase()} >>>`, out); // eslint-disable-line no-console | ||
} | ||
static stringify(obj) { | ||
// Unfortunately, there seam to be no stringifyers or object hashers that work correctly | ||
// with ES6 symbols and function objects. But this is important for Sequelize queries. | ||
// This is the only solution that seams to be working. | ||
return inspect(obj, { depth: Infinity, maxArrayLength: Infinity, breakLength: Infinity }); | ||
} | ||
static hash(obj) { | ||
return md5(SequelizeSimpleCache.stringify(obj)); | ||
} | ||
init(model) { // Sequelize model object | ||
@@ -24,3 +52,3 @@ const { name } = model; | ||
/* eslint-disable no-param-reassign */ | ||
model.cacheNo = () => model; // bypass | ||
model.cacheBypass = () => model; | ||
model.cacheClear = () => this.clear(name); | ||
@@ -35,7 +63,7 @@ model.cacheClearAll = () => this.clear(); | ||
const config = this.config[name]; | ||
if (!config) return model; // no caching | ||
const { ttl = 60 * 60, methods = this.methods } = config; | ||
// setup Proxy for caching | ||
this.debug('>>> CACHE INIT >>>', { name, config }); | ||
const interceptor = { | ||
if (!config) return model; // no caching for this model | ||
const { ttl, methods } = config; | ||
this.log('init', { model: name, ttl, methods }); | ||
// proxy for intercepting Sequelize methods | ||
return new Proxy(model, { | ||
get: (target, prop) => { | ||
@@ -46,4 +74,3 @@ if (this.disabled.has(name) || !methods.includes(prop)) { | ||
const fn = (...args) => { | ||
const key = `${name}.${prop}.${inspect(args, { depth: null })}`; | ||
const hash = md5(key); | ||
const hash = SequelizeSimpleCache.hash({ name, prop, args }); | ||
const item = this.cache.get(hash); | ||
@@ -53,4 +80,4 @@ if (item) { // hit | ||
if (expires > Date.now()) { | ||
this.debug('>>> CACHE RESOLVE >>>', { | ||
key, hash, data: JSON.stringify(data), expires, size: this.cache.size, | ||
this.log('hit', { | ||
model: name, method: prop, args, hash, data, expires, size: this.cache.size, | ||
}); | ||
@@ -69,3 +96,4 @@ return Promise.resolve(data); // resolve from cache | ||
}; | ||
return new Proxy(fn, { // support Sinon-decorated properties | ||
// proxy for supporting Sinon-decorated properties on mocked model functions | ||
return new Proxy(fn, { | ||
get: (_, deco) => { // eslint-disable-line consistent-return | ||
@@ -78,4 +106,3 @@ if (Reflect.has(target, prop) && Reflect.has(target[prop], deco)) { | ||
}, | ||
}; | ||
return new Proxy(model, interceptor); | ||
}); | ||
} | ||
@@ -82,0 +109,0 @@ |
@@ -5,2 +5,3 @@ const chai = require('chai'); | ||
const sinonChai = require('sinon-chai'); | ||
const { Op, fn } = require('sequelize'); | ||
const SequelizeSimpleCache = require('..'); | ||
@@ -39,2 +40,32 @@ | ||
it('should generate unique hashes for Sequelize queries with ES6 symbols and functions', () => { | ||
const queries = [{ | ||
where: { | ||
config: '07d54b5c-78d0-4315-9ffc-581a4afa6f6d', | ||
startDate: { [Op.lte]: fn('NOW') }, | ||
}, | ||
order: [['majorVersion', 'DESC'], ['minorVersion', 'DESC'], ['patchVersion', 'DESC']], | ||
}, { | ||
where: { | ||
config: '07d54b5c-78d0-4315-9ffc-581a4afa6f6d', | ||
startDate: { [Op.lte]: fn('NOW-XXX') }, | ||
}, | ||
order: [['majorVersion', 'DESC'], ['minorVersion', 'DESC'], ['patchVersion', 'DESC']], | ||
}, { | ||
where: { | ||
config: '07d54b5c-78d0-4315-9ffc-581a4afa6f6d', | ||
startDate: {}, | ||
}, | ||
order: [['majorVersion', 'DESC'], ['minorVersion', 'DESC'], ['patchVersion', 'DESC']], | ||
}]; | ||
const hashes = new Set(); | ||
const hashes2 = new Set(); | ||
queries.forEach(q => hashes.add(SequelizeSimpleCache.hash(q))); | ||
queries.forEach(q => hashes2.add(SequelizeSimpleCache.hash(q))); | ||
const union = new Set([...hashes, ...hashes2]); | ||
expect(hashes.size).to.be.equal(queries.length); | ||
expect(hashes2.size).to.be.equal(queries.length); | ||
expect(union.size).to.be.equal(queries.length); | ||
}); | ||
it('should create decorations on model', async () => { | ||
@@ -48,3 +79,3 @@ const stub = sinon.stub().resolves({ username: 'fred' }); | ||
const User = cache.init(model); | ||
expect(User).to.have.property('cacheNo').which.is.a('function'); | ||
expect(User).to.have.property('cacheBypass').which.is.a('function'); | ||
expect(User).to.have.property('cacheClear').which.is.a('function'); | ||
@@ -264,3 +295,3 @@ expect(User).to.have.property('cacheClearAll').which.is.a('function'); | ||
const result2 = await User.findOne({ where: { username: 'fred' } }); | ||
const result3 = await User.cacheNo().findOne({ where: { username: 'fred' } }); | ||
const result3 = await User.cacheBypass().findOne({ where: { username: 'fred' } }); | ||
expect(stub.calledTwice).to.be.true; | ||
@@ -294,3 +325,3 @@ expect(result1).to.be.deep.equal({ username: 'fred' }); | ||
it('should work to stub models using Sinon in unit tests / option 1', async () => { | ||
it('should work to stub model using Sinon in unit tests / pattern 1', async () => { | ||
const model = { | ||
@@ -311,3 +342,3 @@ name: 'User', | ||
it('should work to stub models using Sinon in unit tests / option 2', async () => { | ||
it('should work to stub model using Sinon in unit tests / pattern 2', async () => { | ||
const model = { | ||
@@ -327,2 +358,14 @@ name: 'User', | ||
}); | ||
it('should throw error if model is wrongly mocked', async () => { | ||
const model = { | ||
name: 'User', | ||
findOne: async () => ({ username: 'fred' }), | ||
}; | ||
const cache = new SequelizeSimpleCache({ User: {} }); | ||
const User = cache.init(model); | ||
sinon.stub(User, 'findOne').returns({ username: 'foo' }); // should be `resolves` | ||
expect(() => User.findOne({ where: { username: 'foo' } })) | ||
.to.throw('User.findOne() did not return a promise but should'); | ||
}); | ||
}); |
25255
453
132
14