MEAN Stack – Avoid callback hell with RxJS

What is RxJS?

RxJS is a JavaScript library for reactive programming using Observables to simplify composing asynchronous code. Roughly put, the library allows subscribing to a stream of values (Observable) that change overtime and execute methods based on the changes in value.

What is callback hell?

Simply put, callback hell is the result of nesting too many success and error callback methods in asynchronous JavaScript code. To learn more, please read http://callbackhell.com/

What about Promises?

Promises alleviate the pain of using callbacks by chaining function calls to asynchronous methods.  However, when one method depends on execution of multiple asynchronous methods, nesting method calls is inevitable.

Enough with the 101, let’s see a use case:

We have an event registration form that allows users to register to my online event. The registration form contains the following fields: FirstName, LastName, EmailAddress,Country and Office

Requirement #1 – Users cannot register twice. User email address must be unique.

import * as express from "express";
import * as HttpStatus from "http-status-codes";
import * as mongodb from "mongodb";

function registerUser(req: express.Request, res: express.Response, next: express.NextFunction) {
  let user = req.body;
  let onError = (error) => { handleError(error, res) };
  findUser(user, res, insertUser, onError);
}

function findUser(user: any, res: express.Response, onFindSuccess: Function, onFindError: Function) {
  mongodb.MongoClient.connect("mongodb://localhost:27017/events").then(db => {
    db.collection("users").findOne({ emailAddress: user.emailAddress }).then(user => {
      if (user) {
        onFindSuccess(user, res, onFindError);
      }
      else {
        onFindError("Email already exists")
      }
    }).catch(error => onFindError(error));
  }).catch(error => onFindError(error));
}
function insertUser(user: any, res: express.Response, onInsertError: Function) {
  mongodb.MongoClient.connect("mongodb://localhost:27017/events").then(db => {
    db.collection("users")
      .insert(user)
      .then(user => {
        res.status(HttpStatus.CREATED).send("Registration Created");
      })
      .catch(error => onInsertError(error, res));
  }).catch(error => onInsertError(error, res));
}

function handleError(error: any, res: any) {
  res.status(HttpStatus.INTERNAL_SERVER_ERROR).send("Server Error");
}
Requirement #2 – Okay, that was not so bad. Now, how about also validating that countryId and officeId are also valid.
import * as express from "express";
import * as HttpStatus from "http-status-codes";
import * as mongodb from "mongodb";
function registerUser(req: express.Request, res: express.Response, next: express.NextFunction) {
  let user = req.body;
  let onError = (error) => { handleError(error, res) };
  findOffice(user, res, onError);
}
function findOffice(user: any, res: express.Response, onFindError: Function) {
  mongodb.MongoClient.connect("mongodb://localhost:27017/events").then(db => {
    db.collection("offices").findOne({ _id: new mongodb.ObjectID(user.officeId) })
    .then(office => {
      if (office) {
        findCountry(user, res, onFindError);
      }
      else {
        onFindError("Bad Request");
      }
    }).catch(error => onFindError(error));
  }).catch(error => onFindError(error));
}
function findCountry(user: any, res: express.Response, onFindError: Function) {
  mongodb.MongoClient.connect("mongodb://localhost:27017/events").then(db => {
      db.collection("countries").findOne({ _id: new mongodb.ObjectID(user.countryId) })
        .then(office => {
          if (office) {
            findUser(user, res, onFindError);
          }
          else {
            onFindError("Bad Request");
          }
        }).catch(error => onFindError(error));
  }).catch(error => onFindError(error));
}
function findUser(user: any, res: express.Response, onFindError: Function) {
  mongodb.MongoClient.connect("mongodb://localhost:27017/events").then(db => {
    db.collection("users")
      .findOne({ emailAddress: user.emailAddress })
      .then(user => {
        if (user) {
          insertUser(user, res, onFindError);
        }
        else {
          onFindError("Email already exists")
        }
      }).catch(error => onFindError(error));
  }).catch(error => onFindError(error));
}
function insertUser(user: any, res: express.Response, onInsertError: Function) {
  mongodb.MongoClient.connect("mongodb://localhost:27017/events").then(db => {
    db.collection("users")
      .insert(user)
      .then(user => {
        res.status(HttpStatus.CREATED).send("Registration Created");
      })
      .catch(error => onInsertError(error, res));
  }).catch(error => onInsertError(error, res));
}
function handleError(error: any, res: any) {
  res.status(HttpStatus.INTERNAL_SERVER_ERROR).send("Server Error");
}

