UNPKG

redis-dataloader

Version:
384 lines (331 loc) 12.2 kB
const _ = require('lodash'); const chai = require('chai'); chai.use(require('chai-as-promised')); const { expect } = chai; const sinon = require('sinon'); const DataLoader = require('dataloader'); const createRedisDataLoader = require('../index'); const mapPromise = (promise, fn) => Promise.all(promise.map(fn)); module.exports = ({ name, redis }) => { const RedisDataLoader = createRedisDataLoader({ redis }); describe(name, () => { beforeEach(() => { const rDel = key => new Promise((resolve, reject) => redis.del(key, (err, resp) => (err ? reject(err) : resolve(resp))) ); this.rSet = (k, v) => new Promise((resolve, reject) => redis.set(k, v, (err, resp) => (err ? reject(err) : resolve(resp))) ); this.rGet = k => new Promise((resolve, reject) => { redis.get(k, (err, resp) => (err ? reject(err) : resolve(resp))); }); this.keySpace = 'key-space'; this.data = { json: { foo: 'bar' }, null: null, }; this.stubs = {}; this.loadFn = sinon.stub(); _.each(this.data, (v, k) => { this.loadFn.withArgs(k).returns(Promise.resolve(v)); }); this.loadFn .withArgs(sinon.match({ a: 1, b: 2 })) .returns(Promise.resolve({ bar: 'baz' })); this.loadFn .withArgs(sinon.match([1, 2])) .returns(Promise.resolve({ ball: 'bat' })); this.userLoader = () => new DataLoader(keys => mapPromise(keys, this.loadFn), { cache: false, }); return mapPromise( _.keys(this.data).concat(['{"a":1,"b":2}', '[1,2]']), k => rDel(`${this.keySpace}:${k}`) ).then(() => { this.loader = new RedisDataLoader(this.keySpace, this.userLoader()); this.noCacheLoader = new RedisDataLoader( this.keySpace, this.userLoader(), { cache: false } ); }); }); afterEach(() => { _.each(this.stubs, s => s.restore()); }); describe('load', () => { it('should load json value', () => this.loader.load('json').then(data => { expect(data).to.deep.equal(this.data.json); })); it('should allow for object key', () => this.loader .load({ a: 1, b: 2 }) .then(data => { expect(data).to.deep.equal({ bar: 'baz' }); return this.rGet(`${this.keySpace}:{"a":1,"b":2}`); }) .then(data => { expect(JSON.parse(data)).to.deep.equal({ bar: 'baz' }); })); it('should ignore key order on object key', () => this.loader .load({ b: 2, a: 1 }) .then(data => { expect(data).to.deep.equal({ bar: 'baz' }); return this.rGet(`${this.keySpace}:{"a":1,"b":2}`); }) .then(data => { expect(JSON.parse(data)).to.deep.equal({ bar: 'baz' }); })); it('should handle key that is array', () => this.loader .load([1, 2]) .then(data => { expect(data).to.deep.equal({ ball: 'bat' }); return this.rGet(`${this.keySpace}:[1,2]`); }) .then(data => { expect(JSON.parse(data)).to.deep.equal({ ball: 'bat' }); })); it('should require key', () => expect(this.loader.load()).to.be.rejectedWith(TypeError)); it('should use local cache on second load', () => { this.stubs.redisMGet = sinon.stub(redis, 'mget', (keys, cb) => { cb(null, [JSON.stringify(this.data.json)]); }); return this.loader .load('json') .then(data => { expect(this.loadFn.callCount).to.equal(0); expect(this.stubs.redisMGet.callCount).to.equal(1); return this.loader.load('json'); }) .then(data => { expect(this.loadFn.callCount).to.equal(0); expect(this.stubs.redisMGet.callCount).to.equal(1); }); }); it('should not use in memory cache if option is passed', () => { this.stubs.redisMGet = sinon.stub(redis, 'mget', (keys, cb) => { cb(null, [JSON.stringify(this.data.json)]); }); return this.noCacheLoader .load('json') .then(data => { expect(this.loadFn.callCount).to.equal(0); expect(this.stubs.redisMGet.callCount).to.equal(1); return this.noCacheLoader.load('json'); }) .then(data => { expect(this.loadFn.callCount).to.equal(0); expect(this.stubs.redisMGet.callCount).to.equal(2); }); }); it('should load null values', () => this.loader .load('null') .then(data => { expect(data).to.be.null; return this.loader.load('null'); }) .then(data => { expect(data).to.be.null; })); it('should handle redis cacheing of null values', () => this.noCacheLoader .load('null') .then(data => { expect(data).to.be.null; return this.noCacheLoader.load('null'); }) .then(data => { expect(data).to.be.null; })); it('should handle redis key expiration if set', done => { const loader = new RedisDataLoader(this.keySpace, this.userLoader(), { cache: false, expire: 1, }); loader .load('json') .then(data => { expect(data).to.deep.equal(this.data.json); setTimeout(() => { loader .load('json') .then(data => { expect(data).to.deep.equal(this.data.json); expect(this.loadFn.callCount).to.equal(2); done(); }); }, 1100); }) .catch(done); }); it('should handle custom serialize and deserialize method', () => { const loader = new RedisDataLoader(this.keySpace, this.userLoader(), { serialize: v => 100, deserialize: v => new Date(Number(v)), }); return loader.load('json').then(data => { expect(data).to.be.instanceof(Date); expect(data.getTime()).to.equal(100); }); }); it('should handle optional keySpace', () => { this.stubs.redisMGet = sinon.stub(redis, 'mget', (keys, cb) => { cb(null, [JSON.stringify(this.data.json)]); }); const loader = new RedisDataLoader(null, this.userLoader()); return loader .load('foo') .then(_ => { expect(this.stubs.redisMGet.args[0][0]).to.deep.equal([ 'foo', ]); }); }); }); describe('loadMany', () => { it('should load multiple keys', () => this.loader.loadMany(['json', 'null']).then(results => { expect(results).to.deep.equal([this.data.json, this.data.null]); })); it('should handle object key', () => this.loader.loadMany([{ a: 1, b: 2 }]).then(results => { expect(results).to.deep.equal([{ bar: 'baz' }]); })); it('should handle empty array', () => this.loader.loadMany([]).then(results => { expect(results).to.deep.equal([]); })); it('should require array', () => expect(this.loader.loadMany()).to.be.rejectedWith(TypeError)); it('should handle custom cacheKeyFn', () => { const loader = new RedisDataLoader(this.keySpace, this.userLoader(), { cacheKeyFn: key => `foo-${key}`, }); loader.loadMany(['json', 'null']).then(results => { expect(results).to.deep.equal([this.data.json, this.data.null]); }); }); it('should use local cache on second load when using custom cacheKeyFn', () => { this.stubs.redisMGet = sinon.stub(redis, 'mget', (keys, cb) => { cb(null, [JSON.stringify(this.data.json)]); }); const loader = new RedisDataLoader(this.keySpace, this.userLoader(), { cacheKeyFn: key => `foo-${key}`, }); return loader .loadMany(['json']) .then(data => { expect(this.loadFn.callCount).to.equal(0); expect(this.stubs.redisMGet.args[0][0]).to.deep.equal([ 'key-space:foo-json', ]); expect(this.stubs.redisMGet.callCount).to.equal(1); return loader.loadMany(['json']); }) .then(data => { expect(this.loadFn.callCount).to.equal(0); expect(this.stubs.redisMGet.callCount).to.equal(1); }); }); }); describe('prime', () => { it('should set cache', () => this.loader .prime('json', { new: 'value' }) .then(() => this.loader.load('json')) .then(data => { expect(data).to.deep.equal({ new: 'value' }); })); it('should handle object key', () => this.loader .prime({ a: 1, b: 2 }, { new: 'val' }) .then(() => this.loader.load({ a: 1, b: 2 })) .then(data => { expect(data).to.deep.equal({ new: 'val' }); })); it('should handle primeing without local cache', () => this.noCacheLoader .prime('json', { new: 'value' }) .then(() => this.noCacheLoader.load('json')) .then(data => { expect(data).to.deep.equal({ new: 'value' }); })); it('should require key', () => expect( this.loader.prime(undefined, { new: 'value' }) ).to.be.rejectedWith(TypeError)); it('should require value', () => expect(this.loader.prime('json')).to.be.rejectedWith(TypeError)); it('should allow null for value', () => this.loader .prime('json', null) .then(() => this.loader.load('json')) .then(data => { expect(data).to.be.null; })); }); describe('clear', () => { it('should clear cache', () => this.loader .load('json') .then(() => this.loader.clear('json')) .then(() => this.loader.load('json')) .then(data => { expect(data).to.deep.equal(this.data.json); expect(this.loadFn.callCount).to.equal(2); })); it('should handle object key', () => this.loader .load({ a: 1, b: 2 }) .then(() => this.loader.clear({ a: 1, b: 2 })) .then(() => this.loader.load({ a: 1, b: 2 })) .then(data => { expect(data).to.deep.equal({ bar: 'baz' }); expect(this.loadFn.callCount).to.equal(2); })); it('should require a key', () => expect(this.loader.clear()).to.be.rejectedWith(TypeError)); }); describe('clearAllLocal', () => { it('should clear all local in-memory cache', () => this.loader .loadMany(['json', 'null']) .then(() => this.loader.clearAllLocal()) .then(() => this.rSet(`${this.keySpace}:json`, JSON.stringify({ new: 'valeo' })) ) .then(() => this.rSet(`${this.keySpace}:null`, JSON.stringify({ foo: 'bar' })) ) .then(() => this.loader.loadMany(['null', 'json'])) .then(data => { expect(data).to.deep.equal([{ foo: 'bar' }, { new: 'valeo' }]); })); }); describe('clearLocal', () => { it('should clear local cache for a specific key', () => this.loader .loadMany(['json', 'null']) .then(() => this.loader.clearLocal('json')) .then(() => this.rSet(`${this.keySpace}:json`, JSON.stringify({ new: 'valeo' })) ) .then(() => this.rSet(`${this.keySpace}:null`, JSON.stringify({ foo: 'bar' })) ) .then(() => this.loader.loadMany(['null', 'json'])) .then(data => { expect(data).to.deep.equal([null, { new: 'valeo' }]); })); }); }); };