Node Tutorial Part 12

07 Feb 2011 | By Alex Young | Tags server node tutorials lmawa nodepad npm express

Welcome to part 12 of Let’s Make a Web App, a tutorial series about building a web app with Node. This series will walk you through the major areas you’ll need to face when building your own applications. These tutorials are tagged with lmawa.

Previous tutorials:

Updating Mongoose

Mongoose 1.0.0 has been released, which changes the API quite significantly. In particular, model definitions are very different to the version we were using. I’ve updated the app to use Mongoose 1.0.7 so you’ll be able to learn how to use the new API.

Because we’ve locked all of our packages to versions using npm’s @version syntax, it’s easy to switch between versions of a package. The new version of Mongoose can be installed like this:

npm install mongoose@1.0.7

I then went through and updated the requires:

mongoose = require('mongoose@1.0.7')

The file that needs the most changes is models.js. Let’s look at the Document model to see how it can be defined with the new API.

Document Model

Mongoose 1.0 models are defined using Schema objects. Once they’ve been defined it’s possible to decorate them with virtual attributes and middleware. Virtual attributes are getters and setters, and middleware is a convenient way of injecting functions into key lifecycle events.

The schema just defines attributes and their associated properties — validations can even be defined. Nodepad’s Document model is actually very simple:

var Schema = mongoose.Schema,
    ObjectId = Schema.ObjectId;

Document = new Schema({
  'title': { type: String, index: true },
  'data': String,
  'tags': [String],
  'user_id': ObjectId
});

I’ve defined an index on the title attribute, and told Mongoose what types I want to use. Notice that the tag array (which is currently unused) is defined using [String] rather than Array or some other collection type.

Once a schema has been defined it’s important to tell Mongoose about it:

mongoose.model('Document', Document);

Now the Document model can be used like this:

mongoose.model('Document');

All of these commands will be queued if there isn’t a database connection yet.

User Model

The User schema is a bit more complicated. We need to validate the presence of an email address and password. It’s easy for email addresses:

function validatePresenceOf(value) {
  return value && value.length;
}

User = new Schema({
  'email': { type: String, validate: [validatePresenceOf, 'an email is required'], index: { unique: true } },
  'hashed_password': String,
  'salt': String
});

Mongoose lets us define functions for validators. It’ll also accept a regular expression.

I haven’t defined a password attribute because we don’t want to store the plaintext password. Previously this was defined as a ‘getter’; now it needs to be a virtual attribute:

User.virtual('password')
  .set(function(password) {
    this._password = password;
    this.salt = this.makeSalt();
    this.hashed_password = this.encryptPassword(password);
  })
  .get(function() { return this._password; });

There are a few interesting points here:

  • The syntax for getters and setters requires a call to virtual first
  • The getter and setters are defined with get and set
  • These definitions can be chained

The next thing we need to do is make sure this password is validated. We can’t do it with the schema definition because it’s a virtual attribute, so let’s use the new middleware feature instead:

User.pre('save', function(next) {
  if (!validatePresenceOf(this.password)) {
    next(new Error('Invalid password'));
  } else {
    next();
  }
});

I’ve reused the function from the email validation. This function will run before save is called. Calling next() will move to the next middleware or save itself, but passing an Error will cause the error to be returned to the function passed to save from Nodepad’s app code:

user.save(function(err) {
  if (err) return userSaveFailed();
  // ...

The old API wasn’t as flexible as this, so I hope middleware makes switching to the new API worth the effort.

Instance Methods

One thing I couldn’t find in the Mongoose documentation was how to define instance methods. I tried calling Schema.method and it just happened to work, so for reference it looks like this:

User.method('authenticate', function(plainText) {
  return this.encryptPassword(plainText) === this.hashed_password;
});

User.method('makeSalt', function() {
  return Math.round((new Date().valueOf() * Math.random())) + '';
});

User.method('encryptPassword', function(password) {
  return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
});

These are the methods we use to make dealing with passwords easier.

The other model in the project, LoginToken, just uses the same techniques discussed here.

Query Signature Changes

For a start, Model.find().first() no-longer works. We now need to use findOne:

app.post('/sessions', function(req, res) {
  User.findOne({ email: req.body.user.email }, function(err, user) {
    if (user && user.authenticate(req.body.user.password)) {
      req.session.user_id = user.id;
      // ...

Secondly, the query callback signature has changed to function(err, user). This makes error handling more convenient but meant I had to change every single finder in the entire project.

Conclusion

It took me about 3 hours to learn Mongoose 1.0 and port Nodepad to it. However, Nodepad is a pretty small project, so keep this in mind if you’d like to move your own projects to the newer API.

This week’s code is commit 2a8725.


blog comments powered by Disqus