Merge pull request #63 from strongloop/feature/keyvalue-helpers
KeyValue helpers
This commit is contained in:
commit
9c3fec9a5a
5
index.js
5
index.js
|
@ -12,3 +12,8 @@ exports.SQLConnector = exports.SqlConnector = require('./lib/sql');
|
||||||
exports.ParameterizedSQL = exports.SQLConnector.ParameterizedSQL;
|
exports.ParameterizedSQL = exports.SQLConnector.ParameterizedSQL;
|
||||||
exports.Transaction = require('./lib/transaction');
|
exports.Transaction = require('./lib/transaction');
|
||||||
|
|
||||||
|
exports.createPromiseCallback = require('./lib/utils').createPromiseCallback;
|
||||||
|
|
||||||
|
// KeyValue helpers
|
||||||
|
exports.ModelKeyComposer = require('./lib/model-key-composer');
|
||||||
|
exports.BinaryPacker = require('./lib/binary-packer');
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright IBM Corp. 2014,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 msgpack = require('msgpack5');
|
||||||
|
|
||||||
|
module.exports = BinaryPacker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Packer instance that can be used to convert between JavaScript
|
||||||
|
* objects and a binary representation in a Buffer.
|
||||||
|
*
|
||||||
|
* Compared to JSON, this encoding preserves the following JavaScript types:
|
||||||
|
* - Date
|
||||||
|
*/
|
||||||
|
function BinaryPacker() {
|
||||||
|
this._packer = msgpack({ forceFloat64: true });
|
||||||
|
this._packer.register(1, Date, encodeDate, decodeDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the provided value to a `Buffer`.
|
||||||
|
*
|
||||||
|
* @param {*} value Any value (string, number, object)
|
||||||
|
* @callback {Function} cb The callback to receive the parsed result.
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {Buffer} data The encoded value
|
||||||
|
* @promise
|
||||||
|
*/
|
||||||
|
BinaryPacker.prototype.encode = function(value, cb) {
|
||||||
|
cb = cb || createPromiseCallback();
|
||||||
|
try {
|
||||||
|
// msgpack5 returns https://www.npmjs.com/package/bl instead of Buffer
|
||||||
|
// use .slice() to convert to a Buffer
|
||||||
|
var data = this._packer.encode(value).slice();
|
||||||
|
setImmediate(function() {
|
||||||
|
cb(null, data);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setImmediate(function() {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cb.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode the binary value back to a JavaScript value.
|
||||||
|
* @param {Buffer} binary The binary input.
|
||||||
|
* @callback {Function} cb The callback to receive the composed value.
|
||||||
|
* @param {Error} err
|
||||||
|
* @param {*} value Decoded value.
|
||||||
|
* @promise
|
||||||
|
*/
|
||||||
|
BinaryPacker.prototype.decode = function(binary, cb) {
|
||||||
|
cb = cb || createPromiseCallback();
|
||||||
|
try {
|
||||||
|
var value = this._packer.decode(binary);
|
||||||
|
setImmediate(function() {
|
||||||
|
cb(null, value);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
setImmediate(function() {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return cb.promise;
|
||||||
|
};
|
||||||
|
|
||||||
|
function encodeDate(obj) {
|
||||||
|
return new Buffer(obj.toISOString(), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeDate(buf) {
|
||||||
|
return new Date(buf.toString('utf8'));
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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;
|
||||||
|
}
|
|
@ -20,7 +20,9 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^1.0.0",
|
"async": "^1.0.0",
|
||||||
|
"bluebird": "^3.4.6",
|
||||||
"debug": "^2.2.0",
|
"debug": "^2.2.0",
|
||||||
|
"msgpack5": "^3.4.1",
|
||||||
"strong-globalize": "^2.5.8"
|
"strong-globalize": "^2.5.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
// 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 BinaryPacker = require('../lib/binary-packer');
|
||||||
|
var expect = require('chai').expect;
|
||||||
|
|
||||||
|
describe('BinaryPacker', function() {
|
||||||
|
var packer;
|
||||||
|
|
||||||
|
beforeEach(function createPacker() {
|
||||||
|
packer = new BinaryPacker();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('encode()', function() {
|
||||||
|
it('supports invocation with a callback', function(done) {
|
||||||
|
packer.encode('a-value', done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('decode()', function() {
|
||||||
|
it('supports invocation with a callback', function(done) {
|
||||||
|
packer.encode('a-value', function(err, binary) {
|
||||||
|
if (err) return done(err);
|
||||||
|
packer.decode(binary, function(err, result) {
|
||||||
|
if (err) return done(err);
|
||||||
|
expect(result).to.eql('a-value');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('roundtrip', function() {
|
||||||
|
var TEST_CASES = {
|
||||||
|
String: 'a-value',
|
||||||
|
Object: { a: 1, b: 2 },
|
||||||
|
Buffer: new Buffer([1, 2, 3]),
|
||||||
|
Date: new Date('2016-08-03T11:53:03.470Z'),
|
||||||
|
Integer: 12345,
|
||||||
|
Float: 12.345,
|
||||||
|
Boolean: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(TEST_CASES).forEach(function(tc) {
|
||||||
|
it('works for ' + tc + ' values', function() {
|
||||||
|
var value = TEST_CASES[tc];
|
||||||
|
return encodeAndDecode(value)
|
||||||
|
.then(function(result) {
|
||||||
|
expect(result).to.eql(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works for nested properties', function() {
|
||||||
|
return encodeAndDecode(TEST_CASES)
|
||||||
|
.then(function(result) {
|
||||||
|
expect(result).to.eql(TEST_CASES);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function encodeAndDecode(value) {
|
||||||
|
return packer.encode(value)
|
||||||
|
.then(function(binary) {
|
||||||
|
return packer.decode(binary);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -182,7 +182,7 @@ describe('transactions', function() {
|
||||||
expect(posts.length).to.be.eql(1);
|
expect(posts.length).to.be.eql(1);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
}, 100);
|
}, 300);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue