Showing preview only (271K chars total). Download the full file or copy to clipboard to get everything.
Repository: Medium/dynamite
Branch: master
Commit: a149c0dd1282
Files: 46
Total size: 257.8 KB
Directory structure:
gitextract_lce59bue/
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── dynamite.js
├── externs/
│ └── aws-sdk.js
├── lib/
│ ├── BatchGetItemBuilder.js
│ ├── Builder.js
│ ├── Client.js
│ ├── ConditionBuilder.js
│ ├── DeleteItemBuilder.js
│ ├── DescribeTableBuilder.js
│ ├── DynamoRequest.js
│ ├── DynamoResponse.js
│ ├── FakeDynamo.js
│ ├── GetItemBuilder.js
│ ├── PutItemBuilder.js
│ ├── QueryBuilder.js
│ ├── ScanBuilder.js
│ ├── UpdateBuilder.js
│ ├── UpdateExpressionBuilder.js
│ ├── common.js
│ ├── errors.js
│ ├── localUpdater.js
│ ├── reserved.js
│ └── typeUtil.js
├── package.json
└── test/
├── testBatchGetItem.js
├── testConditions.js
├── testDeleteItem.js
├── testDescribeTable.js
├── testFakeDynamo.js
├── testGetItem.js
├── testGetSet.js
├── testHashKeyOnly.js
├── testLocalUpdater.js
├── testPutItem.js
├── testPutSet.js
├── testQuery.js
├── testScan.js
├── testStringSet.js
├── testTypeUtil.js
├── testUpdateItem.js
└── utils/
└── testUtils.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
externs
================================================
FILE: .eslintrc
================================================
{
"rules": {
"no-console": 0,
"linebreak-style": [
2,
"unix"
],
"semi": [
2,
"never"
]
},
"env": {
"node": true
},
"extends": "eslint:recommended"
}
================================================
FILE: .gitignore
================================================
lib-cov
*.seed
*.log
*.csv
*.dat
*.out
*.pid
*.gz
pids
logs
results
node_modules
npm-debug.log
.DS_Store
================================================
FILE: .travis.yml
================================================
language: node_js
sudo: false
node_js:
- "6"
- "8"
- "10"
- "11"
================================================
FILE: LICENSE
================================================
Copyright 2013 The Obvious Corporation.
http://obvious.com/
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-------------------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
================================================
FILE: README.md
================================================
# Dynamite [](https://travis-ci.org/Medium/dynamite)
Dynamite is a promise-based DynamoDB client. It was created to address performance issues in our previous DynamoDB client. Dynamite will almost always comply with the latest DynamoDB spec on Amazon.
## Installation
$ npm install dynamite
## Running Tests
Ensure that all of the required node modules are installed in the Dynamite directory by first running:
$ npm install
The tests will be run against a `LocalDynamo` service. Currently, there is no way to change the port without modifying the connection code in `test/utils/TestUtils.js`. To run the tests:
$ npm test
## Creating a Client
```js
var Dynamite = require('dynamite')
var options = {
region: 'us-east-1',
accessKeyId: 'xxx',
secretAccessKey: 'xxx'
}
var client = new Dynamite.Client(options)
```
Options requires all of:
* region
* accessKeyId
* secretAccessKey
If a `region` key is not provided in the `options` hash but a `endpoint` key is present, Dynamite will try to infer the region from the `host` key.
Options can also optionally take a hash with a key `dbClient` which points to an object that implements the AWS SDK interface for node.js.
#### Optional Options Keys
* `sslEnabled`: a boolean to turn ssl on or off for the connection.
* `endPoint`: the address of the DynamoDB instance to try to communicate with.
* `retryHandler`: a `function(method, table, response)` that will be triggered if Dynamite needs to retry a command.
### Foreword: Kew and You
All functions return [Kew](https://github.com/Medium/kew) promises on `execute()`. These functions will all then take the form:
```js
client.fn(params)
.execute()
.then(function(){
// handle success
})
.fail(function(e) {
// handle failure
})
.fin(function() {
// when all is said and done
})
```
Therefore, these docs will focus more on function signatures and assume that the developer using those functions will comply with the Kew API in turn.
## Tables
### Creating a Table
Table creation is part of the database's concerns and thus doesn't have its own pretty API built into Dynamite. A snippet successfully creating a table that is compliant with the 2012 DynamoDB spec can be found in `test/utils/TestUtils.js`.
### Describing a Table
Tables can have descriptions. Retrieve them with:
```js
client.describeTable('table-name')
```
## Conditions
Conditions ensure that certain properties of the item are either absent or equal to a certain value before allowing whatever operation to which they were supplied to mutate the item. They become very useful when items should only be updated if they are missing a field or are of the wrong value. There currently exist two kinds of conditions: `expectAttributeEquals` and `expectAttributeAbsent`. Every operation has particular behaviors when conditions are or are not met.
Adding conditions to an operation is fairly trivial:
```js
var conditions = client.newConditionBuilder()
.expectAttributeEquals('age', 29)
client.fn('some-table')
.withCondition(conditions)
.execute()
.then(function () {
// handle the operation output
})
```
There is also a helper method for building conditions from a JSON object.
```js
var conditions = client.conditions({age: 29})
client.fn('some-table')
.withCondition(conditions)
.execute()
```
If a condition fails, the promise will be rejected with a conditional error,
which you can detect with the `isConditionalError` method
```js
client.fn('some-table')
.withCondition(client.conditions({age: 29})
.execute()
.fail(function (e) {
if (!client.isConditionalError(e)) {
throw new Error('Unexpected age; conditional check failed')
} else {
throw e
}
})
```
Catching all conditional errors is a common idiom, so there is a
`throwUnlessConditionalError` helper method for this case.
```js
client.fn('some-table')
.withCondition(client.conditions({age: 29})
.execute()
.fail(client.throwUnlessConditionalError)
```
## Getting an Item From a Table
```js
client.getItem('user-table')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
.then(function(data) {
// data.result: the resulting object
})
```
If an item does not exist, `data.result` will be `undefined`.
### Getting Select Attributes
```js
client.getItem('user-table')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.selectAttributes(['userId', 'column'])
.execute()
.then(function(data) {
// data.result: the resulting object
// only the attributes passed into selectAttributes()
// appear as keys in data.result
})
```
### Batch Get
The batch get API allows you to request multiple items with specific primary keys, from different
tables, in a single fetch.
```js
client.newBatchGetBuilder()
.requestItems('user', [{'userId': 'userA', 'column': '@'}, {'userId': 'userB', 'column': '@'}])
.requestItems('phones', [{'userId': 'userA', 'column': 'phone1'}, {'userId': 'userB', 'column': 'phone1'}])
.execute()
```
`requestItems` can be called multiple times, with a table name and an array of objects representing
primary keys, in the form `{hashKey: 123, rangeKey: 456}`.
## Putting an Item Into a Table
Items are handled as JavaScript Objects by the client. These are then converted into an AWS specific format and sent off. The only accepted types of data that can be stored in DynamoDB are Strings, Numbers, and Sets (Arrays). Sets can contain either only Numbers or Strings.
```js
client.putItem('user-table', {
userId: 'userA',
column: '@',
age: 30,
company: 'Medium',
nickNames: ['Ev', 'Evan'],
postIds: [1, 2, 3]
})
```
### Overrides
If an item with the same hash and range keys as the one that is being inserted, the old item will be replaced with the item that is being put in its place.
```js
// initialData = [{userId: 'userA', column: '@', age: 27]
client.putItem('user-table', {
userId: 'userA',
column: '@',
height: 72
})
```
If the item above were to be retrieved from the table `user-table`, then age would be undefined and a new key `height` would be available.
### Conditional Writes
#### expectAttributeEquals
The item will only be replaced if the field `field` in the item is equal to the param `value`. If the item does not exist in the table, or the condition is not met, the request will fail.
#### expectAttributeAbsent
The item will only be replaced if the field `field` is not set in the item in the table. If the item does not exist in the table, then the item will be written to the table. If the field `field` exists for the item in the table, the request will fail.
## Deleting Items From a Table
If the hash key and range key match an item, it will be deleted. Upon success, the function returns the previous attributes and values of the deleted item.
```js
client.deleteItem('user-table')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
.then(function (data) {
// data.result will contain the origin item attributes and their corresponding values
})
```
### Conditional Deletes
#### expectAttributeEquals
If an item does not exist, then the request will fail. Otherwise, if the condition is met, the item will be deleted.
#### expectAttributeAbsent
If an item does not exist, then the request will fail. Otherwise, if the condition is met, the item will be deleted.
## Updating an Item
There are three methods available to modify columns for items: `putAttribute(field, value)`, `deleteAttribute(field)`, and `addToAttribute(field, value)`.
If an item does not exist, the update query will create the item and update its attributes accordingly.
If a value is updated on an attribute that does not exist, the attribute will be added to the item and set to the `value` passed to `putsAttribute(field, value)`. If an attribute does not exist and it's value is incremented, that attribute will be added to the item and it's value will be set to the `value` passed to `addToAttribute(field, value)`. If an attribute is deleted and it does not exist, the operation becomes a nonsense operation and has no effect on the item.
Putting empty attributes causes the whole update query to fail.
```js
// initialData = [{userId: 'userA', column: '@', age: 27, weight: 180]
client.newUpdateBuilder('user-table')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.enableUpsert()
.putAttribute('age', 30)
.addToAttribute('age', 1)
.deleteAttribute('weight')
.putAttribute('height', 72)
.execute()
.then(function (data) {
// data.result == {userId: 'userA', column: '@', age: 31, height: 72}
})
```
#### Conditional Updates
Conditions should be added with `withCondition` before any update commands.
##### expectAttributeEquals
If the item does not exist, the update query will fail.
##### expectAttributeAbsent
If the item does not exist, the update query will create the item and update its attributes accordingly.
## Querying a Table
Amazon features [extensive documentation](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/QueryAndScan.html) describing querying and scanning in great detail.
A Query operation searches only primary key attribute values and supports a subset of comparison operators on key attribute values to refine the search process. A query returns all of the item data for the matching primary keys (all of each item's attributes) up to 1 MB of data per query operation. A Query operation always returns results, but can return empty results.
A Query operation seeks the specified composite primary key, or range of keys, until one of the following events occur:
+ The result set is exhausted.
+ The number of items retrieved reaches the value of the Limit parameter, if specified.
+ The amount of data retrieved reaches the maximum result set size limit of 1 MB.
### Usage
Our initial data set:
```js
[
{"postId": "post1", "column": "@", "title": "This is my post", "content": "And here is some content!", "tags": ['foo', 'bar']},
{"postId": "post1", "column": "/comment/timestamp/002123", "comment": "this is slightly later"},
{"postId": "post1", "column": "/comment/timestamp/010000", "comment": "where am I?"},
{"postId": "post1", "column": "/comment/timestamp/001111", "comment": "HEYYOOOOO"},
{"postId": "post1", "column": "/comment/timestamp/001112", "comment": "what's up?"},
{"postId": "post1", "column": "/canEdit/user/AAA", "userId": "AAA"}
]
```
Querying all items whose postId is `post1`:
```js
client.newQueryBuilder('comments-table')
.setHashKey('postId', 'post1')
.execute()
.then(function (data) {
// data.result is an array of posts whose hash key is `post1`
})
```
The result of the query will be a DynamoResult object with a `result` property for the result set.
DynamoResult also has two methods:
+ hasNext(): boolean
Returns whether there are remaining results for this query.
+ next(): Promise.<DynamoResult>
Executes a new query that fetches the next page of results.
There are also a variety of methods that refine and restrict the returned set of results that operate on the indexed range key, which in our sample case is `column`.
#### getCount()
Get the count of the number of items, not the actual items themselves.
```js
client.newQueryBuilder('comments-table')
.setHashKey('postId', 'post1')
.getCount()
```
#### scanForward()
Demand that items be returned in ascending ASCII or numerical value. This is the default.
#### scanBackward()
Demand that items be returned in descending ASCII or numerical value.
#### setStartKey(key)
Start the query at a specified hash key. Useful when your request is returned in chunks and subsequent chunks need to be retrieved after the current batch is processed.
When partial results are returned, the `LastEvaluatedKey` can be passed in as an argument to `setStartKey()` on the next query to get the next section of results.
In general, calling setStartKey directly is discouraged in favor of using the `next()` method described above.
#### setLimit(max)
Return at most `max` items. Note that if the response will be larger than 1mb, then at most 1mb of data is returned, and the next batch of items needs to be queried while specifying that the query start at the `LastEvaluatedKey`. That key is returned with the results of the current query.
#### indexBeginsWith(range_key, key_part)
Return only items where the range key begins with `key_part`. For instance, retrieve all comments for posts with a, in our case unique, hash key of `post1`.
```js
client.newQueryBuilder('comments-table')
.setHashKey('postId', 'post1')
.indexBeginsWith('column', '/comment/')
```
#### indexBetween(range_key, key_part_start, key_part_end)
Return only items whose range key is "between" the start and end keys. The range key will be compared to the start and end keys in a lexicographic manner. So 'b' is "between" 'a' and 'c'.
Retrieve all comments for posts with the hash key `post1` up until the `009999` timestamp:
```js
client.newQueryBuilder('comments-table')
.setHashKey('postId', 'post1')
.indexBetween('column', '/comment/', '/comment/timestamp/009999')
```
#### indexLessThan(range_key, value)
#### indexLessThanEqual(range_key, value)
#### indexGreaterThan(range_key, value)
#### indexGreaterThanEqual(range_key, value)
#### indexEqual(range_key, value)
Return all items whose range keys comply with the afore-listed operations.
#### selectAttributes(attributes[])
Returned items will be stripped of all attributes except their hash key, range key, and the provided array of strings `attributes`.
## Scanning A Table
Amazon features [extensive documentation](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/QueryAndScan.html) describing querying and scanning in great detail.
A Scan operation examines every item in the table. You can specify filters to apply to the results to refine the values returned to you, after the scan has finished. Amazon DynamoDB puts a 1 MB limit on the scan (the limit applies before the results are filtered). A Scan can result in no table data meeting the filter criteria.
Scan supports a specific set of comparison operators. For information about each comparison operator available for scan operations, go to the API entry for Scan in the Amazon DynamoDB API Reference.
### Usage
Our initial data set:
```js
[
{"userId": "c", "column": "@", "post": "3", "email": "1@medium.com"},
{"userId": "b", "column": "@", "post": "0", "address": "800 Market St. SF, CA"},
{"userId": "a", "column": "@", "post": "5", "email": "3@medium"},
{"userId": "d", "column": "@", "post": "2", "twitter": "haha"},
{"userId": "e", "column": "@", "post": "2", "twitter": "hoho"},
{"userId": "f", "column": "@", "post": "4", "description": "Designer", "email": "h@w.com"},
{"userId": "h", "column": "@", "post": "6", "tags": ['foo', 'bar']}
]
```
A simple scan looks like this:
```js
client.newScanBuilder('user-table')
.execute()
.then(function (data) {
// data.result contains all of the users
})
```
If your dataset contains more than 1 MB of data, the `data` that is returned will contain a `LastEvaluatedKey` key that will tell you what the last evaluated key for the scan was, so you can start the next `scan` there by passing the `LastEvaluatedKey` to `setStartKey(key)`.
#### .filterAttributeEquals(field, value)
Include items whose `field` equals `value`.
```js
client.newScanBuilder('user-table')
.filterAttributeEquals('twitter', 'haha')
.execute()
.then(function (data) {
// data.result #=> [{"userId": "d", "column": "@", "post": "2", "twitter": "haha"}]
})
```
The other `filterAttribute*` functions are used in the exact same way.
#### .filterAttributeNotEquals(field, value)
Include items whose `field` does not equal `value`.
#### .filterAttributeLessThanEqual(field, value)
Include items whose `field` is less than or equal to `value`.
#### .filterAttributeLessThan(field, value)
Include items whose `field` is less than `value`.
#### .filterAttributeGreaterThanEqual(field, value)
Include items whose `field` is greater than or equal to `value`.
#### .filterAttributeGreaterThan(field, value)
Include items whose `field` is greater than `value`.
#### .filterAttributeNotNull(field)
Include items whose `field` is not `null`, or doesn't exist.
#### .filterAttributeContains(field, value)
Include items whose `field` contains `value`.
If an item's `field` attribute is a string, `filterAttributeContains` will search for `value` in that field's value. If an item's `field` attribute is a set, `filterAttributeContains` will search for `value` in that set.
#### .filterAttributeNotContains(field, value)
Include items whose `field` does not contain `value`. Essentially the inverse of `filterAttributeContains`.
#### .filterAttributeBeginsWith(field, value)
Include items whose `field` attribute begins with `value`.
#### .filterAttributeBetween(field, lower, upper)
Include items whose `field` attribute's value is between `lower` and `upper`, exclusive.
#### .filterAttributeIn(field, array_of_values)
Filter out rows where field is not one of the values in `array_of_values`.
================================================
FILE: dynamite.js
================================================
module.exports = {
Client: require('./lib/Client'),
FakeDynamo: require('./lib/FakeDynamo'),
ConditionBuilder: require('./lib/ConditionBuilder')
}
================================================
FILE: externs/aws-sdk.js
================================================
// Require an event emitter, because some of these apis return emitters.
var EventEmitter = require('events').EventEmitter
var awsResponse = {
CapacityUnits: 0,
UnprocessedKeys: []
}
var queryResponse = {
TableName: null,
AttributesToGet: [],
Limit: 1,
ConsistentRead: true,
Count: true,
HashKeyValue: {
S: '',
N: 1,
B: 'x',
SS: [],
NS: [],
BS: []
},
RangeKeyCondition: {
AttributeValueList: [],
ComparisonOperator: null
},
ScanIndexForward: true,
ExclusiveStartKey: {
HashKeyElement: {S: ''},
RangeKeyElement: {S: ''}
}
}
/** @constructor */
function DynamoDB() {}
module.exports = {
/** @constructor */
DynamoDB: DynamoDB,
config: {
update: function (options) {}
}
}
================================================
FILE: lib/BatchGetItemBuilder.js
================================================
var DynamoRequest = require('./DynamoRequest')
var Builder = require('./Builder')
var Q = require('kew')
var util = require('util')
// TODO(nick): Add an iterative API.
/** @const */
var BATCH_LIMIT = 100
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function BatchGetItemBuilder(options) {
Builder.call(this, options)
this._tableAttributes = {}
this._tableConsistentRead = {}
this._tableKeys = {}
}
util.inherits(BatchGetItemBuilder, Builder)
/**
* Specify if we should do consistent read on a certain table.
*
* @param {string} table The name of the table to configure
* @param {boolean} consistentRead Indicate if we need to do consistent read on the
* given table.
* @return {BatchGetItemBuilder}
*/
BatchGetItemBuilder.prototype.setConsistentReadForTable = function (table, consistentRead) {
this._tableConsistentRead[table] = consistentRead
return this
}
/**
* Request items from a certain table.
*
* @param {string} table The name of the table to request items from
* @param {Array.<Object>} keys A set of primary keys to fetch.
* @return {BatchGetItemBuilder}
*/
BatchGetItemBuilder.prototype.requestItems = function (table, keys) {
if (!this._tableKeys[table]) this._tableKeys[table] = []
this._tableKeys[table].push.apply(this._tableKeys[table], keys)
return this
}
BatchGetItemBuilder.prototype.execute = function () {
var promises = []
var count = 0
var batchTableKeys = {}
// Divide the items into batches, BATCH_LIMIT (100) items each.
for (var tableName in this._tableKeys) {
var keys = this._tableKeys[tableName]
batchTableKeys[tableName] = []
for (var i = 0; i < keys.length; i++) {
if (count === BATCH_LIMIT) {
promises.push(this._getAllItems(this._buildDynamoRequest(batchTableKeys)))
batchTableKeys = {}
batchTableKeys[tableName] = []
count = 0
}
count += 1
batchTableKeys[tableName].push(keys[i])
}
}
if (count > 0) promises.push(this._getAllItems(this._buildDynamoRequest(batchTableKeys)))
var builder = this
return Q.all(promises)
.then(function (batches) {
var all = batches[0]
for (var i = 1; i < batches.length; i++) builder._mergeTwoBatches(all, batches[i])
return all
})
.then(this.prepareOutput.bind(this))
.fail(this.emptyResults)
}
/**
* Return an object that represents the request data, for the purpose of
* logging/debugging.
*
* @return {Object} Information about this request
*/
BatchGetItemBuilder.prototype.toObject = function () {
return {
options: this._options,
attributes: this._tableAttributes,
consistent: this._tableConsistentRead,
items: this._tableKeys
}
}
BatchGetItemBuilder.prototype._buildDynamoRequest = function (keys) {
return new DynamoRequest(this.getOptions())
.setBatchTableAttributes(this._tablePrefix, this._tableAttributes)
.setBatchTableConsistent(this._tablePrefix, this._tableConsistentRead)
.setBatchRequestItems(this._tablePrefix, keys)
.returnConsumedCapacity()
.build()
}
/**
* Merge two batches of responses into one batch.
*
* @param{Object} batch One batch data returned from Dynamo.
* @param{Object} anotherBatch Another batch data returned from Dynamo
*/
BatchGetItemBuilder.prototype._mergeTwoBatches = function (batch, anotherBatch) {
for (var tableName in anotherBatch.Responses) {
if (!(tableName in batch.Responses)) {
batch.Responses[tableName] = {Items: [], ConsumedCapacityUnits: 0}
}
var items = batch.Responses[tableName]
items.push.apply(items, anotherBatch.Responses[tableName])
batch.Responses[tableName].ConsumedCapacityUnits +=
anotherBatch.Responses[tableName].ConsumedCapacityUnits
}
}
/**
* Get all the items and handle the Dynamo API limit size limit.
*
* @param {Object.<string, Array.<Object>>} queryData A map from table name to
* requested keys in Dynamo API format.
* @return {Q.Promise.<Object>} The object is a typical Dynamo response.
*/
BatchGetItemBuilder.prototype._getAllItems = function (queryData) {
var builder = this
return this.request("batchGetItem", queryData)
.then(function (data) {
var unprocessedKeys = data.UnprocessedKeys
if (unprocessedKeys && Object.keys(unprocessedKeys).length > 0) {
// If there are unprocessed keys, keep fetching and append the results to
// the current results.
return builder._getAllItems(
new DynamoRequest(builder.getOptions())
.setRequestItems(unprocessedKeys)
.returnConsumedCapacity()
.build())
.then(function (moreData) {
builder._mergeTwoBatches(data, moreData)
data.UnprocessedKeys = {}
return data
})
} else {
return data
}
})
.failBound(this.convertErrors, null, {data: queryData, isWrite: false})
}
module.exports = BatchGetItemBuilder
================================================
FILE: lib/Builder.js
================================================
var util = require('util')
var Q = require('kew')
var errors = require('./errors')
var DynamoResponse = require('./DynamoResponse')
/**
* @constructor
* @param {Object} options
*/
function Builder(options) {
this._options = options || {}
this._retryHandler = this._options.retryHandler
}
Builder.prototype.setHashKey = function (keyName, keyVal) {
this._hashKey = {
name: keyName,
val: keyVal
}
return this
}
Builder.prototype.setRangeKey = function (keyName, keyVal) {
this._rangeKey = {
name: keyName,
val: keyVal
}
return this
}
Builder.prototype.getRetryHandler = function () {
return this._retryHandler
}
Builder.prototype.setRetryHandler = function (retryHandler) {
this._retryHandler = retryHandler
return this
}
Builder.prototype.getOptions = function () {
return this._options
}
Builder.prototype.setDatabase = function (db) {
this._db = db
return this
}
Builder.prototype.setConsistent = function (isConsistent) {
this._isConsistent = isConsistent
return this
}
Builder.prototype.consistentRead = function () {
return this.setConsistent(true)
}
Builder.prototype.getPrefix = function () {
return this._tablePrefix
}
Builder.prototype.setPrefix = function (prefix) {
this._tablePrefix = prefix
return this
}
Builder.prototype.setTable = function (table) {
this._table = table
return this
}
Builder.prototype.setLimit = function (limit) {
this._limit = limit
return this
}
Builder.prototype.scanForward = function () {
this._shouldScanForward = true
return this
}
Builder.prototype.scanBackward = function () {
this._shouldScanForward = false
return this
}
Builder.prototype.getCount = function () {
this._isCount = true
return this
}
Builder.prototype.withFilter = function (filter) {
if (!this._filters) this._filters = []
if (filter) this._filters.push(filter)
return this
}
Builder.prototype.withCondition = function (condition) {
if (!this._conditions) this._conditions = []
if (condition) this._conditions.push(condition)
return this
}
Builder.prototype.selectAttributes = function (attributes) {
if (!attributes) return this
if (!Array.isArray(attributes)) attributes = Array.prototype.slice.call(arguments, 0)
this._attributes = attributes
return this
}
/** @this {*} */
Builder.prototype.emptyResults = function (e) {
if (e.message === 'Requested resource not found') return {results:[]}
throw e
}
/** @this {*} */
Builder.prototype.emptyResult = function (e) {
if (e.message === 'Requested resource not found') return {results:null}
throw e
}
Builder.prototype.request = function (method, data) {
if (this._options.logQueries) {
this.logQuery(method, data)
}
var defer = Q.defer()
if (!this._db.isFakeDynamo) {
delete data._requestBuilder
}
var req = this._db[method](data, defer.makeNodeResolver())
var startedAt = Date.now()
var retryHandler = this.getRetryHandler()
var table = this._table
var processingStartedAt, byteLength, requestLatencyMs
// FakeDynamo doesn't return a request object
if (req && req.on) {
req.on('httpDone', function (res) {
var now = Date.now()
processingStartedAt = now
requestLatencyMs = now - startedAt
if (res && res.httpResponse) {
byteLength = res.httpResponse.headers && res.httpResponse.headers['content-length']
}
})
if (retryHandler) {
req.on('retry', function (res) {
retryHandler(method, table, res)
})
}
}
return defer.then(function (output) {
output.ByteLength = byteLength
output.ProcessingStartedAt = processingStartedAt
output.RequestLatencyMs = requestLatencyMs
return output
})
}
Builder.prototype.logQuery = function (method, data) {
var cyanBold, cyan, reset
cyanBold = '\u001b[1;36m'
cyan = '\u001b[0;36m'
reset = '\u001b[0m'
console.info(cyanBold + method + cyan)
console.info(util.inspect(data, {depth: null}))
console.info(reset)
}
Builder.prototype.prepareOutput = function (output) {
return new DynamoResponse(this._tablePrefix, output, null)
}
Builder.prototype.convertErrors = function (context, err) {
// Errors in Dynamo response are JSON objects like this:
// {
// "message":"Attribute found when none expected.",
// "code":"ConditionalCheckFailedException",
// "name":"ConditionalCheckFailedException",
// "statusCode":400,
// "retryable":false
// }
//
// More at http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ErrorHandling.html
//
// To be more reliable, we check both err.name and err.code.
// Dynamo doc specifies the value of "code", so the error object
// should have "code" assigned. The node.js SDK assigns "code"
// to "name". "name" is the standard attribute of javascript Error
// object, so we double-check it.
var data = context.data
var isWrite = !!context.isWrite
switch (err.code || err.name) {
case 'ConditionalCheckFailedException':
throw new errors.ConditionalError(data, err.message, err.requestId)
case 'ProvisionedThroughputExceededException':
throw new errors.ProvisioningError(data, err.message, isWrite, err.requestId)
case 'ValidationException':
throw new errors.ValidationError(data, err.message, isWrite, err.requestId)
default:
throw err
}
}
module.exports = Builder
================================================
FILE: lib/Client.js
================================================
var assert = require('assert')
var AWS = require('aws-sdk')
var AWSName = require('./common').AWSName
var ConditionBuilder = require('./ConditionBuilder')
var DeleteItemBuilder = require('./DeleteItemBuilder')
var DescribeTableBuilder = require('./DescribeTableBuilder')
var BatchGetItemBuilder = require('./BatchGetItemBuilder')
var GetItemBuilder = require('./GetItemBuilder')
var PutItemBuilder = require('./PutItemBuilder')
var QueryBuilder = require('./QueryBuilder')
var ScanBuilder = require('./ScanBuilder')
var UpdateBuilder = require('./UpdateBuilder')
var errors = require('./errors')
/**
* Creates an instance of Client which can be used to access Dynamo.
*
* The must-have information in 'options' are:
* - region
* - accessKeyId
* - secretAccessKey
*
* If region does not present, we try to infer it from other keys, e.g., 'host',
* which is mainly for the backward compatibility with some old code that use
* Dynamite.
*
* Dynamite current supports only the 2011 version of the API. It does not
* concern the user of Dynamite though, because we do not expose the low level
* APIs to Dynamite users.
*
* @constructor
* @param {{dbClient:Object, host:string, region:string,
* accessKeyId:string, secretAccessKey:string, prefix: string,
* logQueries: boolean, retryHandler: Function}}
* options map which can be used to either configure accesss to Amazon's DynamoDB
* service using a host/region, accessKeyId, and secrectAccessKey or can provide
* a dbClient object which implements the interface per the AWS SDK for node.js.
*/
function Client(options) {
this._prefix = options.prefix || ''
this._commonOptions = {
// whether to log out queries
logQueries: !!options.logQueries
}
if (options.dbClient) {
this.db = options.dbClient
} else {
if (!('region' in options)) {
// If the options do not contain a 'region' key, we will try to refer
// it from the values of other keys, e.g., 'host'.
//
// 'region' is what we need to initialize a database instance in the
// AWS SDK.
if ('endpoint' in options) {
var endpoint = options['endpoint']
for (var i = 0; i < AWSName.REGION.length; i++) {
if (endpoint.indexOf(AWSName.REGION[i]) >= 0) {
options['region'] = AWSName.REGION[i]
break
}
}
}
}
options['apiVersion'] = AWSName.API_VERSION_2012
AWS.config.update(options)
this.db = new AWS.DynamoDB()
}
}
Client.prototype.describeTable = function (table) {
return new DescribeTableBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
}
Client.prototype.newQueryBuilder = function (table) {
return new QueryBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
}
Client.prototype.newBatchGetBuilder = function () {
return new BatchGetItemBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
}
Client.prototype.newUpdateBuilder = function (table) {
assert.equal(arguments.length, 1, "newUpdateBuilder(table) only takes table name as an arg")
return new UpdateBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
}
Client.prototype.newScanBuilder = function (table) {
assert.equal(arguments.length, 1, "newScanBuilder(table) only takes table name as an arg")
return new ScanBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
}
Client.prototype.getItem = function (table) {
assert.equal(arguments.length, 1, "getItem(table) only takes table name as an arg")
return new GetItemBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
}
Client.prototype.deleteItem = function (table) {
assert.equal(arguments.length, 1, "deleteItem(table) only takes table name as an arg")
return new DeleteItemBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
}
Client.prototype.putItem = function (table, item) {
assert.equal(arguments.length, 2, "putItem(table, item) did not have 2 arguments")
return new PutItemBuilder(this._commonOptions)
.setDatabase(this.db)
.setPrefix(this._prefix)
.setTable(table)
.setItem(item)
}
Client.prototype.andConditions = function (conditions) {
ConditionBuilder.validateConditions(conditions)
return ConditionBuilder.andConditions(conditions)
}
Client.prototype.orConditions = function (conditions) {
return ConditionBuilder.orConditions(conditions)
}
Client.prototype.notCondition = function (condition) {
return ConditionBuilder.notCondition(condition)
}
Client.prototype.newConditionBuilder = function () {
return new ConditionBuilder()
}
/**
* Returns a condition builder that guarantees that an item matches an expected
* state. Keys that have null, undefined, or empty string values are expected
* to not be present in the item being queried.
*
* @param {Object} obj A map of keys to verify.
* @return {ConditionBuilder}
*/
Client.prototype.conditions = function (obj) {
var conditionBuilder = this.newConditionBuilder()
for (var key in obj) {
if (typeof obj[key] === 'undefined' || obj[key] === null || obj[key] === '') {
conditionBuilder.expectAttributeAbsent(key)
} else {
conditionBuilder.expectAttributeEquals(key, obj[key])
}
}
return conditionBuilder
}
/**
* Returns true if error is a Dynamo error indicating the table is throttled
* @param {Error} e
* @return {boolean}
*/
Client.isProvisioningError = function (e) {
return e instanceof errors.ProvisioningError
}
/**
* Returns true if error is a Dynamo error indicating the table is throttled
* @param {Error} e
* @return {boolean}
*/
Client.prototype.isProvisioningError = Client.isProvisioningError
/**
* Returns true if error is a Dynamo error indicating a validation error
* @param {Error} e
* @return {boolean}
*/
Client.isValidationError = function (e) {
return e instanceof errors.ValidationError
}
/**
* Returns true if error is a Dynamo error indicating a validation error
* @param {Error} e
* @return {boolean}
*/
Client.prototype.isValidationError = Client.isValidationError
/**
* Returns true if error is a Dynamo error indicating a condition wasn't met.
* @param {Error} e
* @return {boolean}
*/
Client.isConditionalError = function (e) {
return e instanceof errors.ConditionalError
}
/**
* Returns true if error is a Dynamo error indicating a condition wasn't met.
* @param {Error} e
* @return {boolean}
*/
Client.prototype.isConditionalError = Client.isConditionalError
/**
* Returns false if the error indicates a condition failed, otherwise the error
* is re-thrown.
* @param {Error} e
* @return {boolean}
*/
Client.throwUnlessConditionalError = function (e) {
if (!Client.isConditionalError(e)) throw e
return false
}
/**
* Returns false if the error indicates a condition failed, otherwise the error
* is re-thrown. It is safe to use this without binding it.
* @param {Error} e
* @return {boolean}
*/
Client.prototype.throwUnlessConditionalError = Client.throwUnlessConditionalError
module.exports = Client
================================================
FILE: lib/ConditionBuilder.js
================================================
var assert = require('assert')
var typ = require('typ')
var typeUtil = require('./typeUtil')
var util = require('util')
/**
* @constructor
*/
function ConditionBuilder() {
this._exprs = []
}
/**
* Creates a new Conditional expression with an operator (AND, EQ, etc)
* @param {Op} op One of the ops defined in the Op enum.
* @param {Array<ConditionExpr>} args The arguments to the op.
* @return {ConditionExpr}
*/
ConditionBuilder.prototype._op = function (op, args) {
if (!op) throw new Error('Missing op')
return new ConditionExprOp(op, args)
}
/**
* Creates a new Conditional expression that evaluates to an attribute name
* @param {string} name An attribute name
* @return {ConditionExpr}
*/
ConditionBuilder.prototype._attr = function (name) {
return new ConditionExprAttr(name)
}
/**
* Creates a new Conditional expression that evaluates to a value
* @param {*} val Any literal value representable in a dynamodb expression
* @return {ConditionExpr}
*/
ConditionBuilder.prototype._val = function (val) {
return new ConditionExprVal(val)
}
/**
* Returns this condition builder as a ConditionExpr, the internal representation
* of a condition.
* @return {ConditionExpr}
*/
ConditionBuilder.prototype._asExpr = function () {
return this._op(Op.AND, this._exprs)
}
/**
* @param {Array.<ConditionBuilder>} conditions
* @return {ConditionBuilder}
*/
ConditionBuilder.andConditions = function (conditions) {
ConditionBuilder.validateConditions(conditions)
var builder = new ConditionBuilder()
builder._exprs.push(builder._op(Op.AND, conditions.map(function (c) {
return c._asExpr()
})))
return builder
}
/**
* @param {Array.<ConditionBuilder>} conditions
* @return {ConditionBuilder}
*/
ConditionBuilder.orConditions = function (conditions) {
ConditionBuilder.validateConditions(conditions)
var builder = new ConditionBuilder()
builder._exprs.push(builder._op('OR', conditions.map(function (c) {
return c._asExpr()
})))
return builder
}
/**
* @param {ConditionBuilder} condition
* @return {ConditionBuilder}
*/
ConditionBuilder.notCondition = function (condition) {
var conditions = [condition]
ConditionBuilder.validateConditions(conditions)
var builder = new ConditionBuilder()
builder._exprs.push(builder._op('NOT', [condition._asExpr()]))
return builder
}
ConditionBuilder.validateConditions = function (conditions) {
assert.ok(Array.isArray(conditions), 'Expected array')
for (var i = 0; i < conditions.length; i++) {
var condition = conditions[i]
assert.ok(condition instanceof ConditionBuilder,
'Expected ConditionBuilder')
}
}
/**
* @param {Object} data
* @param {string} fieldName The fieldName on `data`
* @param {Array<ConditionBuilder>} conditions
* @param {{count: number}} nameMutex
* @return {ConditionBuilder}
*/
ConditionBuilder.populateExpressionField = function (data, fieldName, conditions, nameMutex) {
ConditionBuilder.validateConditions(conditions)
var junction = ConditionBuilder.andConditions(conditions)
junction.assignUniqueNames(nameMutex)
if (!data.ExpressionAttributeNames) {
data.ExpressionAttributeNames = {}
}
typeUtil.extendAttributeNames(data.ExpressionAttributeNames, junction.buildAttributeNames())
if (!data.ExpressionAttributeValues) {
data.ExpressionAttributeValues = {}
}
typeUtil.extendAttributeValues(data.ExpressionAttributeValues, junction.buildAttributeValues())
data[fieldName] = junction.buildExpression()
return junction
}
/**
* @param {Object} nameMutex
*/
ConditionBuilder.prototype.assignUniqueNames = function (nameMutex) {
this._asExpr().assignUniqueNames(nameMutex)
}
/** @return {Object} */
ConditionBuilder.prototype.buildAttributeNames = function () {
var result = []
this._asExpr().appendAttributeNames(result)
return typeUtil.buildAttributeNames(result)
}
/** @return {Object} */
ConditionBuilder.prototype.buildAttributeValues = function () {
var result = {}
this._asExpr().appendAttributeValues(result)
return result
}
/**
* @return {string} String suitable for FilterExpression and KeyExpression
* @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html#ConditionExpressionReference.Syntax
*/
ConditionBuilder.prototype.buildExpression = function () {
return this._asExpr().buildExpression()
}
/**
* Iterate through all the expressions. Intended for FakeDynamo
* @param {function(Object)} callback
*/
ConditionBuilder.prototype.visitExpressionsPostOrder = function (callback) {
this._exprs.forEach(function (expr) {
expr.visitPostOrder(callback)
})
}
/**
* Get the appropriate comparator function to compare the passed in values against
* @return {function(Object): boolean}
*/
ConditionBuilder.prototype.buildFilterFn = function () {
var builder = this
return function (item) {
return !!builder._asExpr().evaluate(item)
}
}
ConditionBuilder.prototype.expectAttributeEqual =
ConditionBuilder.prototype.expectAttributeEquals =
ConditionBuilder.prototype.filterAttributeEquals = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('EQ', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeEqualsAttribute = function (key1, key2) {
if (typ.isNullish(key1)) throw new Error("Key1 must be defined")
if (typ.isNullish(key2)) throw new Error("Key2 must be defined")
this._exprs.push(this._op('EQ', [this._attr(key1), this._attr(key2)]))
return this
}
ConditionBuilder.prototype.filterAttributeNotEquals = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('NE', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeNotEqualsAttribute = function (key1, key2) {
if (typ.isNullish(key1)) throw new Error("Key1 must be defined")
if (typ.isNullish(key2)) throw new Error("Key2 must be defined")
this._exprs.push(this._op('NE', [this._attr(key1), this._attr(key2)]))
return this
}
ConditionBuilder.prototype.filterAttributeLessThanEqual = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('LE', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeLessThanEqualAttribute = function (key1, key2) {
if (typ.isNullish(key1)) throw new Error("Key1 must be defined")
if (typ.isNullish(key2)) throw new Error("Key2 must be defined")
this._exprs.push(this._op('LE', [this._attr(key1), this._attr(key2)]))
return this
}
ConditionBuilder.prototype.filterAttributeLessThan = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('LT', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeLessThanAttribute = function (key1, key2) {
if (typ.isNullish(key1)) throw new Error("Key1 must be defined")
if (typ.isNullish(key2)) throw new Error("Key2 must be defined")
this._exprs.push(this._op('LT', [this._attr(key1), this._attr(key2)]))
return this
}
ConditionBuilder.prototype.filterAttributeGreaterThanEqual = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('GE', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeGreaterThanEqualAttribute = function (key1, key2) {
if (typ.isNullish(key1)) throw new Error("Key1 must be defined")
if (typ.isNullish(key2)) throw new Error("Key2 must be defined")
this._exprs.push(this._op('GE', [this._attr(key1), this._attr(key2)]))
return this
}
ConditionBuilder.prototype.filterAttributeGreaterThan = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('GT', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeGreaterThanAttribute = function (key1, key2) {
if (typ.isNullish(key1)) throw new Error("Key1 must be defined")
if (typ.isNullish(key2)) throw new Error("Key2 must be defined")
this._exprs.push(this._op('GT', [this._attr(key1), this._attr(key2)]))
return this
}
ConditionBuilder.prototype.filterAttributeNotNull = function (key) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
this._exprs.push(this._op('NOT_NULL', [this._attr(key)]))
return this
}
ConditionBuilder.prototype.expectAttributeAbsent =
ConditionBuilder.prototype.filterAttributeNull = function (key) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
this._exprs.push(this._op('NULL', [this._attr(key)]))
return this
}
ConditionBuilder.prototype.filterAttributeContains = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('CONTAINS', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeNotContains = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('NOT_CONTAINS', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeBeginsWith = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._exprs.push(this._op('BEGINS_WITH', [this._attr(key), this._val(val)]))
return this
}
ConditionBuilder.prototype.filterAttributeBetween = function (key, val1, val2) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val1)) throw new Error("Val 1 must be defined")
if (typ.isNullish(val2)) throw new Error("Val 2 must be defined")
this._exprs.push(this._op('BETWEEN', [this._attr(key), this._val(val1), this._val(val2)]))
return this
}
ConditionBuilder.prototype.filterAttributeIn = function (key, vals) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(vals)) throw new Error("Vals must be defined")
this._exprs.push(this._op('IN', [this._attr(key)].concat(vals.map(this._val.bind(this)))))
return this
}
/**
* Represents an abstract conditional expression internally.
*
* May contain zero or more arguments, which are also ConditionExprs.
*
* @param {Array<ConditionExpr>} args
* @constructor
*/
function ConditionExpr(args) {
this.args = args || []
this.uniqueName = ''
}
/**
* Iterate through all the expressions
* @param {function(Object)} callback
*/
ConditionExpr.prototype.visitPostOrder = function (callback) {
this.args.forEach(function (arg) {
arg.visitPostOrder(callback)
})
callback(this)
}
/**
* @param {Object} nameMutex
*/
ConditionExpr.prototype.assignUniqueNames = function (nameMutex) {
if (!nameMutex.count) {
nameMutex.count = 1
}
this.visitPostOrder(function (expr) {
expr.uniqueName = 'C' + nameMutex.count++
})
}
/**
* Appends the attribute names to the given array.
*/
ConditionExpr.prototype.appendAttributeNames = function (result) {
this.args.forEach(function (arg) {
arg.appendAttributeNames(result)
})
}
/**
* Appends the attribute values to the given object.
*/
ConditionExpr.prototype.appendAttributeValues = function (result) {
this.args.forEach(function (arg) {
arg.appendAttributeValues(result)
})
}
/**
* Builds an expression for consumption by the AWS expression API
*/
ConditionExpr.prototype.buildExpression = function () {
throw new Error('Expected buildExpression impl')
}
/**
* Client-side evaluation.
*/
ConditionExpr.prototype.evaluate = function () {
throw new Error('Expected evaluate impl')
}
/**
* Represents a conditional expression with 1 or more arguments.
* @param {Op} op One of the ops defined in the Op enum.
* @param {Array<ConditionExpr>} args The arguments to the op.
* @constructor
*/
function ConditionExprOp(op, args) {
ConditionExpr.call(this, args)
this.op = op
}
util.inherits(ConditionExprOp, ConditionExpr)
/**
* Builds an expression for arg i, asserting that the arg exists
*/
ConditionExprOp.prototype.buildArgExpr = function (i) {
if (i >= this.args.length) throw new Error('Operator ' + this.op + ' expected arg at position ' + i)
return this.args[i].buildExpression()
}
/** @override */
ConditionExprOp.prototype.buildExpression = function () {
var operator = this.op
switch (operator) {
case Op.NOT:
return '(NOT ' + this.buildArgExpr(0) + ')'
case Op.AND:
if (this.args.length == 0) return ''
if (this.args.length == 1) return this.buildArgExpr(0)
var andExprs = this.args.map(function (arg) {
return arg.buildExpression()
}, this)
return '(' + andExprs.join(' AND ') + ')'
case Op.OR:
if (this.args.length == 0) return ''
if (this.args.length == 1) return this.buildArgExpr(0)
var orExprs = this.args.map(function (arg) {
return arg.buildExpression()
}, this)
return '(' + orExprs.join(' OR ') + ')'
case Op.BEGINS_WITH:
return 'begins_with(' + this.buildArgExpr(0) + ', ' + this.buildArgExpr(1) + ')'
case Op.EQ:
return '(' + this.buildArgExpr(0) + ' = ' + this.buildArgExpr(1) + ')'
case Op.NE:
return '(' + this.buildArgExpr(0) + ' <> ' + this.buildArgExpr(1) + ')'
case Op.LE:
return '(' + this.buildArgExpr(0) + ' <= ' + this.buildArgExpr(1) + ')'
case Op.LT:
return '(' + this.buildArgExpr(0) + ' < ' + this.buildArgExpr(1) + ')'
case Op.GE:
return '(' + this.buildArgExpr(0) + ' >= ' + this.buildArgExpr(1) + ')'
case Op.GT:
return '(' + this.buildArgExpr(0) + ' > ' + this.buildArgExpr(1) + ')'
case Op.BETWEEN:
return '(' + this.buildArgExpr(0) + ' BETWEEN ' + this.buildArgExpr(1) + ' AND ' + this.buildArgExpr(2) + ')'
case Op.IN:
var values = this.args.slice(1).map(function (arg) {
return arg.buildExpression()
}, this)
return '(' + this.buildArgExpr(0) + ' IN (' + values + '))'
case Op.NOT_CONTAINS:
return '(attribute_exists(' + this.buildArgExpr(0) + ') AND NOT contains(' + this.buildArgExpr(0) + ', ' + this.buildArgExpr(1) + '))'
case Op.CONTAINS:
return 'contains(' + this.buildArgExpr(0) + ', ' + this.buildArgExpr(1) + ')'
case Op.NULL:
return 'attribute_not_exists(' + this.buildArgExpr(0) + ')'
case Op.NOT_NULL:
return 'attribute_exists(' + this.buildArgExpr(0) + ')'
default:
throw new Error('Invalid comparison operator \'' + operator + '\'')
}
}
/**
* Evaluate the value for arg i, asserting that the arg exists
*/
ConditionExprOp.prototype.evalArg = function (i, item) {
if (i >= this.args.length) throw new Error('Operator ' + this.op + ' expected arg at position ' + i)
return this.args[i].evaluate(item)
}
/**
* Evaluate the value for arg i, asserting that the arg exists
*/
ConditionExprOp.prototype.evalArgs = function (item) {
return this.args.map(function (arg) {
return arg.evaluate(item)
})
}
/** @override */
ConditionExprOp.prototype.evaluate = function (item) {
var argExprs = this.args
var operator = this.op
switch (operator) {
case Op.NOT:
return !this.evalArg(0, item)
case Op.AND:
return this.evalArgs(item).every(function (val) {
return val
})
case Op.OR:
return this.evalArgs(item).some(function (val) {
return val
})
case Op.BEGINS_WITH:
return item && this.evalArg(0, item).indexOf(this.evalArg(1, item)) === 0
case Op.EQ:
return item && this.evalArg(0, item) == this.evalArg(1, item)
case Op.NE:
return item && this.evalArg(0, item) != this.evalArg(1, item)
case Op.LE:
return item && this.evalArg(0, item) <= this.evalArg(1, item)
case Op.LT:
return item && this.evalArg(0, item) < this.evalArg(1, item)
case Op.GE:
return item && this.evalArg(0, item) >= this.evalArg(1, item)
case Op.GT:
return this.evalArg(0, item) > this.evalArg(1, item)
case Op.BETWEEN:
return item && this.evalArg(0, item) >= this.evalArg(1, item) && this.evalArg(0, item) <= this.evalArg(2, item)
case Op.IN:
return item && this.evalArgs(item).slice(1).indexOf(this.evalArg(0, item)) != -1
case Op.NOT_CONTAINS:
return item && this.evalArg(0, item).indexOf(this.evalArg(1, item)) == -1
case Op.CONTAINS:
return item && this.evalArg(0, item).indexOf(this.evalArg(1, item)) != -1
case Op.NULL:
return !item || !item.hasOwnProperty(argExprs[0].name)
case Op.NOT_NULL:
return item && item.hasOwnProperty(argExprs[0].name)
default:
throw new Error('Invalid comparison operator \'' + operator + '\'')
}
}
/**
* Represents a value in a conditional expression.
* @param {*} val Any literal value representable in a dynamodb expression
* @constructor
*/
function ConditionExprVal(val) {
ConditionExpr.call(this, [])
this.val = typeUtil.valueToObject(val)
}
util.inherits(ConditionExprVal, ConditionExpr)
/** @override */
ConditionExprVal.prototype.buildExpression = function () {
return ':V' + this.uniqueName
}
/** @override */
ConditionExprVal.prototype.appendAttributeValues = function (result) {
result[this.buildExpression()] = this.val
}
/** @override */
ConditionExprVal.prototype.evaluate = function () {
return typeUtil.objectToValue(this.val)
}
/**
* Represents an item attribute in a conditional expression.
* @param {string} name An attribute name
* @constructor
*/
function ConditionExprAttr(name) {
ConditionExpr.call(this, [])
this.name = name
}
util.inherits(ConditionExprAttr, ConditionExpr)
/** @override */
ConditionExprAttr.prototype.buildExpression = function () {
return typeUtil.getAttributeAlias(this.name)
}
/** @override */
ConditionExprAttr.prototype.evaluate = function (item) {
return item[this.name]
}
/** @override */
ConditionExprAttr.prototype.appendAttributeNames = function (result) {
result.push(this.name)
}
/**
* An enum of all the possible Ops in our ConditionExpr syntax tree.
*/
var Op = {
NOT: 'NOT',
AND: 'AND',
OR: 'OR',
BEGINS_WITH: 'BEGINS_WITH',
EQ: 'EQ',
NE: 'NE',
LE: 'LE',
LT: 'LT',
GE: 'GE',
GT: 'GT',
BETWEEN: 'BETWEEN',
IN: 'IN',
NOT_CONTAINS: 'NOT_CONTAINS',
CONTAINS: 'CONTAINS',
NULL: 'NULL',
NOT_NULL: 'NOT_NULL'
}
module.exports = ConditionBuilder
================================================
FILE: lib/DeleteItemBuilder.js
================================================
var DynamoRequest = require('./DynamoRequest')
var DynamoResponse = require('./DynamoResponse')
var Builder = require('./Builder')
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function DeleteItemBuilder(options) {
Builder.call(this, options)
}
require('util').inherits(DeleteItemBuilder, Builder)
/** @override */
DeleteItemBuilder.prototype.prepareOutput = function (output) {
output.UpdatedAttributes = null
return new DynamoResponse(this.getPrefix(), output, null)
}
DeleteItemBuilder.prototype.execute = function () {
var req = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.returnConsumedCapacity()
.setHashKey(this._hashKey, true)
.setExpected(this._conditions)
.setReturnValues('ALL_OLD')
if (this._rangeKey) req.setRangeKey(this._rangeKey, true)
var queryData = req.build()
return this.request("deleteItem", queryData)
.then(this.prepareOutput.bind(this))
.failBound(this.convertErrors, null, {data: queryData, isWrite: true})
}
module.exports = DeleteItemBuilder
================================================
FILE: lib/DescribeTableBuilder.js
================================================
var DynamoRequest = require('./DynamoRequest')
var Builder = require('./Builder')
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function DescribeTableBuilder(options) {
Builder.call(this, options)
}
require('util').inherits(DescribeTableBuilder, Builder)
DescribeTableBuilder.prototype.execute = function () {
var queryData = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.build()
return this.request("describeTable", queryData)
.failBound(this.convertErrors, null, {data: queryData, isWrite: false})
.clearContext()
}
module.exports = DescribeTableBuilder
================================================
FILE: lib/DynamoRequest.js
================================================
var ConditionBuilder = require('./ConditionBuilder')
var UpdateExpressionBuilder = require('./UpdateExpressionBuilder')
var typeUtil = require('./typeUtil')
/**
* @param {Object} options
* @constructor
*/
function DynamoRequest(options) {
this._options = options || {}
this.data = {_requestBuilder: this}
this._nameMutex = {count: 0}
this._keyConditionBuilder = null
this._filterBuilder = null
this._conditionBuilder = null
this._updateExpressionBuilder = null
}
DynamoRequest.prototype.setRequestItems = function (keys) {
this.data.RequestItems = keys
return this
}
DynamoRequest.prototype.setTable = function (prefix, table) {
this.data.TableName = (prefix ? prefix : '') + table
return this
}
/**
* For putItem requests, the item we want to write to DynamoDB
* @param {Object} item
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setItem = function (item) {
if (item) {
this.data.Item = typeUtil.packObjectOrArray(item)
}
return this
}
DynamoRequest.prototype.setParallelScan = function (segment, totalSegments) {
if (typeof segment != 'undefined' && totalSegments) {
this.data.Segment = segment
this.data.TotalSegments = totalSegments
}
return this
}
/**
* @return {DynamoRequest}
*/
DynamoRequest.prototype.returnConsumedCapacity = function () {
this.data.ReturnConsumedCapacity = 'TOTAL'
return this
}
/**
* @param {!Object} attributeUpdates
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setUpdates = function (attributeUpdates) {
if (attributeUpdates) {
this._updateExpressionBuilder = UpdateExpressionBuilder.populateUpdateExpression(
this.data, attributeUpdates, this._nameMutex)
}
return this
}
DynamoRequest.prototype.setReturnValues = function (returnValues) {
if (returnValues) {
this.data.ReturnValues = returnValues
}
return this
}
/**
* @param {Array.<ConditionBuilder>} conditions An array of conditions, possibly null to indicate
* no conditions.
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setKeyConditions = function (conditions) {
if (conditions) {
this._keyConditionBuilder = ConditionBuilder.populateExpressionField(
this.data, 'KeyConditionExpression', conditions, this._nameMutex)
}
return this
}
/**
* @param {Array.<ConditionBuilder>} conditions An array of conditions, possibly null to indicate
* no conditions.
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setQueryFilter = function (conditions) {
if (conditions) {
this._filterBuilder = ConditionBuilder.populateExpressionField(
this.data, 'FilterExpression', conditions, this._nameMutex)
}
return this
}
/**
* @param {Array.<ConditionBuilder>} conditions An array of conditions, possibly null to indicate
* no conditions.
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setScanFilter = function (conditions) {
if (conditions) {
this._filterBuilder = ConditionBuilder.populateExpressionField(
this.data, 'FilterExpression', conditions, this._nameMutex)
}
return this
}
/**
* @param {Array.<ConditionBuilder>} conditions An array of conditions, possibly null to indicate
* no conditions.
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setExpected = function (conditions) {
if (conditions) {
this._conditionBuilder = ConditionBuilder.populateExpressionField(
this.data, 'ConditionExpression', conditions, this._nameMutex)
}
return this
}
DynamoRequest.prototype.setConsistent = function (isConsistent) {
this.data.ConsistentRead = !!isConsistent
return this
}
/**
* For query and scan requests, the number of items to iterate over (before the filter)
* @param {number} limit
* @return {DynamoRequest}
*/
DynamoRequest.prototype.setLimit = function (limit) {
if (limit) {
this.data.Limit = limit
}
return this
}
DynamoRequest.prototype.setHashKey = function (key) {
this.data.Key = {}
if (!key) throw new Error('A hash key is required')
this.data.Key[key.name] = typeUtil.valueToObject(key.val)
return this
}
DynamoRequest.prototype.setRangeKey = function (key) {
if (!this.data.Key) throw new Error('The hash key must be set first')
if (!key) throw new Error('A range key is required')
this.data.Key[key.name] = typeUtil.valueToObject(key.val)
return this
}
DynamoRequest.prototype.setStartKey = function (key) {
if (key) {
this.data.ExclusiveStartKey = typeUtil.packObjectOrArray(key)
}
return this
}
DynamoRequest.prototype.selectAttributes = function (attributes) {
if (attributes) {
if (!this.data.ExpressionAttributeNames) {
this.data.ExpressionAttributeNames = {}
}
typeUtil.extendAttributeNames(this.data.ExpressionAttributeNames, typeUtil.buildAttributeNames(attributes))
this.data.ProjectionExpression = attributes.map(function (attr) {
return typeUtil.getAttributeAlias(attr)
}).join(',')
}
return this
}
DynamoRequest.prototype.setIndexName = function (indexName) {
if(indexName) {
this.data.IndexName = indexName
}
return this
}
DynamoRequest.prototype.setBatchTableAttributes = function (tablePrefix, attributes) {
this._setPerTableValue(tablePrefix, attributes, 'AttributesToGet')
return this
}
DynamoRequest.prototype.setBatchTableConsistent = function (tablePrefix, isConsistentValues) {
this._setPerTableValue(tablePrefix, isConsistentValues, 'ConsistentRead')
return this
}
DynamoRequest.prototype._setPerTableValue = function (tablePrefix, values, propertyName) {
if (values) {
if (!this.data.RequestItems) this.data.RequestItems = {}
for (var key in values) {
var tableName = (tablePrefix || '') + key
if (!this.data.RequestItems[tableName]) this.data.RequestItems[tableName] = {}
this.data.RequestItems[tableName][propertyName] = values[key]
}
}
}
/**
* Takes a map items to Dynamo request format. requestItems is an object containing an array
* of Primary Keys for each table. For example:
*
* { userTable : [{userId: '1234', column: '@'} ... ]}
*/
DynamoRequest.prototype.setBatchRequestItems = function (tablePrefix, requestItems) {
if (!this.data.RequestItems) this.data.RequestItems = {}
for (var tableName in requestItems) {
if (!Array.isArray(requestItems[tableName])) {
throw new Error('RequestedItems not an array, for table=' + tableName)
}
var tableNameWithPrefix = (tablePrefix || '') + tableName
if (!this.data.RequestItems[tableNameWithPrefix]) this.data.RequestItems[tableNameWithPrefix] = {}
if (!this.data.RequestItems[tableNameWithPrefix].Keys) this.data.RequestItems[tableNameWithPrefix].Keys = []
for (var i = 0; i < requestItems[tableName].length; i++) {
var keys = requestItems[tableName][i]
var dynamoKeys = {}
for (var key in keys) dynamoKeys[key] = typeUtil.valueToObject(keys[key])
this.data.RequestItems[tableNameWithPrefix].Keys.push(dynamoKeys)
}
}
return this
}
DynamoRequest.prototype.scanForward = function (isForward) {
this.data.ScanIndexForward = typeof isForward === 'undefined' || isForward
return this
}
DynamoRequest.prototype.getCount = function () {
this.data.Select = "COUNT"
return this
}
DynamoRequest.prototype.build = function () {
// Dynamo doesn't like it when alias objects are empty.
if (this.data.ExpressionAttributeNames &&
!Object.keys(this.data.ExpressionAttributeNames).length) {
delete this.data.ExpressionAttributeNames
}
if (this.data.ExpressionAttributeValues &&
!Object.keys(this.data.ExpressionAttributeValues).length) {
delete this.data.ExpressionAttributeValues
}
// Dynamo doesn't like it when conditions are empty
if (!this.data.KeyConditionExpression) {
delete this.data.KeyConditionExpression
}
if (!this.data.FilterExpression) {
delete this.data.FilterExpression
}
if (!this.data.ConditionExpression) {
delete this.data.ConditionExpression
}
if (!this.data.UpdateExpression) {
delete this.data.UpdateExpression
}
return this.data
}
module.exports = DynamoRequest
================================================
FILE: lib/DynamoResponse.js
================================================
// Copyright 2013. The Obvious Corporation.
var typeUtil = require('./typeUtil')
/**
* @param {?string} tablePrefix
* @param {Object} output Response JSON
* @param {?function(Object, number?): Promise} repeatWithStartKey Make the same query with a different start key.
* Only valid for Query/Scan results.
* @constructor
*/
var DynamoResponse = function (tablePrefix, output, repeatWithStartKey) {
/** @private {?function(Object, number?): Promise} */
this._repeatWithStartKey = repeatWithStartKey
this.ConsumedCapacityUnits = output.ConsumedCapacityUnits
this.Count = output.Count
this.ProcessingStartedAt = output.ProcessingStartedAt
this.RequestLatencyMs = output.RequestLatencyMs
this.ByteLength = output.ByteLength
this.LastEvaluatedKey = undefined
if (output.LastEvaluatedKey) {
this.LastEvaluatedKey = typeUtil.unpackObjectOrArray(output.LastEvaluatedKey)
}
// For batchGet
this.UnprocessedKeys = undefined
var table
if (output.UnprocessedKeys) {
var unprocessed = {}
for (table in output.UnprocessedKeys) {
unprocessed[table] = typeUtil.unpackObjectOrArray(output.UnprocessedKeys[table].Keys)
}
this.UnprocessedKeys = unprocessed
}
this.ConsumedCapacity = undefined
if (output.ConsumedCapacity) {
if (!Array.isArray(output.ConsumedCapacity)) {
output.ConsumedCapacity = [output.ConsumedCapacity]
}
var capacity = {}
output.ConsumedCapacity.forEach(function (outputCapacity) {
capacity[getOriginalTableName(tablePrefix, outputCapacity.TableName)] = outputCapacity.CapacityUnits
})
this.ConsumedCapacity = capacity
}
this.result = undefined
// for Query and Scan, 'result' is {Array.<Object>}
if (output.Items) {
this.result = typeUtil.unpackObjectOrArray(output.Items)
// for GetItem, 'result' is {Object}
} else if (output.Item) {
this.result = typeUtil.unpackObjectOrArray(output.Item)
// for DeleteItem, PutItem and UpdateItem, 'result' is {Object}
} else if (output.UpdatedAttributes || output.Attributes) {
this.result = typeUtil.unpackObjectOrArray(output.UpdatedAttributes)
this.previous = typeUtil.unpackObjectOrArray(output.Attributes)
// for BatchGetItem, 'result' is {Object.<string, Object>}
} else if (output.Responses) {
this.result = {}
for (table in output.Responses) {
var origTableName = getOriginalTableName(tablePrefix, table)
this.result[origTableName] = typeUtil.unpackObjectOrArray(output.Responses[table])
}
}
return this
}
/**
* @return {boolean} If the query or scan has more results.
*/
DynamoResponse.prototype.hasNext = function () {
return !!(this.LastEvaluatedKey && this._repeatWithStartKey)
}
/**
* @param {?number} opt_limit The number of items to check
* @return {Promise.<DynamoResponse>}
*/
DynamoResponse.prototype.next = function (opt_limit) {
if (!this.hasNext()) throw new Error('No more results')
return this._repeatWithStartKey(/** @type {Object} */ (this.LastEvaluatedKey), opt_limit)
}
function getOriginalTableName (tablePrefix, tableName) {
return tablePrefix ? tableName.substr(tablePrefix.length) : tableName
}
module.exports = DynamoResponse
================================================
FILE: lib/FakeDynamo.js
================================================
var Q = require('kew')
var typ = require('typ')
var typeUtil = require('./typeUtil')
var localUpdater = require('./localUpdater')
var util = require('util')
/** @const */
var MAX_GET_BATCH_ITEM_SIZE = 100
/** @const */
var MAX_KEY_SIZE = 1024
function forEachKeyCondition(data, callback) {
return data._requestBuilder._keyConditionBuilder.visitExpressionsPostOrder(function (expr) {
if (expr.op && expr.args[0] && expr.args[0].name) {
callback(expr, expr.args[0].name)
}
})
}
function getKeyConditionByName(data, name) {
var result = null
forEachKeyCondition(data, function (condition, key) {
if (key == name) {
result = condition
}
})
return result
}
function forEachFilterCondition(data, callback) {
if (!data._requestBuilder._filterBuilder) return
return data._requestBuilder._filterBuilder.visitExpressionsPostOrder(function (expr) {
if (expr.op && expr.args[0] && expr.args[0].name) {
callback(expr, expr.args[0].name)
}
})
}
function getKeyConditionFn(data) {
return data._requestBuilder._keyConditionBuilder.buildFilterFn()
}
function getFilterFn(data) {
var filterBuilder = data._requestBuilder._filterBuilder
return filterBuilder ? filterBuilder.buildFilterFn() : function() { return true }
}
function getConditionFn(data) {
var builder = data._requestBuilder._conditionBuilder
return builder ? builder.buildFilterFn() : function() { return true }
}
/**
* @param {string} name
* @param {string} message
* @constructor
* @extends {Error}
*/
function DynamoError(name, message) {
this.name = name
this.code = name
this.message = message
}
util.inherits(DynamoError, Error)
/**
* Fake table which can be used to store mock data and run queries
*
* @constructor
* @param {string} name table name
*/
function FakeTable(name) {
this.name = name
this.primaryKey = {
hash: null,
range: null
}
this.gsiDefinitions = []
this.data = {}
/** @private {number} */
this._maxResultSetSize = MAX_GET_BATCH_ITEM_SIZE
}
/**
* Set a hard upper limit at the amount of results that queries and scans on this table can return.
* @param {number} maxResultSetSize
*/
FakeTable.prototype.setMaxResultSetSize = function (maxResultSetSize) {
this._maxResultSetSize = maxResultSetSize
}
/**
* Set data for the mock table
*
* @param {!Object} data
* @return {FakeTable} the mock table instance
*/
FakeTable.prototype.setData = function(data) {
this.data = data
return this
}
/**
* Get all data from the mock table
*
* @return {Object} data
*/
FakeTable.prototype.getData = function() {
return this.data
}
/**
* Set the hash attribute from the primary key for this table
*
* @param {string} name the name of the attribute
* @param {string} type the type of the attribute ('S', 'N', etc.)
* @return {FakeTable} the current FakeTable instance
*/
FakeTable.prototype.setHashKey = function(name, type) {
this.primaryKey.hash = {
name: name,
type: type
}
return this
}
/**
* Set the range attribute from the primary key for this table
*
* @param {string} name the name of the attribute
* @param {string} type the type of the attribute ('S', 'N', etc.)
* @return {FakeTable} the current FakeTable instance
*/
FakeTable.prototype.setRangeKey = function(name, type) {
this.primaryKey.range = {
name: name,
type: type
}
return this
}
/**
* Set GSI definitions for this table.
*
* @param {Array.<{hash:({name:(string), type:(string)}), ?range:({name:(string), type:(string)})}>} gsiDefinitions
* @return {FakeTable} the current FakeTable instance
*/
FakeTable.prototype.setGsiDefinitions = function(gsiDefinitions) {
this.gsiDefinitions = gsiDefinitions
return this
}
/**
* Run a query against the mock data
*
* @param {Object} data
* @return {Q.Promise}
*/
FakeTable.prototype.query = function(data) {
var indexedKeyName
if (data.IndexName) {
// Global Secondary Index if the index name has three or more
// parts (separated by '-')
var indexParts = data.IndexName.split('-')
var probableGSIIndex = indexParts.length >= 3
if (probableGSIIndex) {
return this._queryGlobalSecondaryIndex(data)
}
// Extract the range key for Local Secondary Indexes
forEachKeyCondition(data, function (condition, key) {
if (key !== this.primaryKey.hash.name) {
indexedKeyName = key
}
}.bind(this))
} else { // no index specified, mock query original table
indexedKeyName = this.primaryKey.range.name
}
this._validateFilterKeys(data, this.primaryKey.hash.name, indexedKeyName)
// retrieve the hashkey
var hash = data.HashKeyValue ||
getKeyConditionByName(data, this.primaryKey.hash.name).args[1].evaluate()
var indexedKeyValues = data.IndexName ? this._getIndexedKeyValuesForHash(hash, indexedKeyName) : this._getRangeKeyValuesForHash(hash)
// maybe add some stuff for secondary indices like
// http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html
// var indexName = data.IndexName || 'primary'
var keyConditionFn = getKeyConditionFn(data)
var queryFilterFn = getFilterFn(data)
var results = []
var isBackward = data.ScanIndexForward === false
for (var i = 0; i < indexedKeyValues.length; i++) {
var j = isBackward ? indexedKeyValues.length - 1 - i : i
var currentRows
if (data.IndexName) {
currentRows = this._getItemsByIndex({
hash: hash,
indexedKey: indexedKeyValues[j]
}, indexedKeyName)
} else {
currentRows = [this._getItemByKey({
hash: hash,
range: indexedKeyValues[j]
})]
}
currentRows.forEach(function (currentRow) {
if (keyConditionFn(currentRow) && queryFilterFn(currentRow)) {
// TODO(artem): if no range key is specified, then all of the items
// with the same hash key need to be returned
results.push(currentRow)
}
})
}
return this._formatPagedResults(results, data)
}
/**
* Query the mock data as if it respects Global Secondary Index.
*
* It does not matter, for the purposes of FakeDynamo, which KeyCondition is
* the hash key or range key. We simply do a scan to find all matches for both
* conditions.
*/
FakeTable.prototype._queryGlobalSecondaryIndex = function(data) {
var indexParts = data.IndexName.split('-')
// Try to find a GSI that matches both hash and range keys
var pickedGsi = this.gsiDefinitions.find(function (gsi) {
return gsi.range && indexParts.indexOf(gsi.hash.name) >= 0 &&
indexParts.indexOf(gsi.range.name) >= 0
})
// If not found, try to find a GSI that only matches the hash key
if (!pickedGsi) {
pickedGsi = this.gsiDefinitions.find(function (gsi) {
return indexParts.indexOf(gsi.hash.name) >= 0
})
}
if (!pickedGsi) {
throw new Error('No gsi found for ' + data.IndexName)
}
var hashKey = pickedGsi.hash.name
var rangeKey = pickedGsi.range && pickedGsi.range.name
this._validateFilterKeys(data, hashKey, rangeKey)
// store keys, values, and conditions from data for easy access in scan
var keyConditionFn = getKeyConditionFn(data)
var queryFilterFn = getFilterFn(data)
var results = []
for (var primary_key in this.data) {
for (var range_key in this.data[primary_key]) {
var currentRow = this.data[primary_key][range_key]
if (keyConditionFn(currentRow) && queryFilterFn(currentRow)) {
results.push(currentRow)
}
}
}
// sort by the range key of the GSI (if there is any)
if (rangeKey) {
results.sort(function (a, b) {
return data.ScanIndexForward ?
a[rangeKey] - b[rangeKey] :
b[rangeKey] - a[rangeKey]
})
}
return this._formatPagedResults(results, data)
}
/**
* Run a scan against the mock data
*
* @param {Object} data
* @return {Q.Promise}
*/
FakeTable.prototype.scan = function(data) {
var sortedData = this._sortDataByPrimaryKey()
var scanFilterFn = getFilterFn(data)
var results = []
for (var primary_key in sortedData) {
var currentRow = sortedData[primary_key]
if (scanFilterFn(currentRow)) {
results.push(currentRow)
}
}
return this._formatPagedResults(results, data)
}
/**
* Filter and format the results of a query or scan.
* @param {Array} results
* @param {Object} queryData
* @return {Q.Promise.<Object>}
*/
FakeTable.prototype._formatPagedResults = function (results, queryData) {
var filteredResults = this._filterByExclusiveStartKey(results, queryData)
var limit = Math.min(queryData.Limit || Infinity, this._maxResultSetSize)
var ret = {}
if (filteredResults.length > limit) {
ret.LastEvaluatedKey = this._createLastEvaluatedKey(filteredResults[limit - 1])
filteredResults.length = limit
}
// If the client only requires the count, return the only the count
if (queryData.Select === 'COUNT') {
ret.Count = filteredResults.length
return Q.resolve(ret)
}
var attributes = this._getAttributesFromData(queryData)
ret.Items = typeUtil.packObjectOrArray(filteredResults, attributes)
ret.ConsumedCapacity = this._getCapacityBlob(ret.Items.length)
return Q.resolve(ret)
}
/**
* @param {Array} sortedDbData
* @param {Object} queryData
* @return {Array} A filtered data array.
*/
FakeTable.prototype._filterByExclusiveStartKey = function (sortedDbData, queryData) {
var hashKeyName = this.primaryKey.hash.name
var rangeKeyName = this.primaryKey.range.name
var exclusiveStartKey = queryData.ExclusiveStartKey ?
typeUtil.unpackObjectOrArray(queryData.ExclusiveStartKey) : undefined
if (!exclusiveStartKey) {
return sortedDbData
}
var startHashKey = exclusiveStartKey[hashKeyName]
var startRangeKey = exclusiveStartKey[rangeKeyName]
var matchIndex = -1
sortedDbData.forEach(function (item, i) {
var itemHashKey = item[hashKeyName]
var itemRangeKey = item[rangeKeyName]
if (itemHashKey == startHashKey && itemRangeKey == startRangeKey) {
matchIndex = i
}
})
return sortedDbData.slice(matchIndex + 1)
}
/**
* @param {Object} item
* @return {Object|undefined}
*/
FakeTable.prototype._createLastEvaluatedKey = function (item) {
var primaryKeyAttribute = {}
primaryKeyAttribute[this.primaryKey.hash.name] = true
primaryKeyAttribute[this.primaryKey.range.name] = true
return typeUtil.packObjectOrArray(item, primaryKeyAttribute)
}
/**
* Put an item into the mock data
*
* @param {Object} data
* @return {Q.Promise}
*/
FakeTable.prototype.putItem = function(data) {
var key = this._extractKey(data)
var item = this._getItemByKey(key)
this._checkExpected(getConditionFn(data), item)
// create the item to store
var obj = {}
for (var field in data.Item) {
obj[field] = typeUtil.objectToValue(data.Item[field])
}
// store the item
this._putItemAtKey(key, obj)
// done (ALL_OLD only returns ConsumedCapacity)
return Q.resolve({
Attributes: typeUtil.packObjectOrArray(item),
ConsumedCapacity: this._getCapacityBlob(1)
})
}
/**
* Update an item in the mock data
*
* @param {Object} data
* @return {Q.Promise}
*/
FakeTable.prototype.updateItem = function(data) {
var key = this._extractKey(data)
var item = this._getItemByKey(key)
// run the conditional if it exists
this._checkExpected(getConditionFn(data), item)
var itemExists = !!item
// if the item doesn't exist, create a temp item which we may save later
if (!itemExists) {
item = {}
item[this.primaryKey.hash.name] = key.hash
if (this.primaryKey.range.name) {
item[this.primaryKey.range.name] = key.range
}
}
var oldItem = typeUtil.packObjectOrArray(item)
var newItem = localUpdater.update(oldItem, data._requestBuilder._updateExpressionBuilder._attributes)
// store the item
this._putItemAtKey(key, typeUtil.unpackObjectOrArray(newItem))
// done (ALL_OLD only returns ConsumedCapacity)
return Q.resolve({
Attributes: itemExists ? oldItem : null,
ConsumedCapacity: this._getCapacityBlob(1)
})
}
/**
* Delete an item from the mock data
*
* @param {Object} data
* @return {Q.Promise}
*/
FakeTable.prototype.deleteItem = function(data) {
var key = this._extractKey(data)
var item = this._getItemByKey(key)
this._checkExpected(getConditionFn(data), item)
this._putItemAtKey(key, undefined)
// done (ALL_OLD only returns ConsumedCapacity)
return Q.resolve({
Attributes: typeUtil.packObjectOrArray(item),
ConsumedCapacity: this._getCapacityBlob(1)
})
}
/**
* Get an item from the mock data
*
* @param {Object} data
* @return {Q.Promise}
*/
FakeTable.prototype.getItem = function(data) {
var key = this._extractKey(data)
var item = this._getItemByKey(key)
var attributes = this._getAttributesFromData(data)
return Q.resolve({
Item: typeUtil.packObjectOrArray(item, attributes),
ConsumedCapacity: this._getCapacityBlob(1)
})
}
/**
* Get a JSON blob for the consumed capacity data.
*/
FakeTable.prototype._getCapacityBlob = function(n) {
return {
CapacityUnits: n,
TableName: this.name
}
}
/**
* Retrieve list of selected attributes from request data.
* If data.AttributesToGet is falsy, that implies that all attributes
* should be returned.
*
* @param {Object} data
* @return {Object|undefined} map of attribute names to a boolean true value
*/
FakeTable.prototype._getAttributesFromData = function(data) {
if (data.AttributesToGet) {
var attributes = {}
for (var i = 0; i < data.AttributesToGet.length; i++) {
attributes[data.AttributesToGet[i]] = true
}
return attributes
} else return undefined // Todo (gianni): this seems a fragile indicator to me
}
/**
* For mutating operations, check if the conditional is valid
*
* @param {function(Object)} filterFn A filter function that returns false if the item is filtered, true if allowed.
* @param {Object|undefined} currentData current data for the data being changed
*/
FakeTable.prototype._checkExpected = function(filterFn, currentData) {
if (!filterFn(currentData)) {
throw new DynamoError('ConditionalCheckFailedException', 'Values are different than expected.')
}
}
/**
* Throw an error if a key is too big.
*
* @param {{hash:(number|string), range:(number|string)}} key
*/
FakeTable.prototype._validateKeySize = function (key) {
var size = Buffer.byteLength(String(key.hash), 'utf8')
if (this.primaryKey.range) {
size += Buffer.byteLength(String(key.range), 'utf8')
}
if (size > MAX_KEY_SIZE) {
throw new DynamoError('ValidationException',
'One or more parameter values were invalid: ' +
'Aggregated size of all range keys has exceeded the size limit of 1024 bytes')
}
}
/**
* Throw an error if a filter expression uses a primary key
*
* @param {Object} data
* @param {string} partitionKey
* @param {?string} opt_sortKey
*/
FakeTable.prototype._validateFilterKeys = function (data, partitionKey, opt_sortKey) {
forEachFilterCondition(data, function (condition, key) {
if (key === partitionKey || key === opt_sortKey) {
throw new DynamoError(
'ValidationException',
'Filter Expression can only contain non-primary key attributes: Primary key attribute: ' + key
)
}
})
}
/**
* Get an item by its key
*
* @param {{hash:(number|string), range:(number|string)}} key
* @return {Object|undefined} object
*/
FakeTable.prototype._getItemByKey = function(key) {
this._validateKeySize(key)
if (!this.data[key.hash]) return undefined
return this.primaryKey.range ? this.data[key.hash][key.range] : this.data[key.hash]
}
/**
* Gets an item by the Range Key (LSI)
*/
FakeTable.prototype._getItemsByIndex = function(key, indexedKey) {
this._validateKeySize(key)
if (!this.data[key.hash]) return undefined
var items = []
for (var rangeKey in this.data[key.hash]) {
if (this.data[key.hash][rangeKey][indexedKey] === key.indexedKey) {
items.push(this.data[key.hash][rangeKey])
}
}
return items
}
/**
* Put an item at its key
*
* @param {{hash:(number|string), range:(number|string)}} key
* @param {*} obj
*/
FakeTable.prototype._putItemAtKey = function(key, obj) {
this._validateKeySize(key)
if (this.primaryKey.range) {
if (!this.data[key.hash]) this.data[key.hash] = {}
if (!obj) {
delete this.data[key.hash][key.range]
} else {
this.data[key.hash][key.range] = obj
}
} else {
if (!obj) {
delete this.data[key.hash]
} else {
this.data[key.hash] = obj
}
}
}
/**
* Retrieve all range keys for a specified hash key
*
* @param {string} hash
* @return {Array.<string>}
*/
FakeTable.prototype._getRangeKeyValuesForHash = function(hash) {
if (!this.data[hash]) return []
var keys = Object.keys(this.data[hash])
this._sortKeys(keys)
return keys
}
/**
* Retrieve all attribute values for a specified hash key and attribute
*
* @param {string} hash
* @return {Array.<string>}
*/
FakeTable.prototype._getIndexedKeyValuesForHash = function(hash, attr) {
if (!this.data[hash]) return []
if (!attr || attr === this.primaryKey.range.name) {
return this._getRangeKeyValuesForHash(hash)
}
var keys = []
for (var key in this.data[hash]) {
var indexedKey = this.data[hash][key][attr]
if (keys.indexOf(indexedKey) < 0) {
keys.push(indexedKey)
}
}
this._sortKeys(keys)
return keys
}
/**
* Sorts the array of keys in place. This method assumes that all the keys are of the same type.
*/
FakeTable.prototype._sortKeys = function (keys) {
if (keys.length > 0 && !isNaN(keys[0])) {
keys.sort(function(a, b) { return a - b })
} else {
keys.sort()
}
}
/**
* Extract a range key from request data
*
* @param {Object} data request data
* @return {{hash:(number|string), range:(number|string)}}
*/
FakeTable.prototype._extractKey = function(data) {
var key = {}
if (data.Key) {
key.hash = data.Key[this.primaryKey.hash.name][this.primaryKey.hash.type]
if (this.primaryKey.range) {
key.range = data.Key[this.primaryKey.range.name][this.primaryKey.range.type]
}
} else {
key.hash = data.Item[this.primaryKey.hash.name][this.primaryKey.hash.type]
if (this.primaryKey.range) {
key.range = data.Item[this.primaryKey.range.name][this.primaryKey.range.type]
}
}
return key
}
/**
* Provides an array of the original data sorted by the Primary key, used as a Hash key here.
*/
FakeTable.prototype._sortDataByPrimaryKey = function() {
var sortedData = []
var hashKeys = Object.keys(this.data).sort()
for (var i = 0; i < hashKeys.length; i++) {
var rangeKeys = Object.keys(this.data[hashKeys[i]]).sort()
for (var j = 0; j < rangeKeys.length; j++) {
sortedData.push(this.data[hashKeys[i]][rangeKeys[j]])
}
}
return sortedData
}
/**
* Get a partial descirption of the table, format as specified by aws docs.
* @return {Object} table description
*/
FakeTable.prototype.describeTable = function() {
return {
'Table': {
'AttributeDefinitions': [
{'AttributeName': this.primaryKey.hash.name, 'AttributeType': this.primaryKey.hash.type},
{'AttributeName': this.primaryKey.range.name, 'AttributeType': this.primaryKey.range.type}
],
'KeySchema': [
{'AttributeName': this.primaryKey.hash.name, 'KeyType': 'HASH'},
{'AttributeName': this.primaryKey.range.name, 'KeyType': 'RANGE'}
],
'TableName': this.name,
'TableStatus': 'ACTIVE',
'ProvisionedThroughput': {
'ReadCapacityUnits': 1,
'WriteCapacityUnits': 1
}
}
}
}
/**
* Create a Fake Dynamo client
* @constructor
*/
function FakeDynamo() {
this.tables = {}
this.resetStats()
this.isFakeDynamo = true
}
/**
* Get stats counters
*/
FakeDynamo.prototype.getStats = function() {
return this.stats
}
/**
* Reset stats counters
*/
FakeDynamo.prototype.resetStats = function() {
this.stats = {
putItem: 0,
getItem: 0,
deleteItem: 0,
updateItem: 0,
query: 0,
batchGetItem: 0,
batchGetItemCount: []
}
}
/**
* Create a new table in the Fake Dynamo client
*
* @param {string} name table name
* @return {FakeTable} a mock table
*/
FakeDynamo.prototype.createTable = function(name) {
return this.tables[name] = new FakeTable(name)
}
/**
* Get a mock table if it exists
*
* @param {string} name
* @return {FakeTable} the mock table
*/
FakeDynamo.prototype.getTable = function(name) {
if (!this.tables[name]) throw new Error('Table "' + name + '" has not been mocked!')
return this.tables[name]
}
function performOp(operation) {
return function (data, callback) {
try {
this.stats[operation] += 1
this.getTable(data.TableName)[operation](data)
.fail(function (e) {
callback(e)
})
.then(function (data) {
callback(null, data)
})
} catch (e) {
callback(e)
}
}
}
FakeDynamo.prototype.putItem = performOp('putItem')
FakeDynamo.prototype.getItem = performOp('getItem')
FakeDynamo.prototype.deleteItem = performOp('deleteItem')
FakeDynamo.prototype.updateItem = performOp('updateItem')
FakeDynamo.prototype.query = performOp('query')
FakeDynamo.prototype.scan = performOp('scan')
/**
* Respond a batchGetItem request.
*
* @param {Object} data The request data
* @return {Q.Promise.<Object>} the fetched items in Dynamo format
*/
FakeDynamo.prototype.batchGetItem = function (data, callback) {
var promises = []
var resp = {Responses: {}, UnprocessedKeys: {}, ConsumedCapacity: []}
this.stats.batchGetItem += 1
try {
// the number of fetched object across all tables.
var count = 0
for (var tableName in data.RequestItems) {
var keys = data.RequestItems[tableName].Keys
// Check for duplicates
var keySet = {}
keys.forEach(function (k) {
keySet[JSON.stringify(k)] = true
})
if (Object.keys(keySet).length != keys.length) {
throw new DynamoError('ValidationException', 'Provided list of item keys contains duplicates')
}
var table = this.getTable(tableName)
var limit = Math.min(table._maxResultSetSize, MAX_GET_BATCH_ITEM_SIZE)
var countPerTable = 0
for (var i = 0; i < keys.length; i++) {
countPerTable++
count++
if (count <= limit) {
promises.push(
table.getItem({Item: keys[i], AttributesToGet: data.RequestItems[tableName].AttributesToGet})
.then(function (data) {
if (!(tableName in resp.Responses)) {
resp.Responses[tableName] = []
}
if (!typ.isNullish(data.Item)) {
resp.Responses[tableName].push(data.Item)
}
})
)
} else {
if (!(tableName in resp.UnprocessedKeys)) {
resp.UnprocessedKeys[tableName] = {Keys: []}
}
resp.UnprocessedKeys[tableName].Keys.push(keys[i])
}
}
resp.ConsumedCapacity.push(table._getCapacityBlob(countPerTable))
}
this.stats.batchGetItemCount.push(count)
return Q.all(promises)
.fail(function (e) {
callback(e)
})
.then(function () {
callback(null, resp)
})
} catch (e) {
callback(e)
}
}
/**
* Get the partial description of a table, format as specified by aws docs.
* @param {Object} tableData data related to the table
* @return {Q.Promise.<Object>} description of the named table
*/
FakeDynamo.prototype.describeTable = function (tableData, callback) {
var table = this.tables[tableData.TableName]
if (!table) callback(new Error('No such table in FakeDynamo: ' + tableData.TableName))
return callback(null, table.describeTable())
}
module.exports = FakeDynamo
================================================
FILE: lib/GetItemBuilder.js
================================================
var DynamoRequest = require('./DynamoRequest')
var Builder = require('./Builder')
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function GetItemBuilder(options) {
Builder.call(this, options)
}
require('util').inherits(GetItemBuilder, Builder)
GetItemBuilder.prototype.execute = function () {
var req = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.returnConsumedCapacity()
.setConsistent(this._isConsistent)
.setHashKey(this._hashKey, true)
.selectAttributes(this._attributes)
if (this._rangeKey) req.setRangeKey(this._rangeKey, true)
var queryData = req.build()
return this.request("getItem", queryData)
.then(this.prepareOutput.bind(this))
.fail(this.emptyResult)
.failBound(this.convertErrors, null, {data: queryData, isWrite: false})
}
module.exports = GetItemBuilder
================================================
FILE: lib/PutItemBuilder.js
================================================
var typ = require('typ')
var DynamoRequest = require('./DynamoRequest')
var DynamoResponse = require('./DynamoResponse')
var Builder = require('./Builder')
var typeUtil = require('./typeUtil')
var errors = require('./errors')
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function PutItemBuilder(options) {
Builder.call(this, options)
/** @private {string} */
this._returnValues = PutItemBuilder.RETURN_VALUES.ALL_OLD
}
require('util').inherits(PutItemBuilder, Builder)
PutItemBuilder.RETURN_VALUES = {
NONE: 'NONE',
ALL_OLD: 'ALL_OLD',
UPDATED_OLD: 'UPDATED_OLD',
ALL_NEW: 'ALL_NEW',
UPDATED_NEW: 'UPDATED_NEW'
}
PutItemBuilder.prototype.setReturnValues = function (val) {
if (!PutItemBuilder.RETURN_VALUES[val]) {
throw new errors.InvalidReturnValuesError(val)
}
this._returnValues = val
return this
}
PutItemBuilder.prototype.setItem = function (item) {
for (var key in item) {
if (typ.isNullish(item[key])) {
throw new Error("Field '" + key + "' on item must not be null or undefined")
}
}
this._item = item
return this
}
/** @override */
PutItemBuilder.prototype.prepareOutput = function (output) {
if (this._returnValues !== 'NONE') {
output.UpdatedAttributes = typeUtil.packObjectOrArray(this._item)
}
return new DynamoResponse(this.getPrefix(), output, null)
}
PutItemBuilder.prototype.execute = function () {
var queryData = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.returnConsumedCapacity()
.setItem(this._item)
.setExpected(this._conditions)
.setReturnValues(this._returnValues)
.build()
return this.request("putItem", queryData)
.then(this.prepareOutput.bind(this))
.failBound(this.convertErrors, null, {data: queryData, isWrite: true})
}
module.exports = PutItemBuilder
================================================
FILE: lib/QueryBuilder.js
================================================
var typ = require('typ')
var ConditionBuilder = require('./ConditionBuilder')
var DynamoRequest = require('./DynamoRequest')
var DynamoResponse = require('./DynamoResponse')
var Builder = require('./Builder')
var util = require('util')
var IndexNotExistError = require('./errors').IndexNotExistError
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function QueryBuilder(options) {
Builder.call(this, options)
/** @private {!ConditionBuilder} */
this._keyConditions = new ConditionBuilder()
}
util.inherits(QueryBuilder, Builder)
/**
* If this query runs on a local index or global index, set a
* function that can generate an index name based on query
* conditions.
*
* @param {function(string, string): string} fn The generator function
*/
QueryBuilder.prototype.setIndexNameGenerator = function (fn) {
this._indexNameGenerator = fn
return this
}
QueryBuilder.prototype.setHashKey = function (name, val) {
this._hashKeyName = name
this._keyConditions.filterAttributeEquals(name, val)
return this
}
QueryBuilder.prototype.setIndexRangeKeyWithoutCondition = function (name) {
this._rangeKeyName = name
return this
}
QueryBuilder.prototype.indexBeginsWith = function (name, prefix) {
this._rangeKeyName = name
this._keyConditions.filterAttributeBeginsWith(name, prefix)
return this
}
QueryBuilder.prototype.indexEqual =
QueryBuilder.prototype.indexEquals = function (name, val) {
this._rangeKeyName = name
this._keyConditions.filterAttributeEquals(name, val)
return this
}
QueryBuilder.prototype.indexLessThanEqual =
QueryBuilder.prototype.indexLessThanEquals = function (name, val) {
this._rangeKeyName = name
this._keyConditions.filterAttributeLessThanEqual(name, val)
return this
}
QueryBuilder.prototype.indexLessThan = function (name, val) {
this._rangeKeyName = name
this._keyConditions.filterAttributeLessThan(name, val)
return this
}
QueryBuilder.prototype.indexGreaterThanEqual =
QueryBuilder.prototype.indexGreaterThanEquals = function (name, val) {
this._rangeKeyName = name
this._keyConditions.filterAttributeGreaterThanEqual(name, val)
return this
}
QueryBuilder.prototype.indexGreaterThan = function (name, val) {
this._rangeKeyName = name
this._keyConditions.filterAttributeGreaterThan(name, val)
return this
}
QueryBuilder.prototype.indexBetween = function (name, val1, val2) {
this._rangeKeyName = name
this._keyConditions.filterAttributeBetween(name, val1, val2)
return this
}
QueryBuilder.prototype.setStartKey = function (key) {
this._startKey = key
return this
}
/**
* Set the index name of this query.
*
* @param {string} indexName
*/
QueryBuilder.prototype.setIndexName = function (indexName) {
this._indexName = indexName
return this
}
/** @override */
QueryBuilder.prototype.prepareOutput = function (output) {
return new DynamoResponse(
this.getPrefix(), output, this._repeatWithStartKey.bind(this))
}
/**
* @param {Object} nextKey
* @param {?number} opt_limit The number of items to check
* @return {Q.Promise.<DynamoResponse>}
* @private
*/
QueryBuilder.prototype._repeatWithStartKey = function (nextKey, opt_limit) {
if (!typ.isNullish(opt_limit)) {
this.setLimit(opt_limit)
}
return this.setStartKey(nextKey).execute()
}
QueryBuilder.prototype.execute = function () {
var query = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.returnConsumedCapacity()
.setQueryFilter(this._filters)
.setConsistent(this._isConsistent)
.setIndexName(this._indexName)
.setKeyConditions([this._keyConditions])
.setStartKey(this._startKey)
.selectAttributes(this._attributes, true)
.scanForward(this._shouldScanForward)
.setLimit(this._limit)
if (this._isCount) query.getCount()
if (this._rangeKeyCondition) {
query.setRangeKey.apply(query, this._rangeKeyCondition)
}
if (this._indexNameGenerator) {
var indexName = this._indexNameGenerator(this._hashKeyName, this._rangeKeyName)
if (!indexName) {
throw new IndexNotExistError(this._hashKeyName, this._rangeKeyName)
}
query.setIndexName(indexName)
}
var queryData = query.build()
return this.request('query', queryData)
.then(this.prepareOutput.bind(this))
.fail(this.emptyResults)
.failBound(this.convertErrors, null, {data: queryData, isWrite: false})
}
module.exports = QueryBuilder
================================================
FILE: lib/ScanBuilder.js
================================================
var typ = require('typ')
var DynamoRequest = require('./DynamoRequest')
var DynamoResponse = require('./DynamoResponse')
var Builder = require('./Builder')
var IndexNotExistError = require('./errors').IndexNotExistError
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function ScanBuilder(options) {
Builder.call(this, options)
}
require('util').inherits(ScanBuilder, Builder)
/**
* If this scan runs on a local index or global index, set a
* function that can generate an index name based on query
* conditions.
*
* @param {function(string, string): string} fn The generator function
*/
ScanBuilder.prototype.setIndexNameGenerator = function (fn) {
this._indexNameGenerator = fn
return this
}
ScanBuilder.prototype.setStartKey = function (key) {
this._startKey = key
return this
}
/**
* @param {number} segment
* @param {number} totalSegments
* @return {ScanBuilder}
*/
ScanBuilder.prototype.setParallelScan = function (segment, totalSegments) {
this._segment = segment
this._totalSegments = totalSegments
return this
}
/** @override */
ScanBuilder.prototype.prepareOutput = function (output) {
return new DynamoResponse(
this.getPrefix(), output, this._repeatWithStartKey.bind(this))
}
/**
* @param {Object} nextKey
* @param {?number} opt_limit The number of items to check
* @return {Q.Promise.<DynamoResponse>}
* @private
*/
ScanBuilder.prototype._repeatWithStartKey = function (nextKey, opt_limit) {
if (!typ.isNullish(opt_limit)) {
this.setLimit(opt_limit)
}
return this.setStartKey(nextKey).execute()
}
ScanBuilder.prototype.execute = function () {
var query = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.returnConsumedCapacity()
.setScanFilter(this._filters)
.setLimit(this._limit)
.setStartKey(this._startKey)
.selectAttributes(this._attributes)
.setParallelScan(this._segment, this._totalSegments)
if (this._indexNameGenerator) {
var rangeKeyName = this._rangeKey ? this._rangeKey.name : ''
var indexName = this._indexNameGenerator(this._hashKey.name, rangeKeyName)
if (!indexName) {
throw new IndexNotExistError(this._hashKey.name, this._rangeKey.name)
}
query.setIndexName(indexName)
}
var queryData = query.build()
return this.request("scan", queryData)
.then(this.prepareOutput.bind(this))
.fail(this.emptyResults)
.failBound(this.convertErrors, null, {data: queryData, isWrite: false})
}
module.exports = ScanBuilder
================================================
FILE: lib/UpdateBuilder.js
================================================
var typ = require('typ')
var util = require('util')
var DynamoRequest = require('./DynamoRequest')
var DynamoResponse = require('./DynamoResponse')
var Builder = require('./Builder')
var localUpdater = require('./localUpdater')
var typeUtil = require('./typeUtil')
var errors = require('./errors')
/**
* @param {Object} options
* @constructor
* @extends {Builder}
*/
function UpdateBuilder(options) {
Builder.call(this, options)
/** @private {!Object.<?>} */
this._attributeUpdates = {}
/** @private {boolean} */
this._enabledUpsert = false
/** @private {string} */
this._returnValues = UpdateBuilder.RETURN_VALUES.ALL_OLD
this._uniqueName = ''
}
util.inherits(UpdateBuilder, Builder)
UpdateBuilder.RETURN_VALUES = {
NONE: 'NONE',
ALL_OLD: 'ALL_OLD',
UPDATED_OLD: 'UPDATED_OLD',
ALL_NEW: 'ALL_NEW',
UPDATED_NEW: 'UPDATED_NEW'
}
UpdateBuilder.prototype.enableUpsert = function () {
this._enabledUpsert = true
return this
}
UpdateBuilder.prototype.setReturnValues = function (val) {
if (!UpdateBuilder.RETURN_VALUES[val]) {
throw new errors.InvalidReturnValuesError(val)
}
this._returnValues = val
return this
}
/**
* @param {string} key
* @param {boolean|number|string|Array.<number>|Array.<string>} val
*/
UpdateBuilder.prototype.putAttribute = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._attributeUpdates[key] = {
Value: typeUtil.valueToObject(val),
Action: 'PUT'
}
return this
}
/**
* @param {string} key
* @param {number|string|Array.<number>|Array.<string>} val
*/
UpdateBuilder.prototype.addToAttribute = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._attributeUpdates[key] = {
Value: typeUtil.valueToObject(val),
Action: 'ADD'
}
return this
}
/**
* @param {string} key
* @param {number|string|Array.<number>|Array.<string>} val
*/
UpdateBuilder.prototype.deleteFromAttribute = function (key, val) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
if (typ.isNullish(val)) throw new Error("Val must be defined")
this._attributeUpdates[key] = {
Value: typeUtil.valueToObject(val),
Action: 'DELETE'
}
return this
}
UpdateBuilder.prototype.deleteAttribute = function (key) {
if (typ.isNullish(key)) throw new Error("Key must be defined")
this._attributeUpdates[key] = {
Action: 'DELETE'
}
return this
}
/** @override */
UpdateBuilder.prototype.prepareOutput = function (output) {
if (this._returnValues !== 'NONE') {
var attributes = output.Attributes
if (!attributes) {
attributes = {}
attributes[this._hashKey.name] = typeUtil.valueToObject(this._hashKey.val)
if (this._rangeKey) {
attributes[this._rangeKey.name] = typeUtil.valueToObject(this._rangeKey.val)
}
}
output.UpdatedAttributes = localUpdater.update(attributes, this._attributeUpdates)
}
return new DynamoResponse(this.getPrefix(), output, null)
}
UpdateBuilder.prototype.execute = function () {
var req = new DynamoRequest(this.getOptions())
.setTable(this._tablePrefix, this._table)
.returnConsumedCapacity()
.setHashKey(this._hashKey, true)
.setUpdates(this._attributeUpdates)
.setExpected(this._conditions)
.setReturnValues(this._returnValues)
if (this._rangeKey) req.setRangeKey(this._rangeKey, true)
var queryData = req.build()
if ((!this._conditions || !this._conditions.length) && !this._enabledUpsert) {
console.warn("Update issued without conditions or .enableUpsert() called")
console.trace()
}
return this.request("updateItem", queryData)
.then(this.prepareOutput.bind(this))
.failBound(this.convertErrors, null, {data: queryData, isWrite: true})
}
module.exports = UpdateBuilder
================================================
FILE: lib/UpdateExpressionBuilder.js
================================================
var typeUtil = require('./typeUtil')
// The UpdateExpression API has different names for all the action types, but they're
// semantically the same as the old actions.
function getActionName(attr) {
switch (attr.Action) {
case 'PUT':
return 'SET'
case 'ADD':
return 'ADD'
case 'DELETE':
return attr.Value ? 'DELETE' : 'REMOVE'
default:
throw new Error('Unrecognized action ' + attr.Action)
}
}
/**
* Translates old AttributeValueUpdate format into the new UpdateExpression format.
*
* @param {Object} attributes
* @constructor
*/
function UpdateExpressionBuilder(attributes) {
this._attributes = attributes
this._uniqueName = ''
}
/**
* @param {Object} data
* @param {Array<UpdateExpressionBuilder>} attributes
* @param {{count: number}} nameMutex
* @return {UpdateExpressionBuilder}
*/
UpdateExpressionBuilder.populateUpdateExpression = function (data, attributes, nameMutex) {
var builder = new UpdateExpressionBuilder(attributes)
builder._assignUniqueNames(nameMutex)
if (!data.ExpressionAttributeNames) {
data.ExpressionAttributeNames = {}
}
typeUtil.extendAttributeNames(data.ExpressionAttributeNames, builder.buildAttributeNames())
if (!data.ExpressionAttributeValues) {
data.ExpressionAttributeValues = {}
}
typeUtil.extendAttributeValues(data.ExpressionAttributeValues, builder.buildAttributeValues())
data.UpdateExpression = builder.buildExpression()
return builder
}
/**
* @param {Object} nameMutex
*/
UpdateExpressionBuilder.prototype._assignUniqueNames = function (nameMutex) {
if (!nameMutex.count) {
nameMutex.count = 1
}
this._uniqueName = 'U' + nameMutex.count++
}
/** @return {Object} */
UpdateExpressionBuilder.prototype.buildAttributeNames = function () {
return typeUtil.buildAttributeNames(Object.keys(this._attributes))
}
/** @return {Object} */
UpdateExpressionBuilder.prototype.buildAttributeValues = function () {
var result = {}
Object.keys(this._attributes).map(function (key) {
var attr = this._attributes[key]
var value = attr.Value
if (value) {
result[this._getValueAlias(key)] = value
}
}, this)
return result
}
/**
* @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ExpressionPlaceholders.html#ExpressionAttributeValues
*/
UpdateExpressionBuilder.prototype._getValueAlias = function (key) {
if (!this._uniqueName) throw new Error('Names have not been assigned yet')
return ':V' + this._uniqueName + 'X' + key
}
/**
* @return {string} String suitable for UpdateExpression
* @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.Modifying.html
*/
UpdateExpressionBuilder.prototype.buildExpression = function () {
var keysByAction = {}
Object.keys(this._attributes).map(function (key) {
var attr = this._attributes[key]
var action = getActionName(attr)
if (!keysByAction[action]) {
keysByAction[action] = []
}
keysByAction[action].push(key)
}, this)
var groups = []
Object.keys(keysByAction).map(function (action) {
var keys = keysByAction[action]
groups.push(
action + ' ' +
keys.map(function (key) {
var attrAlias = typeUtil.getAttributeAlias(key)
var valueAlias = this._getValueAlias(key)
if (action == 'REMOVE') {
return attrAlias
} else if (action == 'SET') {
return attrAlias + ' = ' + valueAlias
} else if (action == 'ADD' || action == 'DELETE') {
return attrAlias + ' ' + valueAlias
} else {
throw new Error('Unrecognized action ' + action)
}
}, this).join(','))
}, this)
return groups.join(' ')
}
module.exports = UpdateExpressionBuilder
================================================
FILE: lib/common.js
================================================
var AWSName = {
REGION: ["us-east-1",
"us-west-1",
"us-west-2",
"eu-west-1",
"ap-northeast-1",
"ap-southeast-1",
"ap-southeast-2",
"sa-east-1"],
API_VERSION_2011: "2011-12-05",
API_VERSION_2012: "2012-08-10"
}
module.exports = {
AWSName: AWSName
}
================================================
FILE: lib/errors.js
================================================
// Copyright 2013. The Obvious Corporation.
var util = require('util')
/**
* @param {Object} data
* @param {string} msg
* @param {string} requestId
* @constructor
* @extends {Error}
*/
function ConditionalError(data, msg, requestId) {
Error.captureStackTrace(this)
this.data = data || {}
this.message = 'The conditional request failed'
this.details = msg
this.table = data.TableName || 'unknown'
this.requestId = requestId
}
util.inherits(ConditionalError, Error)
ConditionalError.prototype.type = 'ConditionalError'
ConditionalError.prototype.name = 'ConditionalError'
/**
* @param {Object} data
* @param {string} msg
* @param {boolean} isWrite
* @param {string} requestId
* @constructor
* @extends {Error}
*/
function ProvisioningError(data, msg, isWrite, requestId) {
Error.captureStackTrace(this)
this.data = data || {}
this.message = 'The level of configured provisioned throughput for the table was exceeded'
this.details = msg
this.table = data.TableName || 'unknown'
this.isWrite = isWrite
this.requestId = requestId
}
util.inherits(ProvisioningError, Error)
ProvisioningError.prototype.type = 'ProvisioningError'
ProvisioningError.prototype.name = 'ProvisioningError'
/**
* @param {Object} data
* @param {string} msg
* @param {boolean} isWrite
* @param {string} requestId
* @constructor
* @extends {Error}
*/
function ValidationError(data, msg, isWrite, requestId) {
Error.captureStackTrace(this)
this.data = data || {}
this.message = msg
this.table = data.TableName || 'unknown'
this.isWrite = isWrite
this.requestId = requestId
}
util.inherits(ValidationError, Error)
ValidationError.prototype.type = 'ValidationError'
ValidationError.prototype.name = 'ValidationError'
/**
* @param {string} hashKeyName
* @param {string} rangeKeyName
* @constructor
* @extends {Error}
*/
function IndexNotExistError(hashKeyName, rangeKeyName) {
Error.captureStackTrace(this)
this.hashKeyName = hashKeyName
this.rangeKeyName = rangeKeyName
}
util.inherits(IndexNotExistError, Error)
IndexNotExistError.prototype.type = 'IndexNotExistError'
IndexNotExistError.prototype.name = 'IndexNotExistError'
/**
* @param {string} returnValues
* @constructor
* @extends {Error}
*/
function InvalidReturnValuesError(returnValues) {
Error.captureStackTrace(this)
this.returnValues = returnValues
}
util.inherits(InvalidReturnValuesError, Error)
InvalidReturnValuesError.prototype.type = 'InvalidReturnValuesError'
InvalidReturnValuesError.prototype.name = 'InvalidReturnValuesError'
module.exports = {
ConditionalError: ConditionalError,
ProvisioningError: ProvisioningError,
ValidationError: ValidationError,
IndexNotExistError: IndexNotExistError,
InvalidReturnValuesError: InvalidReturnValuesError
}
================================================
FILE: lib/localUpdater.js
================================================
// Copyright 2015 A Medium Corporation.
var typeUtil = require('./typeUtil')
/**
* @param {Object} oldItem
* @return {Object}
*/
function _cloneObject (oldItem) {
var newItem = {}
for (var key in oldItem) {
if (oldItem.hasOwnProperty(key)) {
newItem[key] = oldItem[key]
}
}
return newItem
}
/**
* @param {Object} item
* @param {string} field
* @param {Object} update
*/
function _processDeleteAction (item, field, update) {
// From the dynamo docs:
//
// "If no value is specified, the attribute and its value are removed
// from the item. The data type of the specified value must match the
// existing value's data type.
//
// If a set of values is specified, then those values are subtracted
// from the old set. For example, if the attribute value was the set
// [a,b,c] and the DELETE action specified [a,c], then the final
// attribute value would be [b]. Specifying an empty set is an error."
if (typeUtil.objectIsEmpty(update.Value)) {
// delete a field if it exists
delete item[field]
} else if (typeUtil.objectIsNonEmptySet(update.Value)) {
// delete the items from the set if they exist
item[field] = typeUtil.deleteFromSet(item[field], update.Value)
if (!item[field]) delete item[field]
} else {
throw new Error('Trying to DELETE to a specified field from a non-set')
}
}
/**
* @param {Object} item
* @param {string} field
* @param {Object} update
*/
function _processPutAction (item, field, update) {
// Attribute values cannot be null. String and Binary type attributes must have
// lengths greater than zero. Set type attributes must not be empty. Requests
// with empty values will be rejected with a ValidationException exception.
if (!typeUtil.objectIsEmpty(update.Value)) {
// set the value of a field
item[field] = update.Value
} else {
throw new Error('Trying to PUT a field with an empty value')
}
}
/**
* @param {Object} item
* @param {string} field
* @param {Object} update
*/
function _processAddAction (item, field, update) {
if (typeUtil.objectIsNonEmptySet(update.Value)) {
// append to an array
item[field] = typeUtil.addToSet(item[field], update.Value)
} else if (typeUtil.objectToType(update.Value) == 'N') {
// increment a number
item[field] = typeUtil.addToNumber(item[field], update.Value)
} else {
throw new Error('Trying to ADD to a field which isnt an array or number')
}
}
/**
* @param {Object} oldItem
* @param {Object} updates
* @return {Object}
*/
function update (oldItem, updates) {
if (!oldItem) {
throw new Error('oldItem should not be falsy')
}
var newItem = _cloneObject(oldItem)
for (var field in updates) {
var update = updates[field]
// See http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-AttributeUpdates
if (update.Action === 'DELETE') {
_processDeleteAction(newItem, field, update)
} else if (update.Action === 'PUT') {
_processPutAction(newItem, field, update)
} else if (update.Action === 'ADD') {
_processAddAction(newItem, field, update)
}
}
return newItem
}
module.exports = {
update: update
}
================================================
FILE: lib/reserved.js
================================================
/**
* @fileoverview http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html
*/
var list = [
'ABORT',
'ABSOLUTE',
'ACTION',
'ADD',
'AFTER',
'AGENT',
'AGGREGATE',
'ALL',
'ALLOCATE',
'ALTER',
'ANALYZE',
'AND',
'ANY',
'ARCHIVE',
'ARE',
'ARRAY',
'AS',
'ASC',
'ASCII',
'ASENSITIVE',
'ASSERTION',
'ASYMMETRIC',
'AT',
'ATOMIC',
'ATTACH',
'ATTRIBUTE',
'AUTH',
'AUTHORIZATION',
'AUTHORIZE',
'AUTO',
'AVG',
'BACK',
'BACKUP',
'BASE',
'BATCH',
'BEFORE',
'BEGIN',
'BETWEEN',
'BIGINT',
'BINARY',
'BIT',
'BLOB',
'BLOCK',
'BOOLEAN',
'BOTH',
'BREADTH',
'BUCKET',
'BULK',
'BY',
'BYTE',
'CALL',
'CALLED',
'CALLING',
'CAPACITY',
'CASCADE',
'CASCADED',
'CASE',
'CAST',
'CATALOG',
'CHAR',
'CHARACTER',
'CHECK',
'CLASS',
'CLOB',
'CLOSE',
'CLUSTER',
'CLUSTERED',
'CLUSTERING',
'CLUSTERS',
'COALESCE',
'COLLATE',
'COLLATION',
'COLLECTION',
'COLUMN',
'COLUMNS',
'COMBINE',
'COMMENT',
'COMMIT',
'COMPACT',
'COMPILE',
'COMPRESS',
'CONDITION',
'CONFLICT',
'CONNECT',
'CONNECTION',
'CONSISTENCY',
'CONSISTENT',
'CONSTRAINT',
'CONSTRAINTS',
'CONSTRUCTOR',
'CONSUMED',
'CONTINUE',
'CONVERT',
'COPY',
'CORRESPONDING',
'COUNT',
'COUNTER',
'CREATE',
'CROSS',
'CUBE',
'CURRENT',
'CURSOR',
'CYCLE',
'DATA',
'DATABASE',
'DATE',
'DATETIME',
'DAY',
'DEALLOCATE',
'DEC',
'DECIMAL',
'DECLARE',
'DEFAULT',
'DEFERRABLE',
'DEFERRED',
'DEFINE',
'DEFINED',
'DEFINITION',
'DELETE',
'DELIMITED',
'DEPTH',
'DEREF',
'DESC',
'DESCRIBE',
'DESCRIPTOR',
'DETACH',
'DETERMINISTIC',
'DIAGNOSTICS',
'DIRECTORIES',
'DISABLE',
'DISCONNECT',
'DISTINCT',
'DISTRIBUTE',
'DO',
'DOMAIN',
'DOUBLE',
'DROP',
'DUMP',
'DURATION',
'DYNAMIC',
'EACH',
'ELEMENT',
'ELSE',
'ELSEIF',
'EMPTY',
'ENABLE',
'END',
'EQUAL',
'EQUALS',
'ERROR',
'ESCAPE',
'ESCAPED',
'EVAL',
'EVALUATE',
'EXCEEDED',
'EXCEPT',
'EXCEPTION',
'EXCEPTIONS',
'EXCLUSIVE',
'EXEC',
'EXECUTE',
'EXISTS',
'EXIT',
'EXPLAIN',
'EXPLODE',
'EXPORT',
'EXPRESSION',
'EXTENDED',
'EXTERNAL',
'EXTRACT',
'FAIL',
'FALSE',
'FAMILY',
'FETCH',
'FIELDS',
'FILE',
'FILTER',
'FILTERING',
'FINAL',
'FINISH',
'FIRST',
'FIXED',
'FLATTERN',
'FLOAT',
'FOR',
'FORCE',
'FOREIGN',
'FORMAT',
'FORWARD',
'FOUND',
'FREE',
'FROM',
'FULL',
'FUNCTION',
'FUNCTIONS',
'GENERAL',
'GENERATE',
'GET',
'GLOB',
'GLOBAL',
'GO',
'GOTO',
'GRANT',
'GREATER',
'GROUP',
'GROUPING',
'HANDLER',
'HASH',
'HAVE',
'HAVING',
'HEAP',
'HIDDEN',
'HOLD',
'HOUR',
'IDENTIFIED',
'IDENTITY',
'IF',
'IGNORE',
'IMMEDIATE',
'IMPORT',
'IN',
'INCLUDING',
'INCLUSIVE',
'INCREMENT',
'INCREMENTAL',
'INDEX',
'INDEXED',
'INDEXES',
'INDICATOR',
'INFINITE',
'INITIALLY',
'INLINE',
'INNER',
'INNTER',
'INOUT',
'INPUT',
'INSENSITIVE',
'INSERT',
'INSTEAD',
'INT',
'INTEGER',
'INTERSECT',
'INTERVAL',
'INTO',
'INVALIDATE',
'IS',
'ISOLATION',
'ITEM',
'ITEMS',
'ITERATE',
'JOIN',
'KEY',
'KEYS',
'LAG',
'LANGUAGE',
'LARGE',
'LAST',
'LATERAL',
'LEAD',
'LEADING',
'LEAVE',
'LEFT',
'LENGTH',
'LESS',
'LEVEL',
'LIKE',
'LIMIT',
'LIMITED',
'LINES',
'LIST',
'LOAD',
'LOCAL',
'LOCALTIME',
'LOCALTIMESTAMP',
'LOCATION',
'LOCATOR',
'LOCK',
'LOCKS',
'LOG',
'LOGED',
'LONG',
'LOOP',
'LOWER',
'MAP',
'MATCH',
'MATERIALIZED',
'MAX',
'MAXLEN',
'MEMBER',
'MERGE',
'METHOD',
'METRICS',
'MIN',
'MINUS',
'MINUTE',
'MISSING',
'MOD',
'MODE',
'MODIFIES',
'MODIFY',
'MODULE',
'MONTH',
'MULTI',
'MULTISET',
'NAME',
'NAMES',
'NATIONAL',
'NATURAL',
'NCHAR',
'NCLOB',
'NEW',
'NEXT',
'NO',
'NONE',
'NOT',
'NULL',
'NULLIF',
'NUMBER',
'NUMERIC',
'OBJECT',
'OF',
'OFFLINE',
'OFFSET',
'OLD',
'ON',
'ONLINE',
'ONLY',
'OPAQUE',
'OPEN',
'OPERATOR',
'OPTION',
'OR',
'ORDER',
'ORDINALITY',
'OTHER',
'OTHERS',
'OUT',
'OUTER',
'OUTPUT',
'OVER',
'OVERLAPS',
'OVERRIDE',
'OWNER',
'PAD',
'PARALLEL',
'PARAMETER',
'PARAMETERS',
'PARTIAL',
'PARTITION',
'PARTITIONED',
'PARTITIONS',
'PATH',
'PERCENT',
'PERCENTILE',
'PERMISSION',
'PERMISSIONS',
'PIPE',
'PIPELINED',
'PLAN',
'POOL',
'POSITION',
'PRECISION',
'PREPARE',
'PRESERVE',
'PRIMARY',
'PRIOR',
'PRIVATE',
'PRIVILEGES',
'PROCEDURE',
'PROCESSED',
'PROJECT',
'PROJECTION',
'PROPERTY',
'PROVISIONING',
'PUBLIC',
'PUT',
'QUERY',
'QUIT',
'QUORUM',
'RAISE',
'RANDOM',
'RANGE',
'RANK',
'RAW',
'READ',
'READS',
'REAL',
'REBUILD',
'RECORD',
'RECURSIVE',
'REDUCE',
'REF',
'REFERENCE',
'REFERENCES',
'REFERENCING',
'REGEXP',
'REGION',
'REINDEX',
'RELATIVE',
'RELEASE',
'REMAINDER',
'RENAME',
'REPEAT',
'REPLACE',
'REQUEST',
'RESET',
'RESIGNAL',
'RESOURCE',
'RESPONSE',
'RESTORE',
'RESTRICT',
'RESULT',
'RETURN',
'RETURNING',
'RETURNS',
'REVERSE',
'REVOKE',
'RIGHT',
'ROLE',
'ROLES',
'ROLLBACK',
'ROLLUP',
'ROUTINE',
'ROW',
'ROWS',
'RULE',
'RULES',
'SAMPLE',
'SATISFIES',
'SAVE',
'SAVEPOINT',
'SCAN',
'SCHEMA',
'SCOPE',
'SCROLL',
'SEARCH',
'SECOND',
'SECTION',
'SEGMENT',
'SEGMENTS',
'SELECT',
'SELF',
'SEMI',
'SENSITIVE',
'SEPARATE',
'SEQUENCE',
'SERIALIZABLE',
'SESSION',
'SET',
'SETS',
'SHARD',
'SHARE',
'SHARED',
'SHORT',
'SHOW',
'SIGNAL',
'SIMILAR',
'SIZE',
'SKEWED',
'SMALLINT',
'SNAPSHOT',
'SOME',
'SOURCE',
'SPACE',
'SPACES',
'SPARSE',
'SPECIFIC',
'SPECIFICTYPE',
'SPLIT',
'SQL',
'SQLCODE',
'SQLERROR',
'SQLEXCEPTION',
'SQLSTATE',
'SQLWARNING',
'START',
'STATE',
'STATIC',
'STATUS',
'STORAGE',
'STORE',
'STORED',
'STREAM',
'STRING',
'STRUCT',
'STYLE',
'SUB',
'SUBMULTISET',
'SUBPARTITION',
'SUBSTRING',
'SUBTYPE',
'SUM',
'SUPER',
'SYMMETRIC',
'SYNONYM',
'SYSTEM',
'TABLE',
'TABLESAMPLE',
'TEMP',
'TEMPORARY',
'TERMINATED',
'TEXT',
'THAN',
'THEN',
'THROUGHPUT',
'TIME',
'TIMESTAMP',
'TIMEZONE',
'TINYINT',
'TO',
'TOKEN',
'TOTAL',
'TOUCH',
'TRAILING',
'TRANSACTION',
'TRANSFORM',
'TRANSLATE',
'TRANSLATION',
'TREAT',
'TRIGGER',
'TRIM',
'TRUE',
'TRUNCATE',
'TTL',
'TUPLE',
'TYPE',
'UNDER',
'UNDO',
'UNION',
'UNIQUE',
'UNIT',
'UNKNOWN',
'UNLOGGED',
'UNNEST',
'UNPROCESSED',
'UNSIGNED',
'UNTIL',
'UPDATE',
'UPPER',
'URL',
'USAGE',
'USE',
'USER',
'USERS',
'USING',
'UUID',
'VACUUM',
'VALUE',
'VALUED',
'VALUES',
'VARCHAR',
'VARIABLE',
'VARIANCE',
'VARINT',
'VARYING',
'VIEW',
'VIEWS',
'VIRTUAL',
'VOID',
'WAIT',
'WHEN',
'WHENEVER',
'WHERE',
'WHILE',
'WINDOW',
'WITH',
'WITHIN',
'WITHOUT',
'WORK',
'WRAPPED',
'WRITE',
'YEAR',
'ZONE'
]
var set = {}
list.forEach(function (w) {
set[w] = true
})
module.exports = {
list: list,
set: set
}
================================================
FILE: lib/typeUtil.js
================================================
// Copyright 2013 The Obvious Corporation
/**
* @fileoverview Utility functions that convert plain javascript objects to
* Dynamo AttributeValue map (http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html)
* objects back and forth.
*/
var typ = require('typ')
var reserved = require('./reserved')
/**
* From http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html
* - B: A Binary data type.
* - BOOL: A Boolean data type.
* - BS: A Binary Set data type.
* - L: A List of attribute values.
* - M: A Map of attribute values.
* - N: A Number data type.
* - NS: A Number Set data type.
* - NULL: A Null data type.
* - S: A String data type.
* - SS: A String Set data type.
*
* @typedef {{
* B: (string|undefined),
* BOOL: (boolean|undefined),
* BS: (Array.<string>|undefined),
* M: (Object|undefined),
* L: (Array|undefined),
* N: (string|undefined),
* NS: (Array.<string>|undefined),
* NULL: (null|undefined),
* S: (string|undefined),
* SS: (Array.<string>|undefined)
* }}
*/
var AWSAttributeValue
/**
* Convert Dynamo AttributeValue map object(s) to plain javascript object(s)
*
* @param {Object.<string,AWSAttributeValue>|Array.<Object.<string,AWSAttributeValue>>} object Dynamo AttributeValue map object(s)
* @return {Object|Array.<Object>} plain javascript object(s)
*/
function unpackObjectOrArray(object) {
if (typ.isNullish(object)) return object
if (Array.isArray(object)) return object.map(unpackObjectOrArray)
var item = {}
for (var key in object) {
item[key] = objectToValue(object[key])
}
return item
}
/**
* Convert an object to a Dynamo AttributeValue map.
*
* @param {Object|Array.<Object>|undefined} object
* @param {Object=} attributes an optional map of the attributes that need to convert.
* @return {Object.<string,AWSAttributeValue>|Array.<Object.<string,AWSAttributeValue>>|null} The object in Dynamo AttributeValue map.
*/
function packObjectOrArray(object, attributes) {
if (typ.isNullish(object)) return null
if (Array.isArray(object)) {
return object.map(function (obj) { return packObjectOrArray(obj, attributes) })
}
var newObj = {}
for (var key in object) {
if (attributes && !attributes[key]) continue
newObj[key] = valueToObject(object[key])
}
return newObj
}
/**
* Convert a javascript primitive value to an AWS AttributeValue
*
* @param {boolean|number|string|Array} value
* @return {AWSAttributeValue|null}
*/
function valueToObject(value) {
var type = typeof value
switch (typeof value) {
case 'string':
return {S: value}
case 'boolean':
return {BOOL: Boolean(value)}
case 'number':
return {N: String(value)}
default:
if (Array.isArray(value)) {
var firstItemType = typeof value[0]
// check that all of the items are of the same type; that of the first element's
for (var i = 0; i < value.length; i++) {
if (typeof value[i] !== firstItemType) {
throw new Error('Inconsistent types in set! Expecting all types to be the same as the first element\'s: ' + firstItemType)
}
}
if (firstItemType === 'string') {
return {SS: value}
} else if (firstItemType === 'number') {
var numArray = []
for (i = 0; i < value.length; i++) {
numArray.push(String(value[i]))
}
return {NS: numArray}
} else {
throw new Error('Invalid dynamo set value. Type: ' + firstItemType + ', Value: ' + value[0])
}
} else {
throw new Error('Invalid dynamo value. Type: ' + type + ', Value: ' + value)
}
}
}
/**
* Get the type of an AWS AttributeValue
* @param {!AWSAttributeValue} obj Dynamo AttributeValue.
* @return {string}
*/
function objectToType(obj) {
var objectType = Object.keys(obj)
if (objectType.length != 1) {
throw new Error('Expected only one key from Amazon object')
}
return objectType[0]
}
/**
* Convert a Dynamo AttributeValue to a javascript primitive value
*
* @param {!AWSAttributeValue} obj
* @return {string|number|Array.<string>|Array.<number>|boolean|Object} a javascript primitive value
*/
function objectToValue(obj) {
switch (objectToType(obj)) {
case 'SS':
return (/** @type {Array.<string>} */(obj.SS))
case 'S':
return (/** @type {string} */(obj.S))
case 'BOOL':
return Boolean(obj.BOOL)
case 'NS':
return obj.NS.map(function (num) { return Number(num) })
case 'N':
return Number(obj.N)
case 'M':
var mapped = {}
for (var k in obj.M) {
mapped[k] = objectToValue(obj.M[k])
}
return mapped
case 'L':
return obj.L.map(objectToValue)
default:
throw new Error('Unexpected key: ' + objectToType(obj) + ' for attribute: ' + obj)
}
}
/**
* @param {!AWSAttributeValue} obj
* @return {boolean}
*/
function objectIsEmpty(obj) {
return !obj || Object.keys(obj).length === 0
}
/**
* @param {!AWSAttributeValue} obj
* @return {boolean}
*/
function objectIsNonEmptySet(obj) {
if (objectIsEmpty(obj)) return false
var type = objectToType(obj)
if (type != 'NS' && type != 'SS') return false
return Array.isArray(obj[type]) && obj[type].length > 0
}
/**
* @param {!AWSAttributeValue} set
* @param {!AWSAttributeValue} additions
* @return {AWSAttributeValue}
*/
function addToSet(set, additions) {
var type = objectToType(additions)
if (objectIsEmpty(set)) {
set = {}
set[type] = []
} else if (objectToType(set) === type) {
set = clone(set)
} else {
throw new Error('Type mismatch: type of set should match type of additions')
}
for (var i = 0; i < additions[type].length; i++) {
if (set[type].indexOf(additions[type][i]) == -1) {
set[type].push(additions[type][i])
}
}
return set
}
/**
* @param {!AWSAttributeValue} set
* @param {!AWSAttributeValue} deletions
* @return {?AWSAttributeValue}
*/
function deleteFromSet(set, deletions) {
var type = objectToType(deletions)
if (objectIsEmpty(set)) {
return null
} else if (objectToType(set) !== type) {
throw new Error('Type mismatch: type of set should match type of deletions')
}
set = clone(set)
for (var i = 0; i < deletions[type].length; i++) {
var idx = set[type].indexOf(deletions[type][i])
if (idx != -1) {
set[type].splice(idx, 1)
}
}
if (set[type].length) {
return set
} else {
return null
}
}
/**
* @param {!AWSAttributeValue} number
* @param {!AWSAttributeValue} addition
* @return {AWSAttributeValue}
*/
function addToNumber(number, addition) {
if (objectIsEmpty(number)) {
number = {'N': '0'}
} else {
number = clone(number)
}
if (objectToType(number) !== 'N' || objectToType(addition) !== 'N') {
throw new Error('Type mismatch: number and addition should both be numeric types')
}
number.N = String(Number(number.N) + Number(addition.N))
return number
}
/**
* @param {!AWSAttributeValue} oldItem
* @return {AWSAttributeValue}
*/
function clone(oldItem) {
try {
var objectType = objectToType(oldItem)
var newItem = {}
if (Array.isArray(oldItem[objectType])) {
newItem[objectType] = oldItem[objectType].slice()
} else {
newItem[objectType] = oldItem[objectType]
}
return newItem
} catch (e) {
return {NULL:null}
}
}
var VALID_ATTR_RE = /^[a-zA-Z][a-zA-Z0-9]*$/
/**
* @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ExpressionPlaceholders.html#ExpressionAttributeNames
* @return {string} The alias. May just be the key itself if an alias is not needed.
*/
function getAttributeAlias(key) {
if (isReservedWord(key)) {
// If this is just a reserved word, use # + word
return '#' + key
}
if (!isAlphaNumeric(key)) {
// if this is not alphanumeric, hex-encode the string.
return '#' + new Buffer(key).toString('hex')
}
// otherwise, the key is valid in an expression
return key
}
/**
* @return {boolean} True if this is a reserved word.
*/
function isReservedWord(key) {
return (key.toUpperCase() in reserved.set)
}
/**
* @return {boolean} True if this matches the alphanumeric regexp
*/
function isAlphaNumeric(key) {
return VALID_ATTR_RE.test(key)
}
/**
* @see http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ExpressionPlaceholders.html#ExpressionAttributeNames
* @return {boolean} True if we need an attribute alias.
*/
function needsAttributeAlias(key) {
return isReservedWord(key) || !isAlphaNumeric(key)
}
/**
* Given a list of attribute names, return an object suitable for ExpressionAttributeNames
* @param {Array<string>} attrList
* @return {Object}
*/
function buildAttributeNames(attrList) {
var result = {}
attrList.map(function (key) {
if (needsAttributeAlias(key)) {
result[getAttributeAlias(key)] = key
}
}, this)
return result
}
/**
* Extends the attribute names object with new names.
*/
function extendAttributeNames(existingNames, newNames) {
for (var key in newNames) {
if (!existingNames[key]) {
existingNames[key] = newNames[key]
} else if (existingNames[key] != newNames[key]) {
throw new Error('Attribute name conflict ' + key)
}
}
}
/**
* Extends the attribute values object with new values.
*/
function extendAttributeValues(existingValues, newValues) {
for (var key in newValues) {
if (!existingValues[key]) {
existingValues[key] = newValues[key]
} else{
throw new Error('Attribute value conflict ' + key)
}
}
}
module.exports = {
AWSAttributeValue: AWSAttributeValue,
unpackObjectOrArray: unpackObjectOrArray,
packObjectOrArray: packObjectOrArray,
valueToObject: valueToObject,
objectToType: objectToType,
objectToValue: objectToValue,
objectIsEmpty: objectIsEmpty,
objectIsNonEmptySet: objectIsNonEmptySet,
getAttributeAlias: getAttributeAlias,
buildAttributeNames: buildAttributeNames,
extendAttributeNames: extendAttributeNames,
extendAttributeValues: extendAttributeValues,
addToSet: addToSet,
deleteFromSet: deleteFromSet,
addToNumber: addToNumber,
clone: clone
}
================================================
FILE: package.json
================================================
{
"name": "dynamite",
"description": "promise-based DynamoDB client",
"version": "1.0.0",
"homepage": "https://github.com/Medium/dynamite",
"license": "Apache-2.0",
"authors": [
"Jeremy Stanley <github@azulus.com> (https://github.com/azulus)",
"Jean Hsu <jean@medium.com> (https://github.com/jyhsu)",
"Jonathan Fuchs <jon@medium.com> (https://github.com/jfuchs)",
"Artem Titoulenko <artem.titoulenko@gmail.com> (https://github.com/ArtemTitoulenko)",
"Xiao Ma <xiao@medium.com> (https://github.com/x-ma)",
"Jamie Talbot <jamie@medium.com> (https://github.com/majelbstoat)"
],
"keywords": [
"dynamite",
"dynamo",
"dynamodb"
],
"main": "dynamite.js",
"repository": {
"type": "git",
"url": "https://github.com/Medium/dynamite.git"
},
"dependencies": {
"aws-sdk": "^2.368.0",
"kew": "git+https://github.com/Medium/kew#b8aaf9f",
"typ": "0.6.3"
},
"devDependencies": {
"eslint": "4.18.2",
"local-dynamo": "git+https://github.com/Medium/local-dynamo#4d8d3c0",
"nodeunit": "0.11.3",
"nodeunitq": "0.1.1"
},
"externDependencies": {
"aws-sdk": "./externs/aws-sdk.js"
},
"scripts": {
"test": "eslint . && ./node_modules/.bin/nodeunit test"
}
}
================================================
FILE: test/testBatchGetItem.js
================================================
// Copyright 2013 The Obvious Corporation.
var utils = require('./utils/testUtils.js')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var Q = require('kew')
function onError(err) {
console.error(err.stack)
}
var userData = [
{'userId': 'userA', 'column': '@', 'age': '29'},
{'userId': 'userB', 'column': '@', 'age': '44'},
{'userId': 'userC', 'column': '@', 'age': '36'}
]
var phoneData = [
{'userId': 'userA', 'column': 'phone1', 'number': '415-662-1234'},
{'userId': 'userA', 'column': 'phone2', 'number': '650-143-8899'},
{'userId': 'userB', 'column': 'phone1', 'number': '550-555-5555'}
]
// Generate many items that will exceed the batch get count limit
var manyData = []
for (var i = 0; i < 202; i++) {
manyData.push({'hashKey': 'id' + i, 'column': '@', 'data': 'small'})
}
// Generate big items that will exceed amount allowed to be returned.
var muchoData = []
var junk = new Array(62000).join('.')
for (i = 0; i < 101; i++) {
muchoData.push({'hashKey': 'id' + i, 'column': '@', 'data': junk})
}
// basic setup for the tests, creating record userA with range key @
exports.setUp = function (done) {
this.db = utils.getMockDatabase()
this.client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
var userTablePromise = utils.createTable(this.db, 'user', 'userId', 'column')
.thenBound(utils.initTable, null, {db: this.db, tableName: 'user', data: userData})
var phoneTablePromise = utils.createTable(this.db, 'phones', 'userId', 'column')
.thenBound(utils.initTable, null, {db: this.db, tableName: 'phones', data: phoneData})
var manyTablePromise = utils.createTable(this.db, 'pre_many', 'hashKey', 'column')
.thenBound(utils.initTable, null, {db: this.db, tableName: 'pre_many', data: manyData})
var muchoTablePromise = utils.createTable(this.db, 'mucho', 'hashKey', 'column')
.thenBound(utils.initTable, null, {db: this.db, tableName: 'mucho', data: muchoData})
Q.all([userTablePromise, phoneTablePromise, manyTablePromise, muchoTablePromise])
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
Q.all([
utils.deleteTable(this.db, 'user'),
utils.deleteTable(this.db, 'phones'),
utils.deleteTable(this.db, 'pre_many'),
utils.deleteTable(this.db, 'mucho')
])
.fin(done)
}
builder.add(function testBatchGet(test) {
return this.client.newBatchGetBuilder()
.requestItems('user', [{'userId': 'userA', 'column': '@'}, {'userId': 'userB', 'column': '@'}])
.requestItems('phones', [{'userId': 'userA', 'column': 'phone1'}, {'userId': 'userB', 'column': 'phone1'}])
.execute()
.then(function (data) {
var ages = data.result.user.map(function (user) { return user.age })
test.deepEqual(ages, ['29', '44'])
var phones = data.result.phones.map(function (phone) { return phone.number })
test.deepEqual(phones, ['415-662-1234', '550-555-5555'])
})
})
builder.add(function testEmptyBatch(test) {
return this.client.newBatchGetBuilder()
.requestItems('user', [{'userId': 'userE', 'column': '@'}])
.execute()
.then(function (data) {
test.ok(Array.isArray(data.result.user), 'An array should be returned for requested tables')
test.equal(0, data.result.user.length, 'No items should have been returned')
})
})
builder.add(function testBatchGetMany(test) {
return this.client.newBatchGetBuilder()
.setPrefix('pre_')
.requestItems('many', manyData.map(function (o) { return {'hashKey': o.hashKey, 'column': '@'}}))
.execute()
.then(function (data) {
test.equal(202, data.result.many.length, 'All 202 items should be returned')
test.equal(0, Object.keys(data.UnprocessedKeys).length, 'There should be no unprocessed keys')
})
})
builder.add(function testBatchGetMucho(test) {
return this.client.newBatchGetBuilder()
.requestItems('mucho', muchoData.map(function (o) { return {'hashKey': o.hashKey, 'column': '@'}}))
.execute()
.then(function (data) {
test.equal(101, data.result.mucho.length, 'All 101 items should be returned')
test.equal(0, Object.keys(data.UnprocessedKeys), 'There should be no unprocessed keys')
})
})
builder.add(function testBatchGetBadKey(test) {
var client = this.client
return Q.fcall(
function () {
return client.newBatchGetBuilder()
.setPrefix('pre_')
.requestItems('many', manyData.map(function () { return {'hashKey': {}, 'column': '@'}}))
.execute()
})
.then(function () {
test.fail('Expected error')
})
.fail(function (err) {
if (err.message != 'Invalid dynamo value. Type: object, Value: [object Object]') throw err
})
})
================================================
FILE: test/testConditions.js
================================================
// Copyright 2016 A Medium Corporation
'use strict'
var utils = require('./utils/testUtils.js')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var onError = console.error.bind(console)
var tableName = 'user'
var rawData = [
{userId: 'a', column: '@'},
{userId: 'b', column: '@', blacklistedAt: '1'},
{userId: 'c', column: '@', blacklistedAt: '1', unblacklistedAt: '2'},
{userId: 'd', column: '@', blacklistedAt: '3', unblacklistedAt: '2'},
{userId: 'e', column: '@', blacklistedAt: '4', unblacklistedAt: '4'}
]
var db
var client
exports.setUp = function (done) {
db = utils.getMockDatabase()
client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
utils.createTable(db, tableName, 'userId', 'column')
.thenBound(utils.initTable, null, {"db": db, "tableName": tableName, "data": rawData})
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
utils.deleteTable(db, tableName)
.fin(done)
}
builder.add(function testEqualsAttribute(test) {
var filter = client.newConditionBuilder()
.filterAttributeEqualsAttribute('blacklistedAt', 'unblacklistedAt')
return assertUserIds(test, ['e'], filter)
})
builder.add(function testNotEqualsAttribute(test) {
var filter = client.newConditionBuilder()
.filterAttributeNotEqualsAttribute('blacklistedAt', 'unblacklistedAt')
return assertUserIds(test, ['a', 'b', 'c', 'd'], filter)
})
builder.add(function testLessThanAttribute(test) {
var filter = client.newConditionBuilder()
.filterAttributeLessThanAttribute('blacklistedAt', 'unblacklistedAt')
return assertUserIds(test, ['c'], filter)
})
builder.add(function testLessThanEqualAttribute(test) {
var filter = client.newConditionBuilder()
.filterAttributeLessThanEqualAttribute('blacklistedAt', 'unblacklistedAt')
return assertUserIds(test, ['c', 'e'], filter)
})
builder.add(function testGreaterThanAttribute(test) {
var filter = client.newConditionBuilder()
.filterAttributeGreaterThanAttribute('blacklistedAt', 'unblacklistedAt')
return assertUserIds(test, ['d'], filter)
})
builder.add(function testGreaterThanEqualAttribute(test) {
var filter = client.newConditionBuilder()
.filterAttributeGreaterThanEqualAttribute('blacklistedAt', 'unblacklistedAt')
return assertUserIds(test, ['d', 'e'], filter)
})
function assertUserIds(test, expectedUserIds, filter) {
return client.newScanBuilder(tableName)
.withFilter(filter)
.setLimit(10)
.execute()
.then(function (data) {
var userIds = data.result.map(function (r) {
return r.userId
})
test.deepEqual(expectedUserIds, userIds.sort())
})
}
================================================
FILE: test/testDeleteItem.js
================================================
// Copyright 2013 The Obvious Corporation.
var utils = require('./utils/testUtils.js')
var typ = require('typ')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var onError = console.error.bind(console)
var initialData = [{"userId": "userA", "column": "@", "age": "29"}]
// basic setup for the tests, creating record userA with range key @
exports.setUp = function (done) {
this.db = utils.getMockDatabase()
this.client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
utils.createTable(this.db, "user", "userId", "column")
.thenBound(utils.initTable, null, {db: this.db, tableName: "user", data: initialData})
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
utils.deleteTable(this.db, "user")
.fin(done)
}
// check that an item can be deleted
builder.add(function testDeleteExistingItem(test) {
var self = this
return this.client.deleteItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
.then(function () {
return utils.getItemWithSDK(self.db, "userA", "@")
})
.then(function (data) {
test.equal(data['Item'], undefined, "User should be deleted~~~" + JSON.stringify(data))
})
})
// check that an item isn't inadvertently deleted when deleting another
builder.add(function testDeleteNonexistingItem(test) {
var self = this
return this.client.deleteItem('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.execute()
.then(function () {
return utils.getItemWithSDK(self.db, "userA", "@")
})
.then(function (data) {
test.equal(typ.isNullish(data['Item']), false, "User should not be deleted")
})
})
// check that an item matches a conditional when deleting
builder.add(function testDeleteExistingItemWithConditional(test) {
var self = this
var conditions = this.client.newConditionBuilder()
.expectAttributeEquals('column', '@')
return this.client.deleteItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
return utils.getItemWithSDK(self.db, "userA", "@")
})
.then(function (data) {
test.equal(data['Item'], undefined, "User should be deleted")
})
})
// check that an item matches an absent conditional when deleting
builder.add(function testDeleteExistingItemWithConditionalAbsent(test) {
var self = this
var conditions = this.client.newConditionBuilder()
.expectAttributeAbsent('height')
return this.client.deleteItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
return utils.getItemWithSDK(self.db, "userA", "@")
})
.then(function (data) {
test.equal(data['Item'], undefined, "User should be deleted")
})
})
// check that an item fails a conditional when deleting
builder.add(function testDeleteExistingItemWithFailedConditional(test) {
var conditions = this.client.newConditionBuilder()
.expectAttributeEquals('column', 'bug')
return this.client.deleteItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
test.fail("'testDeleteExistingItemWithFailedConditional' failed")
})
.fail(this.client.throwUnlessConditionalError)
})
// check that an item fails an absent conditional when deleting
builder.add(function testDeleteExistingItemWithFailedAbsentConditional(test) {
var conditions = this.client.newConditionBuilder()
.expectAttributeAbsent('age')
return this.client.deleteItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
test.fail("'testDeleteExistingItemWithFailedAbsentConditional' failed")
})
.fail(this.client.throwUnlessConditionalError)
})
// check that non-existent items can't be deleted if a conditional expects a value
builder.add(function testDeleteNonexistingItemWithConditional(test) {
var conditions = this.client.newConditionBuilder()
.expectAttributeEquals('column', '@')
return this.client.deleteItem('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
test.fail("'testDeleteNonexistingItemWithConditional' failed")
})
.fail(this.client.throwUnlessConditionalError)
})
// check that non-existent items can't be deleted if a conditional expects a value
builder.add(function testDeleteNonexistingItemWithConditionalAbsent(test) {
var self = this
var conditions = this.client.newConditionBuilder()
.expectAttributeAbsent('column')
return this.client.deleteItem('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
return utils.getItemWithSDK(self.db, "userA", "@")
})
.then(function (data) {
test.equal(typ.isNullish(data['Item']), false, "User should not be deleted")
})
})
================================================
FILE: test/testDescribeTable.js
================================================
// Copyright 2013 The Obvious Corporation.
var utils = require('./utils/testUtils.js')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var onError = console.error.bind(console)
// basic setup for the tests, creating record userA with range key @
exports.setUp = function (done) {
this.db = utils.getMockDatabase()
this.client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
utils.createTable(this.db, "user", "userId", "column")
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
utils.deleteTable(this.db, "user")
.fin(done)
}
// check that an item exists
builder.add(function testSimpleDescribeTable(test) {
return this.client.describeTable("user")
.execute()
.then(function (data) {
test.equal(data.Table.TableName, "user", "Table name should be 'user'")
test.equal(data.Table.KeySchema[0].AttributeName, "userId", "Hash key name should be 'userId'")
test.equal(data.Table.KeySchema[1].AttributeName, "column", "Hash key name should be 'column'")
})
})
================================================
FILE: test/testFakeDynamo.js
================================================
// Copyright 2013 The Obvious Corporation
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var Client = require('../lib/Client')
var FakeDynamo = require('../lib/FakeDynamo')
var userA = {
'userId': 'userA',
'column': '@',
'age': 29,
'luckyNumbers': [1, 3, 5]
}
var db, client
exports.setUp = function (done) {
db = new FakeDynamo()
client = new Client({dbClient: db})
var userTable = db.createTable('user')
userTable.setHashKey('userId', 'S')
userTable.setRangeKey('column', 'S')
userTable.setData(
JSON.parse(JSON.stringify({userA: {'@': userA}})))
var cookieTable = db.createTable('cookie')
cookieTable.setHashKey('cookieId', 'S')
cookieTable.setRangeKey('createdAt', 'S')
done()
}
exports.tearDown = function (done) {
done()
}
builder.add(function testConditionalUpdateFails(test) {
var conditions = client.newConditionBuilder()
.expectAttributeEquals('userId', 'gibberish')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'gibberish')
.setRangeKey('column', '@')
.withCondition(conditions)
.putAttribute('age', 30)
.execute()
.then(function () {
test.fail('Expected conditional error')
})
.fail(function (e) {
test.ok(client.isConditionalError(e))
test.ok(!!e.stack)
throw e
})
.fail(client.throwUnlessConditionalError)
})
builder.add(function testConditionalUpdateOk(test) {
var conditions = client.newConditionBuilder()
.expectAttributeEquals('userId', 'userA')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.putAttribute('age', 30)
.execute()
.then(function () {
return client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
})
.then(function (data) {
test.equal(data.result.age, 30, 'Age should match 30')
})
})
builder.add(function testAddToAttribute(test) {
var conditions = client.newConditionBuilder()
.expectAttributeEquals('userId', 'userA')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.addToAttribute('luckyNumbers', [8])
.execute()
.then(function () {
return client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
})
.then(function (data) {
data.result.luckyNumbers.sort()
test.deepEqual(data.result.luckyNumbers, [1, 3, 5, 8])
})
})
builder.add(function testBatchGetDupeKeys(test) {
// Real Dynamo throws an exception if a BatchGet has duplicate keys.
// Ruby FakeDynamo does not have this validation.
return client.newBatchGetBuilder()
.requestItems('user', [{'userId': 'userA', 'column': '@'},
{'userId': 'userA', 'column': '@'}])
.execute()
.then(function () {
test.fail('Expected validation failure')
})
.fail(function (e) {
if (!/Provided list of item keys contains duplicates/.test(e.message)) {
throw e
}
})
})
builder.add(function testConditionalBuilderMethods(test) {
var expected = client.newConditionBuilder()
.expectAttributeEquals('userId', 'gibberish')
.expectAttributeAbsent('userId2')
var actual = client.conditions({userId: 'gibberish', 'userId2': null})
test.deepEqual(expected, actual)
test.done()
})
builder.add(function testScan(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27},
2: {userId: 'userA', column: '2', age: 28},
3: {userId: 'userA', column: '3', age: 29}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newScanBuilder('user')
.execute()
.then(function (data) {
var result = data.result
test.deepEqual(result[0], {userId: 'userA', column: '1', age: 27})
test.deepEqual(result[1], {userId: 'userA', column: '2', age: 28})
test.deepEqual(result[2], {userId: 'userA', column: '3', age: 29})
test.deepEqual(result[3], {userId: 'userB', column: '1', age: 29})
})
})
builder.add(function testScanWithSimpleFilter(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27, name: 'Ringo'},
2: {userId: 'userA', column: '4', age: 28, name: 'George'},
3: {userId: 'userA', column: '3', age: 29, name: 'John'},
4: {userId: 'userA', column: '2', age: 30, name: 'Paul'}
}
})
var filter = client.newConditionBuilder()
.filterAttributeBeginsWith('name', 'Geo')
return client.newScanBuilder('user')
.withFilter(filter)
.execute()
.then(function (data) {
test.equal(data.result.length, 1)
test.deepEqual(data.result[0], {userId: 'userA', column: '4', age: 28, name: 'George'})
})
})
builder.add(function testScanWithComplexFilter(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27, name: 'Ringo'},
2: {userId: 'userA', column: '4', age: 28, name: 'George'},
3: {userId: 'userA', column: '3', age: 29, name: 'John'},
4: {userId: 'userA', column: '2', age: 30, name: 'Paul'}
}
})
var filter = client.andConditions([
client.newConditionBuilder().filterAttributeGreaterThan('age', 27),
client.newConditionBuilder().filterAttributeLessThan('age', 30)
])
return client.newScanBuilder('user')
.withFilter(filter)
.execute()
.then(function (data) {
test.equal(data.result.length, 2)
test.deepEqual(data.result[0], {userId: 'userA', column: '4', age: 28, name: 'George'})
test.deepEqual(data.result[1], {userId: 'userA', column: '3', age: 29, name: 'John'})
})
})
builder.add(function testScanWithLimit(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27},
2: {userId: 'userA', column: '2', age: 28},
3: {userId: 'userA', column: '3', age: 29}
}
})
return client.newScanBuilder('user')
.setLimit(2)
.execute()
.then(function (data) {
var result = data.result
test.deepEqual(result[0], {userId: 'userA', column: '1', age: 27})
test.deepEqual(result[1], {userId: 'userA', column: '2', age: 28})
test.deepEqual(data.LastEvaluatedKey, {userId: 'userA', column: '2'})
})
})
builder.add(function testScanWithStartKey(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27},
2: {userId: 'userA', column: '2', age: 28},
3: {userId: 'userA', column: '3', age: 29}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newScanBuilder('user')
.setStartKey({userId: 'userA', column: '2'})
.execute()
.then(function (data) {
var result = data.result
test.deepEqual(result[0], {userId: 'userA', column: '3', age: 29})
test.deepEqual(result[1], {userId: 'userB', column: '1', age: 29})
})
})
// test querying secondary index using greater than condition
builder.add(function testQueryOnSecondaryIndexGreaterThan(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: 3, age: 27},
2: {userId: 'userA', column: 2, age: 28},
3: {userId: 'userA', column: 5, age: 3000},
4: {userId: 'userA', column: 4, age: 29}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThan('age', 28)
.execute()
.then(function (data) {
test.equal(data.result.length, 2, '2 results should be returned')
test.equal(data.result[0].age, 29, 'First entry should be 29')
test.equal(data.result[1].age, 3000, 'Second entry should be 3000')
})
})
// test querying secondary index using less than condition
builder.add(function testQueryOnSecondaryIndexLessThan(test) {
db.getTable('user').setData({
'userA': {
0: {userId: 'userA', column: 0, age: 27},
1: {userId: 'userA', column: 3, age: 26},
2: {userId: 'userA', column: 2, age: 28},
3: {userId: 'userA', column: 1, age: 30},
4: {userId: 'userA', column: 4, age: 29}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexLessThan('age', 29)
.scanBackward()
.execute()
.then(function (data) {
test.equal(data.result[0].age, 28, 'First entry should be 28')
test.equal(data.result[1].age, 27, 'Second entry should be 27')
test.equal(data.result[2].age, 26, 'Third entry should be 26')
test.equal(data.result.length, 3, '3 results should be returned')
})
})
// test querying secondary index using less than equals condition
builder.add(function testQueryOnSecondaryIndexLessThanEquals(test) {
db.getTable('user').setData({
'userA': {
0: {userId: 'userA', column: 0, age: 27},
1: {userId: 'userA', column: 3, age: 26},
2: {userId: 'userA', column: 2, age: 28},
3: {userId: 'userA', column: 1, age: 29},
4: {userId: 'userA', column: 4, age: 30}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexLessThanEqual('age', 29)
.scanBackward()
.execute()
.then(function (data) {
test.equal(data.result[0].age, 29, 'First entry should be 29')
test.equal(data.result[1].age, 28, 'Second entry should be 28')
test.equal(data.result[2].age, 27, 'Third entry should be 27')
test.equal(data.result[3].age, 26, 'Fourth entry should be 26')
test.equal(data.result.length, 4, '4 results should be returned')
})
})
// test querying secondary index using equals condition
builder.add(function testQueryOnSecondaryIndexEquals(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: 3, age: 27},
2: {userId: 'userA', column: 2, age: 28},
3: {userId: 'userA', column: 1, age: 29},
4: {userId: 'userA', column: 4, age: 30}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexEqual('age', 28)
.execute()
.then(function (data) {
test.equal(data.result[0].age, 28, 'Age should match 28')
test.equal(data.result.length, 1, '1 result should be returned')
})
})
// test querying secondary index using equals condition
builder.add(function testQueryOnGlobalSecondaryIndexEquals(test) {
db.getTable('user').setGsiDefinitions([
{
hash: {
name: 'age',
type: 'S'
},
range: {
name: 'userId',
type: 'S'
}
}
])
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: 3, age: 27},
2: {userId: 'userA', column: 2, age: 28},
3: {userId: 'userA', column: 1, age: 29},
4: {userId: 'userA', column: 4, age: 30}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newQueryBuilder('user')
.setHashKey('age', 27)
.setIndexName('age-userId-index')
.indexBeginsWith('userId', 'user')
.execute()
.then(function (data) {
test.equal(data.result[0].age, 27, 'Age should match 28')
test.equal(data.result.length, 1, '1 result should be returned')
})
})
// test querying secondary index that have repeated column values
// this is a test for a regression where fake dynamo may reinsert values
// when the keys match again
builder.add(function testQueryOnMultipleIndexes(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27},
2: {userId: 'userA', column: '4', age: 28},
3: {userId: 'userA', column: '3', age: 29},
4: {userId: 'userA', column: '2', age: 30},
5: {userId: 'userA', column: '5', age: 30}
},
'userB': {
1: {userId: 'userB', column: '1', age: 29}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.execute()
.then(function (data) {
test.equal(data.result[0].age, 28, 'Age should match 28')
test.equal(data.result[1].age, 29, 'Age should match 29')
test.equal(data.result[2].age, 30, 'Age should match 30')
test.equal(data.result[3].age, 30, 'Age should match 30')
test.equal(data.result.length, 4, '4 results should be returned')
})
})
/**
* Basic test for Global Secondary Index support
* Does not currently support testing for existence of GSIs
*/
builder.add(function testQueryOnGlobalSecondaryIndexes(test) {
db.getTable('user').setGsiDefinitions([
{
hash: {
name: 'age',
type: 'S'
},
range: {
name: 'height',
type: 'N'
}
}
])
db.getTable('user').setHashKey('userId', 'S')
.setData({
'userA': {
1: {userId: 'userA', column: '3', age: 27, height: 160},
2: {userId: 'userA', column: '2', age: 28, height: 170},
3: {userId: 'userA', column: '1', age: 28, height: 180},
4: {userId: 'userA', column: '4', age: 29, height: 150}
},
'userB': {
1: {userId: 'userB', column: '3', age: 27, height: 200},
2: {userId: 'userB', column: '2', age: 28, height: 170},
3: {userId: 'userB', column: '1', age: 28, height: 178},
4: {userId: 'userB', column: '4', age: 29, height: 190}
}
})
return client.newQueryBuilder('user')
.setHashKey('age', 28)
// It is important that the index name has three or more terms (separated by
// '-'), it's a DynamoDB index naming convention, and it is how we know that it
// is a GSI query
.setIndexName('age-height-gsi')
.indexGreaterThan('height', 175)
.execute()
.then(function (data) {
// results from userB
test.equal(data.result[0].age, 28, 'Age should match 28')
test.equal(data.result[0].height, 178, 'Height should match 178')
// results from userA
test.equal(data.result[1].age, 28, 'Age should match 28')
test.equal(data.result[1].height, 180, 'Height should match 180')
test.equal(data.result.length, 2, '2 results should be returned')
})
})
// Ensure results are sorted by range key, even when there is no condition
// on the range key
builder.add(function testQueryOnGlobalSecondaryIndexWithoutCondition(test) {
db.getTable('user').setGsiDefinitions([
{
hash: {
name: 'userId',
type: 'S'
},
range: {
name: 'height',
type: 'N'
}
}
])
db.getTable('user')
.setHashKey('userId', 'S')
.setData({
'userA': {
0: {userId: 'userA', column: '0', age: 26, height: 188},
1: {userId: 'userA', column: '1', age: 27, height: 160},
2: {userId: 'userA', column: '2', age: 28, height: 170},
3: {userId: 'userA', column: '3', age: 28, height: 180},
4: {userId: 'userA', column: '4', age: 29, height: 150}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
// It is important that the index name has three or more terms (separated by
// '-'), it's a DynamoDB index naming convention, and it is how we know that it
// is a GSI query
.setIndexName('userId-height-gsi')
.scanBackward()
.execute()
.then(function (data) {
var heights = data.result.map(function (r) { return r.height })
test.deepEqual(heights, [188, 180, 170, 160, 150])
})
})
builder.add(function testQueryWithLimit(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '3', age: 27},
2: {userId: 'userA', column: '2', age: 28},
3: {userId: 'userA', column: '1', age: 29},
4: {userId: 'userA', column: '4', age: 30}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.setLimit(2)
.execute()
.then(function (data) {
test.equal(data.result[0].age, 28)
test.equal(data.result[1].age, 29)
test.equal(data.result.length, 2)
test.deepEqual(data.LastEvaluatedKey, {userId: 'userA', column: '1'})
return client.newQueryBuilder('user')
.setStartKey(data.LastEvaluatedKey)
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.execute()
})
.then(function (data) {
test.equal(data.result[0].age, 30, 'Age should match 30')
test.equal(data.result.length, 1, '1 result should be returned')
})
})
builder.add(function testQueryWithNext(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '3', age: 27},
2: {userId: 'userA', column: '2', age: 28},
3: {userId: 'userA', column: '1', age: 29},
4: {userId: 'userA', column: '4', age: 30}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 25)
.scanBackward()
.setLimit(3)
.execute()
.then(function (data) {
test.equal(data.result.length, 3)
test.equal(data.result[0].age, 30)
test.equal(data.result[1].age, 29)
test.equal(data.result[2].age, 28)
test.ok(data.hasNext())
return data.next()
})
.then(function (data) {
test.equal(data.result.length, 1)
test.equal(data.result[0].age, 27)
test.ok(!data.hasNext())
})
})
builder.add(function testQueryWithLimitBackwards(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '3', age: 27},
2: {userId: 'userA', column: '2', age: 28},
3: {userId: 'userA', column: '1', age: 29},
4: {userId: 'userA', column: '4', age: 30}
}
})
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.scanBackward()
.indexLessThan('age', 30)
.setLimit(2)
.execute()
.then(function (data) {
test.equal(data.result.length, 2)
test.equal(data.result[0].age, 29)
test.equal(data.result[1].age, 28)
test.deepEqual(data.LastEvaluatedKey, {userId: 'userA', column: '2'})
return client.newQueryBuilder('user')
.setStartKey(data.LastEvaluatedKey)
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexLessThan('age', 30)
.scanBackward()
.execute()
})
.then(function (data) {
test.equal(data.result[0].age, 27)
test.equal(data.result.length, 1)
})
})
builder.add(function testQueryWithMaxResultSize(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27},
2: {userId: 'userA', column: '4', age: 28},
3: {userId: 'userA', column: '3', age: 29},
4: {userId: 'userA', column: '2', age: 30}
}
})
db.getTable('user').setMaxResultSetSize(1)
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.setLimit(2)
.execute()
.then(function (data) {
test.equal(data.result.length, 1, '1 result should be returned')
test.equal(data.result[0].age, 28, 'Age should match 28')
test.deepEqual(data.LastEvaluatedKey, {userId: 'userA', column: '4'})
})
})
builder.add(function testDescribeTable(test) {
return client.describeTable('user')
.execute()
.then(function (data) {
var tableDescription = data.Table
var attributes = tableDescription.AttributeDefinitions
var keySchema = tableDescription.KeySchema
test.ok(tableDescription, 'Table description should exist.')
test.equal(attributes.length, 2, 'Table should have 2 attributes in AttributeDefinitions (keys).')
test.equal(keySchema.length, 2, 'Table should have 2 attributes in KeySchema (keys).')
test.equal(tableDescription.TableName, 'user', 'Table name should be user.')
test.equal(tableDescription.TableStatus, 'ACTIVE', 'Table status should be active.')
// deep check attributes
for (var i = 0; i < attributes.length; i++) {
var attribute = attributes[i]
if (attribute.AttributeName == 'userId') {
test.deepEqual(attribute, {AttributeName: 'userId', AttributeType: 'S'})
} else if (attribute.AttributeName == 'column') {
test.deepEqual(attribute, {AttributeName: 'column', AttributeType: 'S'})
}
}
// deep check key schemas
for (i = 0; i < keySchema.length; i++) {
var key = keySchema[i]
if (key.AttributeName == 'userId') {
test.deepEqual(key, {AttributeName: 'userId', KeyType: 'HASH'})
} else if (key.AttributeName == 'column') {
test.deepEqual(key, {AttributeName: 'column', KeyType: 'RANGE'})
}
}
test.expect(9) // make sure the tests in conditionals ran
})
})
builder.add(function testQueryFiltering(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27, name: 'Ringo'},
2: {userId: 'userA', column: '4', age: 28, name: 'George'},
3: {userId: 'userA', column: '3', age: 29, name: 'John'},
4: {userId: 'userA', column: '2', age: 30, name: 'Paul'}
}
})
var filter = client.newConditionBuilder()
.filterAttributeBeginsWith('name', 'Geo')
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.withFilter(filter)
.execute()
.then(function (data) {
test.deepEqual(data.result[0], {userId: 'userA', column: '4', age: 28, name: 'George'})
test.equal(data.result.length, 1)
})
})
builder.add(function testQueryFilterNotNull(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27, name: 'Ringo'},
2: {userId: 'userA', column: '4', age: 28, name: 'George'},
3: {userId: 'userA', column: '3', age: 29},
4: {userId: 'userA', column: '2', age: 30}
}
})
var filter = client.newConditionBuilder()
.filterAttributeNotNull('name')
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.withFilter(filter)
.execute()
.then(function (data) {
test.deepEqual(data.result[0], {userId: 'userA', column: '4', age: 28, name: 'George'})
test.equal(data.result.length, 1)
})
})
builder.add(function testBooleanQueryFilter(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27, isHappy: true},
2: {userId: 'userA', column: '4', age: 28, isHappy: false},
3: {userId: 'userA', column: '3', age: 29, isHappy: true},
4: {userId: 'userA', column: '2', age: 30}
}
})
var filter = client.newConditionBuilder()
.filterAttributeEquals('isHappy', true)
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 27)
.withFilter(filter)
.execute()
.then(function (data) {
test.equal(data.result.length, 2)
test.deepEqual(data.result[0], {userId: 'userA', column: '1', age: 27, isHappy: true})
test.deepEqual(data.result[1], {userId: 'userA', column: '3', age: 29, isHappy: true})
})
})
builder.add(function testQueryFilterWithPartitionKeyThrowsError(test) {
var filter = client.newConditionBuilder()
.filterAttributeNotEquals('userId', 'Me')
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.withFilter(filter)
.execute()
.then(function () {
test.fail('Expected validation exception')
})
.fail(function (e) {
if (!client.isValidationError(e)) throw e
})
})
builder.add(function testQueryFilterWithIndexKeyThrowsError(test) {
var filter = client.newConditionBuilder()
.filterAttributeNotEquals('age', 1)
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.setIndexName('age-index')
.indexGreaterThan('age', 15)
.withFilter(filter)
.execute()
.then(function () {
test.fail('Expected validation exception')
})
.fail(function (e) {
if (!client.isValidationError(e)) throw e
})
})
builder.add(function testQueryFilterWithRangeKeyThrowsError(test) {
var filter = client.newConditionBuilder()
.filterAttributeLessThan('createdAt', 30)
return client.newQueryBuilder('cookie')
.setHashKey('cookieId', 'CookieA')
.indexGreaterThan('createdAt', 15)
.withFilter(filter)
.execute()
.then(function () {
test.fail('Expected validation exception')
})
.fail(function (e) {
if (!client.isValidationError(e)) throw e
})
})
builder.add(function testQueryFilterOnGsiIndexThrowsError(test) {
db.getTable('cookie').setGsiDefinitions([
{
hash: {
name: 'cookieType',
type: 'S'
},
range: {
name: 'orderedAt',
type: 'N'
}
}
])
var filter = client.newConditionBuilder()
.filterAttributeLessThan('orderedAt', 30)
return client.newQueryBuilder('cookie')
.setHashKey('cookieType', 'Oreo')
.setIndexName('cookieType-orderedAt-gsi')
.indexGreaterThanEqual('orderedAt', 28)
.withFilter(filter)
.execute()
.then(function () {
test.fail('Expected validation exception')
})
.fail(function (e) {
if (!client.isValidationError(e)) throw e
})
})
builder.add(function testQueryFilterOnGsiWithNoRangeWorks(test) {
db.getTable('cookie').setGsiDefinitions([
{
hash: {
name: 'cookieType',
type: 'S'
}
}
])
db.getTable('cookie').setData({
'cookieA': {
1: {cookieId: 'cookieA', column: '1', createdAt: 1, orderedAt: 27, cookieType: 'Oreo'},
2: {cookieId: 'cookieA', column: '2', createdAt: 2, orderedAt: 29, cookieType: 'Oreo'}
},
'cookieB': {
1: {cookieId: 'cookieB', column: '1', createdAt: 2, orderedAt: 12, cookieType: 'Snickerdoodle'},
2: {cookieId: 'cookieB', column: '2', createdAt: 1, orderedAt: 14, cookieType: 'Snickerdoodle'}
}
})
var filter = client.newConditionBuilder()
.filterAttributeLessThan('orderedAt', 28)
return client.newQueryBuilder('cookie')
.setHashKey('cookieType', 'Oreo')
.setIndexName('index-cookieType-gsi')
.withFilter(filter)
.execute()
.then(function (data) {
test.equal(data.result.length, 1)
test.deepEqual(data.result[0], {cookieId: 'cookieA', column: '1', createdAt: 1, orderedAt: 27, cookieType: 'Oreo'})
})
})
builder.add(function testQueryFilterWithRangeWithIndexDoesNotThrowsError(test) {
var filter = client.newConditionBuilder()
.filterAttributeLessThan('createdAt', 30)
return client.newQueryBuilder('cookie')
.setHashKey('cookieId', 'CookieA')
.setIndexName('age-index')
.indexGreaterThanEqual('age', 28)
.withFilter(filter)
.execute()
.then(function () {
test.ok(true)
})
.fail(function () {
test.fail('Expected not to fail')
})
})
builder.add(function testDeleteItem(test) {
var conditions = client.newConditionBuilder()
.expectAttributeEquals('userId', 'userA')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.deleteAttribute('age')
.execute()
.then(function (data) {
test.equal(data.result.column, '@')
test.equal(data.result.age, undefined)
return client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
})
.then(function (data) {
test.equal(data.result.age, undefined)
})
})
builder.add(function testPutAttributeNonExisting(test) {
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.enableUpsert()
.putAttribute('age', 30)
.putAttribute('height', 72)
.execute()
.then(function (data) {
test.equal(data.result.age, 30, 'result age should be 30')
test.equal(data.result.height, 72, 'result height should be 72')
return client.getItem('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.execute()
})
.then(function (data) {
test.equal(data.result.age, 30, 'result age should be 30')
test.equal(data.result.height, 72, 'result height should be 72')
})
})
builder.add(function testDeleteItemFromSet(test) {
var conditions = client.newConditionBuilder()
.expectAttributeEquals('userId', 'userA')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.deleteFromAttribute('luckyNumbers', [3])
.execute()
.then(function () {
return client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
})
.then(function (data) {
data.result.luckyNumbers.sort()
test.deepEqual(data.result.luckyNumbers, [1, 5])
})
})
builder.add(function testLongKey(test) {
// Create a string 2^10 chars long.
var str = '.'
for (var i = 0; i < 10; i++) {
str = str + str
}
return client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', str)
.execute()
.then(function () {
test.fail('Expected validation exception')
})
.fail(function (e) {
if (!client.isValidationError(e)) throw e
})
})
builder.add(function testQueryFilterIn(test) {
db.getTable('user').setData({
'userA': {
1: {userId: 'userA', column: '1', age: 27, name: 'Ringo'},
2: {userId: 'userA', column: '4', age: 28, name: 'George'},
3: {userId: 'userA', column: '3', age: 29, name: 'Paul'},
4: {userId: 'userA', column: '2', age: 30}
}
})
var filter = client.newConditionBuilder()
.filterAttributeIn('name', ['Ringo', 'George'])
return client.newQueryBuilder('user')
.setHashKey('userId', 'userA')
.withFilter(filter)
.execute()
.then(function (data) {
test.deepEqual(['George', 'Ringo'], data.result.map(function (r) {
return r.name
}).sort())
})
})
builder.add(function testAbsentConditionUpdateSuccess(test) {
var conditions = client.newConditionBuilder()
.expectAttributeAbsent('userId')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userNew')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
return client.getItem('user')
.setHashKey('userId', 'userNew')
.setRangeKey('column', '@')
.execute()
})
.then(function (data) {
test.deepEqual({userId: 'userNew', column: '@'}, data.result)
})
})
builder.add(function testAbsentConditionUpdateFail(test) {
var conditions = client.newConditionBuilder()
.expectAttributeAbsent('userId')
return client.newUpdateBuilder('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.withCondition(conditions)
.execute()
.then(function () {
test.fail('Expected error')
})
.fail(client.throwUnlessConditionalError)
})
================================================
FILE: test/testGetItem.js
================================================
// Copyright 2013 The Obvious Corporation.
var utils = require('./utils/testUtils.js')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var onError = console.error.bind(console)
var initialData = [{"userId": "userA", "column": "@", "age": "29"}]
// basic setup for the tests, creating record userA with range key @
exports.setUp = function (done) {
this.db = utils.getMockDatabase()
this.client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
utils.createTable(this.db, "user", "userId", "column")
.thenBound(utils.initTable, null, {db: this.db, tableName: "user", data: initialData})
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
utils.deleteTable(this.db, "user")
.fin(done)
}
// check that an item exists
builder.add(function testItemExists(test) {
return this.client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
.then(function (data) {
test.equal(data.result.age, 29, 'Age should match the provided age')
})
})
// check that only selected attributes are returned
builder.add(function testSelectedAttributes(test) {
return this.client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.selectAttributes(['userId', 'column'])
.execute()
.then(function (data) {
test.equal(data.result.column, '@', 'Column should be defined')
test.equal(data.result.age, undefined, 'Age should be undefined')
})
})
// check that an item doesn't exist
builder.add(function testItemDoesNotExist(test) {
return this.client.getItem('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.execute()
.then(function (data) {
test.equal(data.result, undefined, 'Record should not exist')
})
})
================================================
FILE: test/testGetSet.js
================================================
var utils = require('./utils/testUtils.js')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var onError = console.error.bind(console)
var initialData = [ {"userId": "userA", "column": "@", "postIds": ['1a', '1b', '1c']}
, {"userId": "userB", "column": "@", "postIds": [1, 2, 3]}
, {"userId": "userC", "column": "@"}]
/*
* Sets up for test, and creates a record userA with range key @.
*/
exports.setUp = function (done) {
this.db = utils.getMockDatabase()
this.client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
utils.createTable(this.db, "user", "userId", "column")
.thenBound(utils.initTable, null, {db: this.db, tableName: "user", data: initialData})
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
utils.deleteTable(this.db, "user")
.fin(done)
}
// get the set of strings
builder.add(function testStringSetRetrieve(test) {
return this.client.getItem('user')
.setHashKey('userId', 'userA')
.setRangeKey('column', '@')
.execute()
.then(function (data) {
test.deepEqual(data.result.postIds, ['1a', '1b', '1c'], "postIds should be ['1a', '1b', '1c']")
})
})
// get the set of numbers
builder.add(function testNumberSetRetrieve(test) {
return this.client.getItem('user')
.setHashKey('userId', 'userB')
.setRangeKey('column', '@')
.execute()
.then(function (data) {
test.deepEqual(data.result.postIds, [1, 2, 3], "postIds should be [1, 2, 3]")
})
})
// get a set that doesn't exist
builder.add(function testSetDoesNotExist(test) {
return this.client.getItem('user')
.setHashKey('userId', 'userC')
.setRangeKey('column', '@')
.execute()
.then(function (data) {
test.equal(data.result.postIds, undefined, "postIds should not exist for userC")
})
})
================================================
FILE: test/testHashKeyOnly.js
================================================
// Copyright 2013 The Obvious Corporation.
var utils = require('./utils/testUtils.js')
var nodeunitq = require('nodeunitq')
var builder = new nodeunitq.Builder(exports)
var onError = function (err) {
console.error(err.stack)
}
var initialData = [{"userId": "userA", "column": "@", "age": "29"}]
// basic setup for the tests, creating record userA with range key @
exports.setUp = function (done) {
var self = this
this.db = utils.getMockDatabase()
this.client = utils.getMockDatabaseClient()
utils.ensureLocalDynamo()
utils.createTable(self.db, "userRangeOnly", "userId")
.thenBound(utils.initTable, null, {db: self.db, tableName: "userRangeOnly", data: initialData})
.fail(onError)
.fin(done)
}
exports.tearDown = function (done) {
utils.deleteTable(this.db, 'userRangeOnly')
.fin(done)
}
// check that an item exists
builder.add(functio
gitextract_lce59bue/
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── dynamite.js
├── externs/
│ └── aws-sdk.js
├── lib/
│ ├── BatchGetItemBuilder.js
│ ├── Builder.js
│ ├── Client.js
│ ├── ConditionBuilder.js
│ ├── DeleteItemBuilder.js
│ ├── DescribeTableBuilder.js
│ ├── DynamoRequest.js
│ ├── DynamoResponse.js
│ ├── FakeDynamo.js
│ ├── GetItemBuilder.js
│ ├── PutItemBuilder.js
│ ├── QueryBuilder.js
│ ├── ScanBuilder.js
│ ├── UpdateBuilder.js
│ ├── UpdateExpressionBuilder.js
│ ├── common.js
│ ├── errors.js
│ ├── localUpdater.js
│ ├── reserved.js
│ └── typeUtil.js
├── package.json
└── test/
├── testBatchGetItem.js
├── testConditions.js
├── testDeleteItem.js
├── testDescribeTable.js
├── testFakeDynamo.js
├── testGetItem.js
├── testGetSet.js
├── testHashKeyOnly.js
├── testLocalUpdater.js
├── testPutItem.js
├── testPutSet.js
├── testQuery.js
├── testScan.js
├── testStringSet.js
├── testTypeUtil.js
├── testUpdateItem.js
└── utils/
└── testUtils.js
SYMBOL INDEX (61 symbols across 22 files)
FILE: externs/aws-sdk.js
function DynamoDB (line 36) | function DynamoDB() {}
FILE: lib/BatchGetItemBuilder.js
function BatchGetItemBuilder (line 16) | function BatchGetItemBuilder(options) {
FILE: lib/Builder.js
function Builder (line 10) | function Builder(options) {
FILE: lib/Client.js
function Client (line 40) | function Client(options) {
FILE: lib/ConditionBuilder.js
function ConditionBuilder (line 9) | function ConditionBuilder() {
function ConditionExpr (line 318) | function ConditionExpr(args) {
function ConditionExprOp (line 384) | function ConditionExprOp(op, args) {
function ConditionExprVal (line 523) | function ConditionExprVal(val) {
function ConditionExprAttr (line 549) | function ConditionExprAttr(name) {
FILE: lib/DeleteItemBuilder.js
function DeleteItemBuilder (line 10) | function DeleteItemBuilder(options) {
FILE: lib/DescribeTableBuilder.js
function DescribeTableBuilder (line 9) | function DescribeTableBuilder(options) {
FILE: lib/DynamoRequest.js
function DynamoRequest (line 10) | function DynamoRequest(options) {
FILE: lib/DynamoResponse.js
function getOriginalTableName (line 93) | function getOriginalTableName (tablePrefix, tableName) {
FILE: lib/FakeDynamo.js
function forEachKeyCondition (line 14) | function forEachKeyCondition(data, callback) {
function getKeyConditionByName (line 22) | function getKeyConditionByName(data, name) {
function forEachFilterCondition (line 32) | function forEachFilterCondition(data, callback) {
function getKeyConditionFn (line 42) | function getKeyConditionFn(data) {
function getFilterFn (line 46) | function getFilterFn(data) {
function getConditionFn (line 51) | function getConditionFn(data) {
function DynamoError (line 62) | function DynamoError(name, message) {
function FakeTable (line 75) | function FakeTable(name) {
function FakeDynamo (line 729) | function FakeDynamo() {
function performOp (line 780) | function performOp(operation) {
FILE: lib/GetItemBuilder.js
function GetItemBuilder (line 9) | function GetItemBuilder(options) {
FILE: lib/PutItemBuilder.js
function PutItemBuilder (line 13) | function PutItemBuilder(options) {
FILE: lib/QueryBuilder.js
function QueryBuilder (line 14) | function QueryBuilder(options) {
FILE: lib/ScanBuilder.js
function ScanBuilder (line 12) | function ScanBuilder(options) {
FILE: lib/UpdateBuilder.js
function UpdateBuilder (line 15) | function UpdateBuilder(options) {
FILE: lib/UpdateExpressionBuilder.js
function getActionName (line 5) | function getActionName(attr) {
function UpdateExpressionBuilder (line 24) | function UpdateExpressionBuilder(attributes) {
FILE: lib/errors.js
function ConditionalError (line 12) | function ConditionalError(data, msg, requestId) {
function ProvisioningError (line 33) | function ProvisioningError(data, msg, isWrite, requestId) {
function ValidationError (line 55) | function ValidationError(data, msg, isWrite, requestId) {
function IndexNotExistError (line 73) | function IndexNotExistError(hashKeyName, rangeKeyName) {
function InvalidReturnValuesError (line 87) | function InvalidReturnValuesError(returnValues) {
FILE: lib/localUpdater.js
function _cloneObject (line 9) | function _cloneObject (oldItem) {
function _processDeleteAction (line 25) | function _processDeleteAction (item, field, update) {
function _processPutAction (line 55) | function _processPutAction (item, field, update) {
function _processAddAction (line 74) | function _processAddAction (item, field, update) {
function update (line 93) | function update (oldItem, updates) {
FILE: lib/typeUtil.js
function unpackObjectOrArray (line 46) | function unpackObjectOrArray(object) {
function packObjectOrArray (line 64) | function packObjectOrArray(object, attributes) {
function valueToObject (line 84) | function valueToObject(value) {
function objectToType (line 128) | function objectToType(obj) {
function objectToValue (line 143) | function objectToValue(obj) {
function objectIsEmpty (line 172) | function objectIsEmpty(obj) {
function objectIsNonEmptySet (line 181) | function objectIsNonEmptySet(obj) {
function addToSet (line 195) | function addToSet(set, additions) {
function deleteFromSet (line 220) | function deleteFromSet(set, deletions) {
function addToNumber (line 248) | function addToNumber(number, addition) {
function clone (line 268) | function clone(oldItem) {
function getAttributeAlias (line 290) | function getAttributeAlias(key) {
function isReservedWord (line 308) | function isReservedWord(key) {
function isAlphaNumeric (line 315) | function isAlphaNumeric(key) {
function needsAttributeAlias (line 323) | function needsAttributeAlias(key) {
function buildAttributeNames (line 332) | function buildAttributeNames(attrList) {
function extendAttributeNames (line 345) | function extendAttributeNames(existingNames, newNames) {
function extendAttributeValues (line 358) | function extendAttributeValues(existingValues, newValues) {
FILE: test/testBatchGetItem.js
function onError (line 8) | function onError(err) {
FILE: test/testConditions.js
function assertUserIds (line 72) | function assertUserIds(test, expectedUserIds, filter) {
FILE: test/testQuery.js
function checkResults (line 42) | function checkResults(test, total, offset) {
Condensed preview — 46 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (277K chars).
[
{
"path": ".eslintignore",
"chars": 7,
"preview": "externs"
},
{
"path": ".eslintrc",
"chars": 264,
"preview": "{\n \"rules\": {\n \"no-console\": 0,\n \"linebreak-style\": [\n 2,\n \"unix\"\n ],\n "
},
{
"path": ".gitignore",
"chars": 107,
"preview": "lib-cov\n*.seed\n*.log\n*.csv\n*.dat\n*.out\n*.pid\n*.gz\n\npids\nlogs\nresults\n\nnode_modules\nnpm-debug.log\n.DS_Store\n"
},
{
"path": ".travis.yml",
"chars": 73,
"preview": "language: node_js\nsudo: false\nnode_js:\n - \"6\"\n - \"8\"\n - \"10\"\n - \"11\"\n"
},
{
"path": "LICENSE",
"chars": 10381,
"preview": "Copyright 2013 The Obvious Corporation.\nhttp://obvious.com/\n\nLicensed under the Apache License, Version 2.0 (the \"Licens"
},
{
"path": "README.md",
"chars": 17359,
"preview": "# Dynamite [](https://travis-ci.org/Medium/dynam"
},
{
"path": "dynamite.js",
"chars": 153,
"preview": "module.exports = {\n Client: require('./lib/Client'),\n FakeDynamo: require('./lib/FakeDynamo'),\n ConditionBuilder: req"
},
{
"path": "externs/aws-sdk.js",
"chars": 755,
"preview": "\n// Require an event emitter, because some of these apis return emitters.\nvar EventEmitter = require('events').EventEmit"
},
{
"path": "lib/BatchGetItemBuilder.js",
"chars": 4986,
"preview": "var DynamoRequest = require('./DynamoRequest')\nvar Builder = require('./Builder')\nvar Q = require('kew')\nvar util = requ"
},
{
"path": "lib/Builder.js",
"chars": 5373,
"preview": "var util = require('util')\nvar Q = require('kew')\nvar errors = require('./errors')\nvar DynamoResponse = require('./Dynam"
},
{
"path": "lib/Client.js",
"chars": 7336,
"preview": "var assert = require('assert')\nvar AWS = require('aws-sdk')\n\nvar AWSName = require('./common').AWSName\nvar ConditionBuil"
},
{
"path": "lib/ConditionBuilder.js",
"chars": 19012,
"preview": "var assert = require('assert')\nvar typ = require('typ')\nvar typeUtil = require('./typeUtil')\nvar util = require('util')\n"
},
{
"path": "lib/DeleteItemBuilder.js",
"chars": 1082,
"preview": "var DynamoRequest = require('./DynamoRequest')\nvar DynamoResponse = require('./DynamoResponse')\nvar Builder = require('."
},
{
"path": "lib/DescribeTableBuilder.js",
"chars": 643,
"preview": "var DynamoRequest = require('./DynamoRequest')\nvar Builder = require('./Builder')\n\n/**\n * @param {Object} options\n * @co"
},
{
"path": "lib/DynamoRequest.js",
"chars": 8091,
"preview": "var ConditionBuilder = require('./ConditionBuilder')\nvar UpdateExpressionBuilder = require('./UpdateExpressionBuilder')\n"
},
{
"path": "lib/DynamoResponse.js",
"chars": 3198,
"preview": "// Copyright 2013. The Obvious Corporation.\n\nvar typeUtil = require('./typeUtil')\n\n/**\n * @param {?string} tablePrefix\n "
},
{
"path": "lib/FakeDynamo.js",
"chars": 23840,
"preview": "var Q = require('kew')\nvar typ = require('typ')\nvar typeUtil = require('./typeUtil')\nvar localUpdater = require('./local"
},
{
"path": "lib/GetItemBuilder.js",
"chars": 881,
"preview": "var DynamoRequest = require('./DynamoRequest')\nvar Builder = require('./Builder')\n\n/**\n * @param {Object} options\n * @co"
},
{
"path": "lib/PutItemBuilder.js",
"chars": 1854,
"preview": "var typ = require('typ')\nvar DynamoRequest = require('./DynamoRequest')\nvar DynamoResponse = require('./DynamoResponse')"
},
{
"path": "lib/QueryBuilder.js",
"chars": 4421,
"preview": "var typ = require('typ')\nvar ConditionBuilder = require('./ConditionBuilder')\nvar DynamoRequest = require('./DynamoReque"
},
{
"path": "lib/ScanBuilder.js",
"chars": 2530,
"preview": "var typ = require('typ')\nvar DynamoRequest = require('./DynamoRequest')\nvar DynamoResponse = require('./DynamoResponse')"
},
{
"path": "lib/UpdateBuilder.js",
"chars": 3935,
"preview": "var typ = require('typ')\nvar util = require('util')\nvar DynamoRequest = require('./DynamoRequest')\nvar DynamoResponse = "
},
{
"path": "lib/UpdateExpressionBuilder.js",
"chars": 3738,
"preview": "var typeUtil = require('./typeUtil')\n\n// The UpdateExpression API has different names for all the action types, but they"
},
{
"path": "lib/common.js",
"chars": 342,
"preview": "var AWSName = {\n REGION: [\"us-east-1\",\n \"us-west-1\",\n \"us-west-2\",\n \"eu-west-1\",\n "
},
{
"path": "lib/errors.js",
"chars": 2779,
"preview": "// Copyright 2013. The Obvious Corporation.\n\nvar util = require('util')\n\n/**\n * @param {Object} data\n * @param {string} "
},
{
"path": "lib/localUpdater.js",
"chars": 3232,
"preview": "// Copyright 2015 A Medium Corporation.\n\nvar typeUtil = require('./typeUtil')\n\n/**\n * @param {Object} oldItem\n * @return"
},
{
"path": "lib/reserved.js",
"chars": 7232,
"preview": "/**\n * @fileoverview http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html\n */\n\nvar list = "
},
{
"path": "lib/typeUtil.js",
"chars": 10188,
"preview": "// Copyright 2013 The Obvious Corporation\n\n/**\n * @fileoverview Utility functions that convert plain javascript objects "
},
{
"path": "package.json",
"chars": 1252,
"preview": "{\n \"name\": \"dynamite\",\n \"description\": \"promise-based DynamoDB client\",\n \"version\": \"1.0.0\",\n \"homepage\": \"https://g"
},
{
"path": "test/testBatchGetItem.js",
"chars": 4708,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeuni"
},
{
"path": "test/testConditions.js",
"chars": 2672,
"preview": "// Copyright 2016 A Medium Corporation\n'use strict'\n\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require"
},
{
"path": "test/testDeleteItem.js",
"chars": 5185,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar typ = require('typ')\nvar nod"
},
{
"path": "test/testDescribeTable.js",
"chars": 1073,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeuni"
},
{
"path": "test/testFakeDynamo.js",
"chars": 32121,
"preview": "// Copyright 2013 The Obvious Corporation\n\nvar nodeunitq = require('nodeunitq')\nvar builder = new nodeunitq.Builder(expo"
},
{
"path": "test/testGetItem.js",
"chars": 1837,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeuni"
},
{
"path": "test/testGetSet.js",
"chars": 1867,
"preview": "var utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeunitq')\nvar builder = new nodeunitq.Builder(exp"
},
{
"path": "test/testHashKeyOnly.js",
"chars": 3189,
"preview": "// Copyright 2013 The Obvious Corporation.\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeunit"
},
{
"path": "test/testLocalUpdater.js",
"chars": 3699,
"preview": "// Copyright 2015 A Medium Corporation.\n\nvar typeUtil = require('../lib/typeUtil')\nvar localUpdater = require('../lib/lo"
},
{
"path": "test/testPutItem.js",
"chars": 6368,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeuni"
},
{
"path": "test/testPutSet.js",
"chars": 7023,
"preview": "var utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeunitq')\nvar builder = new nodeunitq.Builder(exp"
},
{
"path": "test/testQuery.js",
"chars": 13818,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar ConditionBuilder = require('../lib/ConditionBuilder')\nvar utils = requir"
},
{
"path": "test/testScan.js",
"chars": 8554,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeuni"
},
{
"path": "test/testStringSet.js",
"chars": 2386,
"preview": "var utils = require('./utils/testUtils.js')\nvar nodeunitq = require('nodeunitq')\nvar builder = new nodeunitq.Builder(exp"
},
{
"path": "test/testTypeUtil.js",
"chars": 2408,
"preview": "// Copyright 2015 A Medium Corporation.\n\nvar typeUtil = require('../lib/typeUtil')\nvar nodeunitq = require('nodeunitq')\n"
},
{
"path": "test/testUpdateItem.js",
"chars": 14999,
"preview": "// Copyright 2013 The Obvious Corporation.\n\nvar utils = require('./utils/testUtils.js')\nvar errors = require('../lib/err"
},
{
"path": "test/utils/testUtils.js",
"chars": 7044,
"preview": "/**\n * Provides ultility function for unit testing.\n *\n * @module utils\n **/\nvar AWS = require('aws-sdk')\nvar localDynam"
}
]
About this extraction
This page contains the full source code of the Medium/dynamite GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 46 files (257.8 KB), approximately 70.6k tokens, and a symbol index with 61 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.