Sunday, December 30, 2012

TypeScript Node.js Development Part 4 - More Packages

In this post I'll go through how to add some useful tools to your Node.js TypeScript app - Underscore for handy utility functions, MongoDB for persistence, and Socket.IO for real-time client-server communication.

Since this is Node, nice people have already made packages for these (underscore, mongodb, socket.io) and adding them is simply done by updating the package.json file:
{
  "name": "package-name"
  , "version": "0.0.1"
  , "private": true
  , "dependencies": {
      "express": "2.5.8"
    , "ejs": ">= 0.8.3"
    , "jade": ">= 0.27.7"
    , "underscore": ">= 1.4.2"
    , "mongodb": ">= 1.1.11"
    , "socket.io": ">= 0.9.11"
  }
}

And since these packages are already included in soywiz's definitions, getting them working in TypeScript is as easy as adding the definitions to the app.d.ts file:
/// <reference path="./node-definitions/node.d.ts" />
/// <reference path="./node-definitions/express.d.ts" />
/// <reference path="./node-definitions/mocha.d.ts" />
/// <reference path="./node-definitions/underscore.d.ts" />
/// <reference path="./node-definitions/mongodb.d.ts" />
/// <reference path="./node-definitions/socket.io.d.ts" />

To test that underscore works, we can add some code to one of our test routes:
import _ = module("underscore")
app.get('/testUrl', function(req, res) {
    console.log('test url ' + req.query['testQS']);

    // Test underscore
    var underscoreTest: string = "";
    var array:string[] = ["a", "b", "c"];
    _.each(array, (item) => { underscoreTest += item });

    res.send(underscoreTest + " env = " + app.settings.env, 200);
});

If you type "_" in to Visual Studio, you'll notice that the TypeScript definitions have given us Intellisense for Underscore, nice!


Socket.IO is also easy to test out, using some of their sample code. We can add this code to our app (again with full Intellisense):
import io = module("socket.io");
var sio = io.listen(app);
sio.sockets.on('connection', function (socket) {
    socket.emit('news', { hello: 'world' });
    socket.on('my other event', function (data) {
        console.log(data);
    });
});

And this code to our index.jade file:
script(src='/socket.io/socket.io.js')
script(type='text/javascript')
  var socket = io.connect();
  socket.on('news', function (data) {
    console.log(data);
    socket.emit('my other event', { my: 'data' });
  });

npm install, then run the app, and we see some console output on the server side logs and the browser, indicating that everything is all going peachy.

MongoDB is a bit more complicated. First, we need to get a local server running so that we can do some testing locally and luckily there is a good tutorial for that over on the MongoDB site. After installation, run mongod.exe and we are ready to write some code to utilize the server.

Here's something I prepared earlier (based on the node package docs), some code that allows addition and querying of a key-value pair to the database (model.ts):
///<reference path='app.d.ts' />

import mongodb = module('mongodb');

interface TestDoc {
    key: string;
    value: string;
}

export class Model {
 private server: mongodb.Server;
 private client: mongodb.Db;

 constructor () {
        var dbname: string = 'testdbsdf';
        var host: string = 'localhost';
        var port: number = 27017;

     this.server = new mongodb.Server(host, port, { auto_reconnect: true });

     this.client = new mongodb.Db(dbname, this.server, { safe: true });
        this.client.open((error) => {
            if(error) { console.error(error); return; }
        });
 }

 putTestDoc(doc: TestDoc, callback: (doc: TestDoc) => void, errorcb: (error: any) => void): void {
     this.client.collection('TestDoc', function (error, docs) {
         if (error) { console.error(error); errorcb(error); return }

         docs.insert(doc, function (error, object) {
             if (error) { console.error(error); errorcb(error); return; }
             callback(object)
         });
     });
 }

 getTestDoc(key: string, callback: (doc: TestDoc) => void, errorcb: (error: any) => void): void {
     this.client.collection('TestDoc', function (error, docs) {
         if (error) { console.error(error); errorcb(error); return; }
            
            docs.findOne({'key': key}, function(error, doc) {
               if(error) { console.error(error); errorcb(error); return; }
               callback(doc);
            });
     });
 }

 getAllTestDocs(callback: (docs: TestDoc[]) => void, errorcb: (error: any) => void): void {
     this.client.collection('TestDoc', function (error, docs) {
         if (error) { console.error(error); errorcb(error); return; }
            
         docs.find({}, { limit: 100 }).toArray(function (err, docs) {
             if (error) { console.error(error); errorcb(error); return; }
             callback(docs);
         });
     });
 }
} 

We can now add some routes that use this class:
import model = module("./model")
app.get('/testMongo/:key/:value', function(req, res) {
    // Add the key/value pair to the DB and then fetch and return it
    database.putTestDoc({ key: req.params.key, value: req.params.value }, function () {
        database.getTestDoc(req.params.key, function (doc) {
            res.send("key = [" + doc.key + "] value = [" + doc.value + "]", 200);
        }, function (err) { res.send(err, 200); });
    }, function (err) { res.send(err, 200); });
});

app.get('/testMongo', function(req, res) {
    // return a list of all pairs
    var response: string = "";
    database.getAllTestDocs(function (docs) {
        _.each(docs, (doc) => { response += ("key = [" + doc.key + "] value = [" + doc.value + "]"); });
        res.send(response, 200);
    }, function (err) { res.send(err, 200); });
});

This is very simplistic - for example, it doesn't check for duplicate keys, but it's good enough to confirm that the database is set up correctly.

So that's all set up locally, but what about ~The Cloud~? Thankfully, The Cloud is generous, and there is a free service to set up cloud-based MongoDB servers called MongoLab. Sign up for an account, create a database, and we are nearly ready to go. Thanks, The Cloud! Before we can use this, we need to modify our model.ts file a bit, so that it can access MongoLab's database. We also now need to authenticate with our username and password. Since we all know that we should never put sensitive information like this in code, we will do this using Azure's app settings. First, we need to update our Model's constructor:
 constructor () {
        var dbname: string = process.env['MONGO_NODE_DRIVER_DBNAME'] || 'testdb';
        var host: string = process.env['MONGO_NODE_DRIVER_HOST'] || 'localhost';
        var port: number = parseInt(process.env['MONGO_NODE_DRIVER_PORT']) || 27017;

     this.server = new mongodb.Server(host, port, { auto_reconnect: true });

     this.client = new mongodb.Db(dbname, this.server, { safe: true });
        this.client.open((error) => {
            if(error) { console.error(error); return; }

            var username: string = process.env['MONGO_NODE_DRIVER_USER'];
            var password: string = process.env['MONGO_NODE_DRIVER_PASSWORD'];
            
            if (username && password) {
                this.client.authenticate(username, password, function (error) {
                    if (error) { console.error(error); return; }
                });
            }
        });
 }

Then we need to put those new settings into our Azure app settings section (in the dashboard, under Configure.

Save those, upload the new code to Azure, and we're up and running all over The Cloud. That's it for now, in the next post, I'll go over testing and debugging your app. As always, here is a Visual Studio template up to this point.

Previously: TypeScript Node.js Development Part 3 - Twitter Bootstrap, next: TypeScript Node.js Development Part 5 - Unit Tests and Debugging

3 comments:

  1. Excellent Tutorial. However, I was getting an - Uncaught TypeError: Object #Object has no method 'connect' - while testing socket.io

    Put this above your code snippet in the layout.jade header section which seemed to fix it.

    script(src='/socket.io/socket.io.js')

    ReplyDelete
  2. Meant to say index.jade in the previous post

    ReplyDelete