From 2320df12273f78296d62663bce29b5e86a000d8d Mon Sep 17 00:00:00 2001 From: Simon Ho Date: Thu, 5 Jan 2017 19:32:41 -0800 Subject: [PATCH] Refactor flush to deleteAll - Rename `flush` to `deleteAll` - Add `delete` - Detect `delete/deleteAll` before running downstream test suites - Fall back to unoptimized `deleteAll` when connector does not support `deleteAll` but supports `delete` - Return 501 for connectors not supporting `delete` or `deleteAll` --- lib/connectors/kv-memory.js | 13 +++- lib/kvao/delete-all.js | 63 +++++++++++++++++++ lib/kvao/delete.js | 45 +++++++++++++ lib/kvao/flush.js | 30 --------- lib/kvao/index.js | 3 +- test/kv-memory.js | 15 ++++- test/kvao/delete-all.suite.js | 36 +++++++++++ test/kvao/{flush.suite.js => delete.suite.js} | 14 ++--- 8 files changed, 178 insertions(+), 41 deletions(-) create mode 100644 lib/kvao/delete-all.js create mode 100644 lib/kvao/delete.js delete mode 100644 lib/kvao/flush.js create mode 100644 test/kvao/delete-all.suite.js rename test/kvao/{flush.suite.js => delete.suite.js} (57%) diff --git a/lib/connectors/kv-memory.js b/lib/connectors/kv-memory.js index d49d8c15..a144ee92 100644 --- a/lib/connectors/kv-memory.js +++ b/lib/connectors/kv-memory.js @@ -201,9 +201,18 @@ KeyValueMemoryConnector.prototype.disconnect = function(callback) { process.nextTick(callback); }; -KeyValueMemoryConnector.prototype.flush = +KeyValueMemoryConnector.prototype.delete = +function(modelName, key, options, callback) { + var store = this._getStoreForModel(modelName); + delete store[key]; + callback(); +}; + +KeyValueMemoryConnector.prototype.deleteAll = function(modelName, options, callback) { - this._store = Object.create(null); + var modelStore = this._getStoreForModel(modelName); + for (var key in modelStore) + delete modelStore[key]; callback(); }; diff --git a/lib/kvao/delete-all.js b/lib/kvao/delete-all.js new file mode 100644 index 00000000..f4e21d54 --- /dev/null +++ b/lib/kvao/delete-all.js @@ -0,0 +1,63 @@ +'use strict'; + +var assert = require('assert'); +var async = require('async'); +var debug = require('debug')('loopback:kvao:delete-all'); +var utils = require('../utils'); + +/** + * Delete all keys (and values) associated to the current model. + * + * @options {Object} options Unused ATM, placeholder for future options. + * @callback {Function} callback + * @param {Error} err Error object. + * @promise + * + * @header KVAO.prototype.deleteAll([options, ]cb) + */ +module.exports = function deleteAll(options, callback) { + if (callback == undefined && typeof options === 'function') { + callback = options; + options = {}; + } else if (!options) { + options = {}; + } + + assert(typeof options === 'object', 'options must be an object'); + + callback = callback || utils.createPromiseCallback(); + + var connector = this.getConnector(); + if (typeof connector.deleteAll === 'function') { + connector.deleteAll(this.modelName, options, callback); + } else if (typeof connector.delete === 'function') { + debug('Falling back to unoptimized key-value pair deletion'); + iterateAndDelete(connector, this.modelName, options, callback); + } else { + var errMsg = 'Connector does not support key-value pair deletion'; + debug(errMsg); + process.nextTick(function() { + var err = new Error(errMsg); + err.statusCode = 501; + callback(err); + }); + } + return callback.promise; +}; + +function iterateAndDelete(connector, modelName, options, callback) { + var iter = connector.iterateKeys(modelName, {}); + var keys = []; + iter.next(onNextKey); + + function onNextKey(err, key) { + if (err) return callback(err); + if (key === undefined) return callback(); + connector.delete(modelName, key, options, onDeleted); + } + + function onDeleted(err) { + if (err) return callback(err); + iter.next(onNextKey); + } +} diff --git a/lib/kvao/delete.js b/lib/kvao/delete.js new file mode 100644 index 00000000..b11cafcb --- /dev/null +++ b/lib/kvao/delete.js @@ -0,0 +1,45 @@ +'use strict'; + +var assert = require('assert'); +var debug = require('debug')('loopback:kvao:delete'); +var utils = require('../utils'); + +/** + * Delete the key-value pair associated to the given key. + * + * @param {String} key Key to use when searching the database. + * @options {Object} options + * @callback {Function} callback + * @param {Error} err Error object. + * @param {*} result Value associated with the given key. + * @promise + * + * @header KVAO.prototype.delete(key[, options], cb) + */ +module.exports = function keyValueDelete(key, options, callback) { + if (callback == undefined && typeof options === 'function') { + callback = options; + options = {}; + } else if (!options) { + options = {}; + } + + assert(typeof key === 'string' && key, 'key must be a non-empty string'); + + callback = callback || utils.createPromiseCallback(); + + var connector = this.getConnector(); + if (typeof connector.delete === 'function') { + connector.delete(this.modelName, key, options, callback); + } else { + var errMsg = 'Connector does not support key-value pair deletion'; + debug(errMsg); + process.nextTick(function() { + var err = new Error(errMsg); + err.statusCode = 501; + callback(err); + }); + } + + return callback.promise; +}; diff --git a/lib/kvao/flush.js b/lib/kvao/flush.js deleted file mode 100644 index 1b6568fb..00000000 --- a/lib/kvao/flush.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -var assert = require('assert'); -var utils = require('../utils'); - -/** - * Delete all keys (and values) associated to the current model. - * - * @options {Object} options Unused ATM, placeholder for future options. - * @callback {Function} callback - * @param {Error} err Error object. - * @promise - * - * @header KVAO.prototype.flush(options, cb) - */ -module.exports = function flush(options, callback) { - if (callback == undefined && typeof options === 'function') { - callback = options; - options = {}; - } else if (!options) { - options = {}; - } - - assert(typeof options === 'object', 'options must be an object'); - - callback = callback || utils.createPromiseCallback(); - - this.getConnector().flush(this.modelName, options, callback); - return callback.promise; -}; diff --git a/lib/kvao/index.js b/lib/kvao/index.js index 24a85dcb..371ee653 100644 --- a/lib/kvao/index.js +++ b/lib/kvao/index.js @@ -5,10 +5,11 @@ function KeyValueAccessObject() { module.exports = KeyValueAccessObject; +KeyValueAccessObject.delete = require('./delete'); +KeyValueAccessObject.deleteAll = require('./delete-all'); KeyValueAccessObject.get = require('./get'); KeyValueAccessObject.set = require('./set'); KeyValueAccessObject.expire = require('./expire'); -KeyValueAccessObject.flush = require('./flush'); KeyValueAccessObject.ttl = require('./ttl'); KeyValueAccessObject.iterateKeys = require('./iterate-keys'); KeyValueAccessObject.keys = require('./keys'); diff --git a/test/kv-memory.js b/test/kv-memory.js index 980be0f6..cca00560 100644 --- a/test/kv-memory.js +++ b/test/kv-memory.js @@ -2,10 +2,23 @@ var kvMemory = require('../lib/connectors/kv-memory'); var DataSource = require('..').DataSource; -describe('KeyValue-Memory connector', function() { +describe('Optimized KeyValue-Memory connector', function() { var dataSourceFactory = function() { return new DataSource({connector: kvMemory}); }; require('./kvao.suite')(dataSourceFactory); }); + +describe('Unoptimized KeyValue-Memory connector', function() { + var dataSourceFactory = function() { + var ds = new DataSource({connector: kvMemory}); + + // disable optimized methods + ds.connector.deleteAll = false; + + return ds; + }; + + require('./kvao.suite')(dataSourceFactory); +}); diff --git a/test/kvao/delete-all.suite.js b/test/kvao/delete-all.suite.js new file mode 100644 index 00000000..65e4109a --- /dev/null +++ b/test/kvao/delete-all.suite.js @@ -0,0 +1,36 @@ +'use strict'; + +const bdd = require('../helpers/bdd-if'); +const helpers = require('./_helpers'); +const should = require('should'); + +module.exports = function(dataSourceFactory, connectorCapabilities) { + var supportsDeleteAll = 'deleteAll' in dataSourceFactory().connector; + + bdd.describeIf(supportsDeleteAll, 'deleteAll', function() { + let CacheItem; + beforeEach(function unpackContext() { + CacheItem = helpers.givenCacheItem(dataSourceFactory); + }); + + it('removes all key-value pairs for the given model', function() { + return helpers.givenKeys(CacheItem, ['key1', 'key2']) + .then(() => CacheItem.deleteAll()) + .then(() => CacheItem.keys()) + .then((keys) => { + should(keys).eql([]); + }); + }); + + it('does not remove data from other existing models', function() { + var AnotherModel = dataSourceFactory().createModel('AnotherModel'); + return helpers.givenKeys(CacheItem, ['key1', 'key2']) + .then(() => helpers.givenKeys(AnotherModel, ['key3', 'key4'])) + .then(() => CacheItem.deleteAll()) + .then(() => AnotherModel.keys()) + .then((keys) => { + should(keys).eql(['key3', 'key4']); + }); + }); + }); +}; diff --git a/test/kvao/flush.suite.js b/test/kvao/delete.suite.js similarity index 57% rename from test/kvao/flush.suite.js rename to test/kvao/delete.suite.js index cbb79132..20136793 100644 --- a/test/kvao/flush.suite.js +++ b/test/kvao/delete.suite.js @@ -5,21 +5,21 @@ const helpers = require('./_helpers'); const should = require('should'); module.exports = function(dataSourceFactory, connectorCapabilities) { - var supportsFlushOperation = - connectorCapabilities.supportsFlushOperation !== false; + var supportsDelete = 'delete' in dataSourceFactory().connector; - bdd.describeIf(supportsFlushOperation, 'flush', function() { + bdd.describeIf(supportsDelete, 'delete', function() { let CacheItem; beforeEach(function unpackContext() { CacheItem = helpers.givenCacheItem(dataSourceFactory); + return CacheItem.deleteAll(); }); - it('removes all associated keys for a given model', function() { + it('removes the key-value pair for the given key', function() { return helpers.givenKeys(CacheItem, ['key1', 'key2']) - .then(() => CacheItem.flush()) + .then(() => CacheItem.delete('key1')) .then(() => CacheItem.keys()) - .done((keys) => { - should(keys).eql([]); + .then((keys) => { + keys.should.eql(['key2']); }); }); });