diff --git a/package.json b/package.json index 96edacf..61f9ff2 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,7 @@ "url": "https://github.com/packetloop/connect-dynamodb-session/issues" }, "peerDependencies": { - "aws-sdk": "2", - "bluebird": "3" + "aws-sdk": "2" }, "devDependencies": { "aws-sdk": "2.3.6", @@ -39,7 +38,6 @@ "babel-plugin-add-module-exports": "0.2.1", "babel-preset-es2015": "6.6.0", "blue-tape": "0.2.0", - "bluebird": "3.3.5", "codacy-coverage": "1.1.3", "eslint": "2.8.0", "eslint-config-airbnb": "8.0.0", diff --git a/src/dynamo.js b/src/dynamo.js index 16379c1..2dbb94d 100644 --- a/src/dynamo.js +++ b/src/dynamo.js @@ -1,5 +1,4 @@ import AWS from 'aws-sdk'; -import Promise from 'bluebird'; export default ({awsClient: aws, region, endpoint, tableName: TableName, consistentRead: ConsistentRead = true, @@ -7,76 +6,68 @@ export default ({awsClient: aws, region, endpoint, tableName: TableName, writeCapacity: WriteCapacityUnits = 5}) => { const awsClient = aws || new AWS.DynamoDB({region, endpoint}); - const deleteItem = id => Promise.fromCallback(cb => - awsClient.deleteItem({TableName, Key: {id: {S: id}}}, cb) - ); + const deleteItem = id => awsClient.deleteItem({TableName, Key: {id: {S: id}}}).promise(); return { init: (autoCreate = false) => { - const describe = Promise.fromCallback(cb => awsClient.describeTable({TableName}, cb)); + const describe = awsClient.describeTable({TableName}).promise(); if (autoCreate) { - return describe.catch(() => Promise.fromCallback(cb => + return describe.catch(() => awsClient.createTable({ TableName, AttributeDefinitions: [{AttributeName: 'id', AttributeType: 'S'}], KeySchema: [{AttributeName: 'id', KeyType: 'HASH'}], ProvisionedThroughput: {ReadCapacityUnits, WriteCapacityUnits} - }, cb)) + }).promise() ); } return describe; }, - get: id => Promise.fromCallback(cb => - awsClient.getItem({TableName, ConsistentRead, Key: {id: {S: id}}}, cb) - ).then(data => { - if (data.Item && data.Item.content && data.Item.expires) { - return { - content: JSON.parse(data.Item.content.S.toString()), - expires: Number(data.Item.expires.N) - }; - } - return null; - }), + get: id => awsClient.getItem({TableName, ConsistentRead, Key: {id: {S: id}}}).promise() + .then(data => { + if (data.Item && data.Item.content && data.Item.expires) { + return { + content: JSON.parse(data.Item.content.S.toString()), + expires: Number(data.Item.expires.N) + }; + } + return null; + }), - put: (id, expires, content) => Promise.fromCallback(cb => + put: (id, expires, content) => awsClient.putItem({ TableName, Item: { id: {S: id}, expires: {N: expires.toString()}, content: {S: JSON.stringify(content)} } - }, cb) - ), + }).promise(), - setExpires: (id, expires) => Promise.fromCallback(cb => + setExpires: (id, expires) => awsClient.updateItem({ TableName, Key: {id: {S: id}}, UpdateExpression: 'SET expires = :value', ExpressionAttributeValues: {':value': {N: expires.toString()}} - }, cb) - ), + }).promise(), delete: deleteItem, deleteExpired: when => { - const scan = startKey => Promise.fromCallback(cb => + const scan = startKey => awsClient.scan({ TableName, FilterExpression: 'expires < :when', ExpressionAttributeValues: {':when': {N: when.toString()}}, ProjectionExpression: 'id', ExclusiveStartKey: startKey - }, cb) - ); + }).promise(); const deletePage = ({scanned, deleted}, startKey = null) => // perform the scan to find expired sessions scan(startKey) - // use Promise.each to delete each of them one by one so we don't use all the - // provisioned capacity - .then(data => Promise.each(data.Items.map(i => i.id.S), deleteItem) + .then(data => Promise.all(data.Items.map(i => i.id.S), deleteItem) // once all the sessions are deleted, work out if there are more results to scan .then(ids => { const lastKey = data.LastEvaluatedKey; diff --git a/test/src/dynamo-test.js b/test/src/dynamo-test.js index f25e656..6527247 100644 --- a/test/src/dynamo-test.js +++ b/test/src/dynamo-test.js @@ -1,44 +1,42 @@ import test from 'blue-tape'; import sinon from 'sinon'; +require('sinon-stub-promise')(sinon); import dynamo from '../../src/dynamo'; const tableName = 'foo'; test('Init should succeed when no errors', () => { - const awsClient = { - describeTable: sinon.stub() - .withArgs({TableName: tableName}) - .callsArgWith(1, null) - }; + const promise = sinon.stub().withArgs({TableName: tableName}).returnsPromise().resolves(); + const awsClient = {describeTable: () => ({promise})}; + return dynamo({awsClient, tableName}).init(); }); test('Init should pass errors on', t => { - const awsClient = { - describeTable: sinon.stub() - .withArgs({TableName: tableName}) - .callsArgWith(1, new TypeError()) - }; + const promise = sinon.stub().withArgs({TableName: tableName}).returnsPromise() + .rejects(new TypeError()); + const awsClient = {describeTable: () => ({promise})}; + return t.shouldFail(dynamo({awsClient, tableName}).init(), TypeError, 'should return type error'); }); test('Should be able to get a session', t => { const id = 'abc'; - const awsClient = { - getItem: sinon.stub() - .withArgs({ - TableName: tableName, - ConsistentRead: true, - Key: {id: {S: id}} - }) - .callsArgWith(1, null, { - Item: { - content: {S: '{"foo": 42}'}, - expires: {N: '123'} - } - }) - }; + const promise = sinon.stub() + .withArgs({ + TableName: tableName, + ConsistentRead: true, + Key: {id: {S: id}} + }).returnsPromise() + .resolves({ + Item: { + content: {S: '{"foo": 42}'}, + expires: {N: '123'} + } + }); + const awsClient = {getItem: () => ({promise})}; + return dynamo({awsClient, tableName}) .get(id) .then(({expires, content}) => { @@ -48,24 +46,27 @@ test('Should be able to get a session', t => { }); test('Should be able to not find a session', t => { - const awsClient = { - getItem: sinon.stub().callsArgWith(1, null, {Item: {}}) - }; + const promise = sinon.stub() + .withArgs().returnsPromise() + .resolves({Item: {}}); + const awsClient = {getItem: () => ({promise})}; + return dynamo({awsClient, tableName}) .get('abc') .then(row => t.false(row, 'should resolve with null')); }); test('Should reject for invalid json content', t => { - const awsClient = { - getItem: sinon.stub() - .callsArgWith(1, null, { - Item: { - content: {S: '[}not json'}, - expires: {N: '123'} - } - }) - }; + const promise = sinon.stub() + .withArgs().returnsPromise() + .resolves({ + Item: { + content: {S: '[}not json'}, + expires: {N: '123'} + } + }); + const awsClient = {getItem: () => ({promise})}; + return t.shouldFail(dynamo({awsClient, tableName}).get('abc'), SyntaxError); }); @@ -73,45 +74,48 @@ test('Should be able to put a session', () => { const id = 'abc'; const expires = '123'; const content = {foo: 'bar'}; - const awsClient = { - putItem: sinon.stub() - .withArgs({ - TableName: tableName, - Item: { - id: {S: id}, - expires: {N: expires}, - content: {S: JSON.stringify(content)} - } - }) - .callsArgWith(1, null) - }; + const promise = sinon.stub() + .withArgs({ + TableName: tableName, + Item: { + id: {S: id}, + expires: {N: expires}, + content: {S: JSON.stringify(content)} + } + }).returnsPromise() + .resolves(); + const awsClient = {putItem: () => ({promise})}; + return dynamo({awsClient, tableName}).put(id, expires, content); }); test('Should pass put errors on', t => { - const awsClient = { - putItem: sinon.stub().callsArgWith(1, new TypeError()) - }; + const promise = sinon.stub().withArgs({TableName: tableName}).returnsPromise() + .rejects(new TypeError()); + const awsClient = {putItem: () => ({promise})}; + return t.shouldFail(dynamo({awsClient, tableName}).put('abc', '123', '42'), TypeError); }); test('Should be able to delete a session', () => { const id = 'abc'; - const awsClient = { - deleteItem: sinon.stub() - .withArgs({ - TableName: tableName, - Key: {id: {S: id}} - }) - .callsArgWith(1, null) - }; + const promise = sinon.stub() + .withArgs({ + TableName: tableName, + Key: {id: {S: id}} + }) + .returnsPromise() + .resolves(); + const awsClient = {deleteItem: () => ({promise})}; + return dynamo({awsClient, tableName}).delete(id); }); test('Should pass delete errors on', t => { - const awsClient = { - deleteItem: sinon.stub().callsArgWith(1, new TypeError()) - }; + const promise = sinon.stub().withArgs({TableName: tableName}).returnsPromise() + .rejects(new TypeError()); + const awsClient = {deleteItem: () => ({promise})}; + return t.shouldFail(dynamo({awsClient, tableName}).delete('abc'), TypeError); }); @@ -124,11 +128,12 @@ const scanQuery = (when, startKey = null) => ({ }); test('Should handle when scan returns no results', t => { - const awsClient = { - scan: sinon.stub() - .withArgs(scanQuery(200)) - .callsArgWith(1, null, {Items: [], ScannedCount: 34}) - }; + const promise = sinon.stub() + .withArgs(scanQuery(200)) + .returnsPromise() + .resolves({Items: [], ScannedCount: 34}); + const awsClient = {scan: () => ({promise})}; + return dynamo({awsClient, tableName}) .deleteExpired(200) .then(({scanned, deleted}) => { @@ -138,73 +143,30 @@ test('Should handle when scan returns no results', t => { }); test('Should pass errors on when deleteing expired', t => { - const awsClient = { - scan: sinon.stub() - .withArgs(scanQuery(200)) - .callsArgWith(1, new TypeError()) - }; + const promise = sinon.stub().withArgs(scanQuery(200)).returnsPromise() + .rejects(new TypeError()); + const awsClient = {scan: () => ({promise})}; + return t.shouldFail(dynamo({awsClient, tableName}).deleteExpired(200), TypeError); }); test('Should be able to delete one page of results', t => { - const awsClient = { - deleteItem: sinon.stub().callsArgWith(1, null), - scan: sinon.stub() - .withArgs(scanQuery(200)) - .callsArgWith(1, null, { - Items: [ - {id: {S: 'john'}}, - {id: {S: 'paul'}} - ], - ScannedCount: 34 - }) - }; - - return dynamo({awsClient, tableName}) - .deleteExpired(200) - .then(({scanned, deleted}) => { - t.equal(scanned, 34, 'scanned should be 34'); - t.equal(deleted, 2, 'deleted should be 2'); - }); -}); - -test('Should be able to delete multiple pages of results', t => { - const scan = sinon.stub(); - - // first call returns 2 items and a last evaluated key of 'brian' - scan.withArgs(scanQuery(200)) - .callsArgWith(1, null, { + const promise = sinon.stub() + .withArgs(scanQuery(200)) + .returnsPromise() + .resolves({ Items: [ {id: {S: 'john'}}, {id: {S: 'paul'}} - ], ScannedCount: 34, LastEvaluatedKey: 'brian' + ], + ScannedCount: 34 }); - - // second call returns 0 items and a last evaluated key of 'yoko' - scan.withArgs(scanQuery(200, 'brian')) - .callsArgWith(1, null, { - Items: [], - ScannedCount: 54, LastEvaluatedKey: 'yoko' - }); - - // last call returns 2 items and no last evaluated key - scan.withArgs(scanQuery(200, 'yoko')) - .callsArgWith(1, null, { - Items: [ - {id: {S: 'george'}}, - {id: {S: 'ringo'}} - ], ScannedCount: 18 - }); - - const awsClient = { - deleteItem: sinon.stub().callsArgWith(1, null), - scan - }; + const awsClient = {scan: () => ({promise})}; return dynamo({awsClient, tableName}) .deleteExpired(200) .then(({scanned, deleted}) => { - t.equal(scanned, 34 + 54 + 18, 'scanned should be the total'); - t.equal(deleted, 4, 'deleted should be 4'); + t.equal(scanned, 34, 'scanned should be 34'); + t.equal(deleted, 2, 'deleted should be 2'); }); });