Using mongoose middleware to add async virtuals

Problem

In a node.js / Mongoose project, I have a schema which contains references to external image files.

var PageSchema = new Schema({
    title: String
  , media: {
        digest: String
      , name: String
    }
});

Those files have additional properties which are stored in the file itself: url, width, height, exif fields, etc. Those fields will need to be populated before the model being sent to res.render().

For some fields, things are synchronous and a virtual just does the job:

PageSchema.virtual('media.url').get(function () {
    return appPaths.fileUrl(this.media);
});

However, width / height, or exif fields require async calls. I thought of using middleware to populate them, but this does not seem to work:

PageSchema.post('init', function(next) {
    var media = this.media;
    var fileName = filedb.absoluteFilePath(media);

    im.identify(fileName, function(err, features) {
        if (err) {
            media.width = 0;
            media.height = 0;
        } else {
            media.width = features.width;
            media.height = features.height;
        }

        next();
    });
});

What am I doing wrong? Is there a common design pattern for solving this kind of problem? (Other than duplicating this information in the database itself?)

Problem courtesy of: Philippe Plantier

Solution

The real problem here is that mongoose currently seems to have a wonky implementation of post callbacks. While pre('init',function(next){ ... }); works as you expect, post('init',function(next){ ... }); does not actually get passed a next function. In fact, the post init callback does not receive any arguments whatsoever when it is called.

As such, I usually write a wrapper for my query callbacks to make a sort of DIY middleware:

var setAsyncVirtuals = function(callback){
  return function(err, docs){
    if(err) return callback(err);
    var i = done = docs.length;
    if(i > 0)
      while(i--){
        (function(i){
          var filename = getFilename();
          im.identify(filename, function(err, features) {
            if (err) {
              docs[i].media.width = 0;
              docs[i].media.height = 0;
            } else {
              docs[i].media.width = features.width;
              docs[i].media.height = features.height;
            }
            done--;
            if(done <= 0) callback(null, docs);
          });
        })(i); // bind i to hold value for async call
      }
    else callback(null, docs);
  }
}

then

Page.find({}, setAsyncVirtuals(function(err,docs){
  res.send(docs); // these have media.width & media.height assigned
}));
Solution courtesy of: Daniel Mendel

Discussion

There is currently no discussion for this recipe.

This recipe can be found in it's original form on Stack Over Flow.