diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..de5bc15 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,62 @@ +version: 2.1 + +orbs: + codecov: codecov/codecov@1.0.5 + +references: + attach_workspace: &attach_workspace + attach_workspace: + at: ~/repo + persist_to_workspace: &persist_to_workspace + persist_to_workspace: + root: ~/repo + paths: . + +executors: + arwen: + docker: + - image: circleci/node:10.15.1 + working_directory: ~/repo + +jobs: + build: + executor: arwen + steps: + - *attach_workspace + - checkout + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "package.json" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + - run: npm install + - save_cache: + paths: + - node_modules + key: v1-dependencies-{{ checksum "package.json" }} + - run: npm test + - codecov/upload: + file: .nyc_output/*.json + - *persist_to_workspace + publish: + executor: arwen + steps: + - *attach_workspace + - checkout + - run: npm run semantic-release + +workflows: + build: + jobs: + - build: + filters: + branches: + only: /.*/ + - publish: + context: pi + requires: + - build + filters: + branches: + only: master diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index a7a9993..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,31 +0,0 @@ -module.exports = { - "env": { - "browser": true, - "commonjs": true, - "es6": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": 2017 - }, - "rules": { - "indent": [ - "error", - 4, - {"SwitchCase": 1} - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "single" - ], - "semi": [ - "error", - "always" - ], - "no-console": "off" - } -}; \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..c5b923a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,33 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Mocha All", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "--timeout", + "999999", + "--colors", + "${workspaceFolder}/test" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + }, + { + "type": "node", + "request": "launch", + "name": "Mocha Current File", + "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", + "args": [ + "--timeout", + "999999", + "--colors", + "${file}" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen" + } + ] + } \ No newline at end of file diff --git a/README.md b/README.md index 7e172e5..064caf3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# Javascript DynamoDB Context Store +# Javascript DynamoDB Context Store -Used by the [SmartApp SDK](https://github.com/SmartThingsCommunity/smartapp-sdk-nodejs) to store IDs and access tokens for an installed instance of a SmartApp -and retrieves that information for use in asynchronous API calls. The use of a context store -is only needed when SmartApps have to call the SmartThings API in response to external -events. SmartApps that only response to lifecycle events from the SmartThings platform -will automatically have the proper context without the app having to store it. +[![CircleCI](https://circleci.com/gh/SmartThingsCommunity/dynamodb-context-store-nodejs/tree/master.svg?style=svg)](https://circleci.com/gh/SmartThingsCommunity/dynamodb-context-store-nodejs/tree/master) + +Used by the [SmartApp SDK](https://github.com/SmartThingsCommunity/smartapp-sdk-nodejs) to store IDs and access tokens for an installed instance of a SmartApp and retrieves that information for use in asynchronous API calls. The use of a context store is only needed when SmartApps have to call the SmartThings API in response to external events. SmartApps that only response to lifecycle events from the SmartThings platform will automatically have the proper context without the app having to store it. The context stored by this module consists of the following data elements: @@ -17,8 +15,9 @@ The context stored by this module consists of the following data elements: _Note: Version 2.X.X is a breaking change to version 1.X.X as far as configuring the context store is concerned, but either one can be used with any version of the SmartThings SDK._ -## Installation: -``` +## Installation + +```bash npm install @smartthings/dynamodb-context-store ``` @@ -26,11 +25,13 @@ npm install @smartthings/dynamodb-context-store Create a `DynamoDBContextStore` object and pass it to the SmartApp connector to store the context in a table named `"smartapp"` in the `us-east-1` AWS region. If the table does not exist it will be created. + ```javascript smartapp.contextStore(new DynamoDBContextStore()) ``` The more extensive set of options are shown in this example: + ```javascript smartapp.contextStore(new DynamoDBContextStore( { @@ -48,6 +49,7 @@ smartapp.contextStore(new DynamoDBContextStore( ``` The **table** configuration options are: + * **name** -- The name of the DynamoDB table storing the context * **hashKey** -- The name of the partition key of the table * **prefix** -- A string pre-pended to the installed app ID and used as the partition key for the entry @@ -56,6 +58,7 @@ The **table** configuration options are: * **sortKey** -- Optional sort key definition (see below for more details) Other configuration options are: + * **AWSRegion** -- The AWS region containing the table * **AWSConfigPath** -- The location of the AWS configuration JSON file * **AWSConfigJSON** -- The AWS credentials and region @@ -67,6 +70,7 @@ Note that only one of the AWS options should be specified or behavior will be in By default, the AWS credentials are picked up from the environment. If you prefer you can read the credentials from a file with this configuration: + ```javascript smartapp.contextStore(new DynamoDBContextStore( { @@ -75,8 +79,8 @@ smartapp.contextStore(new DynamoDBContextStore( )) ``` - You can also explicitly set the credentials in this way: + ```javascript smartapp.contextStore(new DynamoDBContextStore( { @@ -87,11 +91,13 @@ smartapp.contextStore(new DynamoDBContextStore( } } )) -```` +``` ### Sort Key Configuration + In order to support single table schemas, the context store can be configured to use a table with a sort key. The simplest way to do that is by specifying the sort key name: + ```javascript smartapp.contextStore(new DynamoDBContextStore( { @@ -102,10 +108,11 @@ smartapp.contextStore(new DynamoDBContextStore( } } )) - ``` + More control over the sort key can be exercised using this form, which is configured with the default values used when just the sort key name is specified: + ```javascript smartapp.contextStore(new DynamoDBContextStore( { @@ -121,5 +128,4 @@ smartapp.contextStore(new DynamoDBContextStore( } } )) - ``` diff --git a/config/codecov.yml b/config/codecov.yml new file mode 100644 index 0000000..7a4c807 --- /dev/null +++ b/config/codecov.yml @@ -0,0 +1,8 @@ +ignore: + - "[docs|doc]/**/*" # ignore docs + - "[config|]/**/*" # ignore configs + - "*.md" # ignore markdown + +parsers: + javascript: + enable_partials: yes \ No newline at end of file diff --git a/config/release.config.js b/config/release.config.js new file mode 100644 index 0000000..5484b6e --- /dev/null +++ b/config/release.config.js @@ -0,0 +1,14 @@ +module.exports = { + plugins: [ + '@semantic-release/commit-analyzer', + '@semantic-release/release-notes-generator', + ['@semantic-release/changelog', { + changelogFile: 'docs/CHANGELOG.md' + }], + '@semantic-release/npm', + ['@semantic-release/git', { + assets: ['docs', 'package.json'], + message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' + }] + ] +} diff --git a/index.js b/index.js index a2b4424..ae6d331 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,3 @@ -'use strict'; +'use strict' -module.exports = require('./lib/DynamoDBContextStore') +module.exports = require('./lib/dynamodb-context-store') diff --git a/lib/DynamoDBContextStore.js b/lib/DynamoDBContextStore.js deleted file mode 100644 index de6d3a2..0000000 --- a/lib/DynamoDBContextStore.js +++ /dev/null @@ -1,259 +0,0 @@ -'use strict'; -const AWS = require('aws-sdk'); -const primaryKey = Symbol("private"); -const createTableIfNecessary = Symbol("private"); - -module.exports = class DynamoDBContextStore { - - /** - * @typedef {Object} TableOptions - * @prop {String=} name The name of the table - * @prop {String=} hashKey The name of the table - * @prop {String=} prefix String pre-pended to the app ID and used for the hashKey - * @prop {String|Object=} sortKey Optional sort key definition - * @prop {Number=} readCapacityUnits Number of consistent reads per second. Used only when table is created - * @prop {Number=} writeCapacityUnits Number of writes per second. Used only when table is created - */ - /** - * @typedef {Object} DynamoDBContextStoreOptions - * @prop {TableOptions=} table The table options - */ - /** - * Create a context store instance instance - * @param {DynamoDBContextStoreOptions} [options] Optionally, pass in a configuration object - */ - constructor(options = {}) { - this.table = { - name: "smartapp", - hashKey: 'id', - sortKey: null, - prefix: 'ctx:' - }; - - if (options.table) { - const table = options.table; - this.table.name = table.name || this.table.name; - this.table.hashKey = table.hashKey || this.table.hashKey; - this.table.prefix = table.prefix || this.table.prefix; - if (typeof options.table.sortKey === 'string') { - this.table.sortKey = { - AttributeName: options.table.sortKey, - AttributeType: 'S', - AttributeValue: 'context', - KeyType: 'RANGE' - } - } else { - this.table.sortKey = options.table.sortKey - } - } - - if (options.client) { - this.client = options.client; - } - else { - if (options.AWSConfigPath) { - AWS.config.loadFromPath(options.AWSConfigPath); - } - else if (options.AWSConfigJSON) { - AWS.config.update(options.AWSConfigJSON); - } - else { - this.AWSRegion = options.AWSRegion || 'us-east-1'; - AWS.config.update({region: this.AWSRegion}); - } - this.client = new AWS.DynamoDB(); - } - - if (options.autoCreate !== false) { - this[createTableIfNecessary](options); - } - } - - get(installedAppId) { - let params = { - TableName: this.table.name, - Key: { - [this.table.hashKey]: {S: this[primaryKey](installedAppId)} - }, - ConsistentRead: true - }; - - if (this.table.sortKey) { - params.Key[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} - } - - return new Promise((resolve, reject) => { - this.client.getItem(params, function(err, data) { - if (err) { - reject(err); - } else { - if (data && data.Item) { - const item = data.Item - resolve({ - installedAppId: AWS.DynamoDB.Converter.output(item.installedAppId), - locationId: AWS.DynamoDB.Converter.output(item.locationId), - authToken: AWS.DynamoDB.Converter.output(item.authToken), - refreshToken: AWS.DynamoDB.Converter.output(item.refreshToken), - config: AWS.DynamoDB.Converter.output(data.Item.config), - state: AWS.DynamoDB.Converter.output(data.Item.state) - }); - } - else { - resolve({}); - } - } - }); - }); - } - - put(params) { - const data = { - TableName: this.table.name, - Item: { - [this.table.hashKey]: {S: this[primaryKey](params.installedAppId)}, - installedAppId: {S: params.installedAppId}, - locationId: {S: params.locationId}, - authToken: {S: params.authToken}, - refreshToken: {S: params.refreshToken}, - config: AWS.DynamoDB.Converter.input(params.config), - state: AWS.DynamoDB.Converter.input(params.state) - } - }; - - if (this.table.sortKey) { - data.Item[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} - } - - return new Promise((resolve, reject) => { - this.client.putItem(data, function(err, data) { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - - } - - update(installedAppId, params) { - const names = {}; - const values = {}; - const expressions = []; - for (const name of Object.keys(params)) { - - const expressionNameKeys = []; - const nameSegs = name.split('.'); - for (const i in nameSegs) { - const nameKey = `#${nameSegs.slice(0,i+1).join('_')}`; - names[nameKey] = nameSegs[i]; - expressionNameKeys.push(nameKey) - } - const valueKey = `:${nameSegs.join('_')}`; - values[valueKey] = AWS.DynamoDB.Converter.input(params[name]); - expressions.push(`${expressionNameKeys.join('.')} = ${valueKey}`) - } - - const data = { - TableName: this.table.name, - Key: { - [this.table.hashKey]: {S: this[primaryKey](installedAppId)} - }, - UpdateExpression: 'SET ' + expressions.join(', '), - ExpressionAttributeNames: names, - ExpressionAttributeValues: values - }; - - if (this.table.sortKey) { - data.Key[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} - } - - console.log(JSON.stringify(data, null, 2)); - return new Promise((resolve, reject) => { - this.client.updateItem(data, function(err, data) { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - } - - delete(installedAppId) { - let params = { - TableName: this.table.name, - Key: { - [this.table.hashKey]: {S: this[primaryKey](installedAppId)} - } - }; - - if (this.table.sortKey) { - data.Key[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} - } - - return new Promise((resolve, reject) => { - this.client.deleteItem(params, function(err, data) { - if (err) { - reject(err); - } else { - resolve(data); - } - }); - }); - } - - [primaryKey](installedAppId) { - return `${this.table.prefix}${installedAppId}`; - } - - [createTableIfNecessary](options) { - return this.client.describeTable({"TableName": this.table.name}).promise() - .then(table => { - console.log(`DynamoDB context table ${this.table.name}, exists`) - }) - .catch(err => { - if (err.code === 'ResourceNotFoundException') { - console.log(`DynamoDB context table ${this.table.name}, creating`); - const params = { - "TableName": this.table.name, - "AttributeDefinitions": [ - { - "AttributeName": this.table.hashKey, - "AttributeType": "S" - } - ], - "KeySchema": [ - { - "KeyType": "HASH", - "AttributeName": this.table.hashKey - } - ], - "ProvisionedThroughput": { - "WriteCapacityUnits": options.table ? (options.table.writeCapacityUnits || 5) : 5, - "ReadCapacityUnits": options.table ? (options.table.readCapacityUnits || 5) : 5 - } - } - - if (this.table.sortKey) { - params.AttributeDefinitions.push({ - "AttributeName": this.table.sortKey.AttributeName, - "AttributeType": this.table.sortKey.AttributeType - }); - params.KeySchema.push({ - "KeyType": this.table.sortKey.KeyType, - "AttributeName": this.table.sortKey.AttributeName, - }) - } - - return this.client.createTable(params).promise() - } - else { - console.error('Error creating DynamoDB table %j', err); - return new Promise((resolve, reject) => { - reject(err) - }) - } - }) - } -}; diff --git a/lib/dynamodb-context-store.js b/lib/dynamodb-context-store.js new file mode 100644 index 0000000..4078a94 --- /dev/null +++ b/lib/dynamodb-context-store.js @@ -0,0 +1,297 @@ +'use strict' +const AWS = require('aws-sdk') + +const primaryKey = Symbol('private') +const createTableIfNecessary = Symbol('private') + +module.exports = class DynamoDBContextStore { + /** + * @typedef {Object} TableOptions + * @property {String=} name The name of the table + * @property {String=} hashKey The name of the table + * @property {String=} prefix String pre-pended to the app ID and used for the hashKey + * @property {String|Object=} sortKey Optional sort key definition + * @property {Number=} readCapacityUnits Number of consistent reads per second. Used only when table is created + * @property {Number=} writeCapacityUnits Number of writes per second. Used only when table is created + */ + /** + * @typedef AWSConfigJson + * @property {String} accessKeyId The access key to your AWS account + * @property {String} secretAccessKey The secret access key to your AWS account + * @property {String} region AWS region + */ + /** + * @typedef {Object} DynamoDBContextStoreOptions + * @property {TableOptions=} table The table options + * @property {AWS.DynamoDB=} client Optionally, use an existing AWS DynamoDB client + * @property {String=} AWSRegion The AWS region containing the table + * @property {String=} AWSConfigPath The location of the AWS configuration JSON file. Use either this _or_ `AWSConfigJSON`, not both. + * @property {AWS.Config=} AWSConfigJSON The AWS credentials and region. Use either this _or_ `AWSConfigPath`, not both. + * @property {Boolean} autoCreate Controls whether table is created if it doesn't already exist + */ + + /** + * @typedef ContextObject + * @property {String} installedAppId + * @property {String} locationId + * @property {String=} authToken + * @property {String=} refreshToken + * @property {Object=} config + * @property {Object=} state + */ + + /** + * Create a context store instance instance + * @param {DynamoDBContextStoreOptions|Object} [options] Optionally, pass in a configuration object + */ + constructor(options = {}) { + this.table = { + name: 'smartapp', + hashKey: 'id', + sortKey: null, + prefix: 'ctx:' + } + + if (options.table) { + const {table} = options + this.table.name = table.name || this.table.name + this.table.hashKey = table.hashKey || this.table.hashKey + this.table.prefix = table.prefix || this.table.prefix + if (typeof options.table.sortKey === 'string') { + this.table.sortKey = { + AttributeName: options.table.sortKey, + AttributeType: 'S', + AttributeValue: 'context', + KeyType: 'RANGE' + } + } else { + this.table.sortKey = options.table.sortKey + } + } + + if (options.client) { + this.client = options.client + } else { + if (options.AWSConfigPath) { + AWS.config.loadFromPath(options.AWSConfigPath) + } else if (options.AWSConfigJSON) { + AWS.config.update(options.AWSConfigJSON) + } else { + this.AWSRegion = options.AWSRegion || 'us-east-1' + AWS.config.update({region: this.AWSRegion}) + } + + this.client = new AWS.DynamoDB() + } + + if (options.autoCreate !== false) { + this[createTableIfNecessary](options) + } + } + + /** + * Get the data associated with the `installedAppId` + * @param {String} installedAppId Installed app identifier + * @returns {Promise>|Promise} + */ + get(installedAppId) { + const params = { + TableName: this.table.name, + Key: { + [this.table.hashKey]: {S: this[primaryKey](installedAppId)} + }, + ConsistentRead: true + } + + if (this.table.sortKey) { + params.Key[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} + } + + return new Promise((resolve, reject) => { + this.client.getItem(params, (err, data) => { + if (err) { + reject(err) + } else if (data && data.Item) { + const item = data.Item + resolve({ + installedAppId: AWS.DynamoDB.Converter.output(item.installedAppId), + locationId: AWS.DynamoDB.Converter.output(item.locationId), + authToken: AWS.DynamoDB.Converter.output(item.authToken), + refreshToken: AWS.DynamoDB.Converter.output(item.refreshToken), + config: AWS.DynamoDB.Converter.output(data.Item.config), + state: AWS.DynamoDB.Converter.output(data.Item.state) + }) + } else { + resolve({}) + } + }) + }) + } + + /** + * Puts the data into the context store + * @param {ContextObject} params Context object + * @returns {Promise>|Promise} + */ + put(params) { + const data = { + TableName: this.table.name, + Item: { + [this.table.hashKey]: {S: this[primaryKey](params.installedAppId)}, + installedAppId: {S: params.installedAppId}, + locationId: {S: params.locationId}, + authToken: {S: params.authToken}, + refreshToken: {S: params.refreshToken}, + config: AWS.DynamoDB.Converter.input(params.config), + state: AWS.DynamoDB.Converter.input(params.state) + } + } + + if (this.table.sortKey) { + data.Item[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} + } + + return new Promise((resolve, reject) => { + this.client.putItem(data, (err, data) => { + if (err) { + reject(err) + } else { + resolve(data) + } + }) + }) + } + + /** + * Updates the data in the context store by `installedAppId` + * @param {String} installedAppId Installed app identifier + * @param {ContextObject} params Context object + * @returns {Promise>|Promise} + */ + update(installedAppId, params) { + const names = {} + const values = {} + const expressions = [] + for (const name of Object.keys(params)) { + const expressionNameKeys = [] + const nameSegs = name.split('.') + for (const i in nameSegs) { + if (Object.prototype.hasOwnProperty.call(nameSegs, i)) { + const nameKey = `#${nameSegs.slice(0, i + 1).join('_')}` + names[nameKey] = nameSegs[i] + expressionNameKeys.push(nameKey) + } + } + + const valueKey = `:${nameSegs.join('_')}` + values[valueKey] = AWS.DynamoDB.Converter.input(params[name]) + expressions.push(`${expressionNameKeys.join('.')} = ${valueKey}`) + } + + const data = { + TableName: this.table.name, + Key: { + [this.table.hashKey]: {S: this[primaryKey](installedAppId)} + }, + UpdateExpression: 'SET ' + expressions.join(', '), + ExpressionAttributeNames: names, + ExpressionAttributeValues: values + } + + if (this.table.sortKey) { + data.Key[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} + } + + console.log(JSON.stringify(data, null, 2)) + return new Promise((resolve, reject) => { + this.client.updateItem(data, (err, data) => { + if (err) { + reject(err) + } else { + resolve(data) + } + }) + }) + } + + /** + * Delete the row from the table + * @param {String} installedAppId Installed app identifier + * @returns {Promise>|Promise} + */ + delete(installedAppId) { + const data = { + TableName: this.table.name, + Key: { + [this.table.hashKey]: {S: this[primaryKey](installedAppId)} + } + } + + if (this.table.sortKey) { + data.Key[this.table.sortKey.AttributeName] = {[this.table.sortKey.AttributeType]: this.table.sortKey.AttributeValue} + } + + return new Promise((resolve, reject) => { + this.client.deleteItem(data, (err, data) => { + if (err) { + reject(err) + } else { + resolve(data) + } + }) + }) + } + + [primaryKey](installedAppId) { + return `${this.table.prefix}${installedAppId}` + } + + [createTableIfNecessary](options) { + return this.client.describeTable({'TableName': this.table.name}).promise() + .then(table => { + console.log(`DynamoDB context table ${table.name}, exists`) + }) + .catch(error => { + if (error.code === 'ResourceNotFoundException') { + console.log(`DynamoDB context table ${this.table.name}, creating`) + const params = { + 'TableName': this.table.name, + 'AttributeDefinitions': [ + { + 'AttributeName': this.table.hashKey, + 'AttributeType': 'S' + } + ], + 'KeySchema': [ + { + 'KeyType': 'HASH', + 'AttributeName': this.table.hashKey + } + ], + 'ProvisionedThroughput': { + 'WriteCapacityUnits': options.table ? (options.table.writeCapacityUnits || 5) : 5, + 'ReadCapacityUnits': options.table ? (options.table.readCapacityUnits || 5) : 5 + } + } + + if (this.table.sortKey) { + params.AttributeDefinitions.push({ + 'AttributeName': this.table.sortKey.AttributeName, + 'AttributeType': this.table.sortKey.AttributeType + }) + params.KeySchema.push({ + 'KeyType': this.table.sortKey.KeyType, + 'AttributeName': this.table.sortKey.AttributeName + }) + } + + return this.client.createTable(params).promise() + } + + console.error('Error creating DynamoDB table %j', error) + return new Promise((resolve, reject) => { + reject(error) + }) + }) + } +} diff --git a/package.json b/package.json index c812363..9172330 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,26 @@ "name": "@smartthings/dynamodb-context-store", "version": "2.0.0", "description": "Stores SmartApp configuration and auth tokens for use in app-initiated calls", + "displayName": "SmartThings SmartApp DynamoDB Context Store", "main": "index.js", + "author": "SmartThings", + "contributors": [ + "Bob Florian" + ], + "keywords": [ + "smartthings", + "smartapp" + ], "scripts": { - "test": "mocha test/**/*.js" - }, - "repository": { - "type": "git", - "url": "https://github.com/SmartThingsCommunity/dynamodb-context-store-nodejs.git" + "lint": "xo", + "test:unit": "mocha test/unit", + "test": "xo && nyc mocha test/**/*", + "semantic-release": "semantic-release -e ./config/release.config.js", + "snyk-protect": "snyk protect", + "prepare": "npm run snyk-protect" }, + "license": "Apache-2.0", + "repository": "github.com:SmartThingsCommunity/dynamodb-context-store-nodejs.git", "bugs": { "url": "https://github.com/SmartThingsCommunity/dynamodb-context-store-nodejs/issues" }, @@ -17,9 +29,52 @@ "dependencies": { "aws-sdk": "^2.392.0" }, - "author": "SmartThings", - "contributors": [ - "Bob Florian" - ], - "license": "Apache-2.0" -} + "devDependencies": { + "@semantic-release/changelog": "~3.0.4", + "@semantic-release/commit-analyzer": "~6.2.0", + "@semantic-release/git": "~7.0.16", + "@semantic-release/release-notes-generator": "~7.2.1", + "xo": "~0.24.0", + "nyc": "~14.1.1", + "mocha": "~6.2.0", + "sinon": "~7.3.2", + "conventional-changelog-eslint": "~3.0.1", + "chai": "~4.2.0", + "snyk": "1.186.0" + }, + "nyc": { + "watermarks": { + "lines": [ + 40, + 95 + ], + "functions": [ + 40, + 95 + ], + "branches": [ + 40, + 95 + ], + "statements": [ + 40, + 95 + ] + } + }, + "xo": { + "space": false, + "semicolon": false, + "rules": { + "no-useless-constructor": 1, + "promise/prefer-await-to-then": 1, + "prefer-object-spread": 1, + "no-template-curly-in-string": 0, + "quote-props": [ + "error", + "consistent" + ] + } + }, + "snyk": true +} \ No newline at end of file diff --git a/test/dynamodb-context-store-spec.js b/test/dynamodb-context-store-spec.js new file mode 100644 index 0000000..d8c31f2 --- /dev/null +++ b/test/dynamodb-context-store-spec.js @@ -0,0 +1,29 @@ +/* eslint no-undef: "off" */ +const AWS = require('aws-sdk') +const {expect} = require('chai') +const DynamoDBContextStore = require('../lib/dynamodb-context-store') + +describe('context-store-spec', () => { + /** @type {DynamoDBContextStore} */ + let store + + it('should set table defaults', () => { + const tableDefaults = { + name: 'smartapp', + hashKey: 'id', + sortKey: null, + prefix: 'ctx:' + } + store = new DynamoDBContextStore({autoCreate: false}) + expect(store.table).to.include(tableDefaults) + }) + + it('should allow overriding DynamoDB Client', () => { + const testDynamoClient = new AWS.DynamoDB() + store = new DynamoDBContextStore({client: testDynamoClient, autoCreate: false}) + expect(store.client).to.deep.equal(testDynamoClient) + + store = new DynamoDBContextStore({autoCreate: false}) + expect(store.client).to.not.deep.equal(testDynamoClient) + }) +})