You can see that things are starting to get a little insane.. What if I need to add one more validation? This type of code can become increasingly entangled with each modification.

Let’s try this again with RxJS:

Requirement #1 – Users cannot register twice. User email address must be unique.

import * as express from "express";
import * as HttpStatus from "http-status-codes";
import * as mongodb from "mongodb";
import * as rx from "rxjs";

function registerUser(req: express.Request, res: express.Response, next: express.NextFunction) {
  let user = req.body;
  findUser(user).subscribe(success => {
    insertUser(user).subscribe(success => {
         res.status(HttpStatus.OK).send("Request Created");
      }, error => {
         res.status(HttpStatus.INTERNAL_SERVER_ERROR).send("Server Error");
      });
    }, error => {
       res.status(HttpStatus.INTERNAL_SERVER_ERROR).send("Server Error");
    });
}

function insertUser(user: any) {
  return rx.Observable.create((observer: rx.Observer<any>) => {
    mongodb.MongoClient.connect("mongodb://localhost:27017/events")
      .then((db: mongodb.Db) => {
        db.collection("users").insert(user)
          .then(result => {
            observer.next(result);
            observer.complete();
          }).catch((error) => onError(observer, error));
        }).catch((error) => onError(observer, error));
    });
}

function findUser(user: any): rx.Observable<any> {
  return rx.Observable.create((observer: rx.Observer<any>) => {
    mongodb.MongoClient.connect("mongodb://localhost:27017/events")
      .then((db: mongodb.Db) => {
        db.collection("users").findOne({ emailAddress: user.emailAddress })
          .then(result => {
            observer.next(result);
            observer.complete();
          }).catch((error) => onError(observer, error));
      }).catch((error) => onError(observer, error));
});
}

function onError(observer: rx.Observer<any>, error: any) {
  observer.error(error);
  observer.complete();
}

function handleError(error: any, res: any) {
  res.status(HttpStatus.INTERNAL_SERVER_ERROR).send("Server Error");
}

Requirement #2 – Now, how about also validating that countryId and officeId are also valid. This is where RxJS really shines.

import * as express from "express";
import * as HttpStatus from "http-status-codes";
import * as mongodb from "mongodb";
import * as rx from "rxjs";

function registerUser(req: express.Request, res: express.Response, next: express.NextFunction) {
  let user = req.body;
  let countryValidations = validateCountry(user.countryId);
  let officeValidations = validateOffice(user.officeId);
  let userValidations = findUser(user);

  // Validate all my rules
  let validations = rx.Observable.forkJoin(countryValidations, officeValidations, userValidations);

  // If all validations pass
  validations.filter(results => results.every(valid => valid))
    .subscribe(
      results => {
        insertUser(user).subscribe(
          result => res.status(HttpStatus.CREATED).send("Registration Created"),
          error => handleError(error, res)
        );
      },
      error => handleError(error, res)
  );

  // If any validations fail
  validations.filter(results => results.some(valid => !valid))
  .subscribe(results => {
    res.status(HttpStatus.BAD_REQUEST).send("Bad Request");
  });
}

function validateOffice(officeId: any): rx.Observable<boolean> {
  return rx.Observable.create((observer: rx.Observer<any>) => {
    mongodb.MongoClient.connect("mongodb://localhost:27017/events")
      .then((db: mongodb.Db) => {
        db.collection("offices").findOne({ _id: new mongodb.ObjectID(officeId) })
          .then(result => {
          observer.next(result);
          observer.complete();
        }).catch((error) => onError(observer, error));
    }).catch((error) => onError(observer, error));
  });
}

