Quantcast
Channel: MiamiCoder » NodeJS Tutorial
Viewing all articles
Browse latest Browse all 4

The Meeting Room Booking App Tutorial: Creating the Bookings API with Express and MongoDB

$
0
0

In this article we continue building the Meeting Room Booking app.  Our goal this time is to create the server-side modules that send the list of meeting room bookings to the mobile app. We’re building this API with Express and MongoDB.

The bookings list retrieval will happen frequently, as users will start their interaction with the app from the “My Bookings” Screen, where they are taken after a successful logon.

logon-to-my-bookings-1

Displaying Bookings Data in the Mobile App

Our first step will be to set up a screen that will display the user’s bookings.  Open the index.html file in the project’s www directory and insert the following code before the script sections at the end of the page’s body:

<div data-role="page" id="page-bookings">
    <div data-role="header" data-theme="c">
        <h1>Book It</h1>
    </div><!-- /header -->
    <div role="main" class="ui-content">
    </div><!-- /content -->
</div><!-- /page -->

When I first started designing this app, I planned to have a “main menu” Screen that would provide quick access to the its different features. This is still valid for users in the administrator role. However, for users who aren’t administrators, it’s more efficient to just take them to their bookings, which is what they are looking for.

Now we’re going to create the bookings list with a few mock bookings.  I like the approach of first using mock data when building my UIs because it gives me a good idea of a screen’s final look, without having to wire it to the local database or the server backend.

Let’s add an html list to the page like so:

<div data-role="page" id="page-bookings">
    <div data-role="header" data-theme="c">
        <h1>Book It</h1>
    </div><!-- /header -->
    <div role="main" class="ui-content">
        <ul id="bookings-list" data-role="listview">
            <li data-theme="d" data-role="list-divider">6/27/2015</li>
            <li>
                <div class="bi-list-item-secondary"><p>9:00 AM to 11:00 AM</p></div>
                <div class="bi-list-item-primary">HR Systems rollout</div>
            </li>
            <li>
                <div class="bi-list-item-secondary"><p>3:00 PM to 3:30 PM</p></div>
                <div class="bi-list-item-primary">Business Intelligence Training</div>
            </li>
            <li data-theme="d" data-role="list-divider">6/28/2015</li>
            <li>
                <div class="bi-list-item-secondary"><p>11:00 AM to 11:30 AM</p></div>
                <div class="bi-list-item-primary">Development team status check</div>
            </li>
            <li>
                <div class="bi-list-item-secondary"><p>2:00 PM to 4:30 PM</p></div>
                <div class="bi-list-item-primary">Information Security Awareness Training</div>
            </li>
        </ul>
    </div><!-- /content -->
</div><!-- /page -->

We’re decorating the list with the “listview” data-role attribute so the jQuery Mobile framework treats it as a jQuery Mobile list.  For each booking, we’re using an li element that contains a div displaying the booking’s time, and a second div for the booking’s description. We’re also using an li element to render the date. Decorating this element with the “list-divider” data-role attribute will produce a grouping of the bookings by date.

We are using the bi-list-item-secondary and bi-list-item-primary CSS classes to modify the look of the two div elements. Let’s jump to the app.css file and define these classes:

.bi-list-item-primary {
    white-space: nowrap;
    overflow: hidden;
    -ms-text-overflow: ellipsis;
    -o-text-overflow: ellipsis;
    text-overflow: ellipsis;
}
.bi-list-item-secondary {
    color:#662d91;
    white-space: nowrap;
    overflow: hidden;
    -ms-text-overflow: ellipsis;
    -o-text-overflow: ellipsis;
    text-overflow: ellipsis;
}

After these changes, the page will look like this:

bookings-list-scrn-1

Now that we have a good idea of how the bookings list is going to look, let’s focus on our primary goal for this article, creating the meeting room bookings API with Express and MongoDB. The API will allow the mobile app to download the bookings list for a particular user.

Creating a Mongoose Schema to Store User Sessions

We require people to register to use the app. However, we don’t want to annoy them asking them to log on every single time they run the app.

To remember who the user is after the first time they log on, we are going to use an authentication ticket exchange protocol where upon successful logon, the server sends the app a session id that the app will cache. When the app makes any subsequent requests to the server, it will send up the cached ticket so the server can determine which user the request belongs to.

auth-token-exch-1

On the server, after a successful log on, we will save a sessionId-userId pair to the database. As subsequent requests from the app arrive, we will use the sessionId in a request to lookup the corresponding sessionId-userId pair in the database. This is how we will map an incoming request to a userId and therefore know which user made the request.

To represent a sessionId-userId pair, we will use a Mongoose model that we will call UserSession. Let’s create the user-session.js file in the server/models directory:

directories-36

In the file, we will define the UserSession model with this code:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var UserSessionSchema = new Schema({
    sessionId: String,
    userId: String
});

module.exports = mongoose.model('UserSession', UserSessionSchema);

We will use the two model’s properties, sessionId and userId, to map a given session id to a given app user.

Creating a User Session in the Database

With the model in place, we can jump to the AccountController and create a session in the database after a successful logon.

First, we will inject a UserSession instance into the controller. Open the controller/account.js file and modify the Class’s constructor like this:

var AccountController = function (userModel, session, userSession, mailer) {

    this.crypto = require('crypto');
    this.uuid = require('node-uuid');
    this.ApiResponse = require('../models/api-response.js');
    this.ApiMessages = require('../models/api-messages.js');
    this.UserProfile = require('../models/user-profile.js');
    this.userModel = userModel;
    this.session = session;

    this.userSession = userSession;

    this.mailer = mailer;
    this.User = require('../models/user.js');
};

Note how we are passing in a UserSession instance in the controller’s constructor, assigning it to the userSession variable.

Now, let’s modify the controller’s logon method so it saves the session to the database:

AccountController.prototype.logon = function(email, password, callback) {

    var me = this;

    me.userModel.findOne({ email: email }, function (err, user) {

        if (err) {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
        }

        if (user && user.passwordSalt) {
            
            me.hashPassword(password, user.passwordSalt, function (err, passwordHash) {

                if (passwordHash == user.passwordHash) {

                    var userProfileModel = new me.UserProfile({
                        email: user.email,
                        firstName: user.firstName,
                        lastName: user.lastName
                    });

                    // Save to http session.
                    me.session.userProfileModel = userProfileModel;
                    me.session.id = me.uuid.v4();

                    // Save to persistent session.
                    me.userSession.userId = user._id;
                    me.userSession.sessionId = me.session.id;                    

                    me.userSession.save(function (err, sessionData, numberAffected) {

                        if (err) {
                            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
                        }

                        if (numberAffected === 1) {
                            // Return the user profile so the router sends it to the client app doing the logon.
                            return callback(err, new me.ApiResponse({
                                success: true, extras: {
                                    userProfileModel: userProfileModel,
                                    sessionId: me.session.id
                                }
                            }));                            
                        } else {
                            
                            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.COULD_NOT_CREATE_SESSION } }));
                        }
                    });                    
                } else {
                    return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.INVALID_PWD } }));
                }
            });
        } else {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.EMAIL_NOT_FOUND } }));
        }

    });
};

In the code above, pay attention to the lines where we save the session to the database:

// Save to persistent session.
me.userSession.userId = user._id;
me.userSession.sessionId = me.session.id;                    

