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 [![Build Status](https://travis-ci.org/Medium/dynamite.svg?branch=master)](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.} 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.>} queryData A map from table name to * requested keys in Dynamo API format. * @return {Q.Promise.} 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} 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.} 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.} 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} 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} 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} 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.} 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.} 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.} 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.} 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.} 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.} } 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.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.} */ 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.} */ 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.} */ 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.} 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.} 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.} * @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.} * @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.|Array.} 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.|Array.} 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.|Array.} 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} 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.|undefined), * M: (Object|undefined), * L: (Array|undefined), * N: (string|undefined), * NS: (Array.|undefined), * NULL: (null|undefined), * S: (string|undefined), * SS: (Array.|undefined) * }} */ var AWSAttributeValue /** * Convert Dynamo AttributeValue map object(s) to plain javascript object(s) * * @param {Object.|Array.>} object Dynamo AttributeValue map object(s) * @return {Object|Array.} 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.|undefined} object * @param {Object=} attributes an optional map of the attributes that need to convert. * @return {Object.|Array.>|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.|Array.|boolean|Object} a javascript primitive value */ function objectToValue(obj) { switch (objectToType(obj)) { case 'SS': return (/** @type {Array.} */(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} 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 (https://github.com/azulus)", "Jean Hsu (https://github.com/jyhsu)", "Jonathan Fuchs (https://github.com/jfuchs)", "Artem Titoulenko (https://github.com/ArtemTitoulenko)", "Xiao Ma (https://github.com/x-ma)", "Jamie Talbot (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(function testItemExists(test) { return this.client.getItem('userRangeOnly') .setHashKey('userId', 'userA') .execute() .then(function (data) { test.equal(data.result.age, 29, 'Age should match the provided age') }) }) // put an item and check that it exists builder.add(function testSimplePut(test) { var self = this return this.client.putItem("userRangeOnly", { userId: 'userB', column: '@', age: 30 }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userB", null, 'userRangeOnly') }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "Age should be set") }) }) // put a list of strings and check if they exist builder.add(function testStringSetPut(test) { var self = this return this.client.putItem("userRangeOnly", { userId: 'userC', column: '@', postIds: ['3a', '3b', '3c'] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userC", null, 'userRangeOnly') }) .then(function (data) { test.deepEqual(data['Item']['postIds'].SS, ['3a', '3b', '3c'], "postIds should be ['3a', '3b', '3c']") }) }) // test putting an attribute for an existing record builder.add(function testPutAttributeExisting(test) { var self = this return this.client.newUpdateBuilder('userRangeOnly') .setHashKey('userId', 'userA') .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, 'height should be 72') return utils.getItemWithSDK(self.db, "userA", null, 'userRangeOnly') }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "result age should be 30") test.equal(data['Item']['height'].N, "72", "height should be 72") }) }) builder.add(function testDeleteItem(test) { //AWS.config.logger = process.stdout var self = this return self.client.deleteItem('userRangeOnly') .setHashKey('userId', 'userA') .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", null, "userRangeOnly") }) .then(function (data) { test.equal(data['Item'], undefined, "User should be deleted~~~" + JSON.stringify(data)) }) }) ================================================ FILE: test/testLocalUpdater.js ================================================ // Copyright 2015 A Medium Corporation. var typeUtil = require('../lib/typeUtil') var localUpdater = require('../lib/localUpdater') var nodeunitq = require('nodeunitq') var builder = new nodeunitq.Builder(exports) var Q = require('kew') builder.add(function testDelete(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@", "age": 29, "someStringSet": ['a', 'b', 'c'] }) var updated = localUpdater.update(data, { 'age': { Action: 'DELETE' } }) test.equal(data.age.N, 29, 'age should not change') test.ok(updated.age === undefined, 'age should be undefined') return Q.resolve() }) builder.add(function testDeleteFromSet(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@", "age": 29, "someStringSet": ['a', 'b', 'c'] }) var updated = localUpdater.update(data, { 'someStringSet': { Action: 'DELETE', Value: typeUtil.valueToObject(['b','c','d']) } }) test.deepEqual(data.someStringSet.SS, ['a', 'b', 'c'], 'someStringSet should not change') test.deepEqual(updated.someStringSet.SS, ['a'], 'someStringSet should equal [\'a\']') return Q.resolve() }) builder.add(function testAddToSet(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@", "age": 29, "someStringSet": ['a', 'b', 'c'] }) var updated = localUpdater.update(data, { 'someStringSet': { Action: 'ADD', Value: typeUtil.valueToObject(['b','c','d']) } }) test.deepEqual(data.someStringSet.SS, ['a', 'b', 'c'], 'someStringSet should not change') test.deepEqual(updated.someStringSet.SS, ['a', 'b', 'c', 'd'], 'someStringSet should equal [\'a\', \'b\', \'c\', \'d\']') return Q.resolve() }) builder.add(function testAddToEmptySet(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@", "age": 29 }) var updated = localUpdater.update(data, { 'someStringSet': { Action: 'ADD', Value: typeUtil.valueToObject(['b','c','d']) } }) test.ok(data.someStringSet === undefined, 'someStringSet should not change') test.deepEqual(updated.someStringSet.SS, ['b', 'c', 'd'], 'someStringSet should equal [\'a\', \'b\', \'c\', \'d\']') return Q.resolve() }) builder.add(function testAddToNumber(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@", "age": 29 }) var updated = localUpdater.update(data, { 'age': { Action: 'ADD', Value: typeUtil.valueToObject(1) } }) test.deepEqual(data.age.N, 29, 'age should not change') test.deepEqual(updated.age.N, '30', 'age should equal 30') return Q.resolve() }) builder.add(function testAddToEmptyNumber(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@" }) var updated = localUpdater.update(data, { 'age': { Action: 'ADD', Value: typeUtil.valueToObject(30) } }) test.ok(data.age === undefined, 'age should not change') test.deepEqual(updated.age.N, '30', 'age should equal 30') return Q.resolve() }) builder.add(function testPut(test) { var data = typeUtil.packObjectOrArray({ "userId": "userA", "column": "@", "age": 29, "someStringSet": ['a', 'b', 'c'] }) var updated = localUpdater.update(data, { 'someStringSet': { Action: 'PUT', Value: typeUtil.valueToObject(['b','c','d']) } }) test.deepEqual(data.someStringSet.SS, ['a', 'b', 'c'], 'someStringSet should not change') test.deepEqual(updated.someStringSet.SS, ['b', 'c', 'd'], 'someStringSet should equal [\'b\', \'c\', \'d\']') return Q.resolve() }) ================================================ FILE: test/testPutItem.js ================================================ // Copyright 2013 The Obvious Corporation. var utils = require('./utils/testUtils.js') var nodeunitq = require('nodeunitq') var builder = new nodeunitq.Builder(exports) var errors = require('../lib/errors') var onError = console.error.bind(console) var initialData = [{"userId": "userA", "column": "@", "age": "29"}] /* * 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) } builder.add(function testSetInvalidReturnValue(test) { var putBuilder = this.client.putItem('user', { userId: 'userB', age: 30 }) test.throws(function () { putBuilder.setReturnValues('ALL_SOMETHING') }, errors.InvalidReturnValuesError) test.done() }) // put an item and check that it exists builder.add(function testSimplePut(test) { var self = this return this.client.putItem("user", { userId: 'userB', column: '@', age: 30 }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "Age should be set") }) }) builder.add(function testPutItemWithReturnValuesNone(test) { var self = this return this.client.putItem("user", { userId: 'userB', column: '@', age: 30 }) .setReturnValues('NONE') .execute() .then(function (data) { test.equal(data.result, undefined) return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "Age should be set") }) }) // put overrides all fields builder.add(function testOverridePut(test) { var self = this return this.client.putItem("user", { userId: 'userA', column: '@', height: 72 }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'], undefined, "Age should be undefined") test.equal(data['Item']['height'].N, "72", "Height should be 72") }) }) // put with successful conditional exists builder.add(function testPutWithConditional(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeEquals('age', 29) return this.client.putItem("user", { userId: 'userA', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['height'].N, "72", "Height should be 72") }) }) // put with successful absent conditional exists builder.add(function testPutWithAbsentConditional(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('height') return this.client.putItem("user", { userId: 'userA', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['height'].N, "72", "Height should be 72") }) }) // put with successful absent conditional doesn't exist builder.add(function testPutWithAbsentConditionalAndNoRecord(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('age') return this.client.putItem("user", { userId: 'userB', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['height'].N, "72", "Height should be 72") }) }) // put with failed conditional exists builder.add(function testPutWithFailedConditional(test) { var conditions = this.client.newConditionBuilder() .expectAttributeEquals('age', 30) return this.client.putItem("user", { userId: 'userA', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { test.fail("'testPutWithFailedConditional' failed") }) .fail(this.client.throwUnlessConditionalError) }) // put with failed conditional doesn't exist builder.add(function testPutWithFailedConditionalForNoRecord(test) { var conditions = this.client.newConditionBuilder() .expectAttributeEquals('age', 29) return this.client.putItem("user", { userId: 'userB', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { test.fail("'testPutWithFailedConditionalForNoRecord' failed") }) .fail(this.client.throwUnlessConditionalError) }) // put set with failed absent conditional exists builder.add(function testPutWithFailedAbsentConditionalExists(test) { var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('age') return this.client.putItem("user", { userId: 'userA', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { test.fail("'testPutWithFailedAbsentConditionalExists' failed") }) .fail(this.client.throwUnlessConditionalError) }) // trigger a conditional error and check its content builder.add(function testConditionalErrorFormat(test) { var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('age') return this.client.putItem("user", { userId: 'userA', column: '@', height: 72 }) .withCondition(conditions) .execute() .then(function () { test.fail('This putItem request should fail due to a conditional error') }) .fail(function (err) { test.ok(err instanceof errors.ConditionalError) test.equals('The conditional request failed', err.message, 'The "message" field should be the custom message') test.equals('The conditional request failed', err.details, 'The "details" field should be the custom message') test.equals('user', err.table, 'The "table" field should be the right table name') test.ok(!!err.requestId, 'The "requestId" field should exist') }) }) ================================================ FILE: test/testPutSet.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]}] /* * 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) } // put a list of strings and check if they exist builder.add(function testStringSetPut(test) { var self = this return this.client.putItem("user", { userId: 'userC', column: '@', postIds: ['3a', '3b', '3c'] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userC", "@") }) .then(function (data) { test.deepEqual(data['Item']['postIds'].SS, ['3a', '3b', '3c'], "postIds should be ['3a', '3b', '3c']") }) }) // put a list of numbers and check if they exist builder.add(function testNumberSetPut(test) { var self = this return this.client.putItem("user", { userId: 'userD', column: '@', postIds: [1, 2, 3] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userD", "@") }) .then(function (data) { test.deepEqual(data['Item']['postIds'].NS, [1, 2, 3], "postIds should be [1, 2, 3]") }) }) // override all string set fields builder.add(function testStringSetPutOverride(test) { var self = this return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: ['3a', '3b', '3c'] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['postIds'], undefined, "postIds should not exist") test.deepEqual(data['Item']['otherIds'].SS, ['3a', '3b', '3c'], "otherIds should be ['3a', '3b', '3c']") }) }) // override all number set fields builder.add(function testNumberSetPutOverride(test) { var self = this return this.client.putItem("user", { userId: 'userB', column: '@', otherIds: [4, 5, 6] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['postIds'], undefined, "postIds should not exist") test.deepEqual(data['Item']['otherIds'].NS, [4, 5, 6], "otherIds should be [4, 5, 6]") }) }) // override all number set fields with a string set builder.add(function testNumberSetPutOverrideWithStringSet(test) { var self = this return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: [4, 5, 6] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['postIds'], undefined, "postIds should not exist") test.deepEqual(data['Item']['otherIds'].NS, [4, 5, 6], "otherIds should be [4, 5, 6]") }) }) // put string set with successful conditional exists builder.add(function testStringSetPutWithConditional(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeEquals('postIds', ['1a', '1b', '1c']) return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: ['5a', '5b', '5c'] }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['otherIds'].SS[0], '5a', "otherIds[0] should be 5a") }) }) // put string set with successful absent conditional exists builder.add(function testStringSetPutWithAbsentConditional(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('otherIds') return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: ['5a', '5b', '5c'] }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['otherIds'].SS[0], '5a', "otherIds[0] should be 5a") }) }) // put with successful absent conditional doesn't exist builder.add(function testStringSetPutWithAbsentConditionalDoesntExist(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('otherIds') return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: ['5a', '5b', '5c'] }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['otherIds'].SS[0], '5a', "otherIds[0] should be 5a") }) }) // put set with failed conditional exists builder.add(function testStringSetPutWithFailedConditional(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeEquals('postIds', ['a', 'b', 'c']) return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: ['5a', '5b', '5c'] }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function () { test.fail("'testStringSetPutWithFailedConditional' failed") }) .fail(this.client.throwUnlessConditionalError) }) // put set with failed conditional doesn't exist builder.add(function testStringSetPutWithFailedConditionalForNoRecord(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeEquals('postIds', ['a', 'b', 'c']) return this.client.putItem("user", { userId: 'userC', column: '@', otherIds: ['5a', '5b', '5c'] }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userC", "@") }) .then(function () { test.fail("'testStringSetPutWithFailedConditionalForNoRecord' failed") }) .fail(this.client.throwUnlessConditionalError) }) // put set with failed absent conditional exists builder.add(function testStringSetPutWithFailedConditionalExists(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('postIds', ['1a', '1b', '1c']) return this.client.putItem("user", { userId: 'userA', column: '@', otherIds: ['5a', '5b', '5c'] }) .withCondition(conditions) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function () { test.fail("'testStringSetPutWithFailedConditionalForNoRecord' failed") }) .fail(this.client.throwUnlessConditionalError) }) ================================================ FILE: test/testQuery.js ================================================ // Copyright 2013 The Obvious Corporation. var ConditionBuilder = require('../lib/ConditionBuilder') var utils = require('./utils/testUtils.js') var nodeunitq = require('nodeunitq') var builder = new nodeunitq.Builder(exports) var onError = console.error.bind(console) var tableName = "comments" var rawData = [{"postId": "post1", "column": "@", "title": "This is my post", "content": "And here is some content!", "tags": ['bar', 'foo']}, {"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"}] // sorted data for checking the order of returned data var sortedRawData = [] for (var i = 0; i < rawData.length; i++) { sortedRawData[i] = rawData[i] } sortedRawData.sort(function(obj1, obj2) { return obj1.column > obj2.column ? 1 : -1 }) // basic setup for the tests, creating record userA with Index key @ exports.setUp = function (done) { this.db = utils.getMockDatabase() this.client = utils.getMockDatabaseClient() utils.ensureLocalDynamo() utils.createTable(this.db, tableName, "postId", "column") .thenBound(utils.initTable, null, {"db": this.db, "tableName": tableName, "data": rawData}) .fail(onError) .fin(done) } exports.tearDown = function (done) { utils.deleteTable(this.db, tableName) .fin(done) } function checkResults(test, total, offset) { return function (data) { test.equal(data.result.length, total, total + " records should be returned") for (var i = 0; i < data.result.length; i++) { test.deepEqual(data.result[i], sortedRawData[i + offset], "Row should be retrieved in the correct order") } } } // test basic query builder.add(function testBasicQuery(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .execute() .then(checkResults(test, 6, 0)) }) // test basic query with an empty filter builder.add(function testBasicQueryEmptyFilter(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .withFilter(this.client.newConditionBuilder()) .execute() .then(checkResults(test, 6, 0)) }) // test Index key begins with builder.add(function testindexBeginsWith(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBeginsWith('column', '/comment/') .execute() .then(checkResults(test, 4, 1)) }) // test filtering builder.add(function testFilterByComment(test) { var filter = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "HEY") return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBeginsWith('column', '/comment/') .withFilter(filter) .execute() .then(checkResults(test, 1, 1)) }) // test filter with limit builder.add(function testFilterWithLimit(test) { var filter = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "wh") // The limit parameter is applied before the filter return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBeginsWith('column', '/comment/') .withFilter(filter) .setLimit(2) .execute() .then(checkResults(test, 1, 2)) }) // test Index key between builder.add(function testIndexKeyBetween(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/009999') .execute() .then(checkResults(test, 3, 1)) }) // test Index key less than builder.add(function testIndexKeyLessThan(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexLessThan('column', '/comment/timestamp/001111') .execute() .then(checkResults(test, 1, 0)) }) // test Index key less than equal builder.add(function testIndexKeyLessThanEqual(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexLessThanEqual('column', '/comment/timestamp/001111') .execute() .then(checkResults(test, 2, 0)) }) // test Index key greater than builder.add(function testIndexKeyGreaterThan(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexGreaterThan('column', '/comment/timestamp/001111') .execute() .then(checkResults(test, 4, 2)) }) // test Index key greater than equal builder.add(function testIndexKeyGreaterThanEqual(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexGreaterThanEqual('column', '/comment/timestamp/001111') .execute() .then(checkResults(test, 5, 1)) }) // test Index key equal builder.add(function testIndexKeyEqual(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexEqual('column', '/comment/timestamp/001111') .execute() .then(checkResults(test, 1, 1)) }) // test limit builder.add(function testLimit(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .setLimit(3) .execute() .then(checkResults(test, 3, 1)) }) // test scan forward builder.add(function testScanForward(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .execute() .then(checkResults(test, 4, 1)) }) // test cursoring forward builder.add(function testCursorForward(test) { var client = this.client return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .setLimit(3) .execute() .then(function (data) { return client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .setStartKey(data.LastEvaluatedKey) .execute() }) .then(function (data) { test.equal(data.result.length, 1, "1 record should be returned") test.equal(data.result[0].comment, "where am I?", "Row comment should be set") }) }) // test cursoring backward builder.add(function testCursorBackward(test) { var client = this.client return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .setLimit(3) .scanBackward() .execute() .then(function (data) { return client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .scanBackward() .setStartKey(data.LastEvaluatedKey) .execute() }) .then(function (data) { test.equal(data.result.length, 1, "1 record should be returned") test.equal(data.result[0].comment, "HEYYOOOOO", "Row comment should be set") }) }) // test select attributes builder.add(function testSelectAttributes(test) { var keyOffset = 1 return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .selectAttributes(['postId', 'comment']) .execute() .then(function (data) { test.equal(data.result.length, 4, "4 records should be returned") for (var i = 0; i < data.result.length; i++) { test.equal(data.result[i].comment, sortedRawData[i + keyOffset].comment, "Row comment should be set") test.equal(data.result[i].column, undefined, 'Column should not be set') } }) }) // test set existence builder.add(function testSetExistence(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexEqual('column', '@') .selectAttributes(['postId', 'tags']) .execute() .then(function (data) { test.deepEqual(data.result[0].tags, ['bar', 'foo'], "post should have tags ['bar', 'foo']") }) }) // test scan backward builder.add(function testScanBackward(test) { var keyOffset = 1 return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .scanBackward() .execute() .then(function (data) { test.equal(data.result.length, 4, "4 records should be returned") for (var i = 0; i < data.result.length; i++) { test.deepEqual(data.result[i], sortedRawData[(data.result.length - 1 - i) + keyOffset], "Row should be retrieved in the correct order") } }) }) // test count builder.add(function testCount(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/timestamp/002123', '/comment/timestamp/999999') .getCount() .execute() .then(function (data) { test.equal(data.Count, 2, '"2" should be returned') }) }) // test count if it's zero builder.add(function testCountIfZero(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'postDNE') .indexBetween('column', '/comment/timestamp/002123', '/comment/timestamp/999999') .getCount() .execute() .then(function (data) { test.equal(data.Count, 0, '"0" should be returned') }) }) builder.add(function testNext(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .setLimit(3) .execute() .then(function (data) { test.equal(3, data.Count) test.ok(data.hasNext()) return data.next() }) .then(function (data) { test.equal(1, data.Count) test.ok(!data.hasNext()) return data.next() }) .then(function () { test.fail('Expected error') }) .fail(function (e) { if (e.message !== 'No more results') throw e }) }) builder.add(function testNextWithLimit(test) { return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBetween('column', '/comment/', '/comment/timestamp/999999') .setLimit(2) .execute() .then(function (data) { test.equal(2, data.Count) test.ok(data.hasNext()) return data.next(3) }) .then(function (data) { test.equal(2, data.Count) test.ok(!data.hasNext()) return data.next() }) .then(function () { test.fail('Expected error') }) .fail(function (e) { if (e.message !== 'No more results') throw e }) }) builder.add(function testValidationError(test) { var client = this.client return client.newQueryBuilder('comments') .setHashKey('garbage', 'postId') .execute() .then(function () { test.fail('Expected validation exception') }) .fail(function (e) { if (!client.isValidationError(e)) throw e test.equal('comments', e.table) }) }) builder.add(function testRetryHandler(test) { var client = this.client var calledRetryHandler = 0 return client.newQueryBuilder('comments') .setHashKey('invalid', 'post1') .setRetryHandler(function (method, table, response) { test.equal(response.error.retryable, false) ++calledRetryHandler }) .execute() .then(function () { test.fail('Expected validation to fail!') }) .fail(function () { test.equal(calledRetryHandler, 1) }) }) builder.add(function testKeyConditionExpression(test) { var filter = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "HEY") var data = {} ConditionBuilder.populateExpressionField( data, 'KeyConditionExpression', [filter], {}) test.equal('{"#comment":"comment"}', JSON.stringify(data.ExpressionAttributeNames)) test.equal('{":VC2":{"S":"HEY"}}', JSON.stringify(data.ExpressionAttributeValues)) test.equal('begins_with(#comment, :VC2)', data.KeyConditionExpression) test.done() }) builder.add(function testOrConditionExpression(test) { var filter1 = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "HEY") var filter2 = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "what") return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBeginsWith('column', '/comment/') .withFilter(this.client.orConditions([filter1, filter2])) .execute() .then(checkResults(test, 2, 1)) }) builder.add(function testAndConditionExpression(test) { var filter1 = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "HEY") var filter2 = this.client.newConditionBuilder() .filterAttributeEquals("comment", "HEYYOOOOO") return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBeginsWith('column', '/comment/') .withFilter(this.client.andConditions([filter1, filter2])) .execute() .then(checkResults(test, 1, 1)) }) builder.add(function testNotConditionExpression(test) { var filter1 = this.client.newConditionBuilder() .filterAttributeBeginsWith("comment", "HEY") return this.client.newQueryBuilder('comments') .setHashKey('postId', 'post1') .indexBeginsWith('column', '/comment/') .withFilter(this.client.notCondition(filter1)) .execute() .then(checkResults(test, 3, 2)) }) ================================================ FILE: test/testScan.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 tableName = "user" var rawData = [{"userId": "a", "column": "@", "post": "3", "email": "1@medium.com"}, {"userId": "b", "column": "@", "post": "0", "address": "800 Market St. SF, CA"}, {"userId": "c", "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": ['bar', 'foo']}] // 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, tableName, "userId", "column", [{hashKey: "post", hashKeyType: "N"}, {hashKey: "post", hashKeyType: "N", rangeKey: "email"}, {hashKey: "description"}]) .thenBound(utils.initTable, null, {"db": this.db, "tableName": tableName, "data": rawData}) .fail(onError) .fin(done) } exports.tearDown = function (done) { utils.deleteTable(this.db, tableName) .fin(done) } /** * A helper function that runs a query and check the result. * * @param query {Query} The query that has been built and ready to execute. * @param expect {Array} The expected returned results * @param test {Object} The test object from nodeunit. * @return {Q} */ var scanAndCheck = function (scan, expect, test) { return scan.execute() .then(function (data) { test.equal(data.result.length, expect.length, expect.length + " records should be returned, got " + data.result.length) data.result.sort(function(a, b) {return (a.userId < b.userId) ? -1 : ((a.userId > b.userId) ? 1 : 0)}) for (var i = 0; i < data.result.length; i++) { test.deepEqual(data.result[i], rawData[expect[i]]) } }) } // test basic scan on the entire table builder.add(function testScanAll(test) { var scan = this.client.newScanBuilder(tableName) return scanAndCheck(scan, [0, 1, 2, 3, 4, 5, 6], test) }) builder.add(function testScanSegment(test) { var scan = this.client.newScanBuilder(tableName) .setParallelScan(0, 2) return scanAndCheck(scan, [1, 3, 4], test) }) builder.add(function testScanOnGlobalSecondaryIndex(test) { var scan = this.client.newScanBuilder(tableName) .setIndexNameGenerator(utils.indexNameGenerator) .setHashKey('post') .setRangeKey('email') return scanAndCheck(scan, [0, 2, 5], test) }) builder.add(function testScanOnGlobalSecondaryIndexWithoutRangeKey(test) { var scan = this.client.newScanBuilder(tableName) .setIndexNameGenerator(utils.indexNameGenerator) .setHashKey('description') return scanAndCheck(scan, [5], test) }) builder.add(function testParallelScanOnGlobalSecondaryIndex(test) { var scan = this.client.newScanBuilder(tableName) .setIndexNameGenerator(utils.indexNameGenerator) .setHashKey('post') .setParallelScan(1, 2) return scanAndCheck(scan, [0, 2, 5, 6], test) }) // test filtering with post == 2 builder.add(function testFilterByEqual(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeEquals("post", 2)) return scanAndCheck(scan, [3, 4], test) }) // test filtering with post != 2 builder.add(function testFilterByNotEqual(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeNotEquals("post", 2)) return scanAndCheck(scan, [0, 1, 2, 5, 6], test) }) // test filtering with post <= 2 builder.add(function testFilterByLessThanEqual(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeLessThanEqual("post", 2)) return scanAndCheck(scan, [1, 3, 4], test) }) // test filtering with post < 2 builder.add(function testFilterByLessThan(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeLessThan("post", 2)) return scanAndCheck(scan, [1], test) }) // test filtering with post >= 2 builder.add(function testFilterByGreaterThanEqual(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeGreaterThanEqual("post", 2)) return scanAndCheck(scan, [0, 2, 3, 4, 5, 6], test) }) // test filtering with post > 2 builder.add(function testFilterByGreaterThan(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeGreaterThan("post", 2)) return scanAndCheck(scan, [0, 2, 5, 6], test) }) // test filtering with not null builder.add(function testFilterByNotNull(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeNotNull("post")) return scanAndCheck(scan, [0, 1, 2, 3, 4, 5, 6], test) }) // test filtering with email 'CONTAINS' 'medium' builder.add(function testFilterByContains(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeContains("email", "medium")) return scanAndCheck(scan, [0, 2], test) }) // test filters with tags 'CONTAINS' 'foo' builder.add(function testFilterBySetContains(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeContains("tags", "foo")) return scanAndCheck(scan, [6], test) }) // test filtering with email 'NOT_CONTAINS' 'medium' builder.add(function testFilterByNotContains(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeNotContains("email", "medium")) return scanAndCheck(scan, [5], test, "testFilterByNotContains") }) // test filters with tags 'NOT_CONTAINS' 'baz' builder.add(function testFilterBySetNotContains(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeNotContains("tags", "baz")) return scanAndCheck(scan, [6], test, "testFilterBySetNotContains") }) // test filtering with twitter 'BEGIN_WITH' 'h' builder.add(function testFilterByBeginWith(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeBeginsWith("twitter", "h")) return scanAndCheck(scan, [3, 4], test, "testFilterByBeginWith") }) // test filtering with post 'BETWEEN' 2 3 builder.add(function testFilterByBetween(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeBetween("post", 2, 3)) return scanAndCheck(scan, [0, 3, 4], test, "testFilterByBetween") }) // test filtering with post 'IN' 2 3 builder.add(function testFilterByIn(test) { var scan = this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeIn("post", [2, 3])) return scanAndCheck(scan, [0, 3, 4], test, "testFilterByIn") }) builder.add(function testNext(test) { var numInFirstScan = 0 return this.client.newScanBuilder(tableName) .withFilter(this.client.newConditionBuilder().filterAttributeGreaterThan("post", 2)) // The limit is *not* the number of records to return; instead it is // the number of records to scan. So the actual number of records returned // is not specified when a filter is given. .setLimit(4) .execute() .then(function (data) { numInFirstScan = data.Count test.ok(data.hasNext()) return data.next() }) .then(function (data) { test.equal(4, numInFirstScan + data.Count, 'Scan should return 4 records in total') test.ok(!data.hasNext()) return data.next() }) .then(function () { test.fail('Expected error') }) .fail(function (e) { if (e.message !== 'No more results') throw e }) }) ================================================ FILE: test/testStringSet.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]}] /* * 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) } // put a list of strings and check if they exist builder.add(function testStringSetPut(test) { var self = this return this.client.putItem("user", { userId: 'userC', column: '@', postIds: ['3a', '3b', '3c'] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userC", "@") }) .then(function (data) { test.deepEqual(data['Item']['postIds'].SS, ['3a', '3b', '3c'], "postIds should be ['3a', '3b', '3c']") }) }) // put a list of numbers and check if they exist builder.add(function testNumberSetPut(test) { var self = this return this.client.putItem("user", { userId: 'userD', column: '@', postIds: [1, 2, 3] }) .execute() .then(function () { return utils.getItemWithSDK(self.db, "userD", "@") }) .then(function (data) { test.deepEqual(data['Item']['postIds'].NS, [1, 2, 3], "postIds should be [1, 2, 3]") }) }) // 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]") }) }) ================================================ FILE: test/testTypeUtil.js ================================================ // Copyright 2015 A Medium Corporation. var typeUtil = require('../lib/typeUtil') var nodeunitq = require('nodeunitq') var builder = new nodeunitq.Builder(exports) var Q = require('kew') builder.add(function testAddToSet(test) { var set = typeUtil.valueToObject([1,2,3]) test.equal(typeUtil.objectToType(set), 'NS') var additions = typeUtil.valueToObject([4]) test.equal(typeUtil.objectToType(additions), 'NS') var modified = typeUtil.addToSet(set, additions) test.equal(typeUtil.objectToType(modified), 'NS') modified.NS.sort() test.deepEqual(set.NS, [1,2,3].map(String)) test.deepEqual(modified.NS, [1,2,3,4].map(String)) return Q.resolve() }) builder.add(function testAddToNullSet(test) { var set = null var additions = typeUtil.valueToObject([4]) test.equal(typeUtil.objectToType(additions), 'NS') var modified = typeUtil.addToSet(set, additions) test.equal(typeUtil.objectToType(modified), 'NS') modified.NS.sort() test.deepEqual(modified.NS, [4].map(String)) return Q.resolve() }) builder.add(function testDeleteFromSet(test) { var set = typeUtil.valueToObject([1,2,3]) test.equal(typeUtil.objectToType(set), 'NS') var deletions = typeUtil.valueToObject([1, 4]) test.equal(typeUtil.objectToType(deletions), 'NS') var modified = typeUtil.deleteFromSet(set, deletions) test.equal(typeUtil.objectToType(modified), 'NS') modified.NS.sort() test.deepEqual(modified.NS, [2, 3].map(String)) return Q.resolve() }) builder.add(function testObjectIsNonEmptySet(test) { test.ok(!typeUtil.objectIsNonEmptySet()) test.ok(!typeUtil.objectIsNonEmptySet(null)) test.ok(!typeUtil.objectIsNonEmptySet({})) test.ok(!typeUtil.objectIsNonEmptySet(typeUtil.valueToObject(4))) test.ok(!typeUtil.objectIsNonEmptySet(typeUtil.valueToObject('4'))) test.ok(typeUtil.objectIsNonEmptySet(typeUtil.valueToObject([4]))) test.ok(typeUtil.objectIsNonEmptySet(typeUtil.valueToObject(['4']))) return Q.resolve() }) builder.add(function testGetAttributeAlias(test) { var getAlias = typeUtil.getAttributeAlias test.equal('userId', getAlias('userId')) test.equal('userId2', getAlias('userId2')) test.equal('#comment', getAlias('comment')) // reserved word test.equal('#5f5f757365724964', getAlias('__userId')) test.equal('#7573657249645f', getAlias('userId_')) test.equal('#30757365724964', getAlias('0userId')) return Q.resolve() }) ================================================ FILE: test/testUpdateItem.js ================================================ // Copyright 2013 The Obvious Corporation. var utils = require('./utils/testUtils.js') var errors = require('../lib/errors') var nodeunitq = require('nodeunitq') var builder = new nodeunitq.Builder(exports) var onError = console.error.bind(console) var initialData = [{ "userId": "userA", "column": "@", "age": "29", "someStringSet": ['a', 'b', 'c'] }] // 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) } builder.add(function testSetInvalidReturnValue(test) { var updateBuilder = this.client.newUpdateBuilder('user') test.throws(function () { updateBuilder.setReturnValues('ALL_SOMETHING') }, errors.InvalidReturnValuesError) test.done() }) // test putting an attribute for an existing record builder.add(function testPutAttributeExisting(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .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') test.equal(data.previous.age, 29, 'previous age should be 29') test.equal(data.previous.height, undefined, 'previous height should be undefined') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "result age should be 30") test.equal(data['Item']['height'].N, "72", "height should be 72") }) }) // test putting an attribute for a non-existing record builder.add(function testPutAttributeNonExisting(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userB') .setRangeKey('column', '@') .enableUpsert() .putAttribute('age', 30) .putAttribute('height', 72) .execute() .then(function (data) { test.equal(data.previous, null, 'previous should be null') test.equal(data.result.age, 30, 'result age should be 30') test.equal(data.result.height, 72, 'result height should be 72') return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "result age should be 30") test.equal(data['Item']['height'].N, "72", "Height should be 72") }) }) //test putting attributes with empty would succeed builder.add(function testPutAttributeEmpty(test) { return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .putAttribute('name', '') .execute() .then(function () { test.fail("'testPutAttributeEmpty' failed - the query is expected to fail, but it didn't.") }) .fail(function (e) { test.equal(e.message.indexOf('An AttributeValue may not contain an empty') !== -1, true, "Conditional request should fail") }) }) // test adding an attribute for an existing record builder.add(function testAddAttributeExisting(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .addToAttribute('age', 1) .addToAttribute('views', -1) .execute() .then(function (data) { test.equal(data.result.age, 30, 'result age should be 30') test.equal(data.result.views, -1, 'result views should be -1') test.equal(data.previous.age, 29, 'previous age should be 29') test.equal(data.previous.views, undefined, 'previous views should be undefined') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", "result age should be 30") test.equal(data['Item']['views'].N, "-1", "views should be -1") }) }) // test adding an attribute for a non-existing record builder.add(function testAddAttributeNonExisting(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userB') .setRangeKey('column', '@') .enableUpsert() .addToAttribute('age', 1) .addToAttribute('views', -1) .execute() .then(function (data) { test.equal(data.result.age, 1, 'result age should be 30') test.equal(data.result.views, -1, 'views should be -1') return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "1", "result age should be 1") test.equal(data['Item']['views'].N, "-1", "views should be -1") }) }) // test deleting an attribute for an existing record builder.add(function testDeleteAttributeExisting(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .deleteAttribute('age') .deleteAttribute('height') .deleteFromAttribute('someStringSet', ['b', 'c', 'd']) .execute() .then(function (data) { test.equal(data.result.age, undefined, 'result age should be undefined') test.equal(data.result.height, undefined, 'height should be undefined') test.deepEqual(data.result.someStringSet, ['a'], 'someStringSet should contain "a"') test.equal(data.previous.age, 29, 'previous age should be 29') test.equal(data.previous.height, undefined, 'height should be undefined') test.deepEqual(data.previous.someStringSet, ['a', 'b', 'c'], 'someStringSet should contain "a", "b", "c"') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'], undefined, 'result age should be undefined') test.equal(data['Item']['height'], undefined, 'height should be undefined') test.deepEqual(data['Item']['someStringSet'].SS, ['a'], 'someStringSet should contain only "a"') }) }) builder.add(function testDeleteAllItemsFromStringSet(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .deleteFromAttribute('someStringSet', ['a', 'b', 'c', 'd']) .execute() .then(function (data) { test.deepEqual(data.result.someStringSet, undefined, 'someStringSet should be undefined') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.deepEqual(data['Item']['someStringSet'], undefined, 'someStringSet should be undefined') }) }) // test deleting an attribute for a non-existing record builder.add(function testDeleteAttributeNonExisting(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .deleteAttribute('age') .deleteAttribute('height') .execute() .then(function (data) { // The data from AWS SDK is something like this: // { ConsumedCapacityUnits: 1, // LastEvaluatedKey: undefined, // Count: undefined } // whereas the data from dynamo-client is like this: // { ConsumedCapacityUnits: 1, // LastEvaluatedKey: undefined, // Count: undefined, // result: {} } // so the original testing code is: // test.deepEqual(data.result, {}, 'result should be undefined') test.deepEqual(data.result, {userId: 'userA', column: '@', someStringSet: ['a', 'b', 'c']}, 'fields should be updated') return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item'], undefined, "userB with range key @ should be undefined") }) }) // test updating with conditional exists builder.add(function testUpdateWithConditional(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeEquals('column', '@') return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .withCondition(conditions) .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, 'height should be 72') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", 'result age should be 30') test.equal(data['Item']['height'].N, "72", 'height should be 72') }) }) // test updating with returnValues set to NONE builder.add(function testUpdateWithReturnValuesNone(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .putAttribute('age', 30) .putAttribute('height', 72) .setReturnValues('NONE') .execute() .then(function (data) { test.equal(data.result, undefined) return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", 'result age should be 30') test.equal(data['Item']['height'].N, "72", 'height should be 72') }) }) // test updating with absent conditional exists builder.add(function testUpdateWithAbsentConditionalExists(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('height') return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .withCondition(conditions) .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, 'height should be 72') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", 'result age should be 30') test.equal(data['Item']['height'].N, "72", 'height should be 72') }) }) // test updating with absent conditional doesn't exist builder.add(function testUpdateWithAbsentConditionalDoesNotExist(test) { var self = this var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('height') return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userB') .setRangeKey('column', '@') .withCondition(conditions) .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, 'height should be 72') return utils.getItemWithSDK(self.db, "userB", "@") }) .then(function (data) { test.equal(data['Item']['age'].N, "30", 'result age should be 30') test.equal(data['Item']['height'].N, "72", 'height should be 72') }) }) // Test that deleting from a non-existant record upserts a new item builder.add(function testUpdateWithDeleteAttributeDoesNotExist(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userC') .setRangeKey('column', '@') .enableUpsert() .deleteAttribute('age') .deleteAttribute('height') .execute() .then(function (data) { test.equal(data.result.userId, 'userC', 'userId should be set') return utils.getItemWithSDK(self.db, "userC", "@") }) .then(function (data) { test.equal(data.Item.userId.S, 'userC', 'userId should be set') }) }) // test updating fails with conditional exists builder.add(function testUpdateFailsWithConditional(test) { var conditions = this.client.newConditionBuilder() .expectAttributeEquals('age', 30) return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .withCondition(conditions) .putAttribute('age', 30) .putAttribute('height', 72) .execute() .then(function () { test.fail("'testUpdateFailsWithConditional' failed - the query is expected to fail, but it didn't.") }) .fail(this.client.throwUnlessConditionalError) }) // test updating fails with conditional doesnt exist builder.add(function testUpdateFailsWithConditionalDoesNotExist(test) { var conditions = this.client.newConditionBuilder() .expectAttributeEquals('age', 30) return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userB') .setRangeKey('column', '@') .withCondition(conditions) .putAttribute('age', 30) .putAttribute('height', 72) .execute() .then(function () { test.fail("'testUpdateFailsWithConditionalDoesNotExist' failed - the query is expected to fail, but it didn't.") }) .fail(this.client.throwUnlessConditionalError) }) // test updating fails with absent conditional exists builder.add(function testUpdateFailsWithAbsentConditional(test) { var conditions = this.client.newConditionBuilder() .expectAttributeAbsent('age') return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .withCondition(conditions) .putAttribute('age', 30) .putAttribute('height', 72) .execute() .then(function () { test.fail("'testUpdateFailsWithAbsentConditional' failed - the query is expected to fail, but it didn't.") }) .fail(this.client.throwUnlessConditionalError) }) builder.add(function testUpdateFailsWhenConditionalArgumentBad(test) { try { this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .withCondition({age: null}) .putAttribute('age', 30) .execute() test.fail('Expected error') } catch (e) { if (!/Expected ConditionBuilder/.test(e.message)) { throw e } } test.done() }) builder.add(function testPutAttributeWithUnderscores(test) { var self = this return this.client.newUpdateBuilder('user') .setHashKey('userId', 'userA') .setRangeKey('column', '@') .enableUpsert() .putAttribute('__age', 30) .putAttribute('00height', 72) .execute() .then(function (data) { test.equal(data.result.__age, 30, 'result __age should be 30') test.equal(data.result['00height'], 72, 'result 00height should be 72') return utils.getItemWithSDK(self.db, "userA", "@") }) .then(function (data) { test.equal(data['Item']['__age'].N, "30", "result age should be 30") test.equal(data['Item']['00height'].N, "72", "height should be 72") }) }) ================================================ FILE: test/utils/testUtils.js ================================================ /** * Provides ultility function for unit testing. * * @module utils **/ var AWS = require('aws-sdk') var localDynamo = require('local-dynamo') var Q = require('kew') var dynamite = require('../../dynamite') var AWSName = require('../../lib/common').AWSName var utils = {} var apiVersion = AWSName.API_VERSION_2012 // These options make dynamite connect to the fake Dynamo DB instance. // The good thing here is that we can initialize a dynamite.Client // using the exact same way as we use in production. var options = { apiVersion: apiVersion, sslEnabled: false, endpoint: 'localhost:4567', accessKeyId: 'xxx', secretAccessKey: 'xxx', region: 'xxx', retryHandler: function (method, table, response) { console.log('retrying', method, table, response) } } utils.getMockDatabase = function () { AWS.config.update(options) return new AWS.DynamoDB() } utils.getMockDatabaseClient = function () { return new dynamite.Client(options) } var localDynamoProc = null utils.ensureLocalDynamo = function () { if (!localDynamoProc) { localDynamoProc = localDynamo.launch({ port: 4567, detached: true, heap: '1g' }) localDynamoProc.on('exit', function () { localDynamoProc = null }) localDynamoProc.unref() } return localDynamoProc } process.on('exit', function () { if (localDynamoProc) { localDynamoProc.kill() } }) /* * A helper function that delete the testing table. * * @param db {AWS.DynamoDB} The database instance. * @return {Promise} */ utils.deleteTable = function (db, tableName) { var defer = Q.defer() db.deleteTable( {TableName: tableName}, defer.makeNodeResolver() ) return defer.promise } /* * A helper to generate an index name for testing indices. * * @param hashKey {string} * @param rangeKey {string} */ utils.indexNameGenerator = function (hashKey, rangeKey) { var name = 'index-' + hashKey if (rangeKey) name = name + '-' + rangeKey return name } /* * A helper function that creates the testing table. * * @param db {AWS.DynamoDB} The database instance. * @return {Promise} */ utils.createTable = function (db, tableName, hashKey, rangeKey, gsiDefinitions) { var defer = Q.defer() var opts = {} if (apiVersion === AWSName.API_VERSION_2011) { opts = { TableName: tableName, KeySchema: { HashKeyElement: {AttributeName: hashKey, AttributeType: "S"} }, ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1} } if (rangeKey) { opts.KeySchema.RangeKeyElement = {AttributeName: rangeKey, AttributeType: "S"} } db.createTable(opts, defer.makeNodeResolver()) } else if (apiVersion === AWSName.API_VERSION_2012) { var attributeDefinitions = {} attributeDefinitions[hashKey] = "S" opts = { TableName: tableName, AttributeDefinitions: [], KeySchema: [ {AttributeName: hashKey, KeyType: "HASH"} ], ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1} } if (rangeKey) { attributeDefinitions[rangeKey] = "S" opts.KeySchema.push({ AttributeName: rangeKey, KeyType: "RANGE" }) } if (gsiDefinitions) { opts.GlobalSecondaryIndexes = gsiDefinitions.map(function (index) { var keySchema = [ {AttributeName: index.hashKey, KeyType: "HASH"} ] var hashKeyType = index.hashKeyType || "S" attributeDefinitions[index.hashKey] = hashKeyType if (index.rangeKey) { var rangeKeyType = index.rangeKeyType || "S" keySchema.push({AttributeName: index.rangeKey, KeyType: "RANGE"}) attributeDefinitions[index.rangeKey] = rangeKeyType } return { IndexName: utils.indexNameGenerator(index.hashKey, index.rangeKey), KeySchema: keySchema, Projection: { ProjectionType: "ALL" }, ProvisionedThroughput: {ReadCapacityUnits: 1, WriteCapacityUnits: 1} } }) } for (var field in attributeDefinitions) { opts.AttributeDefinitions.push({AttributeName: field, AttributeType: attributeDefinitions[field]}) } db.createTable(opts, defer.makeNodeResolver()) } else { defer.reject(new Error('No api version found')) } return defer.promise } /* * A helper function that converts raw data JSON into AWS JSON format. * * Example: * * raw data JSON: { userId: 'userA', column: '@', age: '29' } * * AWS JSON: { userId: { S: 'userA' }, column: { S: '@' }, age: { N: '29' } } * * @param obj {Object} The raw JSON data * @return {Object} The same data in AWS JSON */ var convert = function (obj) { var items = {} for (var key in obj) { if (Array.isArray(obj[key]) && isNaN(obj[key][0])) { items[key] = {"SS": obj[key]} } else if (Array.isArray(obj[key])) { var numArray = [] for (var i in obj[key]) { numArray.push(String(obj[key][i])) } items[key] = {"NS": numArray} } else if (isNaN(obj[key])) { items[key] = {"S": obj[key]} } else { items[key] = {"N": obj[key]} } } return items } /* * A helper function that incert one record directly using AWS API (not * our own putItem) * * @param db {Object} The database instance * @param tableName {String} The name of the table to insert * @param record {Object} The raw JSON data * @return {Q.Promise} */ var putOneRecord = function(db, tableName, record) { var defer = Q.defer() db.putItem( {TableName: tableName, Item: convert(record)}, defer.makeNodeResolver() ) return defer.promise } /* * A helper function that initializes the testing database with * some data. * * @return {Promise} */ utils.initTable = function (context) { var db = context.db var promises = [] for (var i = 0; i < context.data.length; i += 1) { promises.push(putOneRecord(db, context.tableName, context.data[i])) } return Q.all(promises) } /* * Get a record from the database with the original AWS SDK. * The reason we don't use dynamite.getItem() here is to focus this test suite * on putItem(). * * @param db {AWS.DynamoDB} The database instance. * @param hashKey {String} * @param rangeKey {String} */ utils.getItemWithSDK = function (db, hashKey, rangeKey, table) { var defer = Q.defer() var opts = {} table = table || 'user' if (apiVersion === AWSName.API_VERSION_2011) { opts = { TableName: table, Key: { HashKeyElement: {"S": hashKey} } } if (rangeKey) { opts.Key.RangeKeyElement = {"S": rangeKey} } db.getItem( opts, defer.makeNodeResolver() ) } else if (apiVersion === AWSName.API_VERSION_2012) { opts = { TableName: table, Key: { userId: {"S": hashKey} } } if (rangeKey) { opts.Key.column = {"S": rangeKey} } db.getItem( opts, defer.makeNodeResolver() ) } return defer.promise } exports = module.exports = utils