DailyJS

Node Tutorial Part 12

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials server node lmawa nodepad npm express

Node Tutorial Part 12

Posted by Alex R. Young on .
Featured

tutorials server node lmawa nodepad npm express

Node Tutorial Part 12

Posted by Alex R. Young on .

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
.