Mongoose getter function doesn't return updated array when making async calls

I have an app which makes use of two models: Movie and Tag. The idea is that one can create a record of a movie and attach tags to it. A Movie references one or more Tag models via the Movie.tags attribute which is a array containing the ObjectId of corresponding Tags.

When displaying the properties of a Movie on the client side it makes sense to display the text of the tag, rather than the ObjectId of the tag (remember: Movie.tags is an array of ObjectIds). I thought about the problem and concluded that the best method would be to make use of a getter function, so that when I retrieve a Movie document, the value of tags attribute is converted from an array of ObjectIds into an array of corresponding tag names.

To do this I must perform a db query for each ObjectId in the array Movie.tags. Since db queries are done asynchronously in Mongoose, I tried implementing the getter function using async.forEach() function from the Async module. The problem is that the final value is not being returned at the end of the async.forEach function.

I have two questions regarding this problem:

  1. Given my overall objective, is using a getter function the best way of going about this?
  2. Why is async.forEach() failing to return the updated array?

from model.js

/**
 * Mongo database models
 */

function defineModels(mongoose, async, fn) {
  var Schema = mongoose.Schema,
      ObjectId = Schema.ObjectId;

  /**
   * Model - Movie
   */

  /**
   * Getter function
   * 
   * Gets tag text as well as tag ObjectId.
   */
  function getTagNames(tags) {
    var newArray = new Array();
    async.forEach(
      tags,
      function(id, done) {
        mongoose.models['Tag'].findOne({ _id: id }, function(err, doc) {
          if (err) {
            done(err);
          }
          else if (doc) {
            newArray.push(doc);
            done(null);
          }
          else {
            console.log(doc);
            // Just incase something weird like no document is found.  
            // Technically this condition should not occur in reality. But we 
            // put something here to catch it just in case.
            done(new Error('No tag document found.'));
          }
        });
      },
      function(err) {
        if (err) {
          throw err;
        }
        console.log(newArray);
        return newArray;
      }
    );
  }

  /**
   * Define schema
   */
  Movie = new Schema({
    'name': String,
    'machineFileName': String,
    'originalFileName': String,
    'size': Number,
    'type': String,
    'permanent': { 
      type: Boolean, 
      default: false 
    },
    'dateUploaded': Date,
    'amountUploaded': {
      type: [], 
      default: 0 
    },
    'viewed': Number,
    'uid': String,
    'flags': [],
    'tags': {
      type: Array, 
      get: getTagNames
    }
  }, { strict: true });

  mongoose.model('Movie', Movie);

  /**
   * Model - Tag
   */
  Tag = new Schema({
    'title': { type: String, unique: true, sparse: true }
  }, { strict: true });

  mongoose.model('Tag', Tag);

  fn();
}

exports.defineModels = defineModels; 

Retrieving the document:

/**
 * View individual movie.
 */
exports.movie = function(req, res) {
  var Movie = mongoose.model('Movie');
  Movie.findById(req.params.id, function(err, doc) {
    if (err) {
      res.send('An error occured.', 500);
    }
    else {
      console.log('View movie');
      console.log(doc);
      res.render('movie', {
        locals: {
          title: 'Watch Movie',
          movie: doc
        }
      });
    }
  });
}

I think you'll be better off making tags an ObjectId refs array and then using Mongoose's populate to populate that with the actual tag objects when needed.

In the Movie schema, tags becomes:

tags: [{ type: ObjectId, ref: 'Tag' }]

And then when you query Movie:

Movie.findById(req.params.id).populate('tags').exec(function(err, doc) {
    // doc.tags is now an array of Tag instances