GithubHelp home page GithubHelp logo

vogels's Introduction

vogels Build Status

vogels is a DynamoDB data mapper for node.js.

Features

Installation

npm install vogels

Getting Started

First, you need to configure the AWS SDK with your credentials.

var vogels = require('vogels');
vogels.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.

var vogels = require('vogels');
vogels.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.
var vogels = require('vogels');
vogels.AWS.config.update({accessKeyId: 'AKID', secretAccessKey: 'SECRET', region: "REGION"});

Currently the following region codes are available in Amazon:

Code Name
ap-northeast-1 Asia Pacific (Tokyo)
ap-southeast-1 Asia Pacific (Singapore)
ap-southeast-2 Asia Pacific (Sydney)
eu-central-1 EU (Frankfurt)
eu-west-1 EU (Ireland)
sa-east-1 South America (Sao Paulo)
us-east-1 US East (N. Virginia)
us-west-1 US West (N. California)
us-west-2 US West (Oregon)

Define a Model

Models are defined through the toplevel define method.

var Account = vogels.define('Account', {
  hashKey : 'email',

  // add the timestamp attributes (updatedAt, createdAt)
  timestamps : true,

  schema : {
    email   : Joi.string().email(),
    name    : Joi.string(),
    age     : Joi.number(),
    roles   : vogels.types.stringSet(),
    settings : {
      nickname      : Joi.string(),
      acceptedTerms : Joi.boolean().default(false)
    }
  }
});

Models can also be defined with hash and range keys.

var BlogPost = vogels.define('BlogPost', {
  hashKey : 'email',
  rangeKey : ‘title’,
  schema : {
    email   : Joi.string().email(),
    title   : Joi.string(),
    content : Joi.binary(),
    tags   : vogels.types.stringSet(),
  }
});

Create Tables for all defined modules

vogels.createTables(function(err) {
  if (err) {
    console.log('Error creating tables: ', err);
  } else {
    console.log('Tables has been created');
  }
});

When creating tables you can pass specific throughput settings for any defined models.

vogels.createTables({
  'BlogPost': {readCapacity: 5, writeCapacity: 10},
  'Account': {readCapacity: 20, writeCapacity: 4}
}, function(err) {
  if (err) {
    console.log('Error creating tables: ', err);
  } else {
    console.log('Tables has been created');
  }
});

Delete Table

BlogPost.deleteTable(function(err) {
  if (err) {
    console.log('Error deleting table: ', err);
  } else {
    console.log('Table has been deleted');
  }
});

Schema Types

Vogels provides the following schema types:

  • String
  • Number
  • StringSet
  • NumberSet
  • Boolean
  • Date
  • UUID
  • TimeUUID

UUID

UUIDs can be declared for any attributes, including hash and range keys. By Default, the uuid will be automatically generated when attempting to create the model in DynamoDB.

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

Configuration

You can configure vogels 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.

var Account = vogels.define('Account', {
  hashKey : 'email',

  // add the timestamp attributes (updatedAt, createdAt)
  timestamps : true,

  schema : {
    email : Joi.string().email(),
  }
});

If you want vogels to handle timestamps, but only want some of them, or want your timestamps to be called something else, you can override each attribute individually:

var Account = vogels.define('Account', {
  hashKey : 'email',

  // enable timestamps support
  timestamps : true,

  // I don't want createdAt
  createdAt: false,

  // I want updatedAt to actually be called updateTimestamp
  updatedAt: 'updateTimestamp'

  schema : {
    email : Joi.string().email(),
  }
});

You can override the table name the model will use.

var Event = vogels.define('Event', {
  hashKey : 'name',
  schema : {
    name : Joi.string(),
    total : Joi.number()
  },

  tableName: 'deviceEvents'
});

if you set the tableName to a function, vogels will use the result of the function as the active table to use. Useful for storing time series data.

var Event = vogels.define('Event', {
  hashKey : 'name',
  schema : {
    name : Joi.string(),
    total : Joi.number()
  },

  // store monthly event data
  tableName: function () {
    var d = new Date();
    return ['events', d.getFullYear(), d.getMonth() + 1].join('_');
  }
});

After you've defined your model you can configure the table name to use. By default, the table name used will be the lowercased and pluralized version of the name you provided when defining the model.

Account.config({tableName: 'AccountsTable'});

You can also pass in a custom instance of the aws-sdk DynamoDB client

var dynamodb = new AWS.DynamoDB();
Account.config({dynamodb: dynamodb});

// or globally use custom DynamoDB instance
// all defined models will now use this driver
vogels.dynamoDriver(dynamodb);

Saving Models to DynamoDB

With your models defined, we can start saving them to DynamoDB.

Account.create({email: '[email protected]', name: 'Foo Bar', age: 21}, function (err, acc) {
  console.log('created account in DynamoDB', acc.get('email'));
});

You can also first instantiate a model and then save it.

var acc = new Account({email: '[email protected]', name: 'Test Example'});
acc.save(function (err) {
  console.log('created account in DynamoDB', acc.get('email'));
});

Saving models that require range and hashkeys are identical to ones with only hashkeys.