me.userSession.save(function (err, sessionData, numberAffected) {…}

As requests from the app come in, we will pull this session instance from the database and use it to look up the id of the user who’s running the app. Specifically, we will parse the sessionId value out an incoming request, query the database for a UserSession record containing the sessionId value, and get the userId value off the record that we find.

Before we look into this process in detail, let’s refactor and re-run our unit tests, to make sure that we didn’t break the logon functionality with our changes.

Creating a Class to Mock User Sessions

We made two changes to the AccountController Class that affect the unit tests we created in previous chapters of this series. One is the injection of a UserSession model through the Class’s constructor. And the other is the use of this model inside the controller’s logon method, to save the sessionId and userId values to the database.

Next, we’re going to create a mock of the UserSession Class, so we can use it when we instantiate the AccountController in our unit tests in place of an actual UserSession instance. This will let us test the controller without sending data to the actual database. Remember that we want to test the logon functionality, not how Mongoose models save data to MongoDB.

Let’s create the user-session-mock.js file in the project’s server/test directory

directories-34

In the file, we will define the UserSessionMock Class as follows:

var UserSessionMock = function () {
    this.err = false;
    this.numberAffected = 0;
};

UserSessionMock.prototype.setError = function (err) {
    this.err = err;
};

UserSessionMock.prototype.setNumberAffected = function (number) {
    this.numberAffected = number;
};

UserSessionMock.prototype.save = function (callback) {
    this.numberAffected = 1;
    return callback(this.err, this, this.numberAffected);
};

module.exports = UserSessionMock;

The setError and setNumberAffected methods allow us to set the private err and numberAffected variables. The save method, which the controller’s logon method invokes, is where we will simulate saving the session to the MongoDB database. You can go back a few paragraphs in this article and review the save method’s implementation.

Inside the save mock, we set the numberAffected variable to 1, indicating that there was one record affected by the save operation; and then invoke the save method’s callback, passing the err and numberAffected values to the caller.

Running Mocha Tests for the Logon Method

Now we can re-run the controller’s tests to confirm that all are green. Let’s open a command window in the project’s server directory, and execute the mocha command:

>mocha

tests-bookings-controller-1

OK, all the tests are green and we can move towards our primary goal in this article, which is to create the server-side code that retrieves the bookings list for the logged on user.

Creating Tests for the Bookings Feature

We will begin with the behavior tests for the bookings list retrieval functions. Let’s create the booking-tests.js file in the project’s test directory:

directories-35

Now, add the following code to the file:

var BookingsController = require('../controllers/bookings.js'),
    should = require('should'),
    BookingMock = require('./booking-mock.js');

describe('BookingsController', function () {

    var controller,
        bookingMock,
        userSessionMock;

    beforeEach(function (done) {
        bookingMock = new BookingMock();
        controller = new BookingsController(bookingMock);
        done();
    });

    describe('#bookings', function () {

        it('Returns bookings list', function (done) {

            var userId = 0,
                fromDate = null,
                toDate = null,
                page = 1,
                pageSize = 10,
                sortColumn = null,
                sortDir = null;

            controller.getBookings(userId, fromDate, toDate, page, pageSize, sortColumn, sortDir, function (err, apiResponse) {

                should(apiResponse.success).equal(true);
                done();
            });
        });
    })
});

In the file, we first create an instance of BookingsController Class. This controller will encapsulate the bookings features of the app.  It doesn’t exist yet, so we will create it in a few minutes.

We also import the Should and BookingMock modules.  BookingMock is another module we haven’t created yet.  It will help us mock the functions of Mongoose model that we will ultimately use to save bookings in the database.

Inside the “Returns a bookings list” test, we invoke the BookingnsController’s getBookings method. This method will return the list of bookings for a given user. We expect it to return an ApiResponse instance whose success property is set to true.

The test will fail when we run it for the first time.  Makes sense because we haven’t created the BookingMock or the BookingsController modules.

Let’s move on to creating the BookingsController Class.

The Bookings Controller

This is the controller where we will manage all things related to bookings.  Let’s create the bookings-controller.js file in the server/controllers directory:

directories-35

We will define the controller like so:

var BookingsController = function (bookingModel) {
    this.ApiResponse = require('../models/api-response.js');
    this.ApiMessages = require('../models/api-messages.js');
    this.bookingModel = bookingModel;
};

module.exports = BookingsController;

The controller’s constructor will take a BookingModel reference as an argument.  The controller will use this module, which we haven’t created yet, to move bookings in and out of the database.

We also define private references to the ApiResponse and ApiMessages  modules. We will use instances of these Classes in the return values of the controller’s methods.

Defining a Method to Return Bookings

The first mission we will give the BookingsController is to return the bookings for a given user.   Let’s define the getBookings method as follows:

BookingsController.prototype.getBookings = function (userId, fromDate, toDate, page, pageSize, sortColumn, sortDir, callback) {

throw "Not implemented.";
};

The sessionId argument identifies the user for whom the bookings are requested.  The fromDate and toDate arguments will limit the bookings to a range of dates.  The page and pageSize arguments will allow us to perform pagination on the bookings and not send all the bookings in the database to the mobile application.  The sortColumn and sortDir will allow us to perform sorting.

For the moment, we are simply going to throw an exception inside the method.  Let’s quickly jump to creating the Booking model so we can complete the getBookings’ method implementation.

The Booking Model

The Booking model is a core model in the application because it will hold the data describing a meeting room booking. Let’s create the booking.js file in the server/models directory of the project:

directories-36

Here’s the model’s definition:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var BookingSchema = new Schema({
    ownerUserId: String,
    locationId: String,
    dateTimeFrom: Date,
    dateTimeTo: Date,
    numberOfAttendees: Number,
    needsNetwork: Boolean,
    needsConfPhone: Boolean,
    needsVideo: Boolean,
    needsInternet: Boolean
});

module.exports = mongoose.model('Booking', BookingSchema);

The only things we do in this model is to define the various properties of a meeting room booking.

The BookingMock Class

We also need a Class that we can use to mock the Booking model in our tests.   Let’s create the booking-mock.js file in the project’s /server/test directory:

directories-37

In the file, we will define the BookingMock Class:

var BookingMock = function () {
    this.err = false;
    this.numberAffected = 0;
    this.bookings = [];
}

BookingMock.prototype.setError = function (err) {
    this.err = err;
};

BookingMock.prototype.setNumberAffected = function (number) {
    this.numberAffected = number;
};

module.exports = BookingMock;

In the Class’s constructor we define the err, numberAffected and bookings variables. They will help us mock the interface of a Mongoose model, in this case the Booking Class.

Later, we will need to add a few methods to the BookingMock Class.  Let’s jump back to the BookingController and complete the getBookings method.

Open the server/controllers/bookings.js file and modify the getBookings method as follows:

BookingsController.prototype.getBookings = function (userId, fromDate, toDate, page, pageSize, sortColumn, sortDir, callback) {

    var me = this;   

    var query = {
        ownerUserId: userId,
        fromDate: { '$gte': fromDate },
        toDate: { '$lt': toDate }
    };

    me.bookingModel.find(query)
        .sort({
            sortColumn: sortDir
        })
        .skip(pageSize * page)
        .limit(pageSize)
        .exec(function (err, bookings) {
            if (err) {
                return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
            }

            return callback(err, new me.ApiResponse({success: true, extras: {bookings: bookings}}));
        });
};

Inside getBookings, we create a query object containing the filters for the userId, fromDate and toDate properties of the Booking model.  We submit this query using the model’s find method, and then chain the sort, skip and limit methods, to perform sorting and pagination on the results.

Implementing Search and Pagination Methods in the BookingMock Class

We need to add the find, sort, skip, limit and exec methods to the BookingMock Class.  Let’s return to the models/booking-mock.js file and add the methods like so:

BookingMock.prototype.find = function (query, callback) {
    return this;
};

BookingMock.prototype.sort = function (sort) {
    return this;
};

BookingMock.prototype.skip = function (count) {
    return this;
};

BookingMock.prototype.limit = function (pageSize) {
    return this;
};

BookingMock.prototype.exec = function (callback) {
    return callback(this.err, []);
};

Remember that these are mock methods to simulate database access in a Mongoose model.  Find, sort, skip and limit only need to return the Class’s instance.  Exec just returns the callback passed to it.

Testing the getBookings Method

Now we can re-run the Mocha tests:

>mocha
 

This time the results should be green:

tests-bookings-controller-1

The Bookings Route

At this point the Booking model and its controller module are in place and we’re going to turn our attention to defining the Express route that will handle requests to download the bookings list for a user.

Let’s create the bookings.js file in the project’s server/routes directory:

directories-39

Add the following code to the file:

var express = require('express'),
    router = express.Router(),
    BookingsController = require('../controllers/bookings.js'),
    Booking = require('../models/booking.js'),
    UserSession = require('../models/user-session.js'),
    ApiResponse = require('../models/api-response.js');
    ApiMessages = require('../models/api-messages.js');

Next, let’s add the route that will return the bookings list:

router.route('/bookings/')
.get(function (req, res) {

    var bookingsController = new BookingsController(Booking);

    UserSession.findOne({ sessionId: req.get('X-Auth-Token') }, function (err, session) {
        
        if (err) {
            return callback(err, new me.ApiResponse({ success: false, extras: { msg: me.ApiMessages.DB_ERROR } }));
        }

        if (session) {

            // TODO: Sanitize query string.

            bookingsController.getBookings(
                session.userId,
                req.query.fromDate,
                req.query.toDate,
                req.query.page,
                req.query.pageSize,
                req.query.sortColumn,
                req.query.sortDir,
                function (err, apiResponse) {

                    return res.send(apiResponse);
                });

        } else {
            return res.send(new ApiResponse({ success: false, extras: { msg: ApiMessages.EMAIL_NOT_FOUND } }));
        }
    });
   
});

module.exports = router;

In the route, we first create a BookingsController instance, passing a reference to the Booking model.  Then, we invoke the UserSession model’s findOne method to locate the userId value corresponding to the sessionId sent from the mobile app.  Note that we’re obtaining the sessionId value from the request’s X-Auth-Token header, which we will need to send from the application’s side.

Adding the Route to the Express Application

The one thing that we’re missing at this point is to add the new route to the Express application.  We need to do this in the server/app.js file.  

Let’s open the file and modify it as shown below:

var express = require('express'),
    bodyParser = require('body-parser'),
    cookieParser = require('cookie-parser'),
    mongoose = require('mongoose'),
    expressSession = require('express-session'),
    MongoStore = require('connect-mongo')(expressSession),
    accountRoutes = require('./routes/account'),
    bookingRoutes = require('./routes/bookings'),
    app = express(),
    port = 30000;

var dbName = 'bookitDB';
var connectionString = 'mongodb://localhost:27017/' + dbName;

mongoose.connect(connectionString);

app.use(expressSession({
    secret: '128013A7-5B9F-4CC0-BD9E-4480B2D3EFE9',
    resave: true,
    saveUninitialized: true,
    store: new MongoStore({
        url: 'mongodb://localhost/test-app',
        ttl: 20 * 24 * 60 * 60 // = 20 days.
    })
}));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use('/api', [accountRoutes, bookingRoutes]);

var server = app.listen(port, function () {
    console.log('Express server listening on port ' + server.address().port);
});

The change involves adding two lines of code.  First, we import the “bookings” route we just created:

bookingRoutes = require('./routes/bookings')

And then, we add the route to the application:

app.use('/api', [accountRoutes, bookingRoutes]);

Now we can move on to testing this route, which will give us a pretty good idea of whether the “get bookings” route->controller->model flow will work.

Testing the Express Route with Postman.

We used [Postman] to test the user registration and logon routes that we created in a [previous article] of this series. We’re going to do the same with the Bookings route.  Let’s begin by logging on our test user first so we can have a session stored in the database:

postman-14

The logon request should return a sessionId value that we can use in subsequent requests:

{"success":true,
"extras":{
    "userProfileModel":{"email":"test@test.com","firstName":"test1","lastName":"test1"},
    "sessionId":"aI4wdCJfG0X_DMOLLmdGP_stEEWa8_Nc"
         }
}

Let’s now submit a request to download the bookings list for the user that we just logged on:

postman-15

This request should return a json-encoded response, with a success property set to true and a bookings array:

{"success":true,"extras":{"bookings":[]}}

If you’re wondering why, the bookings array is empty because we haven’t saved any bookings in the database.

Summary and Next Steps

This concludes the work we wanted to do in this article.  Remember that our goal was to put together the server-side modules that will allow the mobile app to download a list of meeting room bookings for a particular user.

We started off with creating the html markup for the jQuery Mobile page that will render the bookings list in the mobile app.  Later, we moved to the server side, where we modified the AccountController module and created the Mongoose models, controller and router modules that will handle meeting room bookings requests coming from the app.

In the next chapter of this series we will work on the mobile app’s side, where we will learn how to download and render the bookings list.  

Download Source Code

Download source code here: Meeting Room Booking Mobile App (3)

All the Chapters of this Series

You can find all the published parts of this series here: The Meeting Room Booking App Tutorial.

The post The Meeting Room Booking App Tutorial: Creating the Bookings API with Express and MongoDB appeared first on MiamiCoder.


Viewing all articles
Browse latest Browse all 4

Latest Images

Trending Articles





Latest Images