From fe24346246ebf6f9eb299b62fa961abd0088717f Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Thu, 10 Dec 2020 09:51:45 -0500 Subject: [PATCH 1/7] add list endpoints to add and delete from list --- lib/routes/_lists.js | 24 +++++++++++++++++++++- lib/services/lists.js | 42 ++++++++++++++++++++++++++++++++++++++ lib/services/lists.test.js | 0 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 lib/services/lists.js create mode 100644 lib/services/lists.test.js diff --git a/lib/routes/_lists.js b/lib/routes/_lists.js index cc397bfa..914f33e4 100644 --- a/lib/routes/_lists.js +++ b/lib/routes/_lists.js @@ -2,6 +2,7 @@ const _ = require('lodash'), responses = require('../responses'), + controller = require('../services/lists'), { withAuthLevel, authLevels } = require('amphora-auth'); /** @@ -17,13 +18,34 @@ function onlyJSONLists(req, res, next) { } } +/** + * Add to a list, return JSON + * @param {Object} req + * @param {Object} res + */ +function addToList(req, res) { + responses.expectJSON(() => controller.addToList(req.uri, req.body, res.locals), res); +} + +/** + * Remove from a list, return JSON + * @param {Object} req + * @param {Object} res + */ +function removeFromList(req, res) { + responses.expectJSON(() => controller.removeFromList(req.uri, req.body, res.locals), res); +} + function routes(router) { router.use(responses.varyWithoutExtension({varyBy: ['Accept']})); router.all('*', responses.acceptJSONOnly); router.all('/', responses.methodNotAllowed({allow: ['get']})); router.get('/', responses.list()); - router.all('/:name', responses.methodNotAllowed({allow: ['get', 'put']})); router.get('/:name', responses.getRouteFromDB); + router.post('/:name', withAuthLevel(authLevels.WRITE)); + router.post('/:name', addToList); + router.delete('/:name', withAuthLevel(authLevels.WRITE)); + router.delete('/:name', removeFromList); router.use('/:name', onlyJSONLists); router.put('/:name', withAuthLevel(authLevels.WRITE)); router.put('/:name', responses.putRouteFromDB); diff --git a/lib/services/lists.js b/lib/services/lists.js new file mode 100644 index 00000000..597ec7a8 --- /dev/null +++ b/lib/services/lists.js @@ -0,0 +1,42 @@ +'use strict'; + +const _isEqual = require('lodash/isEqual'), + db = require('./db'); + +/** + * Appends an entry to a list and returns the updated list. + * @param {string} uri + * @param {Array|Object} data + * @returns {Array} + */ +async function addToList(uri, data) { + // do a db call for the list + const list = await db.get(uri), + added = list.concat(data); + + // concat with new data, save, and return full list + // db.put wraps result in an object `{ _value: list }`, return list only + return db.put(uri, added).then(() => added); +} + +/** + * Removes an entry from a list and returns the updated list. + * @param {string} uri + * @param {Object} data + * @returns {Array} + */ +async function removeFromList(uri, data) { + // do a db call for the list + let list = await db.get(uri), + removed = list.filter(entry => !_isEqual(entry, data)); + + if (list.length === removed.legnth) { + throw new Error('Nothing was removed from the list.'); + } + + // db.put wraps result in an object `{ _value: list }`, return list only + return db.put(uri, removed).then(() => removed); +} + +module.exports.addToList = addToList; +module.exports.removeFromList = removeFromList; diff --git a/lib/services/lists.test.js b/lib/services/lists.test.js new file mode 100644 index 00000000..e69de29b From 63c9ce2fdbbae2442bed69cdd1c26056f6aed408 Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Thu, 10 Dec 2020 14:39:00 -0500 Subject: [PATCH 2/7] use patch --- lib/routes/_lists.js | 22 +++++----------- lib/services/lists.js | 60 ++++++++++++++++++++++++++++--------------- 2 files changed, 45 insertions(+), 37 deletions(-) diff --git a/lib/routes/_lists.js b/lib/routes/_lists.js index 914f33e4..ef626730 100644 --- a/lib/routes/_lists.js +++ b/lib/routes/_lists.js @@ -19,21 +19,12 @@ function onlyJSONLists(req, res, next) { } /** - * Add to a list, return JSON + * Modify a list, return JSON * @param {Object} req * @param {Object} res */ -function addToList(req, res) { - responses.expectJSON(() => controller.addToList(req.uri, req.body, res.locals), res); -} - -/** - * Remove from a list, return JSON - * @param {Object} req - * @param {Object} res - */ -function removeFromList(req, res) { - responses.expectJSON(() => controller.removeFromList(req.uri, req.body, res.locals), res); +function patchList(req, res) { + responses.expectJSON(() => controller.patchList(req.uri, req.body, res.locals), res); } function routes(router) { @@ -41,11 +32,10 @@ function routes(router) { router.all('*', responses.acceptJSONOnly); router.all('/', responses.methodNotAllowed({allow: ['get']})); router.get('/', responses.list()); + router.all('/:name', responses.methodNotAllowed({allow: ['get', 'put', 'patch']})); router.get('/:name', responses.getRouteFromDB); - router.post('/:name', withAuthLevel(authLevels.WRITE)); - router.post('/:name', addToList); - router.delete('/:name', withAuthLevel(authLevels.WRITE)); - router.delete('/:name', removeFromList); + router.patch('/:name', withAuthLevel(authLevels.WRITE)); + router.patch('/:name', patchList); router.use('/:name', onlyJSONLists); router.put('/:name', withAuthLevel(authLevels.WRITE)); router.put('/:name', responses.putRouteFromDB); diff --git a/lib/services/lists.js b/lib/services/lists.js index 597ec7a8..f0135e0f 100644 --- a/lib/services/lists.js +++ b/lib/services/lists.js @@ -5,38 +5,56 @@ const _isEqual = require('lodash/isEqual'), /** * Appends an entry to a list and returns the updated list. - * @param {string} uri - * @param {Array|Object} data + * @param {Array} list + * @param {Array} data * @returns {Array} */ -async function addToList(uri, data) { - // do a db call for the list - const list = await db.get(uri), - added = list.concat(data); - - // concat with new data, save, and return full list - // db.put wraps result in an object `{ _value: list }`, return list only - return db.put(uri, added).then(() => added); +function addToList(list, data) { + return list.concat(data); } /** * Removes an entry from a list and returns the updated list. - * @param {string} uri - * @param {Object} data + * @param {Array} list + * @param {Array} data * @returns {Array} */ -async function removeFromList(uri, data) { - // do a db call for the list - let list = await db.get(uri), - removed = list.filter(entry => !_isEqual(entry, data)); +function removeFromList(list, data) { + const startLength = list.length; + + for (const deletion of data) { + list = list.filter(entry => !_isEqual(entry, deletion)); + } - if (list.length === removed.legnth) { + if (list.length === startLength) { throw new Error('Nothing was removed from the list.'); } - // db.put wraps result in an object `{ _value: list }`, return list only - return db.put(uri, removed).then(() => removed); + return list; +} + +/** + * + * @param {string} uri + * @param {Array} data + */ +function patchList(uri, data) { + if (!Array.isArray(data.add) && !Array.isArray(data.remove)) { + throw new Error(`Bad Request. List PATCH requires 'add' or 'remove' to be an array.`); + } + + return db.get(uri).then(list => { + if (Array.isArray(data.add)) { + list = addToList(list, data.add); + } + + if (Array.isArray(data.remove)) { + list = removeFromList(list, data.remove); + } + + // db.put wraps result in an object `{ _value: list }`, return list only + return db.put(uri, list).then(list => list._value); + }); } -module.exports.addToList = addToList; -module.exports.removeFromList = removeFromList; +module.exports.patchList = patchList; From 1b6ed84af6043dac4687b946d1e6307ff9576c0f Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Thu, 10 Dec 2020 15:46:12 -0500 Subject: [PATCH 3/7] add tests --- lib/routes/_lists.js | 1 + lib/routes/_lists.test.js | 13 ++++++++ lib/services/lists.js | 13 +++++--- lib/services/lists.test.js | 62 ++++++++++++++++++++++++++++++++++++++ test/api/_lists/patch.js | 0 5 files changed, 85 insertions(+), 4 deletions(-) create mode 100644 test/api/_lists/patch.js diff --git a/lib/routes/_lists.js b/lib/routes/_lists.js index ef626730..52c12d3d 100644 --- a/lib/routes/_lists.js +++ b/lib/routes/_lists.js @@ -44,3 +44,4 @@ function routes(router) { module.exports = routes; module.exports.onlyJSONLists = onlyJSONLists; +module.exports.patchList = patchList; diff --git a/lib/routes/_lists.test.js b/lib/routes/_lists.test.js index d67995f2..f53e6c2d 100644 --- a/lib/routes/_lists.test.js +++ b/lib/routes/_lists.test.js @@ -4,6 +4,7 @@ const _ = require('lodash'), filename = __filename.split('/').pop().split('.').shift(), lib = require('./' + filename), responses = require('../responses'), + controller = require('../services/lists'), sinon = require('sinon'), expect = require('chai').expect; @@ -12,12 +13,24 @@ describe(_.startCase(filename), function () { beforeEach(function () { sandbox = sinon.sandbox.create(); + + sandbox.stub(controller, 'patchList'); }); afterEach(function () { sandbox.restore(); }); + describe('patchList', function () { + const fn = lib[this.title]; + + it('calls expectjson and lists.patchList', function () { + fn({}, { json: item => item }); + + expect(controller.patchList.called).to.equal(true); + }); + }); + describe('onlyJSONLists', function () { const fn = lib[this.title]; diff --git a/lib/services/lists.js b/lib/services/lists.js index f0135e0f..b94ce953 100644 --- a/lib/services/lists.js +++ b/lib/services/lists.js @@ -1,7 +1,8 @@ 'use strict'; -const _isEqual = require('lodash/isEqual'), - db = require('./db'); +const _isEqual = require('lodash/isEqual'); + +let db = require('./db'); /** * Appends an entry to a list and returns the updated list. @@ -34,13 +35,14 @@ function removeFromList(list, data) { } /** - * + * Add or Remove an item from a list * @param {string} uri * @param {Array} data + * @returns {Promise} */ function patchList(uri, data) { if (!Array.isArray(data.add) && !Array.isArray(data.remove)) { - throw new Error(`Bad Request. List PATCH requires 'add' or 'remove' to be an array.`); + throw new Error('Bad Request. List PATCH requires `add` or `remove` to be an array.'); } return db.get(uri).then(list => { @@ -58,3 +60,6 @@ function patchList(uri, data) { } module.exports.patchList = patchList; + +// For testing +module.exports.setDb = mock => db = mock; diff --git a/lib/services/lists.test.js b/lib/services/lists.test.js index e69de29b..e5fcf750 100644 --- a/lib/services/lists.test.js +++ b/lib/services/lists.test.js @@ -0,0 +1,62 @@ +'use strict'; + +const _ = require('lodash'), + filename = __filename.split('/').pop().split('.').shift(), + lib = require('./' + filename), + sinon = require('sinon'), + expect = require('chai').expect, + storage = require('../../test/fixtures/mocks/storage'); + +describe(_.startCase(filename), function () { + let sandbox, db; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + + db = storage(); + lib.setDb(db); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('patchList', function () { + const fn = lib.patchList; + + it('will throw if bad request body', function () { + try { + fn('domain.com/_lists/test', {}); + } catch (e) { + expect(e.message).to.eql('Bad Request. List PATCH requires `add` or `remove` to be an array.'); + } + }); + + it('adds to existing lists if has add property', function () { + db.get.resolves([]); + db.put.callsFake((uri, list) => Promise.resolve({ _value: list })); + + return fn('domain.com/_lists/test', { add: [ 'hello' ] }).then(list => { + expect(list).to.eql([ 'hello' ]); + }); + }); + + it('removes from existing lists if has remove property', function () { + db.get.resolves([ 'hello' ]); + db.put.callsFake((uri, list) => Promise.resolve({ _value: list })); + + return fn('domain.com/_lists/test', { remove: [ 'hello' ] }).then(list => { + expect(list).to.eql([]); + }); + }); + + it('throws if object to remove does not exist', function () { + db.get.resolves([ 'hello' ]); + + return fn('domain.com/_lists/test', { remove: [ 'world' ]}) + .catch(e => { + expect(e.message).to.eql('Nothing was removed from the list.'); + }); + }); + }); +}); diff --git a/test/api/_lists/patch.js b/test/api/_lists/patch.js new file mode 100644 index 00000000..e69de29b From 267c797a0fe07382dcc208e8699943bbb414411c Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Mon, 14 Dec 2020 14:28:56 -0500 Subject: [PATCH 4/7] add integration test --- lib/services/lists.js | 3 ++- test/api/_lists/patch.js | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/lib/services/lists.js b/lib/services/lists.js index b94ce953..df4a720e 100644 --- a/lib/services/lists.js +++ b/lib/services/lists.js @@ -55,7 +55,8 @@ function patchList(uri, data) { } // db.put wraps result in an object `{ _value: list }`, return list only - return db.put(uri, list).then(list => list._value); + // hopefully db.put never does anything to the data bc we're just returning list + return db.put(uri, list).then(() => list); }); } diff --git a/test/api/_lists/patch.js b/test/api/_lists/patch.js index e69de29b..648c5c5d 100644 --- a/test/api/_lists/patch.js +++ b/test/api/_lists/patch.js @@ -0,0 +1,38 @@ +'use strict'; + +const _ = require('lodash'), + apiAccepts = require('../../fixtures/api-accepts'), + endpointName = _.startCase(__dirname.split('/').pop()), + filename = _.startCase(__filename.split('/').pop().split('.')[0]), + sinon = require('sinon'); + +describe(endpointName, function () { + describe(filename, function () { + let sandbox, + hostname = 'localhost.example.com', + acceptsJsonBody = apiAccepts.acceptsJsonBody(_.camelCase(filename)), + start = ['item1', 'item2'], + data = { + add: ['item3', 'item4'], + remove: ['item1'] + }, + end = ['item2', 'item3', 'item4']; + + beforeEach(function () { + sandbox = sinon.sandbox.create(); + return apiAccepts.beforeEachTest({ sandbox, hostname, pathsAndData: {'/_lists/valid': start} }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('/_lists/:name', function () { + const path = this.title; + + // overrides existing data + acceptsJsonBody(path, {name: 'valid'}, data, 200, end); + acceptsJsonBody(path, {name: 'missing'}, data, 404); + }); + }); +}); From 6db3456c11b63296a26834ab1d4690cbffaa6ef6 Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Mon, 4 Jan 2021 10:07:53 -0500 Subject: [PATCH 5/7] remove error throw --- lib/services/lists.js | 6 ------ lib/services/lists.test.js | 9 --------- 2 files changed, 15 deletions(-) diff --git a/lib/services/lists.js b/lib/services/lists.js index df4a720e..46899197 100644 --- a/lib/services/lists.js +++ b/lib/services/lists.js @@ -21,16 +21,10 @@ function addToList(list, data) { * @returns {Array} */ function removeFromList(list, data) { - const startLength = list.length; - for (const deletion of data) { list = list.filter(entry => !_isEqual(entry, deletion)); } - if (list.length === startLength) { - throw new Error('Nothing was removed from the list.'); - } - return list; } diff --git a/lib/services/lists.test.js b/lib/services/lists.test.js index e5fcf750..98989f71 100644 --- a/lib/services/lists.test.js +++ b/lib/services/lists.test.js @@ -49,14 +49,5 @@ describe(_.startCase(filename), function () { expect(list).to.eql([]); }); }); - - it('throws if object to remove does not exist', function () { - db.get.resolves([ 'hello' ]); - - return fn('domain.com/_lists/test', { remove: [ 'world' ]}) - .catch(e => { - expect(e.message).to.eql('Nothing was removed from the list.'); - }); - }); }); }); From e3c7c5d3876affce683de8262d2f3ea95a4a2f46 Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Mon, 11 Jan 2021 11:19:09 -0500 Subject: [PATCH 6/7] 7.9.0-0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 35e02731..19531c6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "amphora", - "version": "7.8.1", + "version": "7.9.0-0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 26bd210e..e787203a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amphora", - "version": "7.8.1", + "version": "7.9.0-0", "description": "An API mixin for Express that saves, publishes and composes data with the key-value store of your choice.", "main": "index.js", "scripts": { From 23aa4c85276dbac202ed7f4e0cd7b46698aa4a1d Mon Sep 17 00:00:00 2001 From: Jeffrey Pope Date: Mon, 11 Jan 2021 11:26:26 -0500 Subject: [PATCH 7/7] Revert "7.9.0-0" This reverts commit e3c7c5d3876affce683de8262d2f3ea95a4a2f46. --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19531c6e..35e02731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "amphora", - "version": "7.9.0-0", + "version": "7.8.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e787203a..26bd210e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amphora", - "version": "7.9.0-0", + "version": "7.8.1", "description": "An API mixin for Express that saves, publishes and composes data with the key-value store of your choice.", "main": "index.js", "scripts": {