UNPKG

dynode

Version:

node.js client for Amazon's DynamoDB

350 lines (269 loc) 10.2 kB
var events = require('events'), util = require('utile'), async = util.async, _ = require('underscore'), retry = require('retry'), Types = require('./types'), Request = require('./request').Request; var Client = exports.Client = function Client(config) { if ( !config.accessKeyId || ! config.secretAccessKey) { throw new Error('You must set the AWS credentials: accessKeyId + secretAccessKey'); } events.EventEmitter.call(this); this.config = config; this.request = new Request(config); }; util.inherits(Client, events.EventEmitter); Client.prototype.listTables = function(options, cb) { if (!cb || typeof cb != "function") { cb = options; options = {}; } this._request("ListTables", options, cb); }; Client.prototype.describeTable = function(name, cb) { this._request("DescribeTable", {"TableName": name}, function(err, resp){ if(err) return cb(err); return cb(null, resp.Table); }); }; Client.prototype.createTable = function(name, options, cb) { // option is an optional parameter if (!cb || typeof cb != "function") { cb = options; options = {}; } var config = util.mixin({}, {"TableName": name, read: 10, write: 5}, {hash : {id: String}}, options); this._request("CreateTable", this._toCreateTableSchema(config), cb); }; Client.prototype.deleteTable = function(name, cb) { this._request("DeleteTable", {"TableName": name}, cb); }; Client.prototype.updateTable = function(name, options, cb) { var config = util.mixin({}, {"TableName": name}, this._toUpdateTableSchema(options)); this._request("UpdateTable", config, cb); }; Client.prototype.putItem = function(tableName, item, options, cb) { if (!cb || typeof cb != "function") { cb = options; options = {}; } var request = util.mixin({}, {TableName: tableName, Item: Types.stringify(item)}, options); this._request("PutItem", request, cb); }; Client.prototype.updateItem = function(tableName, keys, item, options, cb) { // option is an optional parameter if (!cb || typeof cb != "function") { cb = options; options = {}; } var request = util.mixin({}, {TableName: tableName, Key: Types.toKeys(keys), AttributeUpdates: Types.updateAttributes(item)}, options); this._request("UpdateItem", request, function (err, meta) { if (meta && meta.Attributes) meta.Attributes = Types.parse(meta.Attributes); cb(err, meta); }); }; Client.prototype.getItem = function(tableName, key, options, cb) { if (!cb || typeof cb != "function") { cb = options; options = {}; } var request = util.mixin({}, {TableName: tableName, Key: Types.toKeys(key)}, options); this._request("GetItem", request, function(err, resp){ if(err) return cb(err); return cb(null, Types.parse(resp.Item), {ConsumedCapacityUnits: resp.ConsumedCapacityUnits}); }); }; Client.prototype.deleteItem = function(tableName, key, options, cb) { if (!cb || typeof cb != "function") { cb = options; options = {}; } var request = util.mixin({}, {"TableName":tableName, Key: Types.toKeys(key)}, options); this._request("DeleteItem", request, function(err, resp){ if(err) return cb(err); if(resp.Attributes) resp.Attributes = Types.parse(resp.Attributes); return cb(null, resp); }); }; Client.prototype.query = function(tableName, hashkey, options, cb) { if (!cb || typeof cb != "function") { cb = options; options = {}; } var request = util.mixin({}, {"TableName":tableName}, Types.stringify( {"HashKeyValue": hashkey} ), options); this._request("Query", request, function(err, resp) { if(err) return cb(err); var items = {}; if(resp.Items) { resp.Items = _.map(resp.Items, Types.parse); } return cb(null, resp); }); }; Client.prototype.scan = function(tableName, options, cb) { var args = Array.prototype.slice.call(arguments, 0); var cb = args.pop(); var options = args.pop(); if (!args.length) { tableName = options; options = {}; } var filters = util.mixin({}, {"TableName":tableName}, options); this._request("Scan", filters, function(err, resp) { if(err) return cb(err); var items = {}; if(resp.Items) { items = _.map(resp.Items, Types.parse); delete resp.Items; } return cb(null, items, resp); }); }; Client.prototype.batchGetItem = function(options, cb) { var request = _.reduce(options, function(memo, opt, table) { var attrs = {Keys : _.map(opt.keys, Types.toKeys) }; if(opt.AttributesToGet) attrs.AttributesToGet = opt.AttributesToGet; memo[table] = attrs; return memo; }, {}); this._request("BatchGetItem", {RequestItems: request}, function(err, resp){ if(err) return cb(err); var responses = {}; var meta = {UnprocessedKeys: resp.UnprocessedKeys, ConsumedCapacityUnits: {}}; for (var table in resp.Responses) { meta.ConsumedCapacityUnits[table] = resp.Responses[table].ConsumedCapacityUnits; responses[table] = resp.Responses[table].Items.map(Types.parse); } return cb(null, responses, meta); }); }; Client.prototype.batchWriteItem = function(options, cb) { var self = this; var request = _.reduce(options, function(memo, requests, table) { memo[table] = _.map(requests, function(req){ if(req.put) { return {PutRequest : {Item: Types.stringify(req.put)}}; } else if(req.del) { return {DeleteRequest : {Key: Types.toKeys(req.del)}}; } else { return req; } }); return memo; }, {}); this._request("BatchWriteItem", {RequestItems: request}, function(err, response){ if(err) return cb(err); if(response.UnprocessedItems && Object.keys(response.UnprocessedItems).length > 0){ return self.batchWriteItem(response.UnprocessedItems, cb); } else { return cb(null, response); } }); }; Client.prototype.truncate = function(tableName, options, cb) { if (!cb || typeof cb != "function") { cb = options; options = {}; } var self = this; var throughputPercent = options.throughputPercent || 1.0; this.describeTable(tableName, function(err, table) { if(err) return cb(err); var throughput = Math.ceil(table.ProvisionedThroughput.WriteCapacityUnits * throughputPercent), hashKey = table.KeySchema.HashKeyElement.AttributeName, rangeKey; if(table.KeySchema.RangeKeyElement) rangeKey = table.KeySchema.RangeKeyElement.AttributeName; self.scan(tableName, function (err, items, stats) { if(!items || items.length === 0) return cb(); var batchWrites = []; var i,j,chunkSize = 25; if(throughput < chunkSize) chunkSize = throughput; var toDeleteStatment = function(item){ var key = {hash: item[hashKey]}; if(rangeKey) key.range = item[rangeKey]; return {del : key}; }; for (i=0,j=items.length; i<j; i+=chunkSize) { var chunks = items.slice(i,i+chunkSize); var writes = {}; writes[tableName] = chunks.map(toDeleteStatment); batchWrites.push(async.apply(self.batchWriteItem.bind(self), writes)); } async.series(batchWrites, cb); }); }); }; Client.prototype._request = function(action, options, cb) { var self = this; options = this._prefixTableName(action, options); var operation = retry.operation({retries: 10, factor: 2, minTimeout: 50, randomize: true}); operation.attempt(function(currentAttempt) { self.request.send(action, options, function(err, resp) { if(err && err.retry && operation.retry(err)) return; // check to see if should retry request cb(err, self._removeTableNamePrefix(resp)); }); }); }; Client.prototype._prefixTableName = function(action, options) { var self = this; if(!this.config.tableNamePrefix) return options; if(options.TableName) { options.TableName = self.config.tableNamePrefix + options.TableName; } else if(action === 'BatchGetItem' || action === 'BatchWriteItem') { var items = _.reduce(options.RequestItems, function(memo, attrs, table) { var prefixTableName = self.config.tableNamePrefix + table; memo[prefixTableName] = attrs; return memo; },{}); options.RequestItems = items; } else if(action === 'ListTables' && options.ExclusiveStartTableName) { options.ExclusiveStartTableName = self.config.tableNamePrefix + options.ExclusiveStartTableName; } return options; }; Client.prototype._removeTableNamePrefix = function(response) { if(!this.config.tableNamePrefix || !response) return response; var self = this; if(response.UnprocessedItems) { response.UnprocessedItems = _.reduce(response.UnprocessedItems, function(memo, req, table) { var name = table.substr(self.config.tableNamePrefix.length, table.length); memo[name] = req; return memo; }, {}); } if(response.Responses) { response.Responses = _.reduce(response.Responses, function(memo, req, table) { var name = table.substr(self.config.tableNamePrefix.length, table.length); memo[name] = req; return memo; }, {}); } return response; }; Client.prototype._toCreateTableSchema = function(options) { return { "TableName" : options.TableName, "KeySchema" : this._parseKeySchema(options), "ProvisionedThroughput":this._parseThroughputSchema(options) }; }; Client.prototype._toUpdateTableSchema = function(options) { return { "ProvisionedThroughput":this._parseThroughputSchema(options) }; }; Client.prototype._parseThroughputSchema = function(options) { return {"ReadCapacityUnits":options.read,"WriteCapacityUnits":options.write}; }; Client.prototype._parseKeySchema = function(options) { var hashKeyName = Object.keys(options.hash)[0]; var hashKeyType = options.hash[hashKeyName].name[0].toUpperCase(); var keySchema = {"HashKeyElement":{"AttributeName": hashKeyName, "AttributeType": hashKeyType}}; if(options.range) { var rangeKeyName = Object.keys(options.range)[0]; var rangeKeyType = options.range[rangeKeyName].name[0].toUpperCase(); keySchema["RangeKeyElement"] = {"AttributeName":rangeKeyName,"AttributeType":rangeKeyType}; } return keySchema; };