{"id":24641077,"url":"https://github.com/clarkie/dynogels","last_synced_at":"2025-10-05T15:37:55.823Z","repository":{"id":9395652,"uuid":"61985272","full_name":"clarkie/dynogels","owner":"clarkie","description":"DynamoDB data mapper for node.js. Originally forked from https://github.com/ryanfitz/vogels","archived":false,"fork":false,"pushed_at":"2023-04-14T07:37:02.000Z","size":638,"stargazers_count":492,"open_issues_count":51,"forks_count":107,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-10-05T15:37:53.569Z","etag":null,"topics":["aws","dynamodb","javascript","orm"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clarkie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-06-26T11:01:56.000Z","updated_at":"2025-07-16T14:46:07.000Z","dependencies_parsed_at":"2024-06-18T12:37:13.680Z","dependency_job_id":"d03bd33a-8f26-4edc-8536-782dd6c7728e","html_url":"https://github.com/clarkie/dynogels","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/clarkie/dynogels","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarkie%2Fdynogels","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarkie%2Fdynogels/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarkie%2Fdynogels/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarkie%2Fdynogels/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clarkie","download_url":"https://codeload.github.com/clarkie/dynogels/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clarkie%2Fdynogels/sbom","scorecard":{"id":285150,"data":{"date":"2025-08-11","repo":{"name":"github.com/clarkie/dynogels","commit":"e7fd8693f42197b7cce4a8b89a1199ba83f0b10f"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.5,"checks":[{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Code-Review","score":4,"reason":"Found 9/21 approved changesets -- score normalized to 4","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":9,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Warn: project license file does not contain an FSF or OSI license."],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 21 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":0,"reason":"45 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-6chw-6frg-f759","Warn: Project is vulnerable to: GHSA-v88g-cgmw-v5xw","Warn: Project is vulnerable to: GHSA-93q8-gq69-wqmw","Warn: Project is vulnerable to: GHSA-fwr7-v2mv-hh25","Warn: Project is vulnerable to: GHSA-rrc9-gqf8-8rwg","Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw","Warn: Project is vulnerable to: GHSA-3xgq-45jj-v275","Warn: Project is vulnerable to: GHSA-9vvw-cc9w-f27h","Warn: Project is vulnerable to: GHSA-gxpj-cx7g-858c","Warn: Project is vulnerable to: GHSA-h6ch-v84p-w6p9","Warn: Project is vulnerable to: GHSA-3w5v-p54c-f74x","Warn: Project is vulnerable to: GHSA-6x77-rpqf-j6mw","Warn: Project is vulnerable to: GHSA-hwcf-pp87-7x6p","Warn: Project is vulnerable to: GHSA-phwq-j96m-2c2q","Warn: Project is vulnerable to: GHSA-ghr5-ch3p-vcr6","Warn: Project is vulnerable to: GHSA-fjxv-7rqg-78g4","Warn: Project is vulnerable to: GHSA-4q6p-r6v2-jvc5","Warn: Project is vulnerable to: GHSA-w457-6q6x-cgp9","Warn: Project is vulnerable to: GHSA-62gr-4qp9-h98f","Warn: Project is vulnerable to: GHSA-f52g-6jhx-586p","Warn: Project is vulnerable to: GHSA-2cf5-4w76-r9qv","Warn: Project is vulnerable to: GHSA-3cqr-58rm-57f8","Warn: Project is vulnerable to: GHSA-g9r4-xpmj-mj65","Warn: Project is vulnerable to: GHSA-q2c6-c6pm-g3gh","Warn: Project is vulnerable to: GHSA-765h-qjxv-5f44","Warn: Project is vulnerable to: GHSA-f2jv-r9rf-7988","Warn: Project is vulnerable to: GHSA-c429-5p7v-vgjp","Warn: Project is vulnerable to: GHSA-43f8-2h32-f4cj","Warn: Project is vulnerable to: GHSA-896r-f27r-55mw","Warn: Project is vulnerable to: GHSA-p6mc-m468-83gw","Warn: Project is vulnerable to: GHSA-29mw-wpgm-hmr9","Warn: Project is vulnerable to: GHSA-35jh-r3h4-6jhm","Warn: Project is vulnerable to: GHSA-f8q6-p94x-37v3","Warn: Project is vulnerable to: GHSA-vh95-rmgr-6w4m","Warn: Project is vulnerable to: GHSA-xvch-5gv4-984h","Warn: Project is vulnerable to: GHSA-hj48-42vr-x3v9","Warn: Project is vulnerable to: GHSA-9wv6-86v2-598j","Warn: Project is vulnerable to: GHSA-g6ww-v8xp-vmwg","Warn: Project is vulnerable to: GHSA-hrpp-h998-j3pp","Warn: Project is vulnerable to: GHSA-p8p7-x288-28g6","Warn: Project is vulnerable to: GHSA-c2qf-rxjj-qqgw","Warn: Project is vulnerable to: GHSA-52f5-9888-hmc6","Warn: Project is vulnerable to: GHSA-72xf-g2v4-qvf3","Warn: Project is vulnerable to: GHSA-c9f4-xj24-8jqx","Warn: Project is vulnerable to: GHSA-776f-qx25-q3cc"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-17T17:03:09.220Z","repository_id":9395652,"created_at":"2025-08-17T17:03:09.221Z","updated_at":"2025-08-17T17:03:09.221Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278477836,"owners_count":25993540,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-05T02:00:06.059Z","response_time":54,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aws","dynamodb","javascript","orm"],"created_at":"2025-01-25T12:12:11.488Z","updated_at":"2025-10-05T15:37:55.791Z","avatar_url":"https://github.com/clarkie.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# dynogels\n[![Build Status](https://travis-ci.org/clarkie/dynogels.png?branch=master)](https://travis-ci.org/clarkie/dynogels)\n[![Coverage Status](https://coveralls.io/repos/github/clarkie/dynogels/badge.svg)](https://coveralls.io/github/clarkie/dynogels)\n[![npm version](https://badge.fury.io/js/dynogels.svg)](http://badge.fury.io/js/dynogels)\n[![Dependencies Status](https://david-dm.org/clarkie/dynogels.svg)](https://david-dm.org/clarkie/dynogels)\n[![DevDependencies Status](https://david-dm.org/clarkie/dynogels/dev-status.svg)](https://david-dm.org/clarkie/dynogels#info=devDependencies)\n[![Known Vulnerabilities](https://snyk.io/test/npm/dynogels/badge.svg)](https://snyk.io/test/npm/dynogels)\n\nDynogels is a [DynamoDB][5] data mapper for [node.js][1]. This project has been forked from\n[Vogels](https://github.com/ryanfitz/vogels) and republished to npm under a different name.\n\n## Features\n* Simplified data modeling and mapping to DynamoDB types\n* Advanced chainable apis for [query](#query) and [scan](#scan) operations\n* [Data validation](#data-validation)\n* [Autogenerating UUIDs](#uuid)\n* [Global Secondary Indexes](#global-indexes)\n* [Local Secondary Indexes](#local-secondary-indexes)\n* [Parallel Scans](#parallel-scan)\n\n## Installation\n\n    npm install dynogels\n\n## Getting Started\nFirst, you need to configure the [AWS SDK][2] with your credentials.\n\n```js\nvar dynogels = require('dynogels');\ndynogels.AWS.config.loadFromPath('credentials.json');\n```\n\nWhen running on EC2 it's recommended to leverage EC2 IAM roles. If you have configured your instance to use IAM roles,\nVogels will automatically select these credentials for use in your application,\nand you do not need to manually provide credentials in any other format.\n\n```js\nvar dynogels = require('dynogels');\ndynogels.AWS.config.update({region: \"REGION\"}); // region must be set\n```\n\nYou can also directly pass in your access key id, secret and region.\n  * It's recommended not to hard-code credentials inside an application.\n  Use this method only for small personal scripts or for testing purposes.\n\n```js\nvar dynogels = require('dynogels');\ndynogels.AWS.config.update({accessKeyId: 'AKID', secretAccessKey: 'SECRET', region: \"REGION\"});\n```\n\nCurrently the following region codes are available in Amazon:\n\n|      Code      |           Name           |\n| -------------- | ------------------------ |\n| ap-northeast-1 | Asia Pacific (Tokyo)     |\n| ap-southeast-1 | Asia Pacific (Singapore) |\n| ap-southeast-2 | Asia Pacific (Sydney)    |\n| eu-central-1   | EU (Frankfurt)           |\n| eu-west-1      | EU (Ireland)             |\n| sa-east-1      | South America (Sao Paulo)|\n| us-east-1      | US East (N. Virginia)    |\n| us-west-1      | US West (N. California)  |\n| us-west-2      | US West (Oregon)         |\n\n### Define a Model\nModels are defined through the toplevel define method.\n\n```js\nvar Account = dynogels.define('Account', {\n  hashKey : 'email',\n\n  // add the timestamp attributes (updatedAt, createdAt)\n  timestamps : true,\n\n  schema : {\n    email   : Joi.string().email(),\n    name    : Joi.string(),\n    age     : Joi.number(),\n    roles   : dynogels.types.stringSet(),\n    settings : {\n      nickname      : Joi.string(),\n      acceptedTerms : Joi.boolean().default(false)\n    }\n  }\n});\n```\n\nModels can also be defined with hash and range keys.\n\n```js\nvar BlogPost = dynogels.define('BlogPost', {\n  hashKey : 'email',\n  rangeKey : ‘title’,\n  schema : {\n    email   : Joi.string().email(),\n    title   : Joi.string(),\n    content : Joi.binary(),\n    tags   : dynogels.types.stringSet(),\n  }\n});\n```\n\nYou can pass through validation options to Joi like so:\n\n```js\nvar BlogPost = dynogels.define('BlogPost', {\n  hashKey : 'email',\n  rangeKey : 'title',\n  schema : {\n    email   : Joi.string().email(),\n    title   : Joi.string()\n  },\n  validation: {\n    // allow properties not defined in the schema\n    allowUnknown: true\n  }\n});\n```\n\n\n### Create Tables for all defined models\n\n```js\ndynogels.createTables(function(err) {\n  if (err) {\n    console.log('Error creating tables: ', err);\n  } else {\n    console.log('Tables have been created');\n  }\n});\n```\n\nWhen creating tables you can pass specific throughput settings or stream specification for any defined models.\n\n```js\ndynogels.createTables({\n  'BlogPost': {readCapacity: 5, writeCapacity: 10},\n  'Account': {\n    readCapacity: 20,\n    writeCapacity: 4,\n    streamSpecification: {\n      streamEnabled: true,\n      streamViewType: 'NEW_IMAGE'\n    }\n  }\n}, function(err) {\n  if (err) {\n    console.log('Error creating tables: ', err);\n  } else {\n    console.log('Tables has been created');\n  }\n});\n```\n\nYou can also pass operational options using the `$dynogels` key:\n\n* `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)\n\n```js\ndynogels.createTables({\n  $dynogels: { pollingInterval: 100 }\n}, function(err) {\n  if (err) {\n    console.log('Error creating tables: ', err);\n  } else {\n    console.log('Tables has been created');\n  }\n});\n```\n\n### Delete Table\n\n```js\nBlogPost.deleteTable(function(err) {\n  if (err) {\n    console.log('Error deleting table: ', err);\n  } else {\n    console.log('Table has been deleted');\n  }\n});\n```\n\n### Get Dynamo API Parameters\nYou can get the raw parameters needed for the DynamoDB [CreateTable API](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_CreateTable.html):\n\n```js\nvar parameters = BlogPost.dynamoCreateTableParams();\nvar dynamodb = new AWS.DynamoDB();\ndynamodb.createTable(params, (err)=\u003e{ ... });\n```\n\n### Schema Types\nVogels provides the following schema types:\n\n* String\n* Number\n* StringSet\n* NumberSet\n* Boolean\n* Date\n* UUID\n* TimeUUID\n\n#### UUID\nUUIDs can be declared for any attributes, including hash and range keys. By\nDefault, the uuid will be automatically generated when attempting to create\nthe model in DynamoDB.\n\n```js\nvar Tweet = dynogels.define('Tweet', {\n  hashKey : 'TweetID',\n  timestamps : true,\n  schema : {\n    TweetID : dynogels.types.uuid(),\n    content : Joi.string(),\n  }\n});\n```\n\n#### Data Validation\nDynogels 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.\n\n```js\nvar Tweet = dynogels.define('Tweet', {\n  hashKey : 'TweetID',\n  timestamps : true,\n  schema : {\n    TweetID : dynogels.types.uuid(),\n    content : Joi.string(),\n  }\n});\n\nconst tweet = new Tweet({ content: 123 })\nconst fail_result = Tweet.validate(tweet)\nconsole.log(fail_result.error.name) // ValidationError\n\ntweet.set('content', 'This is the content')\nconst pass_result = Tweet.validate(tweet)\nconsole.log(pass_result.error) // null\n```\n\n### Configuration\nYou can configure dynogels to automatically add `createdAt` and `updatedAt` timestamp attributes when\nsaving and updating a model. `updatedAt` will only be set when updating a record\nand will not be set on initial creation of the model.\n\n```js\nvar Account = dynogels.define('Account', {\n  hashKey : 'email',\n\n  // add the timestamp attributes (updatedAt, createdAt)\n  timestamps : true,\n\n  schema : {\n    email : Joi.string().email(),\n  }\n});\n```\n\nIf you want dynogels to handle timestamps, but only want some of them, or want your\ntimestamps to be called something else, you can override each attribute individually:\n\n```js\nvar Account = dynogels.define('Account', {\n  hashKey : 'email',\n\n  // enable timestamps support\n  timestamps : true,\n\n  // I don't want createdAt\n  createdAt: false,\n\n  // I want updatedAt to actually be called updateTimestamp\n  updatedAt: 'updateTimestamp'\n\n  schema : {\n    email : Joi.string().email(),\n  }\n});\n```\n\nYou can override the table name the model will use.\n\n```js\nvar Event = dynogels.define('Event', {\n  hashKey : 'name',\n  schema : {\n    name : Joi.string(),\n    total : Joi.number()\n  },\n\n  tableName: 'deviceEvents'\n});\n```\n\nif you set the tableName to a function, dynogels will use the result of the function as the active table to use.\nUseful for storing time series data.\n\n```js\nvar Event = dynogels.define('Event', {\n  hashKey : 'name',\n  schema : {\n    name : Joi.string(),\n    total : Joi.number()\n  },\n\n  // store monthly event data\n  tableName: function () {\n    var d = new Date();\n    return ['events', d.getFullYear(), d.getMonth() + 1].join('_');\n  }\n});\n```\n\nAfter you've defined your model you can configure the table name to use.\nBy default, the table name used will be the lowercased and pluralized version\nof the name you provided when defining the model.\n\n```js\nAccount.config({tableName: 'AccountsTable'});\n```\n\nYou can also pass in a custom instance of the aws-sdk DynamoDB client\n```js\nvar dynamodb = new AWS.DynamoDB();\nAccount.config({dynamodb: dynamodb});\n\n// or globally use custom DynamoDB instance\n// all defined models will now use this driver\ndynogels.dynamoDriver(dynamodb);\n```\n\n### Saving Models to DynamoDB\nWith your models defined, we can start saving them to DynamoDB.\n\n```js\nAccount.create({email: 'foo@example.com', name: 'Foo Bar', age: 21}, function (err, acc) {\n  console.log('created account in DynamoDB', acc.get('email'));\n});\n```\n\nYou can also first instantiate a model and then save it.\n\n```js\nvar acc = new Account({email: 'test@example.com', name: 'Test Example'});\nacc.save(function (err) {\n  console.log('created account in DynamoDB', acc.get('email'));\n});\n```\n\nSaving models that require range and hashkeys are identical to ones with only\nhashkeys.\n\n```js\nBlogPost.create({\n  email: 'werner@example.com',\n  title: 'Expanding the Cloud',\n  content: 'Today, we are excited to announce the limited preview...'\n  }, function (err, post) {\n    console.log('created blog post', post.get('title'));\n  });\n```\n\nPass an array of items and they will be saved in parallel to DynamoDB.\n\n```js\nvar item1 = {email: 'foo1@example.com', name: 'Foo 1', age: 10};\nvar item2 = {email: 'foo2@example.com', name: 'Foo 2', age: 20};\nvar item3 = {email: 'foo3@example.com', name: 'Foo 3', age: 30};\n\nAccount.create([item1, item2, item3], function (err, acccounts) {\n  console.log('created 3 accounts in DynamoDB', accounts);\n});\n```\n\nUse expressions api to do conditional writes\n\n```js\n  var params = {};\n  params.ConditionExpression = '#i \u003c\u003e :x';\n  params.ExpressionAttributeNames = {'#i' : 'id'};\n  params.ExpressionAttributeValues = {':x' : 123};\n\n  User.create({id : 123, name : 'Kurt Warner' }, params, function (error, acc) { ... });\n```\n\nUse the `overwrite` option to prevent over writing of existing records.\n  * By default `overwrite` is set to true, allowing create operations to overwrite existing records\n```js\n  // setting overwrite to false will generate\n  // the same Condition Expression as in the previous example\n  User.create({id : 123, name : 'Kurt Warner' }, {overwrite : false}, function (error, acc) { ... });\n```\n\n### Updating\n\nWhen updating a model the hash and range key attributes must be given, all\nother attributes are optional\n\n```js\n// update the name of the foo@example.com account\nAccount.update({email: 'foo@example.com', name: 'Bar Tester'}, function (err, acc) {\n  console.log('update account', acc.get('name'));\n});\n```\n\n`Model.update` accepts options to pass to DynamoDB when making the updateItem request\n\n```js\nAccount.update({email: 'foo@example.com', name: 'Bar Tester'}, {ReturnValues: 'ALL_OLD'}, function (err, acc) {\n  console.log('update account', acc.get('name')); // prints the old account name\n});\n\n// Only update the account if the current age of the account is 22\nAccount.update({email: 'foo@example.com', name: 'Bar Tester'}, {expected: {age: 22}}, function (err, acc) {\n  console.log('update account', acc.get('name'));\n});\n\n// setting an attribute to null will delete the attribute from DynamoDB\nAccount.update({email: 'foo@example.com', age: null}, function (err, acc) {\n  console.log('update account', acc.get('age')); // prints null\n});\n```\n\nTo 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.\n```js\nAccount.update(\n  { email: 'foo@example.com', name: 'FooBar Testers' },\n  { expected: { email: { Exists: true } } },\n  (err, acc) =\u003e {\n    console.log(acc.get('name')); // FooBar Testers\n  }\n);\n\nAccount.update(\n  { email: 'baz@example.com', name: 'Bar Tester' },\n  { expected: { email: { Exists: true } } },\n  (err, acc) =\u003e {\n    console.log(err); // Condition Expression failed: no Account with that hash key\n  }\n);\n```\n\nThis is essentially short-hand for:\n```js\nvar params = {};\n    params.ConditionExpression = 'attribute_exists(#hashKey)';\n    params.ExpressionAttributeNames = { '#hashKey' : 'email' };\n```\n\nYou can also pass what action to perform when updating a given attribute\nUse $add to increment or decrement numbers and add values to sets\n\n```js\nAccount.update({email : 'foo@example.com', age : {$add : 1}}, function (err, acc) {\n  console.log('incremented age by 1', acc.get('age'));\n});\n\nBlogPost.update({\n  email : 'werner@example.com',\n  title : 'Expanding the Cloud',\n  tags  : {$add : 'cloud'}\n}, function (err, post) {\n  console.log('added single tag to blog post', post.get('tags'));\n});\n\nBlogPost.update({\n  email : 'werner@example.com',\n  title : 'Expanding the Cloud',\n  tags  : {$add : ['cloud', 'dynamodb']}\n}, function (err, post) {\n  console.log('added tags to blog post', post.get('tags'));\n});\n```\n\n$del will remove values from a given set\n\n```js\nBlogPost.update({\n  email : 'werner@example.com',\n  title : 'Expanding the Cloud',\n  tags  : {$del : 'cloud'}\n}, function (err, post) {\n  console.log('removed cloud tag from blog post', post.get('tags'));\n});\n\nBlogPost.update({\n  email : 'werner@example.com',\n  title : 'Expanding the Cloud',\n  tags  : {$del : ['aws', 'node']}\n}, function (err, post) {\n  console.log('removed multiple tags', post.get('tags'));\n});\n```\n\nUse the expressions api to update nested documents\n\n```js\nvar params = {};\n  params.UpdateExpression = 'SET #year = #year + :inc, #dir.titles = list_append(#dir.titles, :title), #act[0].firstName = :firstName ADD tags :tag';\n  params.ConditionExpression = '#year = :current';\n  params.ExpressionAttributeNames = {\n    '#year' : 'releaseYear',\n    '#dir' : 'director',\n    '#act' : 'actors'\n  };\n\n  params.ExpressionAttributeValues = {\n    ':inc' : 1,\n    ':current' : 2001,\n    ':title' : ['The Man'],\n    ':firstName' : 'Rob',\n    ':tag' : dynogels.Set(['Sports', 'Horror'], 'S')\n  };\n\nMovie.update({title : 'Movie 0', description : 'This is a description'}, params, function (err, mov) {});\n```\n\n### Deleting\nYou delete items in DynamoDB using the hashkey of model\nIf your model uses both a hash and range key, then both need to be provided\n\n```js\nAccount.destroy('foo@example.com', function (err) {\n  console.log('account deleted');\n});\n\n// Destroy model using hash and range key\nBlogPost.destroy('foo@example.com', 'Hello World!', function (err) {\n  console.log('post deleted')\n});\n\nBlogPost.destroy({email: 'foo@example.com', title: 'Another Post'}, function (err) {\n  console.log('another post deleted')\n});\n```\n\n`Model.destroy` accepts options to pass to DynamoDB when making the deleteItem request\n\n```js\nAccount.destroy('foo@example.com', {ReturnValues: 'ALL_OLD'}, function (err, acc) {\n  console.log('account deleted');\n  console.log('deleted account name', acc.get('name'));\n});\n\nAccount.destroy('foo@example.com', {expected: {age: 22}}, function (err) {\n  console.log('account deleted if the age was 22');\n});\n```\n\n\nUse expression apis to perform conditional deletes\n\n```js\nvar params = {};\nparams.ConditionExpression = '#v = :x';\nparams.ExpressionAttributeNames = {'#v' : 'version'};\nparams.ExpressionAttributeValues = {':x' : '2'};\n\nUser.destroy({id : 123}, params, function (err, acc) {});\n```\n\n### Loading models from DynamoDB\nThe simpliest way to get an item from DynamoDB is by hashkey.\n\n```js\nAccount.get('test@example.com', function (err, acc) {\n  console.log('got account', acc.get('email'));\n});\n```\n\nPerform the same get request, but this time peform a consistent read.\n\n```js\nAccount.get('test@example.com', {ConsistentRead: true}, function (err, acc) {\n  console.log('got account', acc.get('email'));\n});\n```\n\n`Model.get` accepts any options that DynamoDB getItem request supports. For\nexample:\n\n```js\nAccount.get('test@example.com', {ConsistentRead: true, AttributesToGet : ['name','age']}, function (err, acc) {\n  console.log('got account', acc.get('email'))\n  console.log(acc.get('name'));\n  console.log(acc.get('age'));\n  console.log(acc.get('email')); // prints null\n});\n```\n\nGet a model using hash and range key.\n\n```js\n// load up blog post written by Werner, titled DynamoDB Keeps Getting Better and cheaper\nBlogPost.get('werner@example.com', 'dynamodb-keeps-getting-better-and-cheaper', function (err, post) {\n  console.log('loaded post by range and hash key', post.get('content'));\n});\n```\n\n`Model.get` also supports passing an object which contains hash and range key\nattributes to load up a model\n\n```js\nBlogPost.get({email: 'werner@example.com', title: 'Expanding the Cloud'}, function (err, post) {\n  console.log('loded post', post.get('content'));\n});\n```\n\nUse expressions api to select which attributes you want returned\n\n```js\n  User.get({ id : '123456789'},{ ProjectionExpression : 'email, age, settings.nickname' }, function (err, acc) {});\n```\n\n### Query\nFor models that use hash and range keys Vogels provides a flexible and\nchainable query api\n\n```js\n// query for blog posts by werner@example.com\nBlogPost\n  .query('werner@example.com')\n  .exec(callback);\n\n// same as above, but load all results\nBlogPost\n  .query('werner@example.com')\n  .loadAll()\n  .exec(callback);\n\n// only load the first 5 posts by werner\nBlogPost\n  .query('werner@example.com')\n  .limit(5)\n  .exec(callback);\n\n// query for posts by werner where the tile begins with 'Expanding'\nBlogPost\n  .query('werner@example.com')\n  .where('title').beginsWith('Expanding')\n  .exec(callback);\n\n// return only the count of documents that begin with the title Expanding\nBlogPost\n  .query('werner@example.com')\n  .where('title').beginsWith('Expanding')\n  .select('COUNT')\n  .exec(callback);\n\n// query the first 10 posts by werner@example.com but only return\n// the title and content from posts where the title starts with 'Expanding'\n// WARNING: See notes below on the implementation of limit in DynamoDB\nBlogPost\n  .query('werner@example.com')\n  .where('title').beginsWith('Expanding')\n  .attributes(['title', 'content'])\n  .limit(10)\n  .exec(callback);\n\n// sorting by title ascending\nBlogPost\n  .query('werner@example.com')\n  .ascending()\n  .exec(callback)\n\n// sorting by title descending\nBlogPost\n  .query('werner@example.com')\n  .descending()\n  .exec(callback)\n\n// All query options are chainable\nBlogPost\n  .query('werner@example.com')\n  .where('title').gt('Expanding')\n  .attributes(['title', 'content'])\n  .limit(10)\n  .ascending()\n  .loadAll()\n  .exec(callback);\n\n// Traversing Map Data Types\nAccount\n  .query('werner@example.com')\n  .filter('settings.acceptedTerms').equals(true)\n  .exec(callback);\n```\n\n**Warning, limit is applied first before the where filter. The limit value limits the scanned count,\nnot the number of returned items. See #12**\n\nVogels supports all the possible KeyConditions that DynamoDB currently\nsupports.\n\n```js\nBlogPost\n  .query('werner@example.com')\n  .where('title').equals('Expanding')\n  .exec();\n\n// less than equals\nBlogPost\n  .query('werner@example.com')\n  .where('title').lte('Expanding')\n  .exec();\n\n// less than\nBlogPost\n  .query('werner@example.com')\n  .where('title').lt('Expanding')\n  .exec();\n\n// greater than\nBlogPost\n  .query('werner@example.com')\n  .where('title').gt('Expanding')\n  .exec();\n\n// greater than equals\nBlogPost\n  .query('werner@example.com')\n  .where('title').gte('Expanding')\n  .exec();\n\n// attribute doesn't exist\nBlogPost\n  .query('werner@example.com')\n  .where('title').null()\n  .exec();\n\n// attribute exists\nBlogPost\n  .query('werner@example.com')\n  .where('title').exists()\n  .exec();\n\nBlogPost\n  .query('werner@example.com')\n  .where('title').beginsWith('Expanding')\n  .exec();\n\nBlogPost\n  .query('werner@example.com')\n  .where('title').between('foo@example.com', 'test@example.com')\n  .exec();\n```\n\nQuery Filters allow you to further filter results on non-key attributes.\n\n```js\nBlogPost\n  .query('werner@example.com')\n  .where('title').equals('Expanding')\n  .filter('tags').contains('cloud')\n  .exec();\n```\n\nExpression Filters also allow you to further filter results on non-key attributes.\n\n```javascript\nBlogPost\n  .query('werner@example.com')\n  .filterExpression('#title \u003c :t')\n  .expressionAttributeValues({ ':t' : 'Expanding' })\n  .expressionAttributeNames({ '#title' : 'title'})\n  .projectionExpression('#title, tag')\n  .exec();\n```\n\nSee the queryFilter.js [example][0] for more examples of using query filters\n\n#### Global Indexes\n\nFirst, define a model with a global secondary index.\n\n```js\nvar GameScore = dynogels.define('GameScore', {\n  hashKey : 'userId',\n  rangeKey : 'gameTitle',\n  schema : {\n    userId           : Joi.string(),\n    gameTitle        : Joi.string(),\n    topScore         : Joi.number(),\n    topScoreDateTime : Joi.date(),\n    wins             : Joi.number(),\n    losses           : Joi.number()\n  },\n  indexes : [{\n    hashKey : 'gameTitle', rangeKey : 'topScore', name : 'GameTitleIndex', type : 'global'\n  }]\n});\n```\n\nNow we can query against the global index\n\n```js\nGameScore\n  .query('Galaxy Invaders')\n  .usingIndex('GameTitleIndex')\n  .descending()\n  .exec(callback);\n```\n\nWhen can also configure the attributes projected into the index.\nBy default all attributes will be projected when no Projection pramater is\npresent\n\n```js\nvar GameScore = dynogels.define('GameScore', {\n  hashKey : 'userId',\n  rangeKey : 'gameTitle',\n  schema : {\n    userId           : Joi.string(),\n    gameTitle        : Joi.string(),\n    topScore         : Joi.number(),\n    topScoreDateTime : Joi.date(),\n    wins             : Joi.number(),\n    losses           : Joi.number()\n  },\n  indexes : [{\n    hashKey : 'gameTitle',\n    rangeKey : 'topScore',\n    name : 'GameTitleIndex',\n    type : 'global',\n    projection: { NonKeyAttributes: [ 'wins' ], ProjectionType: 'INCLUDE' } //optional, defaults to ALL\n\n  }]\n});\n```\n\nFilter items against the configured rangekey for the global index.\n\n```js\nGameScore\n  .query('Galaxy Invaders')\n  .usingIndex('GameTitleIndex')\n  .where('topScore').gt(1000)\n  .descending()\n  .exec(function (err, data) {\n    console.log(_.map(data.Items, JSON.stringify));\n  });\n```\n\n#### Local Secondary Indexes\nFirst, define a model using a local secondary index\n\n```js\nvar BlogPost = dynogels.define('Account', {\n  hashKey : 'email',\n  rangeKey : 'title',\n  schema : {\n    email             : Joi.string().email(),\n    title             : Joi.string(),\n    content           : Joi.binary(),\n    PublishedDateTime : Joi.date()\n  },\n\n  indexes : [{\n    hashKey : 'email', rangeKey : 'PublishedDateTime', type : 'local', name : 'PublishedIndex'\n  }]\n});\n```\n\nNow we can query for blog posts using the secondary index\n\n```js\nBlogPost\n  .query('werner@example.com')\n  .usingIndex('PublishedIndex')\n  .descending()\n  .exec(callback);\n```\n\nCould also query for published posts, but this time return oldest first\n\n```js\nBlogPost\n  .query('werner@example.com')\n  .usingIndex('PublishedIndex')\n  .ascending()\n  .exec(callback);\n```\n\nFinally lets load all published posts sorted by publish date\n```js\nBlogPost\n  .query('werner@example.com')\n  .usingIndex('PublishedIndex')\n  .descending()\n  .loadAll()\n  .exec(callback);\n```\n\nLearn more about [secondary indexes][3]\n\n### Scan\nVogels provides a flexible and chainable api for scanning over all your items\nThis api is very similar to the query api.\n\n```js\n// scan all accounts, returning the first page or results\nAccount.scan().exec(callback);\n\n// scan all accounts, this time loading all results\n// note this will potentially make several calls to DynamoDB\n// in order to load all results\nAccount\n  .scan()\n  .loadAll()\n  .exec(callback);\n\n// Load 20 accounts\nAccount\n  .scan()\n  .limit(20)\n  .exec();\n\n// Load All accounts, 20 at a time per request\nAccount\n  .scan()\n  .limit(20)\n  .loadAll()\n  .exec();\n\n// Load accounts which match a filter\n// only return email and created attributes\n// and return back the consumed capacity the request took\nAccount\n  .scan()\n  .where('email').gte('f@example.com')\n  .attributes(['email','created'])\n  .returnConsumedCapacity()\n  .exec();\n\n// Load All accounts, if settings.acceptedTerms is true\nAccount\n  .scan()\n  .where('settings.acceptedTerms').equals(true)\n  .exec();\n\n// Returns number of matching accounts, rather than the matching accounts themselves\nAccount\n  .scan()\n  .where('age').gte(21)\n  .select('COUNT')\n  .exec();\n\n// Start scan using start key\nAccount\n  .scan()\n  .where('age').notNull()\n  .startKey('foo@example.com')\n  .exec()\n```\n\nVogels supports all the possible Scan Filters that DynamoDB currently supports.\n\n```js\n// equals\nAccount\n  .scan()\n  .where('name').equals('Werner')\n  .exec();\n\n// not equals\nAccount\n  .scan()\n  .where('name').ne('Werner')\n  .exec();\n\n// less than equals\nAccount\n  .scan()\n  .where('name').lte('Werner')\n  .exec();\n\n// less than\nAccount\n  .scan()\n  .where('name').lt('Werner')\n  .exec();\n\n// greater than equals\nAccount\n  .scan()\n  .where('name').gte('Werner')\n  .exec();\n\n// greater than\nAccount\n  .scan()\n  .where('name').gt('Werner')\n  .exec();\n\n// name attribute doesn't exist\nAccount\n  .scan()\n  .where('name').null()\n  .exec();\n\n// name attribute exists\nAccount\n  .scan()\n  .where('name').notNull()\n  .exec();\n\n// contains\nAccount\n  .scan()\n  .where('name').contains('ner')\n  .exec();\n\n// not contains\nAccount\n  .scan()\n  .where('name').notContains('ner')\n  .exec();\n\n// in\nAccount\n  .scan()\n  .where('name').in(['foo@example.com', 'bar@example.com'])\n  .exec();\n\n// begins with\nAccount\n  .scan()\n  .where('name').beginsWith('Werner')\n  .exec();\n\n// between\nAccount\n  .scan()\n  .where('name').between('Bar', 'Foo')\n  .exec();\n\n// multiple filters\nAccount\n  .scan()\n  .where('name').equals('Werner')\n  .where('age').notNull()\n  .exec();\n```\n\nYou can also use the new expressions api when filtering scans\n\n```javascript\nUser.scan()\n  .filterExpression('#age BETWEEN :low AND :high AND begins_with(#email, :e)')\n  .expressionAttributeValues({ ':low' : 18, ':high' : 22, ':e' : 'test1'})\n  .expressionAttributeNames({ '#age' : 'age', '#email' : 'email'})\n  .projectionExpression('#age, #email')\n  .exec();\n```\n\n### Parallel Scan\nParallel scans increase the throughput of your table scans.\nThe parallel scan operation is identical to the scan api.\nThe only difference is you must provide the total number of segments\n\n**Caution** you can easily consume all your provisioned throughput with this api\n\n```js\nvar totalSegments = 8;\n\nAccount.parallelScan(totalSegments)\n  .where('age').gte(18)\n  .attributes('age')\n  .exec(callback);\n\n// Load All accounts\nAccount\n  .parallelScan(totalSegments)\n  .exec()\n```\n\nMore info on [Parallel Scans][4]\n\n### Batch Get Items\n`Model.getItems` allows you to load multiple models with a single request to DynamoDB.\n\nDynamoDB limits the number of items you can get to 100 or 1MB of data for a single request.\nVogels automatically handles splitting up into multiple requests to load all\nitems.\n\n```js\nAccount.getItems(['foo@example.com','bar@example.com', 'test@example.com'], function (err, accounts) {\n  console.log('loaded ' + accounts.length + ' accounts'); // prints loaded 3 accounts\n});\n\n// For models with range keys you must pass in objects of hash and range key attributes\nvar postKey1 = {email : 'test@example.com', title : 'Hello World!'};\nvar postKey2 = {email : 'test@example.com', title : 'Another Post'};\n\nBlogPost.getItems([postKey1, postKey2], function (err, posts) {\n  console.log('loaded posts');\n});\n```\n\n`Model.getItems` accepts options which will be passed to DynamoDB when making the batchGetItem request\n\n```js\n// Get both accounts, using a consistent read\nAccount.getItems(['foo@example.com','bar@example.com'], {ConsistentRead: true}, function (err, accounts) {\n  console.log('loaded ' + accounts.length + ' accounts'); // prints loaded 2 accounts\n});\n```\n\n### Streaming api\ndynogels supports a basic streaming api in addition to the callback\napi for `query`, `scan`, and `parallelScan` operations.\n\n```js\nvar stream = Account.parallelScan(4).exec();\n\nstream.on('readable', function () {\n  console.log('single parallel scan response', stream.read());\n});\n\nstream.on('end', function () {\n  console.log('Parallel scan of accounts finished');\n});\n\nvar querystream = BlogPost.query('werner@dynogels.com').loadAll().exec();\n\nquerystream.on('readable', function () {\n  console.log('single query response', stream.read());\n});\n\nquerystream.on('end', function () {\n  console.log('query for blog posts finished');\n});\n```\n\n### Dynamic Table Names\ndynogels supports dynamic table names, useful for storing time series data.\n\n```js\nvar Event = dynogels.define('Event', {\n  hashKey : 'name',\n  schema : {\n    name : Joi.string(),\n    total : Joi.number()\n  },\n\n  // store monthly event data\n  tableName: function () {\n    var d = new Date();\n    return ['events', d.getFullYear(), d.getMonth() + 1].join('_');\n  }\n});\n```\n\n### Logging\nA [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.\nOther 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.\n\n```js\nconst bunyan = require('bunyan');\nconst logger = bunyan.createLogger(\n  {\n    name: 'globalLogger',\n    level:'info'\n  })\n\ndynogels.log = logger;\n```\n\n\n```js\nconst bunyan = require('bunyan');\nconst accountLogger = bunyan.createLogger(\n  {\n    name: 'modelLogger',\n    level:'info'\n  })\n\nvar Account = dynogels.define('Account', {\n  hashKey: 'email',\n  log: accountLogger\n}); // INFO level on account table\n```\n\n* [Bunyan log levels](https://github.com/trentm/node-bunyan#levels)\n\n## Examples\n\n```js\nvar dynogels = require('dynogels');\n\nvar Account = dynogels.define('Account', {\n  hashKey : 'email',\n\n  // add the timestamp attributes (updatedAt, createdAt)\n  timestamps : true,\n\n  schema : {\n    email   : Joi.string().email(),\n    name    : Joi.string().required(),\n    age     : Joi.number(),\n  }\n});\n\nAccount.create({email: 'test@example.com', name : 'Test Account'}, function (err, acc) {\n  console.log('created account at', acc.get('created')); // prints created Date\n\n  acc.set({age: 22});\n\n  acc.update(function (err) {\n    console.log('updated account age');\n  });\n\n});\n```\n\nSee the [examples][0] for more working sample code.\n\n### Support\n\nDynogels is provided as-is, free of charge. For support, you have a few choices:\n\n- Ask your support question on [Stackoverflow.com](http://stackoverflow.com), and tag your question with **dynogels**.\n- If you believe you have found a bug in dynogels, please submit a support ticket on\n the [Github Issues page for dynogels](http://github.com/clarkie/dynogels/issues). We'll get to them as soon as we can.\n- For general feedback message me on [twitter](https://twitter.com/clarkieclarkie)\n- For more personal or immediate support, I’m available for hire to consult on your project.\n[Contact](mailto:andrew.t.clarke@gmail.com) me for more detals.\n\n### Maintainers\n\n- [Clarkie](https://github.com/clarkie) ([@clarkieclarkie](https://twitter.com/clarkieclarkie))\n- [Ryan Fitzgerald](http://github.com/ryanfitz) ([@ryanfitz](https://twitter.com/theryanfitz))\n\n### License\n\n(The MIT License)\n\nCopyright (c) 2016 Ryan Fitzgerald\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n[0]: https://github.com/clarkie/dynogels/tree/master/examples\n[1]: http://nodejs.org\n[2]: http://aws.amazon.com/sdkfornodejs\n[3]: http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html\n[4]: http://aws.typepad.com/aws/2013/05/amazon-dynamodb-parallel-scans-and-other-good-news.html\n[5]: http://aws.amazon.com/dynamodb\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclarkie%2Fdynogels","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclarkie%2Fdynogels","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclarkie%2Fdynogels/lists"}