Sharing Our Passion for Technology
& continuous learning
〈  Back to Blog

Node Reference - Put Product

Teammates sitting together and looking at code

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

If you have questions or feedback on this series, contact the authors at nodereference@sourceallies.com.

〈  Back to Blog