Sharing Our Passion for Technology
& Continuous Learning
Node Reference - Put Product
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{:target="_blank"} so that we can intercept node require{:target="_blank"} calls and replace them by returning a mock. We are using the aws-sdk{:target="_blank"} to access DynamoDB. We also need a way to generate unique Ids. shortid{:target="_blank"} 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"{:target="_blank"}! Feel free to lookup the AWS DocumentClient{:target="_blank"} and Koa context{:target="_blank"} 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{:target="_blank"}. 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{:target="_blank}.
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.