Using Azure DocumentDB with Cordova Tools in Visual Studio (or any JS client)

Abstract

A few weeks ago, I set on a mission to learn more about Azure DocumentDB. My end goal was to use Azure DocumentDB with a JavaScript client application specifically with a Cordova app built with the new Tools for Apache Cordova or TACO which the team introduced a while ago.

Background

In April 2015, Microsoft officially released DocumentDB, its cloud, document oriented, NoSQL database (It was in preview since 2014). Azure DocumentDB, a new player in the NoSQL market, is built to work within the Azure Cloud ecosystem much like SQL Azure, SQL Storage, Azure search, etc.

You would use DocumentDB when you need fast deployment, low cost, superior scalability, higher availability and a powerful query engine with atomic transactions (not just simple reads and writes). Therefore, it is a great candidate for storing your next mobile application data.

Getting Started

If you look at the DocumentDB documentation, there are many tutorials and videos on how to get started. Creating an Azure DocumentDB service is trivial, all it requires is your patience the second you hit the “create” button. Once the service is running, you can proceed by creating your first database (I called mine the silly “mydb”). After you create the database, you will need to create at least one collection to hold your data. Then of course, you will be intrigued to add a few documents as well, which you can generate using a JSON generator.

1- Create service

Service is Running, Now What?

Now it’s time to read the data that we just stored in the collection. Therefore, we need to code our client application. At the time of writing, Azure DocumentDB officially supports the following languages:

  • .NET
  • NodeJS
  • JavaScript
  • Python
  • Java

Since Cordova applications are built using standard web technologies, it makes perfect sense to use the JavaScript Client SDK. Looking at the documentation portal, there is no mention of any JavaScript client tutorial (hmm).

There is another JSDoc site, which looks very much “unfinished”. It does however, give us some indications on how to get started. It clearly states that we should be initializing our client using the following:

    var host = (hostendpoint); 
    var resourceToken = {};
    resourceTokens[(collection._rid)] = (resourceToken); // Add the collection _rid (NOT the Collection Id but the internal resource id) and resourceToken for read/write on the collection

    var collectionUrl = (collectionUrl); // Add the collection self-link
    var client = DocumentDB.createClient(host, {
        resourceTokens: resourceTokens
    });

Resource Tokens? You must be kidding me! Why not the master key?

Trying to be Smart

At that moment, I thought to myself, well, I have the master key, why can’t I just use it to initialize my client. A few seconds later, I was greeted with this error message:

Master Key error

It must have been silly of me to even try. I mean, I plainly read this on the FAQ page:

A master key is a security token to access all resources for an account. Individuals with the key have read and write access to the all resources in the database account. Use caution when distributing master keys. 

Get me my Resource Tokens

To understand the usefulness of Resource Tokens vs Master Key, I looked at this article. In essence:

The master key token is the all access key token that allows individuals to have full control of DocumentDB resources in a particular account.

Resource tokens are created when users in a database are set up with access permissions for precise access control on a resource, also known as a permission resource.

To generate those Resource Tokens (permissions), you will need a Permission Server. There is a NodeJS application that connects to DocumentDB and generates tokens. It can be found on Github. Let’s set it up.

6- run server

NOTE: This is a sample that uses a single user and a single permission object. If you wish to have multiple users, each with their own permissions, then adjust accordingly.

Yey, It’s Up and Running!

Within a few minutes, I had the Permission Server up and running. I then started coding my client application using the client SDK and the generated resource token.

var host = ("xxxxx");
var collection;
var collectionId = "xxxx";
var databaseId = "mydb";
var resourceTokens = {};
var client;

function getClient() {
    var dfr = $.Deferred();

    if (client) {
        dfr.resolve(client);
    } else if (!resourceTokens[collectionId]) {
        getResourceTokens()
            .then(function (data) {
                resourceTokens[collectionId] = data;
                dfr.resolve(createClient());
            })
            .fail(function () {
                dfr.reject();
            });
    } else {
        dfr.resolve(createClient());
    }

    return dfr.promise();
}

function createClient() {
    client = DocumentDB.createClient(host, {
        resourceTokens: resourceTokens
    });

    return client;
}

function getResourceTokens() {
    var dfr = $.Deferred();

    $.ajax({
            url: "(permission server url)",
            crossDomain: true
        })
        .done(function (data) {
            var tokenData = JSON.parse(data);
            collection = tokenData.collection;
            dfr.resolve(tokenData.token);
        })
        .fail(function () {
            dfr.reject();
        });

    return dfr.promise();
}

The DocumentDB client object was getting resolved (Excitement). Let’s get the list of collections associated with our database:

//Get collections
getClient().then(function () {
    var colQuery = 'SELECT * FROM root r WHERE r.id="' + collectionId + '"';
    client.queryCollections(databaseId, colQuery).toArray(function (err, result) {
        console.log(err, result);
    });

});

Oh No! CORS is not…

You read this right. Although CORS is supported by other Azure Services like Storage, it is not supported by DocumentDB (at the time of writing). This means that you are unable to make requests to DocumentDB from a client application if it’s hosted on a different domain (which is like 99.99% of the time). You can help by voting for CORS support.

Note: You may be able to work around the issue by creating a proxy, but that’s not the purpose of this article

An API Layer for the Rescue

