├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── dynamo_client.js ├── package.json └── test ├── fast.js ├── setup.js ├── slow.js └── teardown.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | env: 5 | global: 6 | - secure: "C1+k93MqoB2wD0VJbH5afLJq9SZ4rlb9VQVE4uzSZHkx1zp7+7bgftDfCxm+\nrHHWfXTTj0MK1H12m7URxkqGcV7jmnOMaSMakaDbn/K70W/ap9zqFNmRfClJ\nMztcFkGXzLMHje24z1UOM8967b26D6S1J/1vFQ8YgcfzLprePDA=" 7 | - secure: "UVWP1Oywk/27zk5MhWmPjJsGqW6uIMjAcYIj+vlIV9GKWwek1MIP4OctcwvA\n7e82HkQv+WQrAP8xF+LJGU0JVm+M1agqO5MO/KXlGpJDkbubC3X9FGnaHoXP\nl5jtTHzAaqmWNnI1SR8O680mh7DmqjYw6NMQFPu+5gCJhMr1Mg4=" 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Jed Schmidt, http://jed.is/ 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This library is deprecated, use AWS's [DocumentClient](http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/DynamoDB/DocumentClient.html) instead. 2 | 3 | dynamo-client 4 | ------------- 5 | 6 | [![Build Status](https://secure.travis-ci.org/jed/dynamo-client.png?branch=master)](http://travis-ci.org/jed/dynamo-client) 7 | 8 | This is a low-level client for accessing DynamoDB from node.js. It offers a simpler and more node-friendly API than [Amazon's SDK](http://aws.amazon.com/sdkfornodejs/), in the style of [@mikeal](https://github.com/mikeal)'s popular [request](https://github.com/mikeal/request) library. 9 | 10 | Example 11 | ------- 12 | 13 | ```javascript 14 | // assuming AWS credentials are available from process.ENV 15 | var dynamo = require("dynamo-client") 16 | , region = "us-east-1" 17 | , db = dynamo.createClient(region) 18 | 19 | db.request("ListTables", null, function(err, data) { 20 | console.log(data.TableNames.length + " tables found.") 21 | }) 22 | ``` 23 | 24 | API 25 | --- 26 | 27 | ### db = dynamo.createClient(region, [credentials]) 28 | 29 | This creates a database instance for the given DynamoDB region, which can be one of the following: 30 | 31 | - `us-east-1` (Northern Virginia) 32 | - `us-west-1` (Northern California) 33 | - `us-west-2` (Oregon) 34 | - `eu-west-1` (Ireland) 35 | - `ap-northeast-1` (Tokyo) 36 | - `ap-southeast-1` (Singapore) 37 | - `ap-southeast-2` (Sydney) 38 | - `sa-east-1` (Sao Paulo) 39 | 40 | The official region list can be found in the [AWS documentation](http://docs.amazonwebservices.com/general/latest/gr/rande.html#ddb_region). 41 | 42 | You can also pass an object in here with `host`, `port`, `region`, `version`, and/or 43 | `credentials` parameters: 44 | 45 | ```javascript 46 | var db = dynamo.createClient({host: "localhost", port: 4567, version: "20111205"}) 47 | ``` 48 | 49 | This is especially useful if you want to connect to a mock DynamoDB 50 | instance (such as [FakeDynamo](https://github.com/ananthakumaran/fake_dynamo) or 51 | [ddbmock](https://bitbucket.org/Ludia/dynamodb-mock)). 52 | 53 | For backwards compatibility with versions <= 0.2.4, you can also pass 54 | the full host in here too (should detect most hostnames unless they're 55 | incredibly similar to an AWS region name): 56 | 57 | ```javascript 58 | var db = dynamo.createClient("dynamodb.eu-west-1.amazonaws.com") 59 | ``` 60 | 61 | Your AWS credentials (which can be found in your [AWS console](https://portal.aws.amazon.com/gp/aws/securityCredentials)) can be specified in one of two ways: 62 | 63 | - As the second argument, like this: 64 | 65 | ```javascript 66 | dynamo.createClient("us-east-1", { 67 | secretAccessKey: "", 68 | accessKeyId: "" 69 | }) 70 | ``` 71 | 72 | - From `process.env`, such as like this: 73 | 74 | ``` 75 | export AWS_SECRET_ACCESS_KEY="" 76 | export AWS_ACCESS_KEY_ID="" 77 | ``` 78 | 79 | ### db.request(targetName, data, callback) 80 | 81 | Database instances have only one method, `request`, which takes a target name, data object, and callback. 82 | 83 | The target name can be any of the [operations available for DynamoDB](http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/operationlist.html 84 | ), which currently include the following: 85 | 86 | - `BatchGetItem` 87 | - `BatchWriteItem` 88 | - `CreateTable` 89 | - `DeleteItem` 90 | - `DeleteTable` 91 | - `DescribeTable` 92 | - `GetItem` 93 | - `ListTables` 94 | - `PutItem` 95 | - `Query` 96 | - `Scan` 97 | - `UpdateItem` 98 | - `UpdateTable` 99 | 100 | The data object needs to serialize into the [DynamoDB JSON format](http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/DataFormat.html). 101 | 102 | The callback is called with the usual `(err, data)` signature, in which data is an object parsed from the JSON returned by DynamoDB. 103 | 104 | To match [AWS expectations](http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/ErrorHandling.html#APIRetries), the following requests are automatically retried with exponential backoff (50ms, 100ms, 200ms, 400ms, etc) upon failure: 105 | 106 | - 5xx errors 107 | - 400 ThrottlingException errors 108 | - 400 ProvisionedThroughputExceededException errors 109 | 110 | Retries are attempted up to 10 times by default, but this amount can be changed by setting `dynamo.Request.prototype.maxRetries` to the desired number. 111 | -------------------------------------------------------------------------------- /dynamo_client.js: -------------------------------------------------------------------------------- 1 | var dynamo = exports 2 | var http = require("http") 3 | var https = require("https") 4 | var aws4 = require("aws4") 5 | 6 | function Database(region, credentials) { 7 | if (typeof region === "object") { 8 | this.host = region.host 9 | this.port = region.port 10 | this.region = region.region 11 | this.version = region.version // '20120810' or '20111205' 12 | this.agent = region.agent 13 | this.https = region.https 14 | credentials = region.credentials || credentials 15 | } else { 16 | if (/^[a-z]{2}\-[a-z]+\-\d$/.test(region)) 17 | this.region = region 18 | else 19 | // Backwards compatibility for when 1st param was host 20 | this.host = region 21 | } 22 | if (!this.region) this.region = (this.host || "").split(".", 2)[1] || "us-east-1" 23 | if (!this.host) this.host = "dynamodb." + this.region + ".amazonaws.com" 24 | if (!this.version) this.version = "20120810" 25 | 26 | this.credentials = new Credentials(credentials || {}) 27 | } 28 | 29 | Database.prototype.request = function(target, data, cb) { 30 | !function retry(database, i) { 31 | var req = new Request(database, target, data || {}) 32 | 33 | aws4.sign(req, database.credentials) 34 | 35 | req.send(function(err, data) { 36 | if (err) { 37 | if (i < Request.prototype.maxRetries && ( 38 | err.statusCode >= 500 || 39 | err.name == "ProvisionedThroughputExceededException" || 40 | err.name == "ThrottlingException" 41 | )) { 42 | setTimeout(retry, 50 << i, database, i + 1) 43 | } 44 | 45 | else cb(err) 46 | } 47 | 48 | else cb(null, data) 49 | }) 50 | }(this, 0) 51 | } 52 | 53 | function Request(opts, target, data) { 54 | var headers = this.headers = {} 55 | 56 | this.host = opts.host 57 | this.port = opts.port 58 | this.http = opts.https ? https : http 59 | 60 | if ("agent" in opts) this.agent = opts.agent 61 | 62 | this.body = JSON.stringify(data) 63 | 64 | this.method = Request.prototype.method 65 | this.path = Request.prototype.path 66 | this.maxRetries = Request.prototype.maxRetries 67 | this.contentType = Request.prototype.contentType 68 | 69 | headers["Host"] = this.host 70 | headers["Date"] = new Date().toUTCString() 71 | headers["Content-Length"] = Buffer.byteLength(this.body) 72 | headers["Content-Type"] = Request.prototype.contentType 73 | 74 | headers["X-Amz-Target"] = "DynamoDB_" + opts.version + "." + target 75 | } 76 | 77 | Request.prototype.method = "POST" 78 | Request.prototype.path = "/" 79 | Request.prototype.maxRetries = 10 80 | Request.prototype.contentType = "application/x-amz-json-1.0" 81 | 82 | Request.prototype.send = function(cb) { 83 | var request = this.http.request(this, function(res) { 84 | var json = "" 85 | 86 | res.setEncoding("utf8") 87 | 88 | res.on("data", function(chunk){ json += chunk }) 89 | res.on("end", function() { 90 | var response 91 | 92 | try { response = JSON.parse(json) } catch (e) { } 93 | 94 | if (res.statusCode == 200 && response != null) return cb(null, response) 95 | 96 | error.statusCode = res.statusCode 97 | if (response != null) { 98 | error.name = (response.__type || "").split("#").pop() 99 | error.message = response.message || response.Message || JSON.stringify(response) 100 | } else { 101 | if (res.statusCode == 413) json = "Request Entity Too Large" 102 | error.message = "HTTP/1.1 " + res.statusCode + " " + json 103 | } 104 | 105 | cb(error) 106 | }) 107 | }) 108 | 109 | var error = new Error 110 | 111 | request.on("error", cb) 112 | 113 | request.write(this.body) 114 | request.end() 115 | } 116 | 117 | function Credentials(attrs) { 118 | var env = process.env 119 | 120 | this.secretAccessKey = attrs.secretAccessKey || env.AWS_SECRET_ACCESS_KEY 121 | this.accessKeyId = attrs.accessKeyId || env.AWS_ACCESS_KEY_ID 122 | 123 | if (!this.secretAccessKey) { 124 | throw new Error("No secret access key available.") 125 | } 126 | 127 | if (!this.accessKeyId) { 128 | throw new Error("No access key id available.") 129 | } 130 | 131 | // Optional session token, if the user wants to supply one 132 | this.sessionToken = attrs.sessionToken 133 | } 134 | 135 | dynamo.Database = Database 136 | dynamo.Request = Request 137 | dynamo.Credentials = Credentials 138 | 139 | dynamo.createClient = function(region, credentials) { 140 | return new Database(region, credentials) 141 | } 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dynamo-client", 3 | "version": "1.1.5", 4 | "description": "A low-level client for accessing DynamoDB", 5 | "author": "Jed Schmidt (http://jed.is)", 6 | "main": "dynamo_client.js", 7 | "keywords": [ 8 | "amazon", 9 | "aws", 10 | "DynamoDB", 11 | "dynamo", 12 | "nosql", 13 | "database" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jed/dynamo-client.git" 18 | }, 19 | "license": "MIT", 20 | "dependencies": { 21 | "aws4": "~0.2.0" 22 | }, 23 | "devDependencies": { 24 | "should": "~1.2.2", 25 | "mocha": "~1.10.0" 26 | }, 27 | "scripts": { 28 | "pretest": "mocha ./test/setup.js -b -t 100s -R list", 29 | "test": "mocha ./test/fast.js ./test/slow.js -b -t 100s -R list", 30 | "posttest": "mocha ./test/teardown.js -b -t 100s -R list" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/fast.js: -------------------------------------------------------------------------------- 1 | var should = require("should") 2 | , dynamo = require("../") 3 | 4 | describe("Database", function() { 5 | 6 | // Save and ensure we restore process.env 7 | var envAccessKeyId, envSecretAccessKey 8 | 9 | before(function() { 10 | envAccessKeyId = process.env.AWS_ACCESS_KEY_ID 11 | envSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY 12 | process.env.AWS_ACCESS_KEY_ID = "ABCDEF" 13 | process.env.AWS_SECRET_ACCESS_KEY = "abcdef1234567890" 14 | }) 15 | 16 | after(function() { 17 | process.env.AWS_ACCESS_KEY_ID = envAccessKeyId 18 | process.env.AWS_SECRET_ACCESS_KEY = envSecretAccessKey 19 | }) 20 | 21 | describe("created with no params", function() { 22 | it("should have default region", function() { 23 | var db = dynamo.createClient() 24 | db.region.should.equal("us-east-1") 25 | }) 26 | it("should have correct host", function() { 27 | var db = dynamo.createClient() 28 | db.host.should.equal("dynamodb.us-east-1.amazonaws.com") 29 | }) 30 | }) 31 | 32 | describe("created with region", function() { 33 | it("should have correct region", function() { 34 | var db = dynamo.createClient("ap-southeast-2") 35 | db.region.should.equal("ap-southeast-2") 36 | }) 37 | it("should have correct host", function() { 38 | var db = dynamo.createClient("ap-southeast-2") 39 | db.host.should.equal("dynamodb.ap-southeast-2.amazonaws.com") 40 | }) 41 | }) 42 | 43 | describe("created with host", function() { 44 | it("should have correct region", function() { 45 | var db = dynamo.createClient("dynamodb.ap-southeast-2.amazonaws.com") 46 | db.region.should.equal("ap-southeast-2") 47 | }) 48 | it("should have correct host", function() { 49 | var db = dynamo.createClient("dynamodb.ap-southeast-2.amazonaws.com") 50 | db.host.should.equal("dynamodb.ap-southeast-2.amazonaws.com") 51 | }) 52 | }) 53 | 54 | describe("created with options", function() { 55 | it("should have correct region", function() { 56 | var db = dynamo.createClient({host: "myhost", port: 8080, region: "myregion"}) 57 | db.region.should.equal("myregion") 58 | }) 59 | it("should have correct host", function() { 60 | var db = dynamo.createClient({host: "myhost", port: 8080, region: "myregion"}) 61 | db.host.should.equal("myhost") 62 | }) 63 | it("should have correct port", function() { 64 | var db = dynamo.createClient({host: "myhost", port: 8080, region: "myregion"}) 65 | db.port.should.equal(8080) 66 | }) 67 | it("should have correct version", function() { 68 | var db = dynamo.createClient({version: "20111205"}) 69 | db.version.should.equal("20111205") 70 | }) 71 | it("should have correct agent", function() { 72 | var db = dynamo.createClient({agent: false}) 73 | db.agent.should.equal(false) 74 | }) 75 | it("should have correct https", function() { 76 | var db = dynamo.createClient({https: true}) 77 | db.https.should.equal(true) 78 | }) 79 | }) 80 | 81 | describe("created with partial options", function() { 82 | it("should have default region", function() { 83 | var db = dynamo.createClient({host: "myhost", port: 8080}) 84 | db.region.should.equal("us-east-1") 85 | }) 86 | it("should have default host", function() { 87 | var db = dynamo.createClient({region: "ap-southeast-2"}) 88 | db.host.should.equal("dynamodb.ap-southeast-2.amazonaws.com") 89 | }) 90 | it("should have default version", function() { 91 | var db = dynamo.createClient({region: "ap-southeast-2"}) 92 | db.version.should.equal("20120810") 93 | }) 94 | it("should have default agent", function() { 95 | var db = dynamo.createClient({region: "ap-southeast-2"}) 96 | should.strictEqual(undefined, db.agent) 97 | }) 98 | it("should have default https", function() { 99 | var db = dynamo.createClient({region: "ap-southeast-2"}) 100 | should.strictEqual(undefined, db.https) 101 | }) 102 | }) 103 | 104 | }) 105 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | // secure env vars not available in pull requests 2 | if (process.env.TRAVIS_SECURE_ENV_VARS == 'false') return 3 | 4 | var should = require("should") 5 | , dynamo = require("../") 6 | , options = {region: "us-east-1", version: "20111205"} 7 | , name = "jed_dynamo-client_test" 8 | 9 | describe("dynamo", function() { 10 | it("starting tests at " + JSON.stringify(new Date)) 11 | 12 | describe("'DescribeTable'", function() { 13 | it("should ensure the table is not being deleted", function describe(done) { 14 | var db = dynamo.createClient(options) 15 | db.request("DescribeTable", {TableName: name}, function(err, data) { 16 | if (err && err.name == "ResourceNotFoundException") done() 17 | 18 | else if (err) done(err) 19 | 20 | else if (data.Table.TableStatus == "DELETING") setTimeout(describe, 5000, done) 21 | 22 | else done() 23 | }) 24 | }) 25 | }) 26 | 27 | describe("'CreateTable'", function() { 28 | it("should create a table", function(done) { 29 | var db = dynamo.createClient(options) 30 | db.request("CreateTable", { 31 | TableName: name, 32 | KeySchema: { 33 | HashKeyElement: { 34 | AttributeName: "id", 35 | AttributeType: "N" 36 | } 37 | }, 38 | ProvisionedThroughput: { 39 | ReadCapacityUnits: 3, 40 | WriteCapacityUnits: 5 41 | } 42 | }, function(err, data) { 43 | if (err && err.name == "ResourceInUseException") err = null 44 | 45 | done(err) 46 | }) 47 | }) 48 | }) 49 | 50 | describe("'DescribeTable'", function() { 51 | it("should return table information", function describe(done) { 52 | var db = dynamo.createClient(options) 53 | db.request("DescribeTable", {TableName: name}, function(err, data) { 54 | if (err) done(err) 55 | 56 | else if (data.Table.TableStatus.slice(-3) != "ING") done() 57 | 58 | else setTimeout(describe, 5000, done) 59 | }) 60 | }) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /test/slow.js: -------------------------------------------------------------------------------- 1 | // secure env vars not available in pull requests 2 | if (process.env.TRAVIS_SECURE_ENV_VARS == 'false') return 3 | 4 | var should = require("should") 5 | , dynamo = require("../") 6 | , options = {region: "us-east-1", version: "20111205"} 7 | , name = "jed_dynamo-client_test" 8 | 9 | describe("dynamo", function() { 10 | describe("'PutItem' x 50", function() { 11 | it("should not throw ProvisionedThroughputExceededException", function(done) { 12 | var db = dynamo.createClient(options) 13 | for (var i = 0, n = 50, e = null; i < n; i++) { 14 | db.request("PutItem", { 15 | TableName: name, 16 | Item: {id: {N: i.toString()}} 17 | }, function(err, data) { 18 | if (e) return 19 | 20 | if (err) return done(e = err) 21 | 22 | --n || done() 23 | }) 24 | } 25 | }) 26 | }) 27 | 28 | }) 29 | 30 | -------------------------------------------------------------------------------- /test/teardown.js: -------------------------------------------------------------------------------- 1 | // secure env vars not available in pull requests 2 | if (process.env.TRAVIS_SECURE_ENV_VARS == 'false') return 3 | 4 | var should = require("should") 5 | , dynamo = require("../") 6 | , options = {region: "us-east-1", version: "20111205"} 7 | , name = "jed_dynamo-client_test" 8 | 9 | describe("dynamo", function() { 10 | describe("'DeleteTable'", function() { 11 | it("should delete the table", function(done) { 12 | var db = dynamo.createClient(options) 13 | db.request("DeleteTable", {TableName: name}, done) 14 | }) 15 | }) 16 | }) 17 | --------------------------------------------------------------------------------