appirio-tech/lc1-discussion-service

View on GitHub
lib/partialResponseHelper.js

Summary

Maintainability
F
1 wk
Test Coverage
/**
 * Copyright (c) 2014 TopCoder, Inc. All rights reserved.
 */
/**
 * Helper methods for controller logic.
 *
 * @version 1.0
 * @author lovefreya
 */
'use strict';

var _ = require('lodash');
var async = require('async');
var inflection = require('inflection');
var routeHelper = require('./routeHelper');
var dataSource = require('./../datasource');

/**
 * Judge whether the input character is accepted or not.
 * @param char
 * @returns {boolean}
 */
function allowedCharacter(char) {
  var allowedCharacterList = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  allowedCharacterList += 'abcdefghijklmnopqrstuvwxyz';
  allowedCharacterList += '_,()';
  for (var i = 0; i < allowedCharacterList.length; i += 1) {
    if (char === allowedCharacterList[i]) {
      return true;
    }
  }
  return false;
}

/**
 *  return offset
 * @param req
 * @param string
 * @returns {number}
 */
function findRightBracket(req, string) {
  var count = 0;
  for (var i = 0; i < string.length; i += 1) {
    if (string[i] === ')') {
      if (i === 0) {
        routeHelper.addValidationError(req, 'Fields parameter cannot take empty object ().');
        return i + 1;
      }
      if (count === 0) {
        return i + 1;
      } else {
        count -= 1;
      }
    }
    if (string[i] === '(') {
      count += 1;
    }
  }
  routeHelper.addValidationError(req, 'Fields parameter must take entire pair of \'()\' .');
}

/**
 *
 * @param req
 * @param param string waiting to parse
 * @param entity the parsed object will append to this entity's key
 * @param property the key in entity
 */
function iterationParse(req, param, entity, property) {
  var subObject = null;
  if (!property) {
    subObject = entity;
  } else {
    subObject = entity[property];
  }
  var cache = '';
  for (var i = 0; i < param.length; i += 1) {
    if (i === (param.length - 1)) {
      if (allowedCharacter(param[i]) && param[i]!=='(' && param[i]!==',') {
        cache += param[i];
        subObject[cache] = true;
        cache = '';
      } else {
        routeHelper.addValidationError(req, 'Fields parameter cannot end up with a \'' + param[i] + '\' .');
        return;
      }
      return;
    } else if (param[i] === ',') {
      if (i === 0) {
        routeHelper.addValidationError(req, 'Fields parameter cannot start with a \',\' .');
        return;
      }
      subObject[cache] = true;
      cache = '';
    } else if (param[i] === '(') {
      var rightPos = i + findRightBracket(req, param.substring(i + 1, param.length));
      //Now cache is a plural for the name of a known model.
      subObject[cache] = {};
      iterationParse(req, param.substring(i + 1, rightPos), subObject, cache);
      cache = '';
      i = rightPos + 1;
      if (param[i]) {
        if (param[i] === ')') {
          routeHelper.addValidationError(req, 'Fields parameter must take entire pair of \'()\' .');
          return;
        } else if (!allowedCharacter(param[i])) {
          routeHelper.addValidationError(req, 'Fields parameter cannot contain character \'' + param[i] + '\' .');
          return;
        } else if (param[i] !== ',') {
          routeHelper.addValidationError(req, 'Fields parameter format error.');
          return;
        }
      }
    } else {
      if (allowedCharacter(param[i])) {
        cache += param[i];
      } else {
        routeHelper.addValidationError(req, 'Fields parameter cannot contain character \'' + param[i] + '\' .');
        return;
      }
    }
  }
}

/**
 * If Model has this key, return true. Otherwise false.
 * @param Model
 * @param key
 */
function _hasKey(Model, key){
  var has = false;
  _.forEach(_.keys(Model.rawAttributes), function (column) {
    if (key === column) {
      has = true;
    }
  });
  return has;
}

/**
 * If Model has many models, return true. Otherwise false.
 * Support caml-case item: Model Scorecard, models: scorecardItems
 * @param Model
 * @param models
 */
function _hasMany(Model, models){
  var has = false;
  _.forEach(Model.associations, function (association) {
    if ( (association.as === models ||
            models === inflection.camelize(association.as, true) ) &&
      association.associationType === 'HasMany') {
        has = true;
    }
  });
  return has;
}

/**
 * IF Model has this foreign key, return reference Model Name and foreignKey. Otherwise false.
 * This depends on the the sequelize model definition.
 * Model.belongsTo(FatherModel, {foreignKey: key})
 * @param Model
 * @param key
 */
function _hasForeignKey(Model, key){
  var has = null;
  _.forEach(Model.associations, function(association){
    if( (association.identifier===key+'Id' ||
          association.identifier===inflection.singularize(key)+'Id') &&
        association.associationType === 'BelongsTo'){
          has = [];
          has[0] = association.target.name;
          has[1] = association.identifier;
        }
  });
  return has;
}

function _getForeignKey(Model, childModel){
  var foreignKey = Model.name.toLowerCase() + 'Id';
  if(_hasKey(childModel, foreignKey)){
    return foreignKey;
  }else{
    _.forEach(childModel.associations, function(assocaition){
      if(assocaition.as===Model.name.toLowerCase() &&
          assocaition.associationType==='BelongsTo'){
        foreignKey = assocaition.identifier;
      }
    });
    return foreignKey;
  }
}

