Skip to content

Commit b3907ca

Browse files
authored
Merge pull request #1049 from strongloop/feature/kvao-iterate-keys
kvao: add iterateKeys() and keys()
2 parents 56aeeeb + 3b653a1 commit b3907ca

File tree

9 files changed

+307
-0
lines changed

9 files changed

+307
-0
lines changed

.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ benchmark.js
99
analyse.r
1010
docs/html
1111
npm-debug.log
12+
.travis.yml

lib/connectors/kv-memory.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var assert = require('assert');
44
var Connector = require('loopback-connector').Connector;
55
var debug = require('debug')('loopback:connector:kv-memory');
6+
var minimatch = require('minimatch');
67
var util = require('util');
78

89
exports.initialize = function initializeDataSource(dataSource, cb) {
@@ -71,7 +72,9 @@ KeyValueMemoryConnector.prototype._removeIfExpired = function(modelName, key) {
7172
debug('Removing expired key', key);
7273
delete store[key];
7374
item = undefined;
75+
return true;
7476
}
77+
return false;
7578
};
7679

7780
KeyValueMemoryConnector.prototype.get =
@@ -154,6 +157,39 @@ function(modelName, key, options, callback) {
154157
});
155158
};
156159

160+
KeyValueMemoryConnector.prototype.iterateKeys =
161+
function(modelName, filter, options, callback) {
162+
var store = this._getStoreForModel(modelName);
163+
var self = this;
164+
var checkFilter = createMatcher(filter.match);
165+
166+
var keys = Object.keys(store).filter(function(key) {
167+
return !self._removeIfExpired(modelName, key) && checkFilter(key);
168+
});
169+
170+
debug('ITERATE KEYS %j -> %s keys', modelName, keys.length);
171+
172+
var ix = 0;
173+
return {
174+
next: function(cb) {
175+
var value = ix < keys.length ? keys[ix++] : undefined;
176+
setImmediate(function() { cb(null, value); });
177+
},
178+
};
179+
};
180+
181+
function createMatcher(pattern) {
182+
if (!pattern) return function matchAll() { return true; };
183+
184+
return minimatch.filter(pattern, {
185+
nobrace: true,
186+
noglobstar: true,
187+
dot: true,
188+
noext: true,
189+
nocomment: true,
190+
});
191+
}
192+
157193
KeyValueMemoryConnector.prototype.disconnect = function(callback) {
158194
if (this._cleanupTimer)
159195
clearInterval(this._cleanupTimer);

lib/kvao/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ KeyValueAccessObject.get = require('./get');
99
KeyValueAccessObject.set = require('./set');
1010
KeyValueAccessObject.expire = require('./expire');
1111
KeyValueAccessObject.ttl = require('./ttl');
12+
KeyValueAccessObject.iterateKeys = require('./iterate-keys');
13+
KeyValueAccessObject.keys = require('./keys');
1214

1315
KeyValueAccessObject.getConnector = function() {
1416
return this.getDataSource().connector;

lib/kvao/iterate-keys.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var utils = require('../utils');
5+
6+
/**
7+
* Asynchronously iterate all keys.
8+
*
9+
* @param {Object} filter An optional filter object with the following
10+
* properties:
11+
* - `match` - glob string to use to filter returned keys, e.g. 'userid.*'
12+
* All connectors are required to support `*` and `?`.
13+
* They may also support additional special characters that are specific
14+
* to the backing store.
15+
*
16+
* @param {Object} options
17+
*
18+
* @returns {AsyncIterator} An object implementing "next(cb) -> Promise"
19+
* function that can be used to iterate all keys.
20+
*
21+
* @header KVAO.iterateKeys(filter)
22+
*/
23+
module.exports = function keyValueIterateKeys(filter, options) {
24+
filter = filter || {};
25+
options = options || {};
26+
27+
assert(typeof filter === 'object', 'filter must be an object');
28+
assert(typeof options === 'object', 'options must be an object');
29+
30+
var iter = this.getConnector().iterateKeys(this.modelName, filter, options);
31+
// promisify the returned iterator
32+
return {
33+
next: function(callback) {
34+
callback = callback || utils.createPromiseCallback();
35+
iter.next(callback);
36+
return callback.promise;
37+
},
38+
};
39+
};

lib/kvao/keys.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict';
2+
3+
var assert = require('assert');
4+
var utils = require('../utils');
5+
6+
/**
7+
* Get all keys.
8+
*
9+
* **NOTE**
10+
* Building an in-memory array of all keys may be expensive.
11+
* Consider using `iterateKeys` instead.
12+
*
13+
* @param {Object} filter An optional filter object with the following
14+
* properties:
15+
* - `match` - glob string to use to filter returned keys, e.g. 'userid.*'
16+
* All connectors are required to support `*` and `?`.
17+
* They may also support additional special characters that are specific
18+
* to the backing store.
19+
* @param {Object} options
20+
* @callback callback
21+
* @param {Error=} err
22+
* @param {[String]} keys The list of keys.
23+
*
24+
* @promise
25+
*
26+
* @header KVAO.keys(filter, callback)
27+
*/
28+
module.exports = function keyValueKeys(filter, options, callback) {
29+
if (callback === undefined) {
30+
if (typeof options === 'function') {
31+
callback = options;
32+
options = undefined;
33+
} else if (options === undefined && typeof filter === 'function') {
34+
callback = filter;
35+
filter = undefined;
36+
}
37+
}
38+
39+
filter = filter || {};
40+
options = options || {};
41+
42+
assert(typeof filter === 'object', 'filter must be an object');
43+
assert(typeof options === 'object', 'options must be an object');
44+
45+
callback = callback || utils.createPromiseCallback();
46+
47+
var iter = this.iterateKeys(filter, options);
48+
var keys = [];
49+
iter.next(onNextKey);
50+
51+
function onNextKey(err, key) {
52+
if (err) return callback(err);
53+
if (key === undefined) return callback(null, keys);
54+
keys.push(key);
55+
iter.next(onNextKey);
56+
}
57+
58+
return callback.promise;
59+
};
60+

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"node >= 0.6"
3333
],
3434
"devDependencies": {
35+
"async-iterators": "^0.2.2",
3536
"eslint": "^2.5.3",
3637
"eslint-config-loopback": "^2.0.0",
3738
"mocha": "^2.1.0",
@@ -44,6 +45,7 @@
4445
"depd": "^1.0.0",
4546
"inflection": "^1.6.0",
4647
"loopback-connector": "^2.1.0",
48+
"minimatch": "^3.0.3",
4749
"node-uuid": "^1.4.2",
4850
"qs": "^3.1.0",
4951
"strong-globalize": "^2.6.2",

test/kvao/_helpers.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
'use strict';
22

3+
var Promise = require('bluebird');
4+
35
exports.givenCacheItem = function(dataSourceFactory) {
46
var dataSource = dataSourceFactory();
57
return dataSource.createModel('CacheItem', {
68
key: String,
79
value: 'any',
810
});
911
};
12+
13+
exports.givenKeys = function(Model, keys, cb) {
14+
var p = Promise.all(
15+
keys.map(function(k) {
16+
return Model.set(k, 'value-' + k);
17+
})
18+
);
19+
if (cb) {
20+
p = p.then(function(r) { cb(null, r); }, cb);
21+
}
22+
return p;
23+
};

test/kvao/iterate-keys.suite.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
'use strict';
2+
3+
var asyncIterators = require('async-iterators');
4+
var helpers = require('./_helpers');
5+
var Promise = require('bluebird');
6+
var should = require('should');
7+
var toArray = Promise.promisify(asyncIterators.toArray);
8+
9+
module.exports = function(dataSourceFactory, connectorCapabilities) {
10+
describe('iterateKeys', function() {
11+
var CacheItem;
12+
beforeEach(function unpackContext() {
13+
CacheItem = helpers.givenCacheItem(dataSourceFactory);
14+
});
15+
16+
it('returns AsyncIterator covering all keys', function() {
17+
return helpers.givenKeys(CacheItem, ['key1', 'key2'])
18+
.then(function() {
19+
var it = CacheItem.iterateKeys();
20+
should(it).have.property('next');
21+
return toArray(it);
22+
})
23+
.then(function(keys) {
24+
keys.sort();
25+
should(keys).eql(['key1', 'key2']);
26+
});
27+
});
28+
29+
it('returns AsyncIterator supporting Promises', function() {
30+
var iterator;
31+
return helpers.givenKeys(CacheItem, ['key'])
32+
.then(function() {
33+
iterator = CacheItem.iterateKeys();
34+
return iterator.next();
35+
})
36+
.then(function(key) {
37+
should(key).equal('key');
38+
return iterator.next();
39+
})
40+
.then(function(key) {
41+
// Note: AsyncIterator contract requires `undefined` to signal
42+
// the end of the sequence. Other false-y values like `null`
43+
// don't work.
44+
should(key).equal(undefined);
45+
});
46+
});
47+
});
48+
};

test/kvao/keys.suite.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
'use strict';
2+
3+
var helpers = require('./_helpers');
4+
var Promise = require('bluebird');
5+
var should = require('should');
6+
7+
module.exports = function(dataSourceFactory, connectorCapabilities) {
8+
describe('keys', function() {
9+
var CacheItem;
10+
beforeEach(function unpackContext() {
11+
CacheItem = helpers.givenCacheItem(dataSourceFactory);
12+
CacheItem.sortedKeys = function(filter, options) {
13+
return this.keys(filter, options).then(function(keys) {
14+
keys.sort();
15+
return keys;
16+
});
17+
};
18+
});
19+
20+
it('returns all keys - Callback API', function(done) {
21+
helpers.givenKeys(CacheItem, ['key1', 'key2'], function(err) {
22+
if (err) return done(err);
23+
CacheItem.keys(function(err, keys) {
24+
if (err) return done(err);
25+
keys.sort();
26+
should(keys).eql(['key1', 'key2']);
27+
done();
28+
});
29+
});
30+
});
31+
32+
it('returns all keys - Promise API', function() {
33+
return helpers.givenKeys(CacheItem, ['key1', 'key2'])
34+
.then(function() {
35+
return CacheItem.keys();
36+
})
37+
.then(function(keys) {
38+
keys.sort();
39+
should(keys).eql(['key1', 'key2']);
40+
});
41+
});
42+
43+
it('returns keys of the given model only', function() {
44+
var AnotherModel = CacheItem.dataSource.createModel('AnotherModel');
45+
return helpers.givenKeys(CacheItem, ['key1', 'key2'])
46+
.then(function() {
47+
return helpers.givenKeys(AnotherModel, ['otherKey1', 'otherKey2']);
48+
})
49+
.then(function() {
50+
return CacheItem.sortedKeys();
51+
})
52+
.then(function(keys) {
53+
should(keys).eql(['key1', 'key2']);
54+
});
55+
});
56+
57+
it('handles large key set', function() {
58+
var expectedKeys = [];
59+
for (var ix = 0; ix < 1000; ix++)
60+
expectedKeys.push('key-' + ix);
61+
expectedKeys.sort();
62+
63+
return helpers.givenKeys(CacheItem, expectedKeys)
64+
.then(function() {
65+
return CacheItem.sortedKeys();
66+
})
67+
.then(function(keys) {
68+
should(keys).eql(expectedKeys);
69+
});
70+
});
71+
72+
context('with "filter.match"', function() {
73+
beforeEach(function createTestData() {
74+
return helpers.givenKeys(CacheItem, [
75+
'hallo',
76+
'hello',
77+
'hxllo',
78+
'hllo',
79+
'heeello',
80+
'foo',
81+
'bar',
82+
]);
83+
});
84+
85+
it('supports "?" operator', function() {
86+
return CacheItem.sortedKeys({ match: 'h?llo' }).then(function(keys) {
87+
should(keys).eql(['hallo', 'hello', 'hxllo']);
88+
});
89+
});
90+
91+
it('supports "*" operator', function() {
92+
return CacheItem.sortedKeys({ match: 'h*llo' }).then(function(keys) {
93+
should(keys).eql(['hallo', 'heeello', 'hello', 'hllo', 'hxllo']);
94+
});
95+
});
96+
97+
it('handles no matches found', function() {
98+
return CacheItem.sortedKeys({ match: 'not-found' })
99+
.then(function(keys) {
100+
should(keys).eql([]);
101+
});
102+
});
103+
});
104+
});
105+
};

0 commit comments

Comments
 (0)