Since it’s still impractical to use the JavaScript SDK, I decided to take another route and build my API layer using the NodeJS SDK. This way I can expose my collections through different routes and I have complete control over how I am exposing data to my Cordova application. If NodeJS is not your cup of tea, you can achieve the same thing using .NET or Java.

First I add my server.js. Notice how I explicitly set my headers.

var express = require('express'),
  employees = require('./routes/employees');

var app = express();

var enableCors = function (req, res, next) {
  // add needed headers
  res.setHeader("Access-Control-Allow-Origin", "*")
  res.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS")
  res.setHeader("Access-Control-Allow-Credentials", true);
  res.setHeader("Access-Control-Max-Age", "86400"); // 24 hours
  res.setHeader("Access-Control-Allow-Headers",
    "X-Requested-With, Access-Control-Allow-Origin, X-HTTP-Method-Override, Content-Type, Authorization, Accept");

  return next();
};

 app.use(enableCors);


app.get('/employees', employees.findAll);
app.get('/employees/:id', employees.findById);

app.listen(3000);
console.log('Listening on port 3000...');

Then I add my routes:

var dbUtil = require('../utils/db');

exports.findAll = function (req, res) {
    dbUtil.readOrCreateDatabase(function (database) {
        dbUtil.readOrCreateCollection(database, function (collection) {
            dbUtil.listItems(collection, function (items) {

                var response = {
                    title: 'Company',
                    employees: items
                };

                res.send(JSON.stringify(response));

            });
        });
    });
};

exports.findById = function (req, res) {
    var itemId = req.params.id;
    dbUtil.readOrCreateDatabase(function (database) {
        dbUtil.readOrCreateCollection(database, function (collection) {
            dbUtil.getItem(collection, itemId, function (item) {
                res.send(item);
            });
        });
    });
};

And most importantly, my DocumentDB access module:

var DocumentDBClient = require("documentdb").DocumentClient,
    DocumentBase = require("documentdb").DocumentBase,
    nconf = require('nconf');

// set the config file for the environment
nconf.env();
nconf.file({
    file: './config.json'
});

var host = nconf.get("HOST");
var authKey = nconf.get("AUTH_KEY");
var databaseId = nconf.get("DATABASE");
var collectionId = nconf.get("COLLECTION");

// create an instance of the DocumentDB client
exports.client = new DocumentDBClient(host, {
    masterKey: authKey
});

// get item
exports.getItem = function (collection, itemId, callback) {
    this.client.queryDocuments(collection._self, 'SELECT * FROM root r WHERE r.id="' + itemId + '"').toArray(function (err, results) {
        if (err) {
            throw (err);
        }

        callback(results[0]);
    });
};

// create new item
exports.createItem = function (collection, documentDefinition, callback) {
    documentDefinition.completed = false;
    this.client.createDocument(collection._self, documentDefinition, function (err, doc) {
        if (err) {
            throw (err);
        }

        callback();
    });
};

// update the provided item
exports.updateItem = function (collection, itemId, callback) {
    //first fetch the document based on the id
    this.getItem(collection, itemId, function (doc) {
        this.client.replaceDocument(doc._self, doc, function (err, replacedDoc) {
            if (err) {
                throw (err);
            }

            callback();
        });
    });
};

// query the provided collection
exports.listItems = function (collection, callback) {
    this.client.queryDocuments(collection._self, 'SELECT * FROM root r').toArray(function (err, docs) {
        if (err) {
            throw (err);
        }

        callback(docs);
    });
};

// if the database does not exist, then create it, else return the database object
exports.readOrCreateDatabase = function (callback) {
    this.client.queryDatabases('SELECT * FROM root r WHERE r.id="' + databaseId + '"').toArray(function (err, results) {
        if (err) {
            // some error occured, rethrow up
            throw (err);
        }
        if (!err && results.length === 0) {
            // no error occured, but there were no results returned
            // indicating no database exists matching the query
            this.client.createDatabase({
                id: databaseId
            }, function (err, createdDatabase) {
                callback(createdDatabase);
            });
        } else {
            // we found a database
            callback(results[0]);
        }
    });
};

// if the collection does not exist for the database provided, create it, else return the collection object
exports.readOrCreateCollection = function (database, callback) {
    this.client.queryCollections(database._self, 'SELECT * FROM root r WHERE r.id="' + collectionId + '"').toArray(function (err, results) {
        if (err) {
            // some error occured, rethrow up
            throw (err);
        }
        if (!err && results.length === 0) {
            // no error occured, but there were no results returned
            //indicating no collection exists in the provided database matching the query
            this.client.createCollection(database._self, {
                id: collectionId
            }, function (err, createdCollection) {
                callback(createdCollection);
            });
        } else {
            // we found a collection
            callback(results[0]);
        }
    });
};

Note: my DocumentDB configuration is in my config.json which looks as follows:

{
    "HOST": "xxxx",
    "AUTH_KEY": "xxxx",
    "DATABASE": "mydb",
    "COLLECTION": "employees"
}

And the result is as you’d expect:

JSON data

Conclusion

Azure DocumentDB stands tall among its competitors in the NoSQL marketplace. Unfortunately, its support for JavaScript client applications is still inadequate. I am confident that in the near future it will be drastically improved. Meanwhile, we can confidently rely on the server SDKs such as NodeJS and expose data to our client applications any way we want. Happy Halloween Geeks!

1 Comment
  • Joseph Ghassan
    December 5, 2015

    Good one George

Leave a Reply

Your email address will not be published. Required fields are marked *