Source Allies Logo

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{: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

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