BlogPost.create({
  email: '[email protected]', 
  title: 'Expanding the Cloud', 
  content: 'Today, we are excited to announce the limited preview...'
  }, function (err, post) {
    console.log('created blog post', post.get('title'));
  });

Pass an array of items and they will be saved in parallel to DynamoDB.

var item1 = {email: '[email protected]', name: 'Foo 1', age: 10};
var item2 = {email: '[email protected]', name: 'Foo 2', age: 20};
var item3 = {email: '[email protected]', name: 'Foo 3', age: 30};

Account.create([item1, item2, item3], function (err, acccounts) {
  console.log('created 3 accounts in DynamoDB', accounts);
});

Use expressions api to do conditional writes

  var params = {};
  params.ConditionExpression = '#i <> :x';
  params.ExpressionAttributeNames = {'#i' : 'id'};
  params.ExpressionAttributeValues = {':x' : 123};

  User.create({id : 123, name : 'Kurt Warner' }, params, function (error, acc) { ... });

Use the overwrite option to prevent over writing of existing records.

  • By default overwrite is set to true, allowing create operations to overwrite existing records
  // setting overwrite to false will generate
  // the same Condition Expression as in the previous example
  User.create({id : 123, name : 'Kurt Warner' }, {overwrite : false}, function (error, acc) { ... });

Updating

When updating a model the hash and range key attributes must be given, all other attributes are optional

// update the name of the [email protected] account
Account.update({email: '[email protected]', name: 'Bar Tester'}, function (err, acc) {
  console.log('update account', acc.get('name'));
});

Model.update accepts options to pass to DynamoDB when making the updateItem request

