diff --git a/.gitignore b/.gitignore index 2968261..debb5ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -.nyc_output +.nyc_output/ coverage/ +node_modules/ \ No newline at end of file diff --git a/API.md b/API.md index 510bcff..15944ca 100644 --- a/API.md +++ b/API.md @@ -210,6 +210,19 @@ Update a single record. Passthrough to [DocumentClient.update](http://docs.aws.a Returns **Request** +## dynamicUpdate + +Updates a single record, dynamically generating parts of the update statement from an object. +Given a new object to update within a table, the necessary parameters to update every key in this new object will be generated. + +**Parameters** + +- `newObject` **object** all of this object's properties will be used to update the row in the table +- `params` **object** standard update request parameters. See [DocumentClient.update](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#update-property) for details. ExpressionAttributeNames, ExpressionAttributeValues, and UpdateExpression fields not reqiured +- `callback` **[function]** a function to handle the response. See [DocumentClient.update](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#update-property) for details. + +Returns **Request** + # CompleteRequestSet An array of [AWS.Requests](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Request.html) diff --git a/index.js b/index.js index 5af85ac..75f9418 100644 --- a/index.js +++ b/index.js @@ -338,7 +338,18 @@ function Dyno(options) { * @param {function} [callback] - a function to handle the response. See [DocumentClient.scan](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#scan-property) for details. * @returns a Request if not paginating, or a ReadableStream if multiple pages were requested */ - scan: require('./lib/paginated')(docClient).scan + scan: require('./lib/paginated')(docClient).scan, + /** + * Given a object, generates the necessary parameters to update every key in the new object within a given Dynamo table and executes the update + * + * @instanceof + * @memberof client + * @param {object} newObject - all of this object's properties will be used to update the row in the table + * @param {object} updateParams - standard parameters object for a [DocumentClient.update](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#update-property) call, ExpressionAttributeNames, ExpressionAttributeValues, and UpdateExpression not reqiured + * @param {function} callback - a function to handle the response. See [DocumentClient.update](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html#update-property) for details. + * @returns {Request} + */ + dynamicUpdate: require('./lib/update')(docClient).dynamicUpdate }; // Drop specific functions from read/write only clients @@ -350,6 +361,7 @@ function Dyno(options) { delete dynoExtensions.batchWriteItemRequests; delete dynoExtensions.batchWriteAll; delete dynoExtensions.putStream; + delete dynoExtensions.dynamicUpdate; } if (options.write) { diff --git a/lib/update.js b/lib/update.js new file mode 100644 index 0000000..a555140 --- /dev/null +++ b/lib/update.js @@ -0,0 +1,27 @@ +var _ = require('underscore'); + +module.exports = function(client) { + var updates = {}; + + updates.dynamicUpdate = function (newObject, updateParams, callback) { + var expressionObject = { + ExpressionAttributeNames: {}, + ExpressionAttributeValues: {} + }; + var updateExpressionParts = []; + + Object.keys(newObject).forEach(function (key) { + if (Object.keys(updateParams.Key).indexOf(key) !== -1) return; + + expressionObject.ExpressionAttributeNames['#' + key] = key; + expressionObject.ExpressionAttributeValues[':' + key] = newObject[key]; + updateExpressionParts.push('#' + key + ' = :' + key); + }); + expressionObject.UpdateExpression = 'set ' + updateExpressionParts.join(', '); + + updateParams = _.extend(updateParams, expressionObject); + return client.update(updateParams, callback); + }; + + return updates; +}; \ No newline at end of file diff --git a/test/index.test.js b/test/index.test.js index e4a559a..b5bbb0e 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -27,6 +27,7 @@ test('[index] expected properties', function(assert) { assert.equal(typeof dyno.query, 'function', 'exposes query function'); assert.equal(typeof dyno.scan, 'function', 'exposes scan function'); assert.equal(typeof dyno.updateItem, 'function', 'exposes updateItem function'); + assert.equal(typeof dyno.dynamicUpdate, 'function', 'exposes dynamicUpdate function'); assert.equal(typeof dyno.batchGetItemRequests, 'function', 'exposes batchGetItemRequests function'); assert.equal(typeof dyno.batchWriteItemRequests, 'function', 'exposes batchWriteItemRequests function'); assert.equal(typeof dyno.batchGetAll, 'function', 'exposes batchGetAll function'); @@ -50,6 +51,7 @@ test('[index] expected properties', function(assert) { assert.equal(typeof read.query, 'function', 'read-only client exposes query function'); assert.equal(typeof read.scan, 'function', 'read-only client exposes scan function'); assert.equal(typeof read.updateItem, 'undefined', 'read-only client does not expose updateItem function'); + assert.equal(typeof read.dynamicUpdate, 'undefined', 'read-only client does not expose dynamicUpdate function'); assert.equal(typeof read.batchGetItemRequests, 'function', 'read-only client exposes batchGetItemRequests function'); assert.equal(typeof read.batchWriteItemRequests, 'undefined', 'read-only client does not expose batchWriteItemRequests function'); assert.equal(typeof read.batchGetAll, 'function', 'read-only client exposes batchGetAll function'); @@ -73,6 +75,7 @@ test('[index] expected properties', function(assert) { assert.equal(typeof write.query, 'undefined', 'write-only client does not expose query function'); assert.equal(typeof write.scan, 'undefined', 'write-only client does not expose scan function'); assert.equal(typeof write.updateItem, 'function', 'write-only client exposes updateItem function'); + assert.equal(typeof write.dynamicUpdate, 'function', 'write-only client exposes dynamicUpdate function'); assert.equal(typeof write.batchGetItemRequests, 'undefined', 'write-only client does not expose batchGetItemRequests function'); assert.equal(typeof write.batchWriteItemRequests, 'function', 'write-only client exposes batchWriteItemRequests function'); assert.equal(typeof write.batchGetAll, 'undefined', 'write-only client does not expose batchGetAll function'); @@ -99,6 +102,7 @@ test('[index] expected properties', function(assert) { assert.equal(typeof multi.query, 'function', 'multi-client exposes query function'); assert.equal(typeof multi.scan, 'function', 'multi-client exposes scan function'); assert.equal(typeof multi.updateItem, 'function', 'multi-client exposes updateItem function'); + assert.equal(typeof multi.dynamicUpdate, 'function', 'multi-client exposes dynamicUpdate function'); assert.equal(typeof multi.batchGetItemRequests, 'function', 'exposes batchGetItemRequests function'); assert.equal(typeof multi.batchWriteItemRequests, 'function', 'exposes batchWriteItemRequests function'); assert.equal(typeof multi.batchGetAll, 'function', 'exposes batchGetAll function'); diff --git a/test/update.test.js b/test/update.test.js new file mode 100644 index 0000000..f414762 --- /dev/null +++ b/test/update.test.js @@ -0,0 +1,79 @@ +var test = require('tape'); +var testTables = require('./test-tables'); +var dynamodb = require('dynamodb-test')(test, 'dyno', testTables.idhash); +var Dyno = require('..'); +var Update = require('../lib/update'); +var _ = require('underscore'); + +var fixtures = _.range(10).map(function (i) { + return { + id: i.toString(), + idPower2: Math.pow(i, 2), + text: 'string' + }; +}); + +test('[update] properties', function (assert) { + var update = Update(dynamodb.dynamodb); + assert.equal(typeof update.dynamicUpdate, 'function', 'exposes dynamicUpdate function'); + assert.end(); +}); + +dynamodb.start(); + +dynamodb.test('[update] dynamicUpdate - update all fields', fixtures, function (assert) { + var dyno = Dyno({ + table: dynamodb.tableName, + region: 'local', + endpoint: 'http://localhost:4567' + }); + + var newObject = { + id: '2', + idPower2: -99, + text: 'update string' + }; + var updateParams = { + TableName: dynamodb.tableName, + Key: { id: '2' }, + ReturnValues: 'ALL_NEW' + }; + + dyno.dynamicUpdate(newObject, updateParams, function (err, response) { + assert.ifError(err, 'dynamicUpdate errored'); + assert.deepEqual(response, { Attributes: { id: '2', idPower2: -99, text: 'update string' } }, 'expected new values'); + assert.end(); + }); +}); + +dynamodb.test('[update] dynamicUpdate - only add new field', fixtures, function (assert) { + var dyno = Dyno({ + table: dynamodb.tableName, + region: 'local', + endpoint: 'http://localhost:4567' + }); + + var newObject = { + id: '2', + newDateField: '2017-10-31' + }; + var updateParams = { + TableName: dynamodb.tableName, + Key: { id: '0' }, + ReturnValues: 'ALL_NEW' + }; + + var expectedRow = { id: '0', idPower2: 0, newDateField: '2017-10-31', text: 'string' }; + dyno.dynamicUpdate(newObject, updateParams, function (err, response) { + assert.ifError(err, 'dynamicUpdate errored'); + assert.deepEqual(response, { Attributes: expectedRow }, 'expected new values'); + dyno.query({ KeyConditionExpression: 'id = :id', ExpressionAttributeValues: { ':id': '0' } }, function (err, queryResponse) { + assert.ifError(err, 'query error'); + assert.deepEqual(queryResponse.Items, [expectedRow], 'expected query response'); + assert.end(); + }); + }); +}); + +dynamodb.delete(); +dynamodb.close(); \ No newline at end of file