mysociety/popit-api

View on GitHub
src/mongoose/embed.js

Summary

Maintainability
C
1 day
Test Coverage
"use strict";

/**
 * Handles embedding documents using the ?embed parameter as well as populating
 * their associated memberships.
 */

var _ = require('underscore');
var async = require('async');
var mpath = require('mpath');
var util = require('util');
var transform = require('../transform');

function InvalidEmbedError(message, explanation) {
  this.name = 'InvalidQueryError';
  this.message = message || "Invalid embed parameter";
  this.explaination = explanation;
}
util.inherits(InvalidEmbedError, Error);

function getDocs(path, doc) {
  var models = mpath.get(path, doc, '_doc');
  // For a path like 'membership.person.membership.organization mpath will
  // return an array of arrays. We don't need that level of nesting, so we
  // flatten it here before iterating over it.
  models = _.flatten(models);
  // Remove any null values from the models array
  models = _.compact(models);
  return models;
}

/**
 * Populate memberships of embedded documents.
 */
function populateMemberships(req, doc, path, callback) {
  var modelsToPopulate = getDocs(path, doc);
  // Make sure all the models have the necessary method on them.
  modelsToPopulate = _.filter(modelsToPopulate, function(model) {
    return _.isObject(model) && _.isFunction(model.populateMemberships);
  });

  async.each(modelsToPopulate, function(val, done) {
    val.populateMemberships(req, done);
  }, callback);
}

function populateJoins(req, doc, opt, callback) {
  if (opt.newEmbedNames) {
    getDocs(opt.collection, doc).forEach(function(membership) {
      membership[opt.to] = membership[opt.from];
    });
  }
  doc.populate(opt, function(err, doc) {
    if (err) {
      return callback(err);
    }
    // Make sure populated models have been transformed
    var modelsToTransform = getDocs(opt.path, doc);
    modelsToTransform = modelsToTransform.map(function(model) {
      return transform(model, req);
    });
    if (opt.populateMemberships) {
      populateMemberships(req, doc, opt.path, callback);
    } else {
      callback();
    }
  });
}

function embedPlugin(schema) {

  /**
   * Taked the ?embed parameter and embeds any people, organizations or
   * memberships that have been requested.
   */
  schema.methods.embedDocuments = function embedDocuments(req, newEmbedNames, callback) {
    if (typeof newEmbedNames === 'function') {
      callback = newEmbedNames;
      newEmbedNames = false;
    }
    var doc = this;
    var embeds = req.query.embed;

    var allJoins = [];

    // Default to embedding one layer of memberships
    if (!_.isString(embeds)) {
      embeds = 'membership';
    }
    // Run the callback async if there's no embed requested
    if (embeds === '') {
      return process.nextTick(callback);
    }

    embeds.split(',').forEach(function(embed) {
      // Check if the final layer of memberships should be included
      var originalEmbed = embed;
      var parts = embed.split('.');
      var skipLastEmbed = true;
      if (parts[parts.length - 1] === 'membership') {
        skipLastEmbed = false;
        embed = parts.slice(0, -1).join('.');
      }

      var target_re = /((?:membership\.\w+)+)/g;
      var targets = [];
      var match;
      while ( targets.length < 3 && ( match = target_re.exec(embed) ) ) {
        targets.push(match[0]);
      }

      var target_map;
      if (newEmbedNames) {
        target_map = {
          'membership.person': {
            path: 'memberships.person',
            from: 'person_id',
            to: 'person',
            model: 'Person',
          },
          'membership.organization': {
            path: 'memberships.organization',
            from: 'organization_id',
            to: 'organization',
            model: 'Organization',
          },
          'membership.post': {
            path: 'memberships.post',
            from: 'post_id',
            to: 'post',
            model: 'Post',
          },
        };
      } else {
        target_map = {
          'membership.person': {
            path: 'memberships.person_id',
            model: 'Person',
          },
          'membership.organization': {
            path: 'memberships.organization_id',
            model: 'Organization',
          },
          'membership.post': {
            path: 'memberships.post_id',
            model: 'Post',
          },
        };
      }

      var invalidTargets = !_.all(targets, function(target) { return target_map[target]; });
      var missingTargets = targets.join('.') !== embed;
      if (invalidTargets || missingTargets) {
        var message = 'Invalid embed parameter ' + originalEmbed;
        var explaination = 'embed must be one of ' + Object.keys(target_map).join(', ');
        var error = new InvalidEmbedError(message, explaination);
        return callback(error);
      }

      var join_structure = [];
      var path = [];
      _.each(targets, function(target) {
        var this_map = target_map[target];
        path.push( this_map.path );
        join_structure.push({
          path: path.join('.'),
          model: this_map.model,
          populateMemberships: true,
          collection: path.slice(0, -1).concat(['memberships']).join('.'),
          from: this_map.from,
          to: this_map.to,
        });
      });

      var last = join_structure[join_structure.length - 1];
      if (skipLastEmbed && last) {
        last.populateMemberships = false;
      }

      allJoins = allJoins.concat(join_structure);
    });

    doc.populateMemberships(req, function(err) {
      if (err) {
        return callback(err);
      }
      async.eachSeries(allJoins, function(structure, done) {
        structure.newEmbedNames = newEmbedNames;
        populateJoins(req, doc, structure, done);
      }, callback);
    });
  };
}

module.exports = exports = embedPlugin;
exports.InvalidEmbedError = InvalidEmbedError;