bq/composr-core

View on GitHub
src/lib/managers/Phrase.js

Summary

Maintainability
B
6 hrs
Test Coverage
'use strict'
var phraseValidator = require('../validators/phrase.validator')
var PhraseModel = require('../models/PhraseModel')
var phraseDao = require('../daos/phraseDao')
var BaseManager = require('./base.manager')
var queryString = require('query-string')
// var ComposrError = require('../ComposrError')
// var MetricsFirer = require('../MetricsFirer')
var phrasesStore = require('../stores/phrases.store')
// var parseToComposrError = require('../parseToComposrError')
var WrappedResponse = require('../mock').res
var WrappedRequest = require('../mock').req
var corbel = require('corbel-js')
var utils = require('../utils')

var _ = require('lodash')

var PhraseManager = function PhraseManager (options) {
  this.events = options.events
  this.requirer = options.requirer
  this.config = options.config || {}
}

PhraseManager.prototype = new BaseManager({
  itemName: 'phrase',
  store: phrasesStore,
  model: PhraseModel,
  dao: phraseDao,
  validator: phraseValidator
})

PhraseManager.prototype.configure = function configure (config) {
  this.config = {
    urlBase: config.urlBase
  }
}

PhraseManager.prototype.__preAdd = function __preAdd (domain, phraseModel) {
  var phrasesWithTheSamePath = this._filterByRegexp(domain, phraseModel.getRegexp())

  if (phrasesWithTheSamePath.length > 0) {
    this.events.emit('warn', 'phrase:path:duplicated', phraseModel.getId())
  }
}

PhraseManager.prototype._compile = function _compile (domain, phrase) {
  try {
    var PhraseModel = this.model
    var phraseInstance = new PhraseModel(phrase, domain)

    phraseInstance.compile(this.events)

    this.events.emit('debug', 'phrase:compiled', phraseInstance.getId(), Object.keys(phraseInstance.compiled.codes))

    return phraseInstance
  } catch (e) {
    // Somehow it has tried to compile an invalid phrase. Notify it and return false.
    // Catching errors and returning false here is important for not having an unstable phrases stack.
    this.events.emit('errore', 'phrase:not:usable', phrase.url, e)
    return false
  }
}

// Executes a phrase by id
PhraseManager.prototype.runById = function runById (id, verb, params, cb) {
  if (utils.values.isFalsy(verb)) {
    verb = 'get'
  }

  var parameters = getDefaultPhraseParams(params)

  var phrase = this.getById(id)

  if (phrase && phrase.canRun(verb)) {
    var domain = this._extractDomainFromId(id)
    this._run(phrase, verb, parameters, domain, cb)
  } else {
    // @TODO: See if we want to return that error directly or a wrappedResponse with 404 status (or invalid VERB)
    cb('phrase:cant:be:runned')
  }
}

// Executes a phrase matching a path
PhraseManager.prototype.runByPath = function runByPath (domain, path, verb, params, version, cb) {
  if (utils.values.isFalsy(verb)) {
    verb = 'get'
  }

  var phrase = this.getByMatchingPath(domain, path, verb, version)

  if (phrase) {
    var parameters = getDefaultPhraseParams(params)

    var queryParamsString = path.indexOf('?') !== -1 ? path.substring(path.indexOf('?'), path.length) : ''

    if (!parameters.query && !(parameters.req && parameters.req.query && Object.keys(parameters.req.query).length > 0)) {
      // If no reqQuery object or req.querty params are sent, extract them
      parameters.query = queryString.parse(queryParamsString)
    }

    if (!parameters.params) {
      // extract params from path
      var sanitizedPath = path.replace(queryParamsString, '')
      parameters.params = phrase.extractParamsFromPath(sanitizedPath)
    }
    this._run(phrase, verb, parameters, domain, cb)
  } else {
    cb('phrase:cant:be:runned')
  }
}

function getDefaultPhraseParams (options) {
  var defaultParameters = {
    functionMode: true,
    query: null,
    params: null,
    headers: null,
    corbelDriver: null,
    domain: null,
    timeout: null,
    body: null
  }

  return Object.assign({}, defaultParameters, options)
}

/*
  Fills the sandbox with parameters
 */
/* function buildSandbox (options, urlBase, domain, requirer, reqWrapper, resWrapper, version) {
  // The object that will be inject on the phrase itself.
  var sb = {
    req: reqWrapper,
    res: resWrapper,
    // next: nextWrapper.resolve,
    domain: domain,
    config: {
      urlBase: urlBase
    },
    // metrics: new MetricsFirer(domain)
    metrics: null
  }

  // sb.require = options.functionMode ? requirer.forDomain(domain, version, true) : requirer.forDomain(domain, version)
  // sb.require = requirer(domain, version, options.functionMode)

  if (!options.corbelDriver && reqWrapper.get('Authorization')) {
    sb.corbelDriver = corbel.getDriver({
      urlBase: urlBase,
      iamToken: {
        accessToken: reqWrapper.get('Authorization')
      },
      domain: domain
    })
  } else {
    sb.corbelDriver = options.corbelDriver
  }

  return sb
}*/

