diff --git a/index.js b/index.js index 00bb7fd..71911ab 100644 --- a/index.js +++ b/index.js @@ -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'); diff --git a/lib/model-key-composer.js b/lib/model-key-composer.js new file mode 100644 index 0000000..abc073f --- /dev/null +++ b/lib/model-key-composer.js @@ -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; +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..5850905 --- /dev/null +++ b/lib/utils.js @@ -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; +} diff --git a/package.json b/package.json index cbdce02..b1720e7 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "license": "MIT", "dependencies": { "async": "^1.0.0", + "bluebird": "^3.4.6", "debug": "^2.2.0", "strong-globalize": "^2.5.8" }, diff --git a/test/model-key-composer.test.js b/test/model-key-composer.test.js new file mode 100644 index 0000000..4e42649 --- /dev/null +++ b/test/model-key-composer.test.js @@ -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(); + }); + }); + }); + }); +});