function validateCountry(countryId: any): rx.Observable<boolean> {
  return rx.Observable.create((observer: rx.Observer<any>) => {
    mongodb.MongoClient.connect("mongodb://localhost:27017/events")
      .then((db: mongodb.Db) => {
        db.collection("countries").findOne({ _id: new mongodb.ObjectID(countryId) })
          .then(result => {
            observer.next(result);
            observer.complete();
      }).catch((error) => onError(observer, error));
    }).catch((error) => onError(observer, error));
  });
}

function insertUser(user: any) {
  return rx.Observable.create((observer: rx.Observer<any>) => {
    mongodb.MongoClient.connect("mongodb://localhost:27017/events")
      .then((db: mongodb.Db) => {
        db.collection("users").insert(user)
          .then(result => {
             observer.next(result);
             observer.complete();
       }).catch((error) => onError(observer, error));
     }).catch((error) => onError(observer, error));
  });
}

function findUser(user: any): rx.Observable<any> {
  return rx.Observable.create((observer: rx.Observer<any>) => {
    mongodb.MongoClient.connect("mongodb://localhost:27017/events")
      .then((db: mongodb.Db) => {
        db.collection("users").findOne({ emailAddress: user.emailAddress })
          .then(result => {
            observer.next(result);
            observer.complete();
          }).catch((error) => onError(observer, error));
     }).catch((error) => onError(observer, error));
  });
}

function onError(observer: rx.Observer<any>, error: any) {
  observer.error(error);
  observer.complete();
}

function handleError(error: any, res: any) {
  res.status(HttpStatus.INTERNAL_SERVER_ERROR).send("Server Error");
}

By utilizing RxJS, we were able to decouple our validation methods so that each validation can happen independently and concurrently. We also made it possible to add or remove any validations in the future. Additionally, we can filter all validations so that the user is registered only when all validations fail, and alternatively we can return an error as soon as the first validation fails.

References:

RxJS – https://github.com/Reactive-Extensions/RxJS
JavasScript Callback Hell – http://callbackhell.com/
JavaScript Promises – https://developers.google.com/web/fundamentals/getting-started/primers/promises

Rapid Prototype – Restaurant Management Mobile App

In the modern age, accepting technology can allow us to innovate even the oldest industries. The restaurant industry is one such industry. It’s possible to have a successful restaurant business without using any technology, there is always a room to increase revenue or decrease expenses and optimize operational costs. These areas can range from efficient staffing; inventory management; adoption of point of sale devices; customer engagement; floor and kitchen efficiency; etc.

As a challenge to myself, I began rapidly prototyping a mobile app with a time limit of two days. And although speed of development was a main constraint, the application should try not sacrifice usability and feasibility. Finally, more importantly, the prototype should provide value to the industry by: 1. Improving the speed of receiving an order from the menu, and communicating the details of the order to the kitchen. 2. Integrate with existing billing process (POS) and provide insights into the performance of the restaurant menu.

orders
Waiters can take orders quickly with onscreen keyboard and product codes
kitchen
Kitchen is immediately notified when an order is placed
admin
Restaurant managers can see metrics on their top products and sales by week

The source code of the application is available on Github.
A live demo is uploaded to Heroku

 

Nom3 – Look Up and Discover Recipes by Selecting Ingredients

Quite often I find myself on a website featuring tasty recipes. However, being lazy, I simply refuse to do an ingredient run to make sure I have everything ready and prepared in order to make lunch or dinner. If I’m hungry, it’s time to cook, with anything that is available in the house. The website Nom3 (short for NomNomNom) is dedicated to people lazy like me who would rather pick from recipes from whatever ingredients are available in the kitchen rather than spend the time extensively shopping before cooking.

Go forth and search for whatever recipes that you can cook with the ingredients you already have in your kitchen. You might even discover something new!

nom3

Disclaimer… I cannot cook, and my wife does all of the cooking.  Thanks Wifey! ♥

References:
Github Source Code – https://github.com/saichaitanya88/nomnomnom
Live Site – http://nom3.saichaitanya.ca/#/

Secure Chat with SocketIO, NodeJS and AES Encryption