PhraseManager.prototype._run = function _runPhrase (phrase, verb, params, domain, cb) {
  this.events.emit('debug', 'running:phrase:' + phrase.getId() + ':' + verb)

  var urlBase = params.config && params.config.urlBase ? params.config.urlBase : this.config.urlBase

  var resWrapper = new WrappedResponse(params.res)
  var reqWrapper = new WrappedRequest(params.req, params)

  // Fill the sandbox params
  var sandbox = {
    req: reqWrapper,
    res: resWrapper,
    // next: params.next,
    require: this.requirer(domain, phrase.getVersion(), params.functionMode),
    domain: domain,
    config: {
      urlBase: urlBase
    }
  }

  if (!params.corbelDriver && reqWrapper.get('Authorization')) {
    sandbox.corbelDriver = corbel.getDriver({
      urlBase: urlBase,
      iamToken: {
        accessToken: reqWrapper.get('Authorization')
      },
      domain: domain
    })
  } else {
    sandbox.corbelDriver = params.corbelDriver
  }

  sandbox.res.on('end', function (resp) {
    cb(null, resp)
  })

  // Execute the phrase
  if (params.functionMode) {
    phrase.__executeFunctionMode(verb, sandbox, params.timeout, params.file)
  } else {
    phrase.__executeScriptMode(verb, sandbox, params.timeout, params.file, this.events)
  /* try {
  } catch (e) {
    // console.log(e)
    // @TODO this errors can be:
    // - corbel errors
    // - Any thrown error in phrase
    // How do we handle it?
    if (params.functionMode) {
      // Function mode only throws an error when errored
      this.events.emit('warn', 'phrase:internal:error', e, phrase.getUrl())

      var error = parseToComposrError(e, 'error:phrase:exception:' + phrase.getUrl())

      sandbox.res.send(error.status, error)
    } else {
      // vm throws an error when timedout
    }
  }*/
  }
}

// Returns a list of elements matching the same regexp
PhraseManager.prototype._filterByRegexp = function _filterByRegexp (domain, regexp) {
  var candidates = this.store.getAsList(domain)

  return _.filter(candidates, function (candidate) {
    return candidate.getRegexp() === regexp
  })
}

// Get all the phrases, or all the phrases for one domain
PhraseManager.prototype.getPhrases = function getPhrases (domain) {
  return this.store.getAsList(domain)
}

/**
  CORE Entry point. One of the purposes of composr-core is to provide a fast and reliable
  getByMatchingPath method.
 **/
PhraseManager.prototype.getByMatchingPath = function getByMatchingPath (domain, path, verb, version) {
  var candidate = null

  if (!verb) {
    verb = 'get'
  }

  domain = utils.values.isFalsy(domain) ? null : domain

  this.events.emit('debug', 'phrase:getByMatchingPath:' + domain + ':' + path + ':' + verb)

  if (utils.values.isFalsy(path)) {
    this.events.emit('error', 'phrase:getByMatchingPath:path:undefined')
    return candidate
  }

  var queryParamsString = path.indexOf('?') !== -1 ? path.substring(path.indexOf('?'), path.length) : ''

  path = path.replace(queryParamsString, '')

  if (domain === null) {
    this.events.emit('warn', 'phrase:getByMatchingPath:noDomain:matchingAgainstAll:expensiveMethod')
  }

  var candidates = this.store.getAsList(domain)

  this.events.emit('debug', 'evaluating:' + candidates.length + ':candidates')

  candidates = _.compact(candidates.map(function (phrase) {
    if (phrase.canRun(verb) && phrase.matchesPath(path)) {
      if (!version || (version && phrase.getVersion() === version)) {
        return phrase
      }
    }
  }))

  this.events.emit('debug', 'found:' + candidates.length + ':candidates')

  if (candidates.length === 0) {
    this.events.emit('debug', 'notfound:candidates:path:' + path + ':' + verb)
    return candidate
  } else {
    candidate = candidates[0]
    this.events.emit('debug', 'using:candidate:' + candidate.getId() + ':' + verb)
    return candidate
  }
}

// Counts all the loaded phrases
PhraseManager.prototype.count = function count () {
  return this.store.getAsList().length
}

// TODO: Remove for using the MD5 check defined in BaseManager
PhraseManager.prototype.__shouldSave = function __shouldSave () {
  return true
}

module.exports = PhraseManager