Account.update({email: '[email protected]', name: 'Bar Tester'}, {ReturnValues: 'ALL_OLD'}, function (err, acc) {
  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
Account.update({email: '[email protected]', name: 'Bar Tester'}, {expected: {age: 22}}, function (err, acc) {
  console.log('update account', acc.get('name'));
});

// setting an attribute to null will delete the attribute from DynamoDB
Account.update({email: '[email protected]', age: null}, function (err, acc) {
  console.log('update account', acc.get('age')); // prints null
});

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

Account.update({email : '[email protected]', age : {$add : 1}}, function (err, acc) {
  console.log('incremented age by 1', acc.get('age'));
});

BlogPost.update({
  email : '[email protected]',
  title : 'Expanding the Cloud',
  tags  : {$add : 'cloud'}
}, function (err, post) {
  console.log('added single tag to blog post', post.get('tags'));
});

BlogPost.update({
  email : '[email protected]',
  title : 'Expanding the Cloud',
  tags  : {$add : ['cloud', 'dynamodb']}
}, function (err, post) {
  console.log('added tags to blog post', post.get('tags'));
});

$del will remove values from a given set

BlogPost.update({
  email : '[email protected]',
  title : 'Expanding the Cloud',
  tags  : {$del : 'cloud'}
}, function (err, post) {
  console.log('removed cloud tag from blog post', post.get('tags'));
});

BlogPost.update({
  email : '[email protected]',
  title : 'Expanding the Cloud',
  tags  : {$del : ['aws', 'node']}
}, function (err, post) {
  console.log('removed multiple tags', post.get('tags'));
});

Use the expressions api to update nested documents

var params = {};
  params.UpdateExpression = 'SET #year = #year + :inc, #dir.titles = list_append(#dir.titles, :title), #act[0].firstName = :firstName ADD tags :tag';
  params.ConditionExpression = '#year = :current';
  params.ExpressionAttributeNames = {
    '#year' : 'releaseYear',
    '#dir' : 'director',
    '#act' : 'actors'
  };

  params.ExpressionAttributeValues = {
    ':inc' : 1,
    ':current' : 2001,
    ':title' : ['The Man'],
    ':firstName' : 'Rob',
    ':tag' : vogels.Set(['Sports', 'Horror'], 'S')
  };

Movie.update({title : 'Movie 0', description : 'This is a description'}, params, function (err, mov) {});

Deleting

You delete items in DynamoDB using the hashkey of model If your model uses both a hash and range key, than both need to be provided

Account.destroy('[email protected]', function (err) {
  console.log('account deleted');
});

// Destroy model using hash and range key
BlogPost.destroy('[email protected]', 'Hello World!', function (err) {
  console.log('post deleted')
});

BlogPost.destroy({email: '[email protected]', title: 'Another Post'}, function (err) {
  console.log('another post deleted')
});

Model.destroy accepts options to pass to DynamoDB when making the deleteItem request

Account.destroy('[email protected]', {ReturnValues: true}, function (err, acc) {
  console.log('account deleted');
  console.log('deleted account name', acc.get('name'));
});

Account.destroy('[email protected]', {expected: {age: 22}}, function (err) {
  console.log('account deleted if the age was 22');

Use expression apis to perform conditional deletes

var params = {};
params.ConditionExpression = '#v = :x';
params.ExpressionAttributeNames = {'#v' : 'version'};
params.ExpressionAttributeValues = {':x' : '2'};

User.destroy({id : 123}, params, function (err, acc) {});

Loading models from DynamoDB

The simpliest way to get an item from DynamoDB is by hashkey.

Account.get('[email protected]', function (err, acc) {
  console.log('got account', acc.get('email'));
});

Perform the same get request, but this time peform a consistent read.

Account.get('[email protected]', {ConsistentRead: true}, function (err, acc) {
  console.log('got account', acc.get('email'));
});

Model.get accepts any options that DynamoDB getItem request supports. For example:

Account.get('[email protected]', {ConsistentRead: true, AttributesToGet : ['name','age']}, function (err, acc) {
  console.log('got account', acc.get('email'))
  console.log(acc.get('name'));
  console.log(acc.get('age'));
  console.log(acc.get('email')); // prints null
});

Get a model using hash and range key.

// load up blog post written by Werner, titled DynamoDB Keeps Getting Better and cheaper
BlogPost.get('[email protected]', 'dynamodb-keeps-getting-better-and-cheaper', function (err, post) {
  console.log('loaded post by range and hash key', post.get('content'));
});

Model.get also supports passing an object which contains hash and range key attributes to load up a model

BlogPost.get({email: '[email protected]', title: 'Expanding the Cloud'}, function (err, post) {
  console.log('loded post', post.get('content'));
});

Use expressions api to select which attributes you want returned

  User.get({ id : '123456789'},{ ProjectionExpression : 'email, age, settings.nickname' }, function (err, acc) {});

Query

For models that use hash and range keys Vogels provides a flexible and chainable query api

// query for blog posts by [email protected]
BlogPost
  .query('[email protected]')
  .exec(callback);

// same as above, but load all results
BlogPost
  .query('[email protected]')
  .loadAll()
  .exec(callback);

// only load the first 5 posts by werner
BlogPost
  .query('[email protected]')
  .limit(5)
  .exec(callback);

// query for posts by werner where the tile begins with 'Expanding'
BlogPost
  .query('[email protected]')
  .where('title').beginsWith('Expanding')
  .exec(callback);

// return only the count of documents that begin with the title Expanding
BlogPost
  .query('[email protected]')
  .where('title').beginsWith('Expanding')
  .select('COUNT')
  .exec(callback);

// only return title and content attributes of 10 blog posts
// that begin with the title Expanding
BlogPost
  .query('[email protected]')
  .where('title').beginsWith('Expanding')
  .attributes(['title', 'content'])
  .limit(10)
  .exec(callback);

// sorting by title ascending
BlogPost
  .query('[email protected]')
  .ascending()
  .exec(callback)

// sorting by title descending
BlogPost
  .query('[email protected]')
  .descending()
  .exec(callback)

// All query options are chainable
BlogPost
  .query('[email protected]')
  .where('title').gt('Expanding')
  .attributes(['title', 'content'])
  .limit(10)
  .ascending()
  .loadAll()
  .exec(callback);

Vogels supports all the possible KeyConditions that DynamoDB currently supports.

BlogPost
  .query('[email protected]')
  .where('title').equals('Expanding')
  .exec();

// less than equals
BlogPost
  .query('[email protected]')
  .where('title').lte('Expanding')
  .exec();

// less than
BlogPost
  .query('[email protected]')
  .where('title').lt('Expanding')
  .exec();

// greater than
BlogPost
  .query('[email protected]')
  .where('title').gt('Expanding')
  .exec();

// greater than equals
BlogPost
  .query('[email protected]')
  .where('title').gte('Expanding')
  .exec();

BlogPost
  .query('[email protected]')
  .where('title').beginsWith('Expanding')
  .exec();

BlogPost
  .query('[email protected]')
  .where('title').between('[email protected]', '[email protected]')
  .exec();

Query Filters allow you to further filter results on non-key attributes.

BlogPost
  .query('[email protected]')
  .where('title').equals('Expanding')
  .filter('tags').contains('cloud')
  .exec();

Expression Filters also allow you to further filter results on non-key attributes.

BlogPost
  .query('[email protected]')
  .filterExpression('#title < :t')
  .expressionAttributeValues({ ':t' : 'Expanding' })
  .expressionAttributeNames({ '#title' : 'title'})
  .projectionExpression('#title, tag')
  .exec();

See the queryFilter.js example for more examples of using query filters

Global Indexes

First, define a model with a global secondary index.

var GameScore = vogels.define('GameScore', {
  hashKey : 'userId',
  rangeKey : 'gameTitle',
  schema : {
    userId           : Joi.string(),
    gameTitle        : Joi.string(),
    topScore         : Joi.number(),
    topScoreDateTime : Joi.date(),
    wins             : Joi.number(),
    losses           : Joi.number()
  },
  indexes : [{
    hashKey : 'gameTitle', rangeKey : 'topScore', name : 'GameTitleIndex', type : 'global'
  }]
});

Now we can query against the global index

GameScore
  .query('Galaxy Invaders')
  .usingIndex('GameTitleIndex')
  .descending()
  .exec(callback);

When can also configure the attributes projected into the index. By default all attributes will be projected when no Projection pramater is present

var GameScore = vogels.define('GameScore', {
  hashKey : 'userId',
  rangeKey : 'gameTitle',
  schema : {
    userId           : Joi.string(),
    gameTitle        : Joi.string(),
    topScore         : Joi.number(),
    topScoreDateTime : Joi.date(),
    wins             : Joi.number(),
    losses           : Joi.number()
  },
  indexes : [{
    hashKey : 'gameTitle',
    rangeKey : 'topScore',
    name : 'GameTitleIndex',
    type : 'global',
    projection: { NonKeyAttributes: [ 'wins' ], ProjectionType: 'INCLUDE' } //optional, defaults to ALL

  }]
});

Filter items against the configured rangekey for the global index.

GameScore
  .query('Galaxy Invaders')
  .usingIndex('GameTitleIndex')
  .where('topScore').gt(1000)
  .descending()
  .exec(function (err, data) {
    console.log(_.map(data.Items, JSON.stringify));
  });

Local Secondary Indexes

First, define a model using a local secondary index

var BlogPost = vogels.define('Account', {
  hashKey : 'email',
  rangekey : 'title',
  schema : {
    email             : Joi.string().email(),
    title             : Joi.string(),
    content           : Joi.binary(),
    PublishedDateTime : Joi.date()
  },

  indexes : [{
    hashkey : 'email', rangekey : 'PublishedDateTime', type : 'local', name : 'PublishedIndex'
  }]
});

Now we can query for blog posts using the secondary index

BlogPost
  .query('[email protected]')
  .usingIndex('PublishedIndex')
  .descending()
  .exec(callback);

Could also query for published posts, but this time return oldest first

BlogPost
  .query('[email protected]')
  .usingIndex('PublishedIndex')
  .ascending()
  .exec(callback);

Finally lets load all published posts sorted by publish date

BlogPost
  .query('[email protected]')
  .usingIndex('PublishedIndex')
  .descending()
  .loadAll()
  .exec(callback);

Learn more about secondary indexes

Scan

Vogels provides a flexible and chainable api for scanning over all your items This api is very similar to the query api.

// scan all accounts, returning the first page or results
Account.scan().exec(callback);

// scan all accounts, this time loading all results
// note this will potentially make several calls to DynamoDB 
// in order to load all results
Account
  .scan()
  .loadAll()
  .exec(callback);

// Load 20 accounts
Account
  .scan()
  .limit(20)
  .exec();

// Load All accounts, 20 at a time per request
Account
  .scan()
  .limit(20)
  .loadAll()
  .exec();

// Load accounts which match a filter
// only return email and created attributes
// and return back the consumed capacity the request took
Account
  .scan()
  .where('email').gte('[email protected]')
  .attributes(['email','created'])
  .returnConsumedCapacity()
  .exec();

// Returns number of matching accounts, rather than the matching accounts themselves
Account
  .scan()
  .where('age').gte(21)
  .select('COUNT')
  .exec();

// Start scan using start key
Account
  .scan()
  .where('age').notNull()
  .startKey('[email protected]')
  .exec()

Vogels supports all the possible Scan Filters that DynamoDB currently supports.

// equals
Account
  .scan()
  .where('name').equals('Werner')
  .exec();

// not equals
Account
  .scan()
  .where('name').ne('Werner')
  .exec();

// less than equals
Account
  .scan()
  .where('name').lte('Werner')
  .exec();

// less than
Account
  .scan()
  .where('name').lt('Werner')
  .exec();

// greater than equals
Account
  .scan()
  .where('name').gte('Werner')
  .exec();

// greater than
Account
  .scan()
  .where('name').gt('Werner')
  .exec();

// name attribute doesn't exist
Account
  .scan()
  .where('name').null()
  .exec();

// name attribute exists
Account
  .scan()
  .where('name').notNull()
  .exec();

// contains
Account
  .scan()
  .where('name').contains('ner')
  .exec();

// not contains
Account
  .scan()
  .where('name').notContains('ner')
  .exec();

// in
Account
  .scan()
  .where('name').in(['[email protected]', '[email protected]'])
  .exec();

// begins with
Account
  .scan()
  .where('name').beginsWith('Werner')
  .exec();

// between
Account
  .scan()
  .where('name').between('Bar', 'Foo')
  .exec();

// multiple filters
Account
  .scan()
  .where('name').equals('Werner')
  .where('age').notNull()
  .exec();

You can also use the new expressions api when filtering scans

User.scan()
  .filterExpression('#age BETWEEN :low AND :high AND begins_with(#email, :e)')
  .expressionAttributeValues({ ':low' : 18, ':high' : 22, ':e' : 'test1'})
  .expressionAttributeNames({ '#age' : 'age', '#email' : 'email'})
  .projectionExpression('#age, #email')
  .exec();

Parallel Scan

Parallel scans increase the throughput of your table scans. The parallel scan operation is identical to the scan api. The only difference is you must provide the total number of segments

Caution you can easily consume all your provisioned throughput with this api

var totalSegments = 8;

Account.parallelScan(totalSegments)
  .where('age').gte(18)
  .attributes('age')
  .exec(callback);

// Load All accounts
Account
  .parallelScan(totalSegments)
  .exec()

More info on Parallel Scans

Batch Get Items

Model.getItems allows you to load multiple models with a single request to DynamoDB.

DynamoDB limits the number of items you can get to 100 or 1MB of data for a single request. Vogels automatically handles splitting up into multiple requests to load all items.

Account.getItems(['[email protected]','[email protected]', '[email protected]'], function (err, accounts) {
  console.log('loaded ' + accounts.length + ' accounts'); // prints loaded 3 accounts
});

// For models with range keys you must pass in objects of hash and range key attributes
var postKey1 = {email : '[email protected]', title : 'Hello World!'};
var postKey2 = {email : '[email protected]', title : 'Another Post'};

BlogPost.getItems([postKey1, postKey2], function (err, posts) {
  console.log('loaded posts');
});

Model.getItems accepts options which will be passed to DynamoDB when making the batchGetItem request

// Get both accounts, using a consistent read
Account.getItems(['[email protected]','[email protected]'], {ConsistentRead: true}, function (err, accounts) {
  console.log('loaded ' + accounts.length + ' accounts'); // prints loaded 2 accounts
});

Streaming api

vogels supports a basic streaming api in addition to the callback api for query, scan, and parallelScan operations.

var stream = Account.parallelScan(4).exec();

stream.on('readable', function () {
  console.log('single parallel scan response', stream.read());
});

stream.on('end', function () {
  console.log('Parallel scan of accounts finished');
});

var querystream = BlogPost.query('[email protected]').loadAll().exec();

querystream.on('readable', function () {
  console.log('single query response', stream.read());
});

querystream.on('end', function () {
  console.log('query for blog posts finished');
});

Dynamic Table Names

vogels supports dynamic table names, useful for storing time series data.

var Event = vogels.define('Event', {
  hashKey : 'name',
  schema : {
    name : Joi.string(),
    total : Joi.number()
  },

  // store monthly event data
  tableName: function () {
    var d = new Date();
    return ['events', d.getFullYear(), d.getMonth() + 1].join('_');
  }
});

Logging

Logging can be enabled to provide detailed information on data being sent and returned from DynamoDB. By default logging is turned off.

vogels.log.level('info'); // enabled INFO log level 

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

var Account = vogels.define('Account', {hashKey : 'email'});
var Event = vogels.define('Account', {hashKey : 'name'});

Account.log.level('warn'); // enable WARN log level for Account model operations

Examples

var vogels = require('vogels');

var Account = vogels.define('Account', {
  hashKey : 'email',

  // add the timestamp attributes (updatedAt, createdAt)
  timestamps : true,

  schema : {
    email   : Joi.string().email(),
    name    : Joi.string().required(),
    age     : Joi.number(),
  }
});

Account.create({email: '[email protected]', name : 'Test Account'}, function (err, acc) {
  console.log('created account at', acc.get('created')); // prints created Date

  acc.set({age: 22});

  acc.update(function (err) {
    console.log('updated account age');
  });

});

See the examples for more working sample code.

Support

Vogels is provided as-is, free of charge. For support, you have a few choices:

  • Ask your support question on Stackoverflow.com, and tag your question with vogels.
  • If you believe you have found a bug in vogels, please submit a support ticket on the Github Issues page for vogels. We'll get to them as soon as we can.
  • For general feedback message me on twitter
  • For more personal or immediate support, I’m available for hire to consult on your project. Contact me for more detals.

Maintainers

License

(The MIT License)

Copyright (c) 2016 Ryan Fitzgerald

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

vogels's People

Contributors

chadkouse avatar dallasread avatar dvonlehman avatar hasantayyar avatar lsegal avatar ryanfitz avatar sazpaimon avatar trevorrowe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

vogels's Issues

Allow registration of user-defined schema types

Vogels already has support for internal schema types such as Boolean or UUID. It'd be nice if there was an easy way to define and register additional schema types for project specific use.

Get default region from environment?

aws-sdk uses $AWS_ACCESS_KEY and $AWS_SECRET_KEY from the environment if they exist; since vogels uses aws-sdk internally, setting these values also works for it.

I know reading defaults from the environment can be controversial, but would you be open to a PR where vogels also read the region from an environment var called $VOGELS_AWS_REGION or something?

Empty value on String field is not allowed

When defining a schema like:

var Item = vogels.define('Item', function (schema) {
    schema.UUID('id', {hashKey: true});
    schema.Number('asset_id').required();
    schema.Number('user_id').required();
    schema.String('asset');

Calling

Item.create({id: 1, asset_id: 2, user_id: 3, asset: ''})

fails with

details=[message=the value of asset is not allowed to be empty, path=asset, type=any.empty]

Add support for add/drop indexes from live table

AWS announced a new feature today that allows you to create or drop global secondary indexes on a table without having to delete and recreate the table. Vogels should support this feature in updateTable()

Optional Schema Attributes

Are optional non-key schema attributes supported? It seems that they're not, and I was wondering if this is by design.

The issue I'm running into is that I if I set {optional: true} for an attribute in the schema, it passes Joi validation but still gets serialized, which means the AWS validation fails because the key exists, but has a value of null/empty string (which is not allowed).

Need to validate data when doing Table.update

The Table.create method uses joi to check that all of the supplied data is valid before attempting to serialize it and write it to the database (table.js line 116). However, Table.update does not do any data validation. This can result in invalid data being saved to the db. It can also cause a crash since some of the serializer code assumes that it is working with valid data. (For example, if the user performs an update with an invalid Date, a later call to Date.toISOString will raise an uncaught error.)

Query eating dynamodb errors

It looks like at https://github.com/ryanfitz/vogels/blob/master/lib/utils.js#L82 the util class calls the callback (in this case resultsFunc) without reporting that an error happened. If the error is recoverable, like ProvisionedThroughputExceededException the error should either be reported back to the query.execute method, or the library should try to handle the error through a retry. Right now no error is reported for retryable errors, and the query is not automatically retried.

Happy to submit a pr if you have a direction you would like to go with solving this.

Client side/browser support

This looks great! I plan on using DynamoDB a lot on the client side/browser. Are there any plans to support this? Thanks.

Add promise support

I am considering migrating to DynamoDB for one of my projects, and vogels looks like an excellent option. However, I would really like to see support for using promises for handling callbacks. Node promise library: https://github.com/kriskowal/q

Geolocation

Is there a standard way to use geolocation in vogels? If not, what would it take to build such a feature?

Regards,
Debdatta Basu

Better retry logic

It looks like vogels implements retry logic if a request fails, but it seems to just hammer the dynamoDB api until it either fails or gets a success. According to amazon something like an exponential backoff should be followed when getting an error that is retry-able.

References:
Vogel Retry Logic
Amazon Exponential Backoff

Thoughts?

Add a related model subquery API

As regards subqueries, I have already set up instance methods on my models that will go out and grab related records. I am injecting them automatically by parsing my model pseudo-code as such:

model User:
{
  hashKey: 'id',
  schema: {
    id: vogels.types.uuid(),
    orders: vogels.types.stringSet()
  }
}

model Order:
{
  hashKey: 'id',
  schema: {
    id: vogels.types.uuid(),
    user: Joi.string()
  }
}    

In this case I'd store the id attribute from each related order in the Users model instance 'orders' set, then use Order.getItems() to query the orders back out by their keys rather than scanning the whole Order table looking for a user id that matched or building global secondary indexes on that attribute.

Right now when I create my models I inject getters for the related data into Model.prototype. This works fine for lazy loading, but sometimes I want to make a single query and get back related model instances already included in a model instance substructure that lets me interact with them against the other model's API.

So basically, I have lazy loading solved, but I need a solution similar to how you roll up multiple queries into a single response to get around dynamo's max records in a resultset restriction, except synthesizing results from a subquery to another table rather than aggregating results for multiple queries on the same table.

Passing in region from vogels.AWS.config.update isn't working

I'm not doing anything other than loading in my credentials from a json file:

vogels.AWS.config.loadFromPath('./credentials.json');

This is my credentials file:

{"accessKeyId": "key", "secretAccessKey": "secret", "region": "us-west-1"}

When I try to connect to the db, I'm getting this regardless of what region I put in:

[ConfigError: Missing region in config]

why lodash over lazy.js

@ryanfitz - curious, why did you pick lodash over lazy.js for internals? We really like vogels and would like to start using it in some of our products (and contributing back). However we are still in our evaluation phase on what data mapper to use. In practice we have seen lazy.js be faster in most comparisions. Just curious on your thought process. thanks in advance.

data export

Currently, backing up data in DynamoDB is very challenging. I want an export feature to run a parallel scan and stream the json data to a writeable stream.

Schema tableName config issue.

var vogels = require('vogels');

vogels.AWS.config.loadFromPath('config/credentials.json');
var Account = vogels.define('Account', function (schema) {
  schema.String('email', {hashKey: true});
  schema.String('name').required(); // name attribute is required
  schema.Number('age'); // age is optional
  schema.Date('created', {default: Date.now});
});
Account.config({tableName: 'Account'});
Account.create({email: '[email protected]', name: 'Foo Bar', age: 21}, function (err, acc) {
  console.trace(err);
});

fails with

Trace: { [ResourceNotFoundException: Requested resource not found]
  message: 'Requested resource not found',
  code: 'ResourceNotFoundException',
  time: Tue Aug 12 2014 23:18:58 GMT+0530 (IST),
  statusCode: 400,
  retryable: false }

Issue goes away on removing

Account.config({tableName: 'Account'});

Make logging optional

I'm using this module and it generates noise in our logs when it creates a table. I'd like to have the ability to disable this logging. Would you be open to a pull request that uses the debug module for enabling logging based on environment variable settings?

Support BatchGetItem on multiple tables

BatchGetItem right now only supports doing multiple GetItem calls on a single table. It would be nice if it also supported doing GetItem calls on multiple tables as well.

Not sure how the design of this should be, though, since all operations are functions inside a specific table object.

Support for transaction

I assume giving support for dynamodb transactions would be a good value addition to this library. Since most of the application have this requirement.

update item does not work with expected option

When the expected option is passed to update() it throws Error: Need to pass in a valid Condition Object. From what I can tell the Expected.<key>.Value attribute is missing the data type per the updateItem api docs

If you turn on AWS SDK logging you can see the data going over the wire.

...
AWS.config.update({logger: process.stdout});

var mWsAccessToken = vogels.define('WsAccessToken', {
    tableName:  cfg.dataModelPrefix+'-ws-access-tokens',
    timestamps: true,
    hashKey:    'token',
    rangeKey:   'expires',
    schema:     {
        token:        Joi.string(),
        expires:      Joi.date(),
        tokenVersion: Joi.number(),
        userId:       Joi.string(),
        ip:           Joi.string()
    }
});
...

        return pWrapper.ninvoke(mWsAccessToken,"create",{
        token:        token,
        expires:      expires,
        tokenVersion: currentTokenVersion,
        userId:       userId,
        ip:           optionalIp
    })
        .then(function(oWsAccessToken){
            console.error("DATA", oWsAccessToken);
            console.error("token",oWsAccessToken.get("token"),"expires",oWsAccessToken.get("expires"));

            return pWrapper.ninvoke(mWsAccessToken,"update",{token: oWsAccessToken.get("token"), expires: oWsAccessToken.get("expires"), ip: "5.5.5.5"}, {expected: {userId: "1235"}});
        })
        .then(function(oWsAccessToken){
            console.error("DATA After update", oWsAccessToken);
        })
        .catch(function(e){
            console.error("ERROR", e, e.stack);
        });

The update above produces: Error: Need to pass in a valid Condition Object. With the following data over the wire:

updateItem({ TableName: 'ryan-ws-access-tokens',
 Key: 
  { token: 'MSAxNDI4MDg4NzcxODc5IDEyMzQgMS4yLjMuNA==',
    expires: '2015-04-03T19:19:31.879Z' },
 ReturnValues: 'ALL_NEW',
 UpdateExpression: 'SET #ip = :ip, #updatedAt = :updatedAt',
 ExpressionAttributeValues: { ':ip': '5.5.5.5', ':updatedAt': '2015-03-04T19:19:32.536Z' },
 ExpressionAttributeNames: { '#ip': 'ip', '#updatedAt': 'updatedAt' },
 Expected: { userId: { Value: '1235' } } })

For comparison, If the , {expected: {userId: "1235"}}) is removed from the update call it produces the following over the wire (which succeeds):

updateItem({ TableName: 'ryan-ws-access-tokens',
 Key: 
  { token: { S: 'MSAxNDI4MDg4NTQ4NjE4IDEyMzQgMS4yLjMuNA==' },
    expires: { S: '2015-04-03T19:15:48.618Z' } },
 ReturnValues: 'ALL_NEW',
 UpdateExpression: 'SET #ip = :ip, #updatedAt = :updatedAt',
 ExpressionAttributeValues: 
  { ':ip': { S: '5.5.5.5' },
    ':updatedAt': { S: '2015-03-04T19:15:49.294Z' } },
 ExpressionAttributeNames: { '#ip': 'ip', '#updatedAt': 'updatedAt' } })

New JSON Schema type

Have you considered a JSON or Object schema type that is simply a serialized JSON string but which would be automatically stringified or parsed on the way in/out of Dynamo? Would you be open to this functionality as a pull request? Storing a JSON blob in Dynamo is a common pattern I find myself using.

[improvement] Object schema type

Although DynamoDB does not directly support attributes of type Object, it would be nice if vogels supported schema types for Object & ObjectSet where vogels would serialize/deserialize the object's JSON representation to/from a DynamoDB string attribute type.

Sanitization like Mongoosejs

What's your thoughts on adding something similar to what mongoose has that only saves input that is a part of the schema? I need to sanitize input before saving the model using vogels. I thought about using something like this using lodash, which would work, but it would be nice if this was built into vogels.

var _ = require('lodash');

// in a user create express route...
var userData = _.pick(req.body, 'email', 'name', 'username', 'title');
User.create(userData, function (err, user) {
  console.log('created account in DynamoDB', user.get('email'));
});

This would only save the email, name, username, and title to the user. Even if something like admin: true was in the body.

I know mongoosejs has a strict implementation that might be interesting in vogels.

Thoughts?

Status for the IN operator?

What is the status regarding the IN operator?

The IN operator is documented on the main page in the scan section, but apparently the corresponding test is still disabled.

I have tried to use the IN operator based on the example code, but it raises a ValidationException.

Executing this:

'use strict';

var AWS = require('aws-sdk');
var vogels = require('vogels');
var Joi = require('joi');

AWS.config.update({
    accessKeyId: 'accessKeyId',
    secretAccessKey: 'secretAccessKey',
    region: 'region'
});

vogels.dynamoDriver(new AWS.DynamoDB({ endpoint: 'http://localhost:8000' }));

var Account = vogels.define('Account', {
  hashKey : 'email',

  // add the timestamp attributes (updatedAt, createdAt)
  timestamps : true,

  schema : {
    email   : Joi.string().email(),
    name    : Joi.string(),
    age     : Joi.number(),
    roles   : vogels.types.stringSet(),
    settings : {
      nickname      : Joi.string(),
      acceptedTerms : Joi.boolean().default(false)
    }
  }
});

Account.config({tableName: 'AccountsTable'});

vogels.createTables({
  'example-Account'  : {readCapacity: 1, writeCapacity: 10},
}, function (err) {
  if(err) {
    console.log('Error creating tables', err);
    process.exit(1);
  }

  Account.create({email: '[email protected]', name: 'Foo Bar', age: 21}, function (err, acc) {
      console.log('created account in DynamoDB', acc.get('email'));
      Account
          .scan()
          .where('name').in(['[email protected]', '[email protected]'])
          .exec(function (err, accounts) {
            console.log('Error:', err);
            console.log('Accounts:', JSON.stringify(accounts));
          });
    });
});

Gives this output:

created account in DynamoDB [email protected]
Error: { [ValidationException: One or more parameter values were invalid: ComparisonOperator IN is not valid for L AttributeValue type]
  message: 'One or more parameter values were invalid: ComparisonOperator IN is not valid for L AttributeValue type',
  code: 'ValidationException',
  time: Wed Mar 18 2015 16:24:17 GMT+0100 (W. Europe Standard Time),
  statusCode: 400,
  retryable: false,
  retryDelay: 0 }
Accounts: undefined

Should the IN operator be removed from the documentation if it is not possible to use it?

Unprocessed items in batch get item causes AWS error

If Dynamo cannot get all 100 items in a batch get request, vogels replays the UnprocessedKeys. However this object needs to be wrapped back in an object under the key RequestItems. As it stands now a parameter validation error will result in the aws-sdk because it doesn't see a RequestItems key on the request. PR forthcoming.

update joi version

The current version used is extremely out of date. This is however, potentially a major breaking change

Add ability to limit the count of matched items.

In a scan, limit restricts the number of items scanned. It would be nice if there were a way to limit the amount of matched items a scan returns. Vogels should also probably recurse through each LastEvaluatedKey until the limit is met.

Query also appears to have a similar issue, but only when the query response is getting paginated (such as when it hits the 1MB limit per response). If I do a limit(500) and only get, for example, 100 items returned with a LastEvaluatedKey, Vogels should recurse through and continue to get more responses until the limit is met, or no more results are available.

Right now the only workaround for these is doing a loadAll, which does not respect limit and will always get all available items (which can potentially waste throughput).

2.0.0 Release Notes

Summary

vogels v2.0 introduces a new api for defining models. The new define api brings full JSON document support to vogels, including support for deeply nested documents. DynamoDB Expression apis are exposed as options to all model methods. In order to support JSON documents the update api call has been switched to using UpdateExpressions from the deprecated AttributeUpdates api. The riskiest part of migrating to this version will be testing out your update operations.

  • Upgrade time: low - a few hours to a day for most users
  • Complexity: low - small list of well understood, easy to locate, and trivial to modify
  • Risk: moderate - update item calls switched to using DynamoDB UpdateExpressions api
  • Dependencies: moderate - requires using latest 2.1.x AWS-SDK and Joi 5.x.x.

Breaking Changes

New Features

  • Full JSON document support, including support for List, Map, and BOOL datatypes. Closes #16, #44
  • New timestamps config option to automatically add the attributes createdAt and updatedAt when defining a model. These attributes will get automatically get set when you create and update a record.
  • Flexible schema configuration to allow unknown, dynamic attributes both on top level and nested records
  • Ability to fully configure global and local secondary indexes. Allows to configure index names, attribute projection settings and index throughput. Closes #43 , #48
  • adding deleteTable api to remove the table from DynamoDB. Closes #10
  • New integration test suite and integration with travis-ci.
  • 100% code coverage

Bug Fixes

  • CreateTables checks if error occurs while attempting to create a table. Fixes #41
  • Fixed error handling when streaming query, scan and parallel scan requests.
  • Fixed retry handling when running query and scans. Fixes #45

Updated dependencies

  • Joi to v5.1.0
  • aws-sdk to v2.1.8 (Closes #49 )
  • async to v0.9.0
  • lodash to v3.1.0

Migration Checklist

Define a Model

The api for defining a model has been changed to accept a block of json configuration.

var Joi = require('joi');

var Account = vogels.define('Account', {
  hashKey : 'email',

  // add the timestamp attributes (updatedAt, createdAt)
  timestamps : true,

  schema : {
    email   : Joi.string().email(),
    name    : Joi.string(),
    age     : Joi.number(),
    roles   : vogels.types.stringSet(),
    settings : {
      nickname      : Joi.string(),
      acceptedTerms : Joi.boolean().default(false)
    }
  }
});

Secondary Indexes

Both local and global secondary indexes are defined when defining the model.

var BlogPost = vogels.define('Account', {
  hashkey : 'email',
  rangekey : 'title',
  schema : {
    email             : Joi.string().email(),
    title             : Joi.string(),
    content           : Joi.binary(),
    PublishedDateTime : Joi.date()
  },

  indexes : [
    { hashkey : 'email', rangekey : 'PublishedDateTime', type : 'local', name : 'PublishedIndex' },
    { hashkey : 'title', rangekey : 'email', type : 'global', name : 'TitleEmailIndex' },
  ]
});

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.