Skip to content

Commit

Permalink
Merge branch 'master' into add-secondary-index-scan
Browse files Browse the repository at this point in the history
  • Loading branch information
clarkie committed Jan 3, 2018
2 parents 6a2c632 + b807ef3 commit d1f90b2
Show file tree
Hide file tree
Showing 18 changed files with 678 additions and 100 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_js:
- 4
- 5
- 6
- 7

env:
global:
Expand All @@ -15,7 +16,7 @@ env:
before_script:
- wget http://dynamodb-local.s3-website-us-west-2.amazonaws.com/dynamodb_local_latest.tar.gz -O /tmp/dynamodb_local_latest.tar.gz
- tar -xzf /tmp/dynamodb_local_latest.tar.gz -C /tmp
- java -Djava.library.path=/tmp/DynamoDBLocal_lib -jar /tmp/DynamoDBLocal.jar -inMemory &
- java -Xmx256m -Djava.library.path=/tmp/DynamoDBLocal_lib -jar /tmp/DynamoDBLocal.jar -inMemory &
- sleep 2
script: npm run ci
after_success: npm run coveralls
Expand Down
165 changes: 146 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
[![DevDependencies Status](https://david-dm.org/clarkie/dynogels/dev-status.svg)](https://david-dm.org/clarkie/dynogels#info=devDependencies)
[![Known Vulnerabilities](https://snyk.io/test/npm/dynogels/badge.svg)](https://snyk.io/test/npm/dynogels)

Dynogels is a [DynamoDB][5] data mapper for [node.js][1]. This project has been forked from [Vogels](https://github.com/ryanfitz/vogels) and republished to npm under a different name.
Dynogels is a [DynamoDB][5] data mapper for [node.js][1]. This project has been forked from
[Vogels](https://github.com/ryanfitz/vogels) and republished to npm under a different name.

## Features
* Simplified data modeling and mapping to DynamoDB types
* Advanced chainable apis for [query](#query) and [scan](#scan) operations
* Data validation
* [Data validation](#data-validation)
* [Autogenerating UUIDs](#uuid)
* [Global Secondary Indexes](#global-indexes)
* [Local Secondary Indexes](#local-secondary-indexes)
Expand All @@ -29,15 +30,18 @@ var dynogels = require('dynogels');
dynogels.AWS.config.loadFromPath('credentials.json');
```

When running on EC2 its recommended to leverage EC2 IAM roles. If you have configured your instance to use IAM roles, Vogels will automatically select these credentials for use in your application, and you do not need to manually provide credentials in any other format.
When running on EC2 it's recommended to leverage EC2 IAM roles. If you have configured your instance to use IAM roles,
Vogels will automatically select these credentials for use in your application,
and you do not need to manually provide credentials in any other format.

```js
var dynogels = require('dynogels');
dynogels.AWS.config.update({region: "REGION"}); // region must be set
```

You can also directly pass in your access key id, secret and region.
* Its recommend you not hard-code credentials inside an application. Use this method only for small personal scripts or for testing purposes.
* It's recommended not to hard-code credentials inside an application.
Use this method only for small personal scripts or for testing purposes.

```js
var dynogels = require('dynogels');
Expand Down Expand Up @@ -96,14 +100,31 @@ var BlogPost = dynogels.define('BlogPost', {
});
```

You can pass through validation options to Joi like so:

```js
var BlogPost = dynogels.define('BlogPost', {
hashKey : 'email',
rangeKey : 'title',
schema : {
email : Joi.string().email(),
title : Joi.string()
},
validation: {
// allow properties not defined in the schema
allowUnknown: true
}
});
```

### Create Tables for all defined models

```js
dynogels.createTables(function(err) {
if (err) {
console.log('Error creating tables: ', err);
} else {
console.log('Tables has been created');
console.log('Tables have been created');
}
});
```
Expand All @@ -130,6 +151,22 @@ dynogels.createTables({
});
```

You can also pass operational options using the `$dynogels` key:

* `pollingInterval`: When creating a table, Dynogels must poll the DynamoDB server to detect when table creation has completed. This option specifies the *minimum* poll interval, in milliseconds. (Default: 1000)

```js
dynogels.createTables({
$dynogels: { pollingInterval: 100 }
}, function(err) {
if (err) {
console.log('Error creating tables: ', err);
} else {
console.log('Tables has been created');
}
});
```

### Delete Table

```js
Expand Down Expand Up @@ -170,9 +207,32 @@ var Tweet = dynogels.define('Tweet', {
});
```

#### Data Validation
Dynogels automatically validates the model against the schema before attempting to save it, but you can also call the `validate` method to validate an object before saving it. This can be helpful for a handler to validate input.

```js
var Tweet = dynogels.define('Tweet', {
hashKey : 'TweetID',
timestamps : true,
schema : {
TweetID : dynogels.types.uuid(),
content : Joi.string(),
}
});

const tweet = new Tweet({ content: 123 })
const fail_result = Tweet.validate(tweet)
console.log(fail_result.error.name) // ValidationError

tweet.set('content', 'This is the content')
const pass_result = Tweet.validate(tweet)
console.log(pass_result.error) // null
```

### Configuration
You can configure dynogels to automatically add `createdAt` and `updatedAt` timestamp attributes when
saving and updating a model. `updatedAt` will only be set when updating a record and will not be set on initial creation of the model.
saving and updating a model. `updatedAt` will only be set when updating a record
and will not be set on initial creation of the model.

```js
var Account = dynogels.define('Account', {
Expand Down Expand Up @@ -341,7 +401,7 @@ Account.update({email: 'foo@example.com', name: 'Bar Tester'}, {ReturnValues: 'A
console.log('update account', acc.get('name')); // prints the old account name
});

// Only update the account if the current age of the account is 21
// Only update the account if the current age of the account is 22
Account.update({email: 'foo@example.com', name: 'Bar Tester'}, {expected: {age: 22}}, function (err, acc) {
console.log('update account', acc.get('name'));
});
Expand All @@ -352,6 +412,32 @@ Account.update({email: 'foo@example.com', age: null}, function (err, acc) {
});
```

To ensure that an item exists before updating, use the `expected` parameter to check the existence of the hash key. The hash key must exist for every DynamoDB item. This will return an error if the item does not exist.
```js
Account.update(
{ email: 'foo@example.com', name: 'FooBar Testers' },
{ expected: { email: { Exists: true } } },
(err, acc) => {
console.log(acc.get('name')); // FooBar Testers
}
);

Account.update(
{ email: 'baz@example.com', name: 'Bar Tester' },
{ expected: { email: { Exists: true } } },
(err, acc) => {
console.log(err); // Condition Expression failed: no Account with that hash key
}
);
```

This is essentially short-hand for:
```js
var params = {};
params.ConditionExpression = 'attribute_exists(#hashKey)';
params.ExpressionAttributeNames = { '#hashKey' : 'email' };
```

You can also pass what action to perform when updating a given attribute
Use $add to increment or decrement numbers and add values to sets

Expand Down Expand Up @@ -544,7 +630,7 @@ BlogPost
.query('werner@example.com')
.where('title').beginsWith('Expanding')
.exec(callback);

// return only the count of documents that begin with the title Expanding
BlogPost
.query('werner@example.com')
Expand Down Expand Up @@ -583,6 +669,12 @@ BlogPost
.ascending()
.loadAll()
.exec(callback);

// Traversing Map Data Types
Account
.query('werner@example.com')
.filter('settings.acceptedTerms').equals(true)
.exec(callback);
```

**Warning, limit is applied first before the where filter. The limit value limits the scanned count,
Expand Down Expand Up @@ -621,6 +713,18 @@ BlogPost
.where('title').gte('Expanding')
.exec();

// attribute doesn't exist
BlogPost
.query('werner@example.com')
.where('title').null()
.exec();

// attribute exists
BlogPost
.query('werner@example.com')
.where('title').exists()
.exec();

BlogPost
.query('werner@example.com')
.where('title').beginsWith('Expanding')
Expand Down Expand Up @@ -695,6 +799,12 @@ Account
.returnConsumedCapacity()
.exec();

// Load All accounts, if settings.acceptedTerms is true
Account
.scan()
.where('settings.acceptedTerms').equals(true)
.exec();

// Returns number of matching accounts, rather than the matching accounts themselves
Account
.scan()
Expand Down Expand Up @@ -899,7 +1009,7 @@ First, define a model using a local secondary index
```js
var BlogPost = dynogels.define('Account', {
hashKey : 'email',
rangekey : 'title',
rangeKey : 'title',
schema : {
email : Joi.string().email(),
title : Joi.string(),
Expand All @@ -908,7 +1018,7 @@ var BlogPost = dynogels.define('Account', {
},

indexes : [{
hashkey : 'email', rangekey : 'PublishedDateTime', type : 'local', name : 'PublishedIndex'
hashKey : 'email', rangeKey : 'PublishedDateTime', type : 'local', name : 'PublishedIndex'
}]
});
```
Expand Down Expand Up @@ -1053,22 +1163,37 @@ var Event = dynogels.define('Event', {
```

### Logging
Logging can be enabled to provide detailed information on data being sent and returned from DynamoDB.
By default logging is turned off.
A [Bunyan](https://www.npmjs.com/package/bunyan) logger instance can be provided to either dynogels itself or individual models. Dynogels requests are logged at the `info` level.
Other loggers that implement `info` and `warn` methods can also be used. However, [Winston](https://www.npmjs.com/package/winston) uses a different parameter signature than bunyan so the log messages are improperly formatted when using Winston.

```js
dynogels.log.level('info'); // enabled INFO log level
const bunyan = require('bunyan');
const logger = bunyan.createLogger(
{
name: 'globalLogger',
level:'info'
})

dynogels.log = logger;
```

Logging can also be enabled / disabled at the model level.

```js
var Account = dynogels.define('Account', {hashKey : 'email'});
var Event = dynogels.define('Account', {hashKey : 'name'});
const bunyan = require('bunyan');
const accountLogger = bunyan.createLogger(
{
name: 'modelLogger',
level:'info'
})

Account.log.level('warn'); // enable WARN log level for Account model operations
var Account = dynogels.define('Account', {
hashKey: 'email',
log: accountLogger
}); // INFO level on account table
```

* [Bunyan log levels](https://github.com/trentm/node-bunyan#levels)

## Examples

```js
Expand Down Expand Up @@ -1106,9 +1231,11 @@ See the [examples][0] for more working sample code.
Dynogels is provided as-is, free of charge. For support, you have a few choices:

- Ask your support question on [Stackoverflow.com](http://stackoverflow.com), and tag your question with **dynogels**.
- If you believe you have found a bug in dynogels, please submit a support ticket on the [Github Issues page for dynogels](http://github.com/clarkie/dynogels/issues). We'll get to them as soon as we can.
- If you believe you have found a bug in dynogels, please submit a support ticket on
the [Github Issues page for dynogels](http://github.com/clarkie/dynogels/issues). We'll get to them as soon as we can.
- For general feedback message me on [twitter](https://twitter.com/clarkieclarkie)
- For more personal or immediate support, I’m available for hire to consult on your project. [Contact](mailto:andrew.t.clarke@gmail.com) me for more detals.
- For more personal or immediate support, I’m available for hire to consult on your project.
[Contact](mailto:andrew.t.clarke@gmail.com) me for more detals.

### Maintainers

Expand Down
9 changes: 6 additions & 3 deletions lib/batch.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ internals.mergeResponses = (tableName, responses) => {
internals.paginatedRequest = (request, table, callback) => {
const responses = [];

const moreKeysToProcessFunc = () => request !== null && !_.isEmpty(request);

const doFunc = callback => {
table.runBatchGetItems(request, (err, resp) => {
if (err && err.retryable) {
Expand All @@ -44,14 +46,15 @@ internals.paginatedRequest = (request, table, callback) => {
}

request = resp.UnprocessedKeys;
if (moreKeysToProcessFunc()) {
request = { RequestItems: request };
}
responses.push(resp);

return callback();
});
};

const testFunc = () => request !== null && !_.isEmpty(request);

const resulsFunc = err => {
if (err) {
return callback(err);
Expand All @@ -60,7 +63,7 @@ internals.paginatedRequest = (request, table, callback) => {
callback(null, internals.mergeResponses(table.tableName(), responses));
};

async.doWhilst(doFunc, testFunc, resulsFunc);
async.doWhilst(doFunc, moreKeysToProcessFunc, resulsFunc);
};

internals.buckets = keys => {
Expand Down
Loading

0 comments on commit d1f90b2

Please sign in to comment.