├── dynamo
├── .gitignore
├── test
├── mocha.opts
├── teardown.js
├── tests
│ ├── Item.js
│ ├── Database.js
│ └── Table.js
└── setup.js
├── .travis.yml
├── lib
├── Attributes.js
├── index.js
├── Credentials.js
├── Key.js
├── Value.js
├── ProvisionedThroughput.js
├── KeySchema.js
├── Account.js
├── Scan.js
├── Item.js
├── Request.js
├── Predicates.js
├── Query.js
├── Batch.js
├── Update.js
├── Database.js
├── Session.js
└── Table.js
├── package.json
├── LICENSE.txt
└── README.md
/dynamo:
--------------------------------------------------------------------------------
1 | lib
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dynamo.wiki
3 | *.log
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --bail
2 | --timeout 100s
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.8
4 | env:
5 | global:
6 | - secure: "eYjFDPhy1lkcVy9/0oVysjpcJ6owui7m1Gyaq/6luBA3XhRDCU0HQkX5irw8\nZNk2x3W0mKDUH8AynfMCmGZFaefFOTEv+AJYL5W3DGt9OzZtJgMrYS5o5pjj\nKVeBQMM3vm+z798yz1dOxbwP193ApQicuhFGi/r2969H368xQrs="
7 | - secure: "rGo+XgYKV9usM3iFGsH6wsImXlRIrbICEj2ykEEMZkaw9PH/lxs86kw5h6ei\nCSKwDtY4zXDyLu+4VJmwEzCnYVLVxEfRBZPWdjaJwqDgPXfICshfT0hM8BrC\nw6oGP2IZsWWNaub83LJ25tUUigUrV98oatO3VlxDSTBGSlp66Rw="
8 |
--------------------------------------------------------------------------------
/lib/Attributes.js:
--------------------------------------------------------------------------------
1 | var Value = require("./Value")
2 |
3 | function Attributes(attrs) {
4 | var obj = {}
5 |
6 | Object.keys(attrs).forEach(function(key) {
7 | obj[key] = Value(attrs[key])
8 | })
9 |
10 | return obj
11 | }
12 |
13 | Attributes.prototype = {
14 | parse: function(data) {
15 | var obj = {}
16 |
17 | Object.keys(data).forEach(function(key) {
18 | obj[key] = Value.prototype.parse(data[key])
19 | })
20 |
21 | return obj
22 | }
23 | }
24 |
25 | module.exports = Attributes
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | var Account = exports.Account = require("./Account")
2 |
3 | exports.Credentials = require("./Credentials")
4 | exports.Database = require("./Database")
5 | exports.Request = require("./Request")
6 | exports.Session = require("./Session")
7 |
8 | exports.createClient = function(credentials) {
9 | if (!credentials) {
10 | credentials = {
11 | accessKeyId: process.env.AWS_ACCESS_KEY_ID,
12 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
13 | }
14 | }
15 |
16 | return new Account(credentials)
17 | }
18 |
--------------------------------------------------------------------------------
/lib/Credentials.js:
--------------------------------------------------------------------------------
1 | var crypto = require("crypto")
2 |
3 | function Credentials(attrs) {
4 | var secretAccessKey = attrs.secretAccessKey
5 |
6 | this.accessKeyId = attrs.accessKeyId
7 |
8 | if (!secretAccessKey) {
9 | throw new Error("No secret access key provided.")
10 | }
11 |
12 | if (!this.accessKeyId) {
13 | throw new Error("No access key id provided.")
14 | }
15 |
16 | this.sign = function(data) {
17 | return crypto
18 | .createHmac("sha256", secretAccessKey)
19 | .update(data)
20 | .digest("base64")
21 | }
22 | }
23 |
24 | module.exports = Credentials
--------------------------------------------------------------------------------
/lib/Key.js:
--------------------------------------------------------------------------------
1 | var Value = require("./Value")
2 |
3 | function Key(attrs) {
4 | if (!(this instanceof Key)) return new Key(attrs)
5 |
6 | function setSchema(value, pos) {
7 | if (pos > 1) throw new Error("More than two key elements specified.")
8 |
9 | this[["HashKeyElement", "RangeKeyElement"][pos]] = value
10 | }
11 |
12 | if (Array.isArray(attrs)) {
13 | attrs.forEach(function(element, pos) {
14 | setSchema.call(this, Value(element[1]), pos)
15 | }, this)
16 | } else {
17 | Object.keys(attrs).forEach(function(name, pos) {
18 | setSchema.call(this, Value(attrs[name]), pos)
19 | }, this)
20 | }
21 | }
22 |
23 | module.exports = Key
24 |
--------------------------------------------------------------------------------
/test/teardown.js:
--------------------------------------------------------------------------------
1 | var should = require("should")
2 | , dynamo = require("../")
3 | , client = dynamo.createClient()
4 | , db = client.get("us-east-1")
5 |
6 | describe("teardown -", function() {
7 | it("delete existing test tables", function(done) {
8 | db.remove("DYNAMO_TEST_TABLE_1", function() {
9 | db.remove("DYNAMO_TEST_TABLE_2", function() {
10 | db.remove("DYNAMO_TEST_TABLE_3", function() {
11 | done()
12 | })
13 | })
14 | })
15 | })
16 |
17 | it("make sure no test tables exist", function(done) {
18 | db.get("DYNAMO_TEST_TABLE_1").watch(function() {
19 | db.get("DYNAMO_TEST_TABLE_2").watch(function() {
20 | db.get("DYNAMO_TEST_TABLE_3").watch(function() {
21 | done()
22 | })
23 | })
24 | })
25 | })
26 | })
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Jed Schmidt
(http://jed.is)",
3 | "name": "dynamo",
4 | "description": "DynamoDB client for node.js",
5 | "version": "0.2.14",
6 | "homepage": "https://github.com/jed/dynamo",
7 | "repository": {
8 | "type": "git",
9 | "url": "git://github.com/jed/dynamo.git"
10 | },
11 | "keywords": [
12 | "amazon",
13 | "aws",
14 | "DynamoDB",
15 | "dynamo",
16 | "nosql",
17 | "database"
18 | ],
19 | "main": "./lib",
20 | "scripts": {
21 | "pretest": "mocha ./test/setup.js",
22 | "test": "mocha ./test/tests/*",
23 | "posttest": "mocha ./test/teardown.js"
24 | },
25 | "engines": {
26 | "node": ">=0.6.0"
27 | },
28 | "devDependencies": {
29 | "mocha": "*",
30 | "should": "*"
31 | },
32 | "dependencies": {},
33 | "optionalDependencies": {}
34 | }
35 |
--------------------------------------------------------------------------------
/lib/Value.js:
--------------------------------------------------------------------------------
1 | function Value(value) {
2 | switch (typeof value) {
3 | case "number": return {N: String(value)}
4 | case "string": return {S: value}
5 | }
6 |
7 | if (value) switch (typeof value[0]) {
8 | case "number": return {NN: value.map(String)}
9 | case "string": return {SS: value}
10 | }
11 |
12 | throw new Error("Invalid key value type.")
13 | }
14 |
15 | Value.prototype = {
16 | parse: function(data) {
17 | var name = Object.keys(data)[0]
18 | , value = data[name]
19 |
20 | switch (name) {
21 | case "S":
22 | case "SS":
23 | return value
24 |
25 | case "N":
26 | return Number(value)
27 |
28 | case "NS":
29 | return value.map(Number)
30 |
31 | default:
32 | throw new Error("Invalid data type: " + name)
33 | }
34 | }
35 | }
36 |
37 | module.exports = Value
38 |
--------------------------------------------------------------------------------
/test/tests/Item.js:
--------------------------------------------------------------------------------
1 | var should = require("should")
2 | , dynamo = require("../../")
3 | , client = dynamo.createClient()
4 | , db = client.get("us-east-1")
5 |
6 | describe("Item", function() {
7 | describe("#fetch", function() {
8 | it("should return the item's attributes", function(done) {
9 | var item = db.get("DYNAMO_TEST_TABLE_1").get({id: "0"})
10 |
11 | item.fetch(function(err, data) {
12 | should.not.exist(err)
13 | should.exist(data)
14 |
15 | data.should.have.property("id")
16 | data.should.have.property("favoriteColors")
17 | data.should.have.property("name")
18 |
19 | done()
20 | })
21 | })
22 | })
23 |
24 | describe("#destroy", function() {
25 | it("should delete the item", function(done) {
26 | var item = db.get("DYNAMO_TEST_TABLE_1").get({id: "0"})
27 |
28 | item.destroy(function(err, data) {
29 | should.not.exist(err)
30 |
31 | done()
32 | })
33 | })
34 | })
35 |
36 | })
37 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/lib/ProvisionedThroughput.js:
--------------------------------------------------------------------------------
1 | function ProvisionedThroughput(attrs, table) {
2 | Object.defineProperty(
3 | this, "table",
4 | {value: table, enumerable: false}
5 | )
6 |
7 | this.ReadCapacityUnits = attrs.read
8 | this.WriteCapacityUnits = attrs.write
9 | }
10 |
11 | ProvisionedThroughput.prototype = {
12 | update: function(attrs, cb) {
13 | var self = this
14 |
15 | this.ReadCapacityUnits = attrs.read
16 | this.WriteCapacityUnits = attrs.write
17 |
18 | return this.table.database.request(
19 | "UpdateTable",
20 | {
21 | TableName: this.table.TableName,
22 | ProvisionedThroughput: this.toJSON()
23 | },
24 | function(err, data) {
25 | if (err) return cb(err)
26 |
27 | cb(null, self.parse(data.TableDescription.ProvisionedThroughput))
28 | }
29 | )
30 | },
31 |
32 | parse: function(data) {
33 | if (data.LastIncreaseDateTime) {
34 | this.LastIncreaseDateTime = new Date(data.LastIncreaseDateTime * 1000)
35 | }
36 |
37 | if (data.LastDecreaseDateTime) {
38 | this.LastDecreaseDateTime = new Date(data.LastDecreaseDateTime * 1000)
39 | }
40 |
41 | this.ReadCapacityUnits = data.ReadCapacityUnits
42 | this.WriteCapacityUnits = data.WriteCapacityUnits
43 |
44 | return this
45 | }
46 | }
47 |
48 | module.exports = ProvisionedThroughput
49 |
--------------------------------------------------------------------------------
/lib/KeySchema.js:
--------------------------------------------------------------------------------
1 | var types = {S: String, N: Number}
2 | , keys = ["HashKeyElement", "RangeKeyElement"]
3 |
4 | function KeySchema(attrs) {
5 | function setSchemaKey(pos, keyName, keyType) {
6 | if (pos > 1) throw new Error("More than two keys specified.")
7 | this[keys[pos]] = new SchemaKey(keyName, keyType)
8 | }
9 |
10 | if (attrs) {
11 | if (Array.isArray(attrs)) {
12 | attrs.forEach(function(element, pos) {
13 | setSchemaKey.call(this, pos, element[0], element[1])
14 | }, this)
15 | } else {
16 | Object.keys(attrs).forEach(function(name, pos) {
17 | setSchemaKey.call(this, pos, name, attrs[name])
18 | }, this)
19 | }
20 | }
21 | }
22 |
23 | KeySchema.prototype = {
24 | parse: function(data) {
25 | this.HashKeyElement = (new SchemaKey).parse(data.HashKeyElement)
26 |
27 | if (data.RangeKeyElement) {
28 | this.RangeKeyElement = (new SchemaKey).parse(data.RangeKeyElement)
29 | }
30 |
31 | return this
32 | }
33 | }
34 |
35 | function SchemaKey(name, type) {
36 | this.AttributeName = name
37 | this.AttributeType = type
38 | }
39 |
40 | SchemaKey.prototype = {
41 | toJSON: function() {
42 | return {
43 | AttributeName: this.AttributeName,
44 | AttributeType: this.AttributeType.name.charAt(0)
45 | }
46 | },
47 |
48 | parse: function(data) {
49 | this.AttributeName = data.AttributeName,
50 | this.AttributeType = types[data.AttributeType]
51 |
52 | return this
53 | }
54 | }
55 |
56 | module.exports = KeySchema
57 |
--------------------------------------------------------------------------------
/lib/Account.js:
--------------------------------------------------------------------------------
1 | var crypto = require("crypto")
2 | , Database = require("./Database")
3 | , Session = require("./Session")
4 |
5 | , regions = {
6 | "us-east-1": true,
7 | "us-west-1": true,
8 | "us-west-2": true,
9 | "ap-northeast-1": true,
10 | "ap-southeast-1": true,
11 | "eu-west-1": true
12 | }
13 |
14 | function Account(credentials) {
15 | this.session = new Session(credentials)
16 | }
17 |
18 | Account.prototype.get = function(host) {
19 | var database = new Database(this)
20 | , badHost = !(host in regions)
21 |
22 | if (badHost) {
23 | console.error(
24 | "WARN: Assuming 'us-east-1' for backward compatibility.\n" +
25 | "Please use client.get(region).get(table) instead, as this will soon be deprecated."
26 | )
27 |
28 | database.host = "dynamodb.us-east-1.amazonaws.com"
29 | return database.get(host)
30 | }
31 |
32 | database.host = "dynamodb." + host + ".amazonaws.com"
33 | return database
34 | }
35 |
36 | Account.prototype.sign = function sign(request, cb) {
37 | this.session.fetch(function(err, session) {
38 | if (err) return cb(err)
39 |
40 | var hash = crypto.createHash("sha256")
41 | , payload
42 |
43 | request.headers["x-amz-security-token"] = session.token
44 |
45 | payload = new Buffer(request.toString(), "utf8")
46 | hash = hash.update(payload).digest()
47 |
48 | request.headers["x-amzn-authorization"] = "AWS3 " + [
49 | "AWSAccessKeyId=" + session.tokenCredentials.accessKeyId,
50 | "Algorithm=HmacSHA256",
51 | "SignedHeaders=host;x-amz-date;x-amz-security-token;x-amz-target",
52 | "Signature=" + session.tokenCredentials.sign(hash)
53 | ]
54 |
55 | cb(null, request)
56 | })
57 | }
58 |
59 | module.exports = Account
60 |
--------------------------------------------------------------------------------
/lib/Scan.js:
--------------------------------------------------------------------------------
1 | var Predicates = require("./Predicates")
2 | , Attributes = require("./Attributes")
3 |
4 | function Scan(table, database) {
5 | this.TableName = table
6 |
7 | Object.defineProperty(
8 | this, "database",
9 | {value: database, enumerable: false}
10 | )
11 | }
12 |
13 | Scan.prototype = {
14 | filter: function(predicates) {
15 | this.ScanFilter = new Predicates(predicates)
16 |
17 | return this
18 | },
19 |
20 | get: function() {
21 | this.AttributesToGet = Array.prototype.concat.apply([], arguments)
22 |
23 | return this
24 | },
25 |
26 | count: function() {
27 | this.Count = true
28 |
29 | return this
30 | },
31 |
32 | limit: function(limit) {
33 | if(isNaN(limit) || ((limit|0) != limit) || (limit|0) < 1) {
34 | throw new Error("Limit should be an natural number");
35 | }
36 |
37 | this.Limit = limit;
38 | return this;
39 | },
40 |
41 | fetch: function(cb) {
42 | var self = this
43 | , response = []
44 |
45 | !function loop(ExclusiveStartKey) {
46 | self.database.request(
47 | "Scan",
48 | self,
49 | function fetch(err, data) {
50 | if (err) return cb(err)
51 |
52 | if (self.Count) return cb(null, data.Count)
53 |
54 | data.Items.forEach(function(item) {
55 | response.push(Attributes.prototype.parse(item))
56 | })
57 |
58 | if (data.LastEvaluatedKey) {
59 | if (self.Limit != null && self.Limit < response.length) {
60 | loop(data.LastEvaluatedKey)
61 | } else {
62 | cb(null, response)
63 | }
64 | } else {
65 | cb(null, response)
66 | }
67 | }
68 | )
69 | }(this.ExclusiveStartKey)
70 |
71 | }
72 | }
73 |
74 | module.exports = Scan
75 |
--------------------------------------------------------------------------------
/lib/Item.js:
--------------------------------------------------------------------------------
1 | var Key = require("./Key")
2 | , Attributes = require("./Attributes")
3 | , Update = require("./Update")
4 |
5 | function Item(key, table, database) {
6 | Object.defineProperty(
7 | this, "database",
8 | {value: database, enumerable: false}
9 | )
10 |
11 | if (key) this.Key = Key(key)
12 | if (table) this.TableName = table
13 | }
14 |
15 | Item.prototype = {
16 | get: function(attrs) {
17 | this.AttributesToGet = attrs
18 |
19 | return this
20 | },
21 |
22 | update: function(key, value) {
23 | var update = new Update(this.TableName, this.database)
24 | update.Key = this.Key
25 |
26 | if (!key) return update
27 |
28 | if (typeof key == "function") {
29 | key.call(update, update)
30 | return update
31 | }
32 |
33 | return update.put(key, value)
34 | },
35 |
36 | fetch: function(opts, cb) {
37 | if (typeof opts == "function") cb = opts, opts = {}
38 |
39 | var self = this
40 | , attrs = {}
41 | , data = {
42 | TableName: this.TableName,
43 | Key: this.Key
44 | }
45 |
46 | if (this.AttributesToGet) data.AttributesToGet = this.AttributesToGet
47 |
48 | if (opts.consistent) data.ConsistentRead = true
49 |
50 | this.database.request(
51 | "GetItem",
52 | data,
53 | function(err, data) {
54 | if (err || !data.Item) return cb(err)
55 |
56 | cb(null, Attributes.prototype.parse(data.Item))
57 | }
58 | )
59 |
60 | return this
61 | },
62 |
63 | destroy: function(cb) {
64 | this.database.request(
65 | "DeleteItem",
66 | {TableName: this.TableName, Key: this.Key},
67 | function(err, data) {
68 | if (err || !data.Item) return cb(err)
69 |
70 | cb(null, Attributes.prototype.parse(data.Item))
71 | }
72 | )
73 | }
74 | }
75 |
76 | module.exports = Item
77 |
--------------------------------------------------------------------------------
/test/tests/Database.js:
--------------------------------------------------------------------------------
1 | var should = require("should")
2 | , dynamo = require("../../")
3 | , client = dynamo.createClient()
4 | , db = client.get("us-east-1")
5 |
6 | describe("Database", function() {
7 | describe("#get()", function() {
8 | it("should return the appropriate object", function() {
9 | var table = db.get("table")
10 | , batch = db.get(function(){})
11 | , item = db.get("table", {id: 123})
12 |
13 | table.should.have.property("TableName", "table")
14 | table.should.have.property("database", db)
15 |
16 | batch.should.have.property("RequestItems")
17 | batch.should.have.property("database", db)
18 |
19 | item.should.have.property("TableName", "table")
20 | item.should.have.property("Key")
21 | item.should.have.property("database", db)
22 | })
23 | })
24 |
25 | describe("#add()", function() {
26 | it("should return a table", function() {
27 | var table = db.add({name: "table", schema: {id: 123}})
28 |
29 | table.should.have.property("TableName", "table")
30 | table.should.have.property("KeySchema")
31 | })
32 | })
33 |
34 | describe("#put()", function() {
35 | it("should return an update", function() {
36 | var update = db.put("table", {id: 123})
37 |
38 | update.should.have.property("TableName", "table")
39 | update.should.have.property("Item")
40 | update.Item.should.have.property("id")
41 | })
42 | })
43 |
44 | describe("#fetch()", function() {
45 | it("should return a hash of tables", function(done) {
46 | db.fetch(function(err, database) {
47 | should.not.exist(err)
48 | should.exist(database)
49 | database.should.equal(db)
50 |
51 | database.should.have.property("tables")
52 |
53 | database.tables.should.have.property("DYNAMO_TEST_TABLE_1")
54 | database.tables.should.have.property("DYNAMO_TEST_TABLE_2")
55 |
56 | done()
57 | })
58 | })
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/lib/Request.js:
--------------------------------------------------------------------------------
1 | var http = require("http")
2 | , crypto = require("crypto")
3 |
4 | function Request(host, target, data) {
5 | var headers = this.headers = new Headers
6 |
7 | this.json = JSON.stringify(data)
8 |
9 | headers["x-amz-target"] = Request.prototype.target + target
10 | headers["Host"] = this.host = host
11 | headers["Content-Length"] = Buffer.byteLength(this.json)
12 | }
13 |
14 | Request.prototype = {
15 | method: "POST",
16 | pathname: "/",
17 | target: "DynamoDB_20111205.",
18 | data: {},
19 |
20 | toString: function() {
21 | return this.method +
22 | "\n" + this.pathname +
23 | "\n" +
24 | "\n" + this.headers +
25 | "\n" +
26 | "\n" + this.json
27 | },
28 |
29 | send: function(cb) {
30 | var request = http.request(this, function(res) {
31 | var json = ""
32 |
33 | res.setEncoding('utf8')
34 |
35 | res.on("data", function(chunk){ json += chunk })
36 | res.on("end", function() {
37 | var error, response = JSON.parse(json)
38 |
39 | if (res.statusCode == 200) return cb(null, response)
40 |
41 | error = new Error
42 | error.name = response.__type
43 | error.message = response.message
44 | error.statusCode = res.statusCode
45 |
46 | cb(error)
47 | })
48 | })
49 |
50 | request.on("error", cb)
51 |
52 | request.write(this.json)
53 | request.end()
54 | }
55 | }
56 |
57 | function Headers() {
58 | this["x-amz-date"] = this["Date"] = (new Date).toUTCString()
59 | this["Content-Type"] = Headers.prototype["Content-Type"]
60 | }
61 |
62 | Headers.prototype = {
63 | "Content-Type": "application/x-amz-json-1.0",
64 |
65 | toString: function() {
66 | return "host:" + this["Host"] +
67 | "\nx-amz-date:" + this["x-amz-date"] +
68 | "\nx-amz-security-token:" + this["x-amz-security-token"] +
69 | "\nx-amz-target:" + this["x-amz-target"]
70 | }
71 | }
72 |
73 | Request.Headers = Headers
74 | module.exports = Request
75 |
--------------------------------------------------------------------------------
/test/tests/Table.js:
--------------------------------------------------------------------------------
1 | var should = require("should")
2 | , dynamo = require("../../")
3 | , client = dynamo.createClient()
4 | , db = client.get("us-east-1")
5 |
6 | describe("Table", function() {
7 | describe("#get", function() {
8 | it("should return an item", function() {
9 | var item = db.get("DYNAMO_TEST_TABLE_1").get({id: "0"})
10 |
11 | item.should.have.property("Key")
12 | })
13 | })
14 |
15 | describe("#fetch()", function() {
16 | it("should return a table", function(done) {
17 | db.get("DYNAMO_TEST_TABLE_1").fetch(function(err, table) {
18 | should.not.exist(err)
19 | should.exist(table)
20 |
21 | table.should.have.property("TableName", "DYNAMO_TEST_TABLE_1")
22 | done()
23 | })
24 | })
25 | })
26 |
27 | describe("#scan", function() {
28 | it("should return matching items", function(done) {
29 | db.get("DYNAMO_TEST_TABLE_1")
30 | .scan({
31 | name: {"in": ["Paul", "George"]},
32 | id: {">": "2"}
33 | })
34 | .fetch(function(err, items) {
35 | should.not.exist(err)
36 | should.exist(items)
37 | items.should.have.property("0")
38 | items[0].should.have.property("id", "3")
39 | items[0].should.have.property("name", "George")
40 |
41 | done()
42 | })
43 | })
44 | })
45 |
46 | describe("#query", function() {
47 | it("should return matching items", function(done) {
48 | db.get("DYNAMO_TEST_TABLE_2")
49 | .query({
50 | id: "2",
51 | date: {">": new Date - 60000}
52 | })
53 | .fetch(function(err, items) {
54 | should.not.exist(err)
55 | should.exist(items)
56 |
57 | items
58 | .filter(function(item){ return item.id === "2" })
59 | .should.have.length(items.length)
60 |
61 | items
62 | .filter(function(item){ return item.date < new Date - 60000 })
63 | .should.have.length(items.length)
64 |
65 | done()
66 | })
67 | })
68 | })
69 |
70 | })
71 |
--------------------------------------------------------------------------------
/lib/Predicates.js:
--------------------------------------------------------------------------------
1 | var Value = require("./Value")
2 | , isArray = Array.isArray
3 |
4 | function Predicates(name, operator, value) {
5 | switch (typeof name) {
6 | case "object":
7 | Object.keys(name).forEach(function(key) {
8 | Predicates.call(this, key, name[key])
9 | }, this)
10 |
11 | return
12 | }
13 |
14 | if (Object.prototype.hasOwnProperty.call(this, name)) {
15 | throw new Error("Property '" + name + "'' can only be defined once.")
16 | }
17 |
18 | switch (typeof operator) {
19 | case "object":
20 | Object.keys(operator).forEach(function(key) {
21 | Predicates.call(this, name, key, operator[key])
22 | }, this)
23 |
24 | return
25 |
26 | case "undefined":
27 | operator = "!="
28 | value = null
29 | break
30 | }
31 |
32 | switch (typeof value) {
33 | case "undefined":
34 | value = operator
35 | operator = "=="
36 | break
37 | }
38 |
39 | if (!isArray(value)) value = [value]
40 |
41 | switch (operator) {
42 | case "!=":
43 | if (value[0] === null) operator = "NOT_NULL", value = []
44 | else operator = "NE"
45 | break
46 |
47 | case "==":
48 | if (value[0] === null) operator = "NULL", value = []
49 | else operator = "EQ"
50 | break
51 |
52 | case ">":
53 | operator = "GT"
54 | break
55 |
56 | case "<":
57 | operator = "LT"
58 | break
59 |
60 | case ">=":
61 | operator = value.length > 1 ? "BETWEEN" : "GE"
62 | break
63 |
64 | case "<=":
65 | operator = value.length > 1 ? "BETWEEN" : "LE"
66 | break
67 |
68 | case "contains":
69 | operator = "CONTAINS"
70 | break
71 |
72 | case "!contains":
73 | operator = "NOT_CONTAINS"
74 | break
75 |
76 | case "startsWith":
77 | operator = "BEGINS_WITH"
78 | break
79 |
80 | case "in":
81 | operator = "IN"
82 | break
83 | }
84 |
85 | this[name] = {
86 | ComparisonOperator: operator,
87 | AttributeValueList: value.map(Value)
88 | }
89 | }
90 |
91 | module.exports = Predicates
92 |
--------------------------------------------------------------------------------
/lib/Query.js:
--------------------------------------------------------------------------------
1 | var Predicates = require("./Predicates")
2 | , Attributes = require("./Attributes")
3 |
4 | function Query(table, database) {
5 | this.TableName = table
6 |
7 | Object.defineProperty(
8 | this, "database",
9 | {value: database, enumerable: false}
10 | )
11 | }
12 |
13 | Query.prototype = {
14 | get: function() {
15 | this.AttributesToGet = Array.prototype.concat.apply([], arguments)
16 |
17 | return this
18 | },
19 |
20 | reverse: function() {
21 | this.ScanIndexForward = false
22 |
23 | return this
24 | },
25 |
26 | filter: function(predicates) {
27 | var predicates = new Predicates(predicates)
28 | , keys = Object.keys(predicates)
29 | , hashKey = keys[0]
30 | , rangeKey = keys[1]
31 |
32 | if (predicates[hashKey].ComparisonOperator != "EQ") {
33 | throw new Error("Query hash key comparison must be '=='.")
34 | }
35 |
36 | this.HashKeyValue = predicates[hashKey].AttributeValueList[0]
37 | if (rangeKey) {
38 | this.RangeKeyCondition = {
39 | ComparisonOperator: predicates[rangeKey].ComparisonOperator,
40 | AttributeValueList: predicates[rangeKey].AttributeValueList
41 | }
42 | }
43 |
44 | return this
45 | },
46 |
47 | count: function() {
48 | this.Count = true
49 |
50 | return this
51 | },
52 |
53 | limit: function(limit) {
54 | if(isNaN(limit) || ((limit|0) != limit) || (limit|0) < 1) {
55 | throw new Error("Limit should be an natural number");
56 | }
57 |
58 | this.Limit = limit;
59 | return this;
60 | },
61 |
62 | fetch: function(opts, cb) {
63 | var self = this
64 | , response = []
65 |
66 | if (typeof opts == "function") cb = opts, opts = {}
67 | if (opts.consistent) this.ConsistentRead = true
68 |
69 | !function loop(ExclusiveStartKey) {
70 | self.database.request(
71 | "Query",
72 | self,
73 | function fetch(err, data) {
74 | if (err) return cb(err)
75 |
76 | if (self.Count) return cb(null, data.Count)
77 |
78 | data.Items.forEach(function(item) {
79 | response.push(Attributes.prototype.parse(item))
80 | })
81 |
82 | if (data.LastEvaluatedKey) {
83 | if (self.Limit != null && self.Limit < response.length) {
84 | loop(data.LastEvaluatedKey)
85 | } else {
86 | cb(null, response)
87 | }
88 | } else {
89 | cb(null, response)
90 | }
91 | }
92 | )
93 | }(this.ExclusiveStartKey)
94 | }
95 | }
96 |
97 | module.exports = Query
98 |
--------------------------------------------------------------------------------
/lib/Batch.js:
--------------------------------------------------------------------------------
1 | var Table = require("./Table")
2 | , Key = require("./Key")
3 | , Attributes = require("./Attributes")
4 | , push = Array.prototype.push
5 | , isArray = Array.isArray
6 |
7 | function Batch(database) {
8 | Object.defineProperty(
9 | this, "database",
10 | {value: database, enumerable: false}
11 | )
12 |
13 | this.RequestItems = {}
14 | }
15 |
16 | Batch.prototype = {
17 | get: function(name, keys, attrs) {
18 | var items = this.RequestItems
19 | , tableBatch
20 |
21 | switch (typeof name) {
22 | case "function":
23 | name.call(this, this)
24 | return this
25 |
26 | case "object":
27 | for (var item in name) this.get(item, name[item])
28 | return this
29 |
30 | case "string":
31 | tableBatch = items[name] || (items[name] = new TableBatch)
32 |
33 | if (!keys) return tableBatch
34 |
35 | tableBatch.get(keys, attrs)
36 | return this
37 | }
38 | },
39 |
40 | fetch: function(cb) {
41 | var self = this
42 | , response = {}
43 |
44 | !function loop(RequestItems) {
45 | self.database.request(
46 | "BatchGetItem",
47 | {RequestItems: RequestItems},
48 | function fetch(err, data) {
49 | if (err) return cb(err)
50 |
51 | Object.keys(data.Responses).forEach(function(name) {
52 | var table = response[name] || (response[name] = [])
53 |
54 | data.Responses[name].Items.forEach(function(attrs) {
55 | table.push(Attributes.prototype.parse(attrs))
56 | })
57 | })
58 |
59 | if (data.UnprocessedKeys) loop(data.UnprocessedKeys)
60 |
61 | else cb(err, response)
62 | }
63 | )
64 | }(this.RequestItems)
65 | }
66 | }
67 |
68 | function TableBatch() {
69 | this.Keys = []
70 | this.AttributesToGet = []
71 | }
72 |
73 | TableBatch.prototype = {
74 | toJSON: function() {
75 | var obj = {Keys: this.Keys}
76 |
77 | if (this.AttributesToGet.length) obj.AttributesToGet = this.AttributesToGet
78 |
79 | return obj
80 | },
81 |
82 | get: function(keys, attrs){
83 | if (typeof keys == "function") {
84 | keys.call(this, this)
85 | return this
86 | }
87 |
88 | if (!isArray(keys)) keys = [keys]
89 |
90 | push.apply(this.Keys, keys.map(Key))
91 |
92 | if (attrs) attrs.forEach(function(name) {
93 | if (this.AttributesToGet.indexOf(name) < 0) {
94 | this.AttributesToGet.push(name)
95 | }
96 | }, this)
97 |
98 | return this
99 | }
100 | }
101 |
102 | module.exports = Batch
103 |
--------------------------------------------------------------------------------
/lib/Update.js:
--------------------------------------------------------------------------------
1 | var Predicates = require("./Predicates")
2 | , Attributes = require("./Attributes")
3 | , Value = require("./Value")
4 |
5 | function Update(table, database) {
6 | this.TableName = table
7 |
8 | Object.defineProperty(
9 | this, "database",
10 | {value: database, enumerable: false}
11 | )
12 | }
13 |
14 | Update.prototype = {
15 | when: function(name, operator, value) {
16 | var predicates = new Predicates(name, operator, value)
17 | , expectation = this.Expected = {}
18 | , names = Object.keys(predicates)
19 |
20 | if (names.length > 1) {
21 | throw new Error("Only one condition allowed per expectation.")
22 | }
23 |
24 | name = names[0]
25 | operator = predicates[name].ComparisonOperator
26 | value = predicates[name].AttributeValueList[0]
27 |
28 | switch (operator) {
29 | case "NULL":
30 | expectation[name] = {Exists: false}
31 | return this
32 |
33 | case "NOT_NULL":
34 | expectation[name] = {Exists: true}
35 | return this
36 |
37 | case "EQ":
38 | expectation[name] = {Value: value}
39 | return this
40 | }
41 |
42 | throw new Error("Invalid expectation: " + [name, operator, value])
43 | },
44 |
45 | returning: function(constant) {
46 | this.ReturnValues = constant
47 |
48 | return this
49 | },
50 |
51 | update: function(action, key, value) {
52 | var updates = this.AttributeUpdates || (this.AttributeUpdates = {})
53 |
54 | if (updates[key]) {
55 | throw new Error("Attribute '" + key + "' cannot be updated more than once.")
56 | }
57 |
58 | updates[key] = {Action: action}
59 |
60 | if (value != null) updates[key].Value = Value(value)
61 |
62 | return this
63 | },
64 |
65 | put: function(key, value) {
66 | return this.update("PUT", key, value)
67 | },
68 |
69 | add: function(key, value) {
70 | return this.update("ADD", key, value)
71 | },
72 |
73 | remove: function(key, value) {
74 | return this.update("DELETE", key, value)
75 | },
76 |
77 | save: function(cb) {
78 | if (this.Item) {
79 | this.database.request(
80 | "PutItem",
81 | this,
82 | function(err, data) {
83 | if (err) return cb(err)
84 |
85 | if (data = data.Attributes) {
86 | cb(null, Attributes.prototype.parse(data))
87 | }
88 |
89 | else cb()
90 | }
91 | )
92 |
93 | return this
94 | }
95 |
96 | if (this.AttributeUpdates) {
97 | this.database.request(
98 | "UpdateItem",
99 | this,
100 | cb
101 | )
102 |
103 | return this
104 | }
105 |
106 | throw new Error("Nothing to save.")
107 | }
108 | }
109 |
110 | module.exports = Update
111 |
--------------------------------------------------------------------------------
/lib/Database.js:
--------------------------------------------------------------------------------
1 | var Batch = require("./Batch")
2 | , Table = require("./Table")
3 | , Request = require("./Request")
4 |
5 | function Database(account) {
6 | Object.defineProperty(
7 | this, "account",
8 | {value: account, enumerable: false}
9 | )
10 |
11 | this.tables = {}
12 | }
13 |
14 | Database.prototype = {
15 | get: function(table, key) {
16 | if (typeof table != "string") return new Batch(this).get(table)
17 |
18 | table = new Table(table, this)
19 |
20 | return key ? table.get(key) : table
21 | },
22 |
23 | put: function(table, item) {
24 | return this.get(table).put(item)
25 | },
26 |
27 | add: function(table) {
28 | return new Table(table, this)
29 | },
30 |
31 | remove: function(table, cb) {
32 | return this.get(table).destroy(cb)
33 | },
34 |
35 | fetch: function(cb) {
36 | var self = this
37 |
38 | !function loop(cursor) {
39 | self.request(
40 | "ListTables",
41 | {ExclusiveStartTableName: cursor},
42 | function(err, data) {
43 | if (err) return cb(err)
44 |
45 | data.TableNames.forEach(function(name) {
46 | self.tables[name] = new Table(name, self)
47 | })
48 |
49 | if (cursor = data.LastEvaluatedTableName) loop(cursor)
50 |
51 | else cb(null, self)
52 | }
53 | )
54 | }()
55 |
56 | return this
57 | },
58 |
59 | request: function request(target, data, cb) {
60 | var self = this
61 | , req = new Request(this.host, target, data)
62 |
63 | this.account.sign(req, function(err, req) {
64 | if (err) cb(err)
65 |
66 | else req.send(function(err, data) {
67 | if (!err || err.statusCode >= 500) cb(err, data)
68 |
69 | else cb(err, data, request.bind(self, target, data, cb))
70 | })
71 | })
72 |
73 | return this
74 | },
75 |
76 | listTables: function(data, cb) {
77 | return this.request("ListTables", data, cb)
78 | },
79 |
80 | createTable: function(data, cb) {
81 | return this.request("CreateTable", data, cb)
82 | },
83 |
84 | describeTable: function(data, cb) {
85 | return this.request("DescribeTable", data, cb)
86 | },
87 |
88 | updateTable: function(data, cb) {
89 | return this.request("UpdateTable", data, cb)
90 | },
91 |
92 | deleteTable: function(data, cb) {
93 | return this.request("DeleteTable", data, cb)
94 | },
95 |
96 | scan: function(data, cb) {
97 | return this.request("Scan", data, cb)
98 | },
99 |
100 | query: function(data, cb) {
101 | return this.request("Query", data, cb)
102 | },
103 |
104 | batchGetItem: function(data, cb) {
105 | return this.request("BatchGetItem", data, cb)
106 | },
107 |
108 | batchWriteItem: function(data, cb) {
109 | return this.request("BatchWriteItem", data, cb)
110 | },
111 |
112 | getItem: function(data, cb) {
113 | return this.request("GetItem", data, cb)
114 | },
115 |
116 | putItem: function(data, cb) {
117 | return this.request("PutItem", data, cb)
118 | },
119 |
120 | updateItem: function(data, cb) {
121 | return this.request("UpdateItem", data, cb)
122 | },
123 |
124 | deleteItem: function(data, cb) {
125 | return this.request("DeleteItem", data, cb)
126 | }
127 | }
128 |
129 | module.exports = Database
130 |
--------------------------------------------------------------------------------
/lib/Session.js:
--------------------------------------------------------------------------------
1 | var https = require("https")
2 | , crypto = require("crypto")
3 | , Credentials = require("./Credentials")
4 |
5 | function Session(attrs) {
6 | this.sessionCredentials = new Credentials(attrs)
7 | this.tokenCredentials = null
8 | this.listeners = []
9 | }
10 |
11 | Session.prototype = {
12 | duration: 60 * 60 * 1000,
13 | refreshPadding: 60 * 1000, //refresh 1 minute ahead of time
14 | consumedCapacity: 0,
15 |
16 | fetch: function(cb) {
17 | if ((this.expiration - this.refreshPadding) > new Date) return cb(null, this)
18 |
19 | this.listeners.push(cb) > 1 || this.refresh()
20 | },
21 |
22 | refresh: function() {
23 | var req = new Request
24 |
25 | req.query.DurationSeconds = 0 | this.duration / 1000
26 | req.query.AWSAccessKeyId = this.sessionCredentials.accessKeyId
27 | req.query.Signature = this.sessionCredentials.sign(req.toString(), "sha256", "base64")
28 |
29 | req.send(function(err, data) {
30 | var listeners = this.listeners.splice(0)
31 |
32 | if (!err) {
33 | this.expiration = new Date(data.expiration)
34 | this.tokenCredentials = new Credentials(data)
35 | this.token = data.sessionToken
36 | }
37 |
38 | listeners.forEach(function(cb) {
39 | cb(err, err ? null : this)
40 | }, this)
41 | }.bind(this))
42 | }
43 | }
44 |
45 | function Request() {
46 | this.query = new Query
47 | }
48 |
49 | Request.prototype = {
50 | method: "GET",
51 | host: "sts.amazonaws.com",
52 | pathname: "/",
53 |
54 | toString: function() {
55 | return this.method +
56 | "\n" + this.host +
57 | "\n" + this.pathname +
58 | "\n" + this.query.toString().slice(1)
59 | },
60 |
61 | send: function(cb) {
62 | var signature = encodeURIComponent(this.query.Signature)
63 | , query = this.query + "&Signature=" + signature
64 | , path = Request.prototype.pathname + query
65 | , options = { host: this.host, path: path }
66 |
67 | var req = https.get(options, function(res) {
68 | var xml = ""
69 |
70 | res.on("data", function(chunk){ xml += chunk })
71 | res.on("end", function() {
72 | var response = new Response(xml)
73 |
74 | if (res.statusCode == 200) cb(null, response)
75 |
76 | else cb(new Error(
77 | response.type + "(" + response.code + ")\n\n" +
78 | response.message
79 | ))
80 | })
81 | })
82 | req.on("error", cb);
83 | }
84 | }
85 |
86 | function Query() {
87 | this.Timestamp = (new Date).toISOString().slice(0, 19) + "Z"
88 | }
89 |
90 | Query.prototype = {
91 | Action : "GetSessionToken",
92 | SignatureMethod : "HmacSHA256",
93 | SignatureVersion : "2",
94 | Version : "2011-06-15",
95 |
96 | toString: function() {
97 | return (
98 | "?AWSAccessKeyId=" + this.AWSAccessKeyId +
99 | "&Action=" + this.Action +
100 | "&DurationSeconds=" + this.DurationSeconds +
101 | "&SignatureMethod=" + this.SignatureMethod +
102 | "&SignatureVersion=" + this.SignatureVersion +
103 | "&Timestamp=" + encodeURIComponent(this.Timestamp) +
104 | "&Version=" + this.Version
105 | )
106 | }
107 | }
108 |
109 | function Response(xml) {
110 | var tag, key, regexp = /<(\w+)>(.*)=": new Date - 6000 }})
27 | .get("id", "date", "name")
28 | .reverse()
29 | .fetch(function(err, data){ ... })
30 |
31 | // Same call, using low-level API
32 |
33 | db.query({
34 | TableName: "myTable",
35 | HashKeyValue: {S: "123"},
36 | RangeKeyValue: {
37 | ComparisonOperator: "LE",
38 | AttributeValueList: [{N: "1329912311806"}]
39 | },
40 | AttributesToGet: ["id", "date", "name"],
41 | ScanIndexForward: false
42 | }, function(err, data){ ... })
43 | ```
44 |
45 | Installation
46 | ------------
47 |
48 | This library has no dependencies, and can be installed from [npm][npm]:
49 |
50 | npm install dynamo
51 |
52 | API
53 | ---
54 |
55 | ### dynamo = require("dynamo")
56 |
57 | This module exposes the `createClient` method, which is the preferred way to interact with dynamo.
58 |
59 | ### client = dynamo.createClient([_credentials_])
60 |
61 | Returns a client instance attached to the account specified by the given credentials. The credentials can be specified as an object with `accessKeyId` and `secretAccessKey` members such as the following:
62 |
63 | ```javascript
64 | client = dynamo.createClient({
65 | accessKeyId: "...", // your access key id
66 | secretAccessKey: "..." // your secret access key
67 | })
68 | ```
69 |
70 | You can also omit these credentials by storing them in the environment under which the current process is running, as `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
71 |
72 | If neither of the above are provided, an error will be thrown.
73 |
74 | ### db = client.get(_regionName_)
75 |
76 | Returns a database in the selected region. Currently, DynamoDB supports the following regions:
77 |
78 | - `us-east-1`
79 | - `us-west-1`
80 | - `us-west-2`
81 | - `ap-northeast-1`
82 | - `ap-southeast-1`
83 | - `eu-west-1`
84 |
85 | Once you have a database instance, you can use either of the provided APIs:
86 |
87 | ### [High-level API][high-api] (blue pill)
88 |
89 | The primary purpose of this library is to abstract away the often bizzare API design decisions of DynamoDB, into a composable and intuitive interface based on Database, Table, Item, Batch, Query, and Scan objects.
90 |
91 | See [the wiki][high-api] for more information.
92 |
93 | ### [Low-level API][low-api] (red pill)
94 |
95 | All of the [original DynamoDB operations][api] are provided as methods on database instances. You won't need to use them unless you want to sacrifice a clean interdace for more control, and don't mind learning Amazon's JSON format.
96 |
97 | See [the wiki][low-api] for more information.
98 |
99 | Testing
100 | -------
101 |
102 | Testing for dynamo is handled using continuous integration against a real DynamoDB instance, under credentials limited to Travis CI.
103 |
104 | If you'd like to run the test stuie with your own credentials, make sure they're set using the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables, and then run the tests:
105 |
106 | npm test
107 |
108 | The test suite creates three tables called `DYNAMO_TEST_TABLE_1`, `DYNAMO_TEST_TABLE_2`, and 'DYNAMO_TEST_TABLE_3` before the tests are run, and then deletes them once the tests are done. Note that you will need to delete them manually in the event that the tests fail.
109 |
110 | To do
111 | -----
112 |
113 | - Factor out tests into integration tests and unit tests
114 | - Make all callbacks optional, returning an event emitter no callback given
115 | - Add method to specify Limit and ExclusiveStartKey
116 |
117 | Credits
118 | -------
119 |
120 | - [Travis CI][travis] for an awesome open-source testing service
121 | - [@chriso][chriso] for letting me have the "dynamo" name on npm
122 | - [@skomski][skomski] for turning me on to [IAM credentials][iam]
123 | - [@mranney][mranney] for inspiration from the venerable [node_redis][node_redis]
124 | - [@visionmedia][tj] for making testing easy with [mocha][mocha] and [should.js][should]
125 |
126 |
127 | Copyright
128 | ---------
129 |
130 | Copyright (c) 2012 Jed Schmidt. See LICENSE.txt for details.
131 |
132 | Send any questions or comments [here][twitter].
133 |
134 | [travis]: http://travis-ci.org/jed/dynamo
135 | [node]: http://nodejs.org
136 | [dynamo]: http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/Introduction.html
137 | [aws]: http://aws.amazon.com
138 | [api]: http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/operationlist.html
139 | [mranney]: https://github.com/mranney
140 | [skomski]: https://github.com/skomski
141 | [node_redis]: https://github.com/mranney/node_redis
142 | [twitter]: http://twitter.com/jedschmidt
143 | [heroku]: http://heroku.com
144 | [mocha]: https://visionmedia.github.com/mocha
145 | [should]: https://github.com/visionmedia/should.js
146 | [tj]: https://github.com/visionmedia
147 | [iam]: http://docs.amazonwebservices.com/IAM/latest/UserGuide/IAM_Introduction.html
148 | [connect]: http://www.senchalabs.org/connect
149 | [chriso]: https://github.com/chriso
150 | [low-api]: https://github.com/jed/dynamo/wiki/Low-level-API
151 | [high-api]: https://github.com/jed/dynamo/wiki/High-level-API
152 | [npm]: http://npmjs.org
153 |
--------------------------------------------------------------------------------