/**
 *
 * @param req request object
 * @param Model Model need to be reduced
 * @param Entity the response entity wrapper
 * @param Property the key
 * @param Fields fields won't be reduced
 * @param callback
 */
function recursionReduce(req, Model, Entity, Property, Fields, callback){
  var subObject = Entity[Property];
  if(_.isArray(subObject)){

    var tasks = [];
    var index=-1;
    _.forEach(subObject, function(entity){

      tasks.push(function(callback){

        var reducedObject = {};
        var tasksB = [];
        if(!_.isObject(Fields)){
          if(!subObject){
            callback(null);
            return;
          }
          reducedObject = entity.values;
        }else{
          _.forEach(_.keys(Fields), function(key){
            tasksB.push(function(callback){
              if(_hasKey(Model, key)){
                reducedObject[key] = entity.values[key];
                callback(null);
              }else if(_hasMany(Model, key)){
                var modelName = inflection.capitalize(inflection.singularize(key));
                if(!dataSource.getDataSource()[modelName]){
                  modelName = inflection.camelize( inflection.underscore(key) ).slice(0, -1);
                }
                var foreignKey = _getForeignKey(Model, dataSource.getDataSource()[modelName]);
                var filter = {};
                filter[foreignKey] = entity.id;
                dataSource.getDataSource()[modelName].findAll({where: filter}).success(function(entities){
                  reducedObject[key] = entities;
                  recursionReduce(req, dataSource.getDataSource()[modelName], reducedObject, key, Fields[key], callback);
                }).error(function(err) {
                  routeHelper.addError(req, err, 500);
                  reducedObject[key] = [];
                  callback();
                });
              }else{
                var reference = _hasForeignKey(Model, key);
                if(!reference){
                  routeHelper.addValidationError(req, Model.name+' doesn\'t has ' + key);
                  callback(null);
                }else{
                  var filterOne = {};
                  filterOne.id = entity[reference[1]];
                  if(!filterOne.id){
                    reducedObject[key] = null;
                    callback(null);
                    return;
                  }
                  dataSource.getDataSource()[reference[0]].find({where: filterOne}).success(function(entity){
                    reducedObject[key] = entity;
                    recursionReduce(req, dataSource.getDataSource()[reference[0]], reducedObject, key, Fields[key], callback);
                  }).error(function(err){
                    routeHelper.addError(req, err, 500);
                    callback();
                  });
                }
              }
            });
          });
        }

        index += 1;
        subObject[index] = reducedObject;
        async.series(tasksB, function(){
          callback(null);
        });
      });

    });

    async.series(tasks, function(){
      callback();
    });
  }else{

    var reducedObject = {};
    var tasksC = [];

    if(!_.isObject(Fields)){
      if(!subObject){
        callback(null);
        return;
      }
      reducedObject = subObject.values;
    }else{
      _.forEach(_.keys(Fields), function(key){
        tasksC.push(function(callback){
          if(_hasKey(Model, key)){
            reducedObject[key] = subObject.values[key];
            callback(null);
          }else if(_hasMany(Model, key)){
            var modelName = inflection.capitalize(inflection.singularize(key));
            if(!dataSource.getDataSource()[modelName]){
              modelName = inflection.camelize( inflection.underscore(key) ).slice(0, -1);
            }

            var foreignKey = _getForeignKey(Model, dataSource.getDataSource()[modelName]);
            var filter = {};
            filter[foreignKey] = subObject.id;
            dataSource.getDataSource()[modelName].findAll({where: filter}).success(function(entities){
              reducedObject[key] = entities;
              recursionReduce(req, dataSource.getDataSource()[modelName], reducedObject, key, Fields[key], callback);
            }).error(function(err){
              routeHelper.addError(req, err, 500);
              reducedObject[key] = [];
              callback();
            });
          } else{
            var reference = _hasForeignKey(Model, key);
            if(!reference){
              routeHelper.addValidationError(req, Model.name+' doesn\'t has ' + key);
              callback(null);
            }else{
              var filterOne = {};
              filterOne.id = subObject[reference[1]];
              if(!filterOne.id){
                reducedObject[key] = null;
                callback(null);
                return;
              }
              dataSource.getDataSource()[reference[0]].find({where: filterOne}).success(function(entity){
                reducedObject[key] = entity;
                recursionReduce(req, dataSource.getDataSource()[reference[0]], reducedObject, key, Fields[key], callback);
              }).error(function(err){
                routeHelper.addError(req, err, 500);
                callback();
              });
            }
          }
        });
      });
    }
    Property = inflection.singularize(Property);
    delete Entity[inflection.pluralize(Property)];
    Entity[Property] = reducedObject;
    async.series(tasksC, function(){
      callback();
    });
  }
}

/**
 * Parse fields parameter if exist in all get call.
 * @param req
 * @param res
 * @param next
 */
exports.parseFields = function (req, res, next) {
  var param = req.query.fields;
  delete req.query.fields;
  if (param && typeof param === 'string') {
    param = param.trim();
    var fields = {};
    if (req.method !== 'GET') {
      routeHelper.addValidationError(req, 'Fields parameter is not allowed for ' + req.method + ' call.');
    } else {
      iterationParse(req, param, fields);
    }
    //append to req object
    req.partialResponse = fields;
  }
  //console.log(req.partialResponse);

  next();
};

exports.reduceFieldsAndExpandObject = function(Model, req, next){
  if (!req.data || !req.data.content || !req.partialResponse || req.error) {
    next();
  } else {
    recursionReduce(req, Model, req.data, 'content', req.partialResponse, next);
  }
};