This application leverages SocketIO, NodeJS and AES Encryption in order to provide a secure chatting experience. The idea is to encrypt the chat messages before the message is sent over the wire. The application allows people to invite others to a chat room by providing the inviter and invitees’ email addresses; the application creates a unique link for a chat room and generates the chat key and emails it to the participants. It is up to the participants to visit the URL pointing to the chat room and enter the provided chat key. One feature of security here is that the chat key is never broadcasted across the wire since it’s copy pasted by the participants to send and receive encrypted messages.

The application source code is located at – https://github.com/saichaitanya88/secure_chat/

Try it out for yourself at https://sai-secure-chat.herokuapp.com

 

secure chat app flow
Secure Chat – Demo

If you wish to fork the GitHub code, one thing to note is that the NodeJS application requires the Gmail SMTP email and password to be provided when launched. This can be done so like this –

smtp_email=email@gmail.com smtp_pass=password supervisor app.js

The forked code can be deployed in seconds to Heroku by integrating with GitHub!

Live Demo – sai-secure-chat.herokuapp.com
Source Code – https://github.com/saichaitanya88/secure_chat/

An Approach to Multi-Tenant Applications with MongoDB

NoSQL databases like MongoDB allow the ability to scale easily, develop applications faster by supporting flexible schema. However, due to the flexible schema, it’s not the first choice database for most people. I believe that MongoDB can be used innovatively or creatively in order to build multi-tenant applications. This can be accomplished by handling the client metadata in one database, while storing the client specific data in separate databases. Let us explore one such approach below where the application allows users to define their own schema and perform CRUD operations on their schema.

In the application architecture described in Figure 1, we see that if the application is split into one main database to store the metadata for the application users, it’s possible to store the client data in their own separate databases, this allows for us to scale a particular client’s database on demand. For most small companies that provide software as a service, I would speculate that it’s common for them to find themselves with one or two large clients that demand for the architectural changes that benefit them, while affecting the other clients negatively (in poorly designed systems, anyway). The primary advantage in storing the database separately, is that while scaling individual client databases, it’s also possible for us to scale the application by spinning up new instances of the servers, but that’s out of the scope of this article.

app-architecture
Figure1: Multi-Tenant Architecture with Multiple Databases

Storing Client Account Information:

Let’s set up the main database (System DB) to store the account information. The account information contains one user login for the account along with the data schema.

{
    "_id" : ObjectId("557126a6edd785072443982c"),
    "email" : "saichaitanya88@gmail.com",
    "password" : "<password>",
    "lastName" : "lastname",
    "firstName" : "firstname",
    "podName" : "v2gncto",
    "createdAt" : ISODate("2015-06-05T04:33:42.191Z"),
    "updatedAt" : ISODate("2015-06-06T01:44:03.761Z"),
    "otherInfo" : {}
}

The parameter podName contains the database name that contains the client data, the podName parameter can be extended to store more database connection string.

Storing Client Database Schema:

Client specific schema is stored into the CustomObjects collection:

