25 Days of Serverless – Day 4

Build an HTTP API that lets Ezra’s friends add food dishes they want to bring to the potluck, change or remove them if they need to (plans change!), and see a list of what everybody’s committed to bring.

https://25daysofserverless.com/calendar/4

Despite being behind by a day right now, I’d really like to not do what I know (C#) for creating APIs. I found a tutorial for CosmosDB and NodeJS applications… so down the rabbit hole we go. I’m going to follow this Microsoft tutorial – https://docs.microsoft.com/en-us/azure/cosmos-db/sql-api-nodejs-application – and share my notes here.

Azure Infrastructure

For this example, I will have a NodeJS web application and a Cosmos DB instance. There are free tiers of both, but getting to the free CosmosDB instance is a bit more difficult. You’ll want to follow this link to get a free 30 day CosmosDB instance setup: https://azure.microsoft.com/try/cosmosdb/

We’ll be deploying code directly from GitHub into Azure again, so we can start deploying our web application now. You’ll want your local NodeJS version to ensure the code you test locally is running in a similar environment in Azure.

Check Your Node version from a terminal
Select the same version when creating a web app instance

CosmosDB

After establishing a NodeJS web application locally through the express generator, connecting to Cosmos is simpler than I expected. By running the following command, you are installing the Azure Cosmos JS package to streamline connecting the app to Cosmos DB.

npm install @azure/cosmos

The linked tutorial is creating a task list web application… which is similar to a potluck list. The query on line 13 of potluckList.js (tasklist.js in the tutorial) specifies that only items from the database where the completed property is false will be returned to the UI. Generally speaking, I completed the tutorial for a “task list” and after I had a working example went back to edit from tasks to potluck items.

const PotluckItems = require("../models/PotluckItems");

class TaskList {
    /**
     * Handles the various APIs for displaying and managing potluck items
     * @param {PotluckItems} potluckItems
     */
    constructor(potluckItems) {
        this.potluckItems = potluckItems;
    }
    async showTasks(req, res) {
        const querySpec = {
            query: "SELECT * FROM root r WHERE r.completed=@completed",
            parameters: [
                {
                    name: "@completed",
                    value: false
                }
            ]
        };

By the time you’ve worked through the tutorial to creating the config.js file, your (free) CosmosDB instance should be ready. If you’ve already initialized a git repository, make sure you don’t commit this file with your CosmosDB keys in it. Temporarily add it to your .gitignore to keep it from tracking while you test locally.

User Interface (jade)

Building a user interface is not my strength, especially in jade. Fortunately, the tutorial took care of setting up the views. After I ran the app locally and confirmed the view loaded as a task list, I set about converting the whole app to a potluck list. It may take a moment for the CosmosDB connection to initialize when you test locally, give it a minute before loading the app in the browser.

index.jade became:

extends layout
block content
    h1 #{title}
    br

    form(action="/completetask" method="post")
        table.table.table-striped.table-bordered
            tr
                td Name
                td Category
                td Remove
            if (typeof tasks === "undefined")
                tr
                    td
            else
                each task in tasks
                    tr
                        td #{task.name}
                        td #{task.category}
                        td
                            if(task.completed) 
                                input(type="checkbox" name="#{task.id}" value="#{!task.completed}", checked=task.completed)
                            else
                                input(type="checkbox" name="#{task.id}" value="#{!task.completed}", checked=task.completed)
    div.container
        button.btn.btn-warning(type="submit" style="float:right") Remove Checked
    hr
    form.well(action="/addtask" method="post")
        div.form-group
            label Potluck Item:
            input.form-control(name="name" type="textbox")
        div.form-group
            label Category:
            input.form-control(name="category" type="textbox")
        button.btn.btn-success.btn-sm(type="submit") Add to Potluck

Issues

Fortunately, I only have 1 hiccup when building this example – and a little Azure portal sleuthing went a long way.

I could add and view potluck items, but when I attempted to remove them – I received an API error. With a few extra debug statements I found that await this.container.item(itemId, partitionKey).read() on line 74 of potluckItems.js was not returning the item (instead returning our old friend undefined).

// @ts-check
const CosmosClient = require('@azure/cosmos').CosmosClient
const debug = require('debug')('todo:taskDao')

// For simplicity we'll set a constant partition key
const partitionKey = undefined // used to be '0'
class PotluckItems {
    /**
     * Manages reading, adding, and updating Tasks in Cosmos DB
     * @param {CosmosClient} cosmosClient
     * @param {string} databaseId
     * @param {string} containerId
     */
    constructor(cosmosClient, databaseId, containerId) {
        this.client = cosmosClient
        this.databaseId = databaseId
        this.collectionId = containerId

        this.database = null
        this.container = null
    }

    async init() {
        debug('Setting up the database...')
        const dbResponse = await this.client.databases.createIfNotExists({
            id: this.databaseId
        })
        this.database = dbResponse.database
        debug('Setting up the database...done!')
        debug('Setting up the container...')
        const coResponse = await this.database.containers.createIfNotExists({
            id: this.collectionId
        })
        this.container = coResponse.container
        debug('Setting up the container...done!')
    }

    async find(querySpec) {
        debug('Querying for items from the database')
        if (!this.container) {
            throw new Error('Collection is not initialized.')
        }
        const { resources } = await this.container.items.query(querySpec).fetchAll()
        return resources
    }

    async addItem(item) {
        debug('Adding an item to the database')
        item.date = Date.now()
        item.completed = false
        const { resource: doc } = await this.container.items.create(item)
        return doc
    }

    async updateItem(itemId) {
        debug('Update an item in the database')
        debug(itemId);
        const doc = await this.getItem(itemId)
        debug("got this item");
        debug(doc);
        doc.completed = true

        const { resource: replaced } = await this.container
            .item(itemId, partitionKey)
            .replace(doc)
        return replaced
    }

    async getItem(itemId) {
        debug('Getting an item from the database')
        debug('getting item from database');
        debug(itemId);
        debug(partitionKey);
        const { resource } = await this.container.item(itemId, partitionKey).read()
        return resource
    }
}

module.exports = PotluckItems

Going right to the source to find the data was my first step – and CosmosDB makes this quick with the Data Explorer in the Azure portal.

Initially, the JS model had the partition key set to ‘0’ on line 6. By changing this to undefined, the item(id, partionkey) function is able to access our data.

(yes, I will be submitting a PR for the Microsoft tutorial on this)

Deploy

Once the working web application is pushed to the GitHub repository, our web app in Azure will build the code and deploy it. Earlier we made a point to not commit our config.js file as it contained/contains CosmosDB secrets.

Config.JS when testing locally

After sanitizing the config file we should push it to our repository – but we also need to put those values for HOST and AUTH_KEY into our Azure App Service settings.

process.env.HOST and process.env.AUTH_KEY

My potluck list manager is now available here (until the credits run out): https://25daysofserverlessday4.azurewebsites.net/

The code is available here: https://github.com/dzsquared/25-days-of-serverless-day4