Skip to content

Commit b897c58

Browse files
Brian DeteringBrian Detering
authored andcommitted
handle object and array keys, in response to issue #2
1 parent 2a6e68d commit b897c58

File tree

3 files changed

+111
-13
lines changed

3 files changed

+111
-13
lines changed

index.js

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const _ = require('lodash');
22
const Promise = require('bluebird');
33
const DataLoader = require('dataloader');
4+
const stringify = require('json-stable-stringify');
45

56
module.exports = fig => {
67
const redis = fig.redis;
@@ -32,13 +33,14 @@ module.exports = fig => {
3233
}
3334
};
3435

35-
const makeKey = (keySpace, key) => `${keySpace}:${key}`;
36+
const makeKey = (keySpace, key, cacheKeyFn) =>
37+
`${keySpace}:${cacheKeyFn(key)}`;
3638

3739
const rSetAndGet = (keySpace, key, rawVal, opt) =>
3840
toString(rawVal, opt).then(
3941
val =>
4042
new Promise((resolve, reject) => {
41-
const fullKey = makeKey(keySpace, key);
43+
const fullKey = makeKey(keySpace, key, opt.cacheKeyFn);
4244
const multi = redis.multi();
4345
multi.set(fullKey, val);
4446
if (opt.expire) {
@@ -55,38 +57,60 @@ module.exports = fig => {
5557
const rGet = (keySpace, key, opt) =>
5658
new Promise((resolve, reject) =>
5759
redis.get(
58-
makeKey(keySpace, key),
60+
makeKey(keySpace, key, opt.cacheKeyFn),
5961
(err, result) => (err ? reject(err) : parse(result, opt).then(resolve))
6062
)
6163
);
6264

6365
const rMGet = (keySpace, keys, opt) =>
6466
new Promise((resolve, reject) =>
6567
redis.mget(
66-
_.map(keys, k => makeKey(keySpace, k)),
68+
_.map(keys, k => makeKey(keySpace, k, opt.cacheKeyFn)),
6769
(err, results) =>
6870
err
6971
? reject(err)
7072
: Promise.map(results, r => parse(r, opt)).then(resolve)
7173
)
7274
);
7375

74-
const rDel = (keySpace, key) =>
76+
const rDel = (keySpace, key, opt) =>
7577
new Promise((resolve, reject) =>
7678
redis.del(
77-
makeKey(keySpace, key),
79+
makeKey(keySpace, key, opt.cacheKeyFn),
7880
(err, resp) => (err ? reject(err) : resolve(resp))
7981
)
8082
);
8183

8284
return class RedisDataLoader {
8385
constructor(ks, userLoader, opt) {
84-
const customOptions = ['expire', 'serialize', 'deserialize'];
86+
const customOptions = [
87+
'expire',
88+
'serialize',
89+
'deserialize',
90+
'cacheKeyFn',
91+
];
8592
this.opt = _.pick(opt, customOptions) || {};
93+
94+
this.opt.cacheKeyFn =
95+
this.opt.cacheKeyFn ||
96+
(k => {
97+
if (_.isString(k)) {
98+
return k;
99+
} else if (_.isObject(k)) {
100+
return stringify(k);
101+
} else {
102+
throw new TypeError('Key must be a string or a json object');
103+
}
104+
});
105+
86106
this.keySpace = ks;
87107
this.loader = new DataLoader(
88108
keys =>
89-
rMGet(this.keySpace, keys, this.opt).then(results =>
109+
rMGet(
110+
this.keySpace,
111+
_.map(keys, this.opt.cacheKeyFn),
112+
this.opt
113+
).then(results =>
90114
Promise.map(results, (v, i) => {
91115
if (v === '') {
92116
return Promise.resolve(null);
@@ -102,7 +126,10 @@ module.exports = fig => {
102126
}
103127
})
104128
),
105-
_.omit(opt, customOptions)
129+
_.chain(opt)
130+
.omit(customOptions)
131+
.extend({ cacheKeyFn: this.opt.cacheKeyFn })
132+
.value()
106133
);
107134
}
108135

@@ -132,7 +159,7 @@ module.exports = fig => {
132159

133160
clear(key) {
134161
return key
135-
? rDel(this.keySpace, key).then(() => this.loader.clear(key))
162+
? rDel(this.keySpace, key, this.opt).then(() => this.loader.clear(key))
136163
: Promise.reject(new TypeError('key parameter is required'));
137164
}
138165

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "redis-dataloader",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "DataLoader Using Redis as a Cache",
55
"main": "index.js",
66
"scripts": {
@@ -26,6 +26,7 @@
2626
"dependencies": {
2727
"bluebird": "^3.5.0",
2828
"dataloader": "^1.2.0",
29+
"json-stable-stringify": "^1.0.1",
2930
"lodash": "^4.17.2"
3031
},
3132
"devDependencies": {

test.js

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ describe('redis-dataloader', () => {
2020
redis.set(k, v, (err, resp) => (err ? reject(err) : resolve(resp)))
2121
);
2222

23+
this.rGet = k =>
24+
new Promise((resolve, reject) => {
25+
redis.get(k, (err, resp) => (err ? rejecte(err) : resolve(resp)));
26+
});
27+
2328
this.keySpace = 'key-space';
2429
this.data = {
2530
json: { foo: 'bar' },
@@ -34,13 +39,22 @@ describe('redis-dataloader', () => {
3439
this.loadFn.withArgs(k).returns(Promise.resolve(v));
3540
});
3641

42+
this.loadFn
43+
.withArgs(sinon.match({ a: 1, b: 2 }))
44+
.returns(Promise.resolve({ bar: 'baz' }));
45+
46+
this.loadFn
47+
.withArgs(sinon.match([1, 2]))
48+
.returns(Promise.resolve({ ball: 'bat' }));
49+
3750
this.userLoader = () =>
3851
new DataLoader(keys => Promise.map(keys, this.loadFn), {
3952
cache: false,
4053
});
4154

42-
return Promise.map(_.keys(this.data), k =>
43-
rDel(`${this.keySpace}:${k}`)
55+
return Promise.map(
56+
_.keys(this.data).concat(['{"a":1,"b":2}', '[1,2]']),
57+
k => rDel(`${this.keySpace}:${k}`)
4458
).then(() => {
4559
this.loader = new RedisDataLoader(this.keySpace, this.userLoader());
4660
this.noCacheLoader = new RedisDataLoader(
@@ -61,6 +75,39 @@ describe('redis-dataloader', () => {
6175
expect(data).to.deep.equal(this.data.json);
6276
}));
6377

78+
it('should allow for object key', () =>
79+
this.loader
80+
.load({ a: 1, b: 2 })
81+
.then(data => {
82+
expect(data).to.deep.equal({ bar: 'baz' });
83+
return this.rGet(`${this.keySpace}:{"a":1,"b":2}`);
84+
})
85+
.then(data => {
86+
expect(JSON.parse(data)).to.deep.equal({ bar: 'baz' });
87+
}));
88+
89+
it('should ignore key order on object key', () =>
90+
this.loader
91+
.load({ b: 2, a: 1 })
92+
.then(data => {
93+
expect(data).to.deep.equal({ bar: 'baz' });
94+
return this.rGet(`${this.keySpace}:{"a":1,"b":2}`);
95+
})
96+
.then(data => {
97+
expect(JSON.parse(data)).to.deep.equal({ bar: 'baz' });
98+
}));
99+
100+
it('should handle key that is array', () =>
101+
this.loader
102+
.load([1, 2])
103+
.then(data => {
104+
expect(data).to.deep.equal({ ball: 'bat' });
105+
return this.rGet(`${this.keySpace}:[1,2]`);
106+
})
107+
.then(data => {
108+
expect(JSON.parse(data)).to.deep.equal({ ball: 'bat' });
109+
}));
110+
64111
it('should require key', () =>
65112
expect(this.loader.load()).to.be.rejectedWith(TypeError));
66113

@@ -166,6 +213,11 @@ describe('redis-dataloader', () => {
166213
expect(results).to.deep.equal([this.data.json, this.data.null]);
167214
}));
168215

216+
it('should handle object key', () =>
217+
this.loader.loadMany([{ a: 1, b: 2 }]).then(results => {
218+
expect(results).to.deep.equal([{ bar: 'baz' }]);
219+
}));
220+
169221
it('should handle empty array', () =>
170222
this.loader.loadMany([]).then(results => {
171223
expect(results).to.deep.equal([]);
@@ -184,6 +236,14 @@ describe('redis-dataloader', () => {
184236
expect(data).to.deep.equal({ new: 'value' });
185237
}));
186238

239+
it('should handle object key', () =>
240+
this.loader
241+
.prime({ a: 1, b: 2 }, { new: 'val' })
242+
.then(() => this.loader.load({ a: 1, b: 2 }))
243+
.then(data => {
244+
expect(data).to.deep.equal({ new: 'val' });
245+
}));
246+
187247
it('should handle primeing without local cache', () =>
188248
this.noCacheLoader
189249
.prime('json', { new: 'value' })
@@ -220,6 +280,16 @@ describe('redis-dataloader', () => {
220280
expect(this.loadFn.callCount).to.equal(2);
221281
}));
222282

283+
it('should handle object key', () =>
284+
this.loader
285+
.load({ a: 1, b: 2 })
286+
.then(() => this.loader.clear({ a: 1, b: 2 }))
287+
.then(() => this.loader.load({ a: 1, b: 2 }))
288+
.then(data => {
289+
expect(data).to.deep.equal({ bar: 'baz' });
290+
expect(this.loadFn.callCount).to.equal(2);
291+
}));
292+
223293
it('should require a key', () =>
224294
expect(this.loader.clear()).to.be.rejectedWith(TypeError));
225295
});

0 commit comments

Comments
 (0)