{
    "_id" : ObjectId("558a021a0232718a087ed237"),
    "name" : "Person",
    "customObjectName" : "person",
    "description" : "Person customObject",
    "accountId" : ObjectId("557126a6edd785072443982c"),
    "createdAt" : ISODate("2015-06-24T01:04:26.393Z"),
    "updatedAt" : ISODate("2015-06-24T02:03:50.203Z"),
    "createdBy" : ObjectId("557126a6edd785072443982c"),
    "updatedBy" : ObjectId("557126a6edd785072443982c"),
    "modelDefinition" : [
        {
            "_id" : ObjectId("558a021a0232718a087ed238"),
            "name" : "Created At",
            "fieldName" : "createdAt",
            "description" : "Created At",
            "type" : "Date",
            "scope" : "System",
            "createdAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "updatedAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "createdBy" : ObjectId("557126a6edd785072443982c"),
            "updatedBy" : ObjectId("557126a6edd785072443982c")
        },
        {
            "_id" : ObjectId("558a021a0232718a087ed239"),
            "name" : "Updated At",
            "fieldName" : "updatedAt",
            "description" : "Updated At",
            "type" : "Date",
            "scope" : "System",
            "createdAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "updatedAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "createdBy" : ObjectId("557126a6edd785072443982c"),
            "updatedBy" : ObjectId("557126a6edd785072443982c")
        },
        {
            "_id" : ObjectId("558a021a0232718a087ed23a"),
            "name" : "Created By",
            "fieldName" : "createdBy",
            "description" : "Created By",
            "type" : "ObjectId",
            "scope" : "System",
            "createdAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "updatedAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "createdBy" : ObjectId("557126a6edd785072443982c"),
            "updatedBy" : ObjectId("557126a6edd785072443982c")
        },
        {
            "_id" : ObjectId("558a021a0232718a087ed23b"),
            "name" : "Updated By",
            "fieldName" : "updatedBy",
            "description" : "Updated By",
            "type" : "ObjectId",
            "scope" : "System",
            "createdAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "updatedAt" : ISODate("2015-06-24T01:04:26.393Z"),
            "createdBy" : ObjectId("557126a6edd785072443982c"),
            "updatedBy" : ObjectId("557126a6edd785072443982c")
        },
        {
            "_id" : ObjectId("558a1006e42e219619f5b495"),
            "name" : "DOB",
            "fieldName" : "dOB",
            "description" : "Date Of Birth",
            "type" : "Date",
            "scope" : "Application",
            "createdAt" : ISODate("2015-06-24T02:03:50.203Z"),
            "updatedAt" : ISODate("2015-06-24T02:03:50.203Z"),
            "createdBy" : ObjectId("557126a6edd785072443982c"),
            "updatedBy" : ObjectId("557126a6edd785072443982c")
        }
    ]
}

Now, with the schema describing the CustomObjects collection, their fields and data types, we can build a persistence layer to make sure that the data inserted into the client database complies with the schema defined in the CustomObjects collection. (Persistence layer source code)

Client Custom Data:

With these in place, we can build CRUD actions for any conceivable CustomObject collection that the user can define. The details of this is located in the source code. An example Person record according to the schema defined above:

{
    "_id" : ObjectId("558cada3a1899f635d7d3190"),
    "dOB" : ISODate("2015-06-25T03:15:32.229Z"),
    "createdAt" : ISODate("2015-06-26T01:40:51.394Z"),
    "updatedAt" : ISODate("2015-06-28T17:03:14.143Z"),
    "createdBy" : ObjectId("557126a6edd785072443982c"),
    "updatedBy" : ObjectId("557126a6edd785072443982c")
}

Additional Benefits:

While it’s possible for the users to define their own data schema for CustomObjects, it’s also possible for us to provide a single user interface that allows the users to query and modify the objects. The UI looks at the CustomObject schema, and renders the text boxes (for search and edit) based on the data type of the field.

Search UI
Figure 2: Auto-Generated Search UI
Save UI
Figure 3: Auto-Generated Edit UI

Limitations and Considerations:

One limitation in my preliminary work is that MongoDB did not support Collection Joins until version 3.2, which meant that schemas where multiple collections are linked and referenced may not perform well.

It’s also likely that other databases like RethinkDB, CouchDB, Cassandra might be a better fit for this type of application structure, the database that best fits the needs of the application must be chosen.

Future Work:

With client-side JavaScript becoming ever so popular, it’s possible for us to enhance the database schema and allow storage of JavaScript functions that run on the browser. Taking this one step further, we can even store alternative methods to accommodate similar functionality for modules used by the application clients. For example: Calculation of Income Taxes on wages differs from country to country, if each country is a client, then two separate JavaScript functions can be defined in the schema to allow the application to seamlessly use the appropriate Income Tax Calculation. Another example: Two clients (one car sales company, the other software company) wanting to measure employee performance, the car sales company measures performance based on number of sales made, while the software company measures performance based on the ratio of features to bugs per employee.

The schema can also be extended to contain custom Model validation rules allowing validation rules which depend on other collections to be implemented.

References:

MongoDB – https://www.mongodb.org/
NodeJS – https://nodejs.org/en/
Github Source Code – https://github.com/saichaitanya88/contact_manager.prototype