Prerequisites
This article builds on the prior article: Node Reference - DynamoDB.
Create put test/endpoint
First, lets write a spec for our POST /products
endpoint. Create a file called products/createProduct.spec.js
with the following contents:
const proxyquire = require('proxyquire');
describe('products', function () {
describe('createProduct', function () {
beforeEach(function () {
process.env.PRODUCTS_TABLE_NAME = 'Products';
this.product = {
name: 'widget',
imageURL: 'https://example.com/widget.jpg'
};
this.context = {
request: {
body: this.product
}
};
this.awsResult = {
promise: () => Promise.resolve()
};
const documentClient = this.documentClient = {
put: (params) => this.awsResult
};
spyOn(this.documentClient, 'put').and.callThrough();
this.createProduct = proxyquire('./createProduct', {
'aws-sdk': {
DynamoDB: {
DocumentClient: function() {
return documentClient;
}
}
}
});
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('should pass the correct TableName to documentClient.put', async function () {
await this.createProduct(this.context);
expect(this.documentClient.put.calls.argsFor(0)[0].TableName).toEqual('Products');
});
it('should pass the postedProduct to documentClient.put', async function () {
await this.createProduct(this.context);
expect(this.documentClient.put.calls.argsFor(0)[0].Item).toBe(this.product);
});
it('should set the product as the body', async function () {
await this.createProduct(this.context);
expect(this.context.body).toBe(this.product);
});
it('should populate an id on the product', async function () {
await this.createProduct(this.context);
expect(this.documentClient.put.calls.argsFor(0)[0].Item.id).toBeDefined();
});
it('should set the lastModified timestamp', async function () {
jasmine.clock().mockDate(new Date(Date.UTC(2018, 03, 05, 06, 07, 08, 100)));
await this.createProduct(this.context);
expect(this.documentClient.put.calls.argsFor(0)[0].Item.lastModified).toEqual('2018-04-05T06:07:08.100Z');
});
});
});
We will need to add a couple of libraries to your project. We are using Proxyquire so that we can intercept node require calls and replace them by returning a mock. We are using the aws-sdk to access DynamoDB. We also need a way to generate unique Ids. shortid is good for that. Install these packages by running the following command:
npm install --save-dev proxyquire
npm install --save aws-sdk shortid
Create a stub implementation as products/createProduct.js
with these contents:
module.exports = async function createProduct(ctx) {
}
Run npm test
and you should see a bunch of failures. Welcome to the “red” in “Red Green Refactor”! Feel free to lookup the AWS DocumentClient and Koa context documentation and implement the endpoint. Otherwise, replace the contents of products/createProduct.js
with this implementation:
const shortid = require('shortid');
const AWS = require('aws-sdk');
const documentClient = new AWS.DynamoDB.DocumentClient();
const productsTableName = process.env.PRODUCTS_TABLE_NAME;
module.exports = async function createProduct(ctx) {
const product = ctx.request.body;
product.id = shortid.generate();
product.lastModified = (new Date(Date.now())).toISOString();
await saveProduct(product);
ctx.body = product;
};
async function saveProduct(product) {
return await documentClient.put({
TableName: productsTableName,
Item: product
}).promise();
}
Finally, add this as a route inside our buildRouter()
function within server.js
to route the request:
function buildRouter() {
...
router.post('/products', require('./products/createProduct'));
...
}
Export the PRODUCTS_TABLE_NAME
, USER_POOL_ID
and AWS_REGION
environment variables and then start the application with npm start
.
export PRODUCTS_TABLE_NAME=$(aws cloudformation describe-stacks \
--stack-name ProductService-DEV \
--query 'Stacks[0].Outputs[?OutputKey==`ProductsTable`].OutputValue' \
--output text)
export USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name Cognito \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' \
--output text)
export AWS_REGION="us-east-1"
npm start
You should be able to post a sample payload to http://localhost:3000/products. First we need to get some credentials:
AUTH_NAME="theproducts"
USER_POOL_ID=$(aws cloudformation describe-stacks \
--stack-name Cognito \
--query 'Stacks[0].Outputs[?OutputKey==`UserPoolId`].OutputValue' \
--output text)
USER_POOL_CLIENT_ID=$(aws cognito-idp list-user-pool-clients \
--user-pool-id "$USER_POOL_ID" \
--max-results 1 \
--query 'UserPoolClients[0].ClientId' --output text)
CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client --user-pool-id "$USER_POOL_ID" --client-id "$USER_POOL_CLIENT_ID" --query 'UserPoolClient.ClientSecret' --output text)
BEARER_TOKEN=$(curl -s -X POST \
https://${USER_POOL_CLIENT_ID}:${CLIENT_SECRET}@${AUTH_NAME}.auth.us-east-1.amazoncognito.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d grant_type=client_credentials | \
python -c "import sys, json; print json.load(sys.stdin)['access_token']")
Now we can use these credentials to post to the endpoint. Remember to set the “Content-Type” header to “application/json”.
curl --request POST \
--verbose http://localhost:3000/products \
-H "Authorization: Bearer $BEARER_TOKEN" \
-H "Content-Type: application/json" \
--data '{"id": "", "imageURL": "https://example.com/widget2.jpg", "name": "widget2"}'
See the changes we made here.
Table of Contents
- Introduction
- Unit Testing
- Koa
- Docker
- Cloudformation
- CodePipeline
- Fargate
- Application Load Balancer
- HTTPS/DNS
- Cognito
- Authentication
- DynamoDB
- Put Product (this post)
- Validation
- Smoke Testing
- Monitoring
- List Products
- Get Product
- Patch Product
- History Tracking
- Delete
- Change Events
- Conclusion
If you have questions or feedback on this series, contact the authors at nodereference@sourceallies.com.