Add ModelKeyComposer from kv-redis connector

Add two helper methods for composing and parsing key-value keys:
 - in juggler, we use (modelName, key) tuple
 - in backends, there is usually a single string key required
This commit is contained in:
Miroslav Bajtoš 2016-10-19 12:55:35 +02:00
parent 782a68e914
commit 2cbc1143c1
5 changed files with 187 additions and 0 deletions

View File

@ -12,3 +12,7 @@ exports.SQLConnector = exports.SqlConnector = require('./lib/sql');
exports.ParameterizedSQL = exports.SQLConnector.ParameterizedSQL;
exports.Transaction = require('./lib/transaction');
exports.createPromiseCallback = require('./lib/utils').createPromiseCallback;
// KeyValue helpers
exports.ModelKeyComposer = require('./lib/model-key-composer');

76
lib/model-key-composer.js Normal file
View File

@ -0,0 +1,76 @@
// Copyright IBM Corp. 2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var createPromiseCallback = require('./utils').createPromiseCallback;
var debug = require('debug')('loopback:connector:model-key-composer');
var g = require('strong-globalize')();
/**
* Build a single key string from a tuple (modelName, key).
*
* This method is typically used by KeyValue connectors to build a single
* key string for a given modelName+key tuple.
*
* @param {String} modelName
* @param {String} key
* @callback {Function} cb The callback to receive the composed value.
* @param {Error} err
* @param {String} composedKey
* @promise
*/
exports.compose = function composeKeyFromModelNameAndKey(modelName, key, cb) {
cb = cb || createPromiseCallback();
// Escape model name to prevent collision
// 'model' + 'foo:bar' --vs-- 'model:foo' + 'bar'
var value = encodeURIComponent(modelName) + ':' + key;
setImmediate(function() {
cb(null, value);
});
return cb.promise;
};
var PARSE_KEY_REGEX = /^([^:]*):(.*)/;
/**
* Parse a composed key string into a tuple (modelName, key).
*
* This method is typically used by KeyValue connectors to parse a composed
* key string returned by SCAN/ITERATE method back to the expected
* modelName+tuple key.
*
* @param {String} composed The composed key as returned by `composeKey`
* @callback {Function} cb The callback to receive the parsed result.
* @param {Error} err
* @param {Object} result The result with properties `modelName` and `key`.
* @promise
*/
exports.parse = function(composed, cb) {
cb = cb || createPromiseCallback();
var matchResult = composed.match(PARSE_KEY_REGEX);
if (matchResult) {
var result = {
modelName: matchResult[1],
key: matchResult[2],
};
setImmediate(function() {
cb(null, result);
});
} else {
debug('Invalid key - missing model-name prefix: %s', composed);
var err = new Error(g.f(
'Invalid key %j - missing model-name prefix',
composed));
err.code = 'NO_MODEL_PREFIX';
setImmediate(function() {
cb(err);
});
}
return cb.promise;
};

20
lib/utils.js Normal file
View File

@ -0,0 +1,20 @@
// Copyright IBM Corp. 2012,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
var Promise = require('bluebird');
exports.createPromiseCallback = createPromiseCallback;
function createPromiseCallback() {
var cb;
var promise = new Promise(function(resolve, reject) {
cb = function(err, data) {
if (err) return reject(err);
return resolve(data);
};
});
cb.promise = promise;
return cb;
}

View File

@ -20,6 +20,7 @@
"license": "MIT",
"dependencies": {
"async": "^1.0.0",
"bluebird": "^3.4.6",
"debug": "^2.2.0",
"strong-globalize": "^2.5.8"
},

View File

@ -0,0 +1,86 @@
// Copyright IBM Corp. 2016. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
'use strict';
var composer = require('../lib/model-key-composer');
var expect = require('chai').expect;
var Promise = require('bluebird');
describe('ModelKeyComposer', function() {
describe('compose()', function() {
it('honours the key', function() {
return Promise.all([
composer.compose('Car', 'vin'),
composer.compose('Car', 'name'),
]).spread(function(key1, key2) {
expect(key1).to.not.equal(key2);
});
});
it('honours the model name', function() {
return Promise.all([
composer.compose('Product', 'name'),
composer.compose('Category', 'name'),
]).spread(function(key1, key2) {
expect(key1).to.not.equal(key2);
});
});
it('encodes values', function() {
// This test is based on the knowledge that we are using ':' separator
// when building the composed string
return Promise.all([
composer.compose('a', 'b:c'),
composer.compose('a:b', 'c'),
]).spread(function(key1, key2) {
expect(key1).to.not.equal(key2);
});
});
it('supports invocation with a callback', function(done) {
composer.compose('Car', 'vin', done);
});
});
describe('parse()', function() {
it('decodes valid value', function() {
return composer.compose('Car', 'vin')
.then(function(data) {
return composer.parse(data);
})
.then(function(parsed) {
expect(parsed).to.eql({
modelName: 'Car',
key: 'vin',
});
});
});
it('handles invalid values', function() {
return composer.parse('invalid').then(
function onSuccess() {
throw new Error('composer.parse() should have failed');
},
function onError(err) {
expect(err).to.have.property('code', 'NO_MODEL_PREFIX');
});
});
it('supports invocation with a callback', function(done) {
composer.compose('Car', 'vin', function(err, key) {
if (err) return done(err);
composer.parse(key, function(err, parsed) {
if (err) return done(err);
expect(parsed).to.eql({
modelName: 'Car',
key: 'vin',
});
done();
});
});
});
});
});