hairyhenderson/node-fellowshipone

View on GitHub
lib/f1resource.js

Summary

Maintainability
A
3 hrs
Test Coverage
var debug = require('debug')(require('../package.json').name + ':f1resource')
var _ = require('lodash')
var request = require('request')
var async = require('async')

function F1Resource (f1, options) {
  this.f1 = f1
  this.options = options || {}
  this.mediaType = this.options.mediaType || 'application/json'
  this.resourceName = this.options.resourceName
  this.resourceNamePlural = this.options.resourceNamePlural || this.resourceName + 's'
  this.path = this.options.path || '/' + _.capitalize(this.resourceNamePlural)
  this.searchParams = this.options.searchParams || {}
}

/**
 * Generic HTTP request
 *
 * @param  {String}   method   - the HTTP method (GET, POST, PUT, DELETE)
 * @param  {String}   path     - the path (relative to the configured API URL)
 * of the resource
 * @param  {Object}   query    - optional query object
 * @param  {Object}   body     - the JSON data to POST or PUT (don't set for GET/DELETE)
 * @param  {Function} callback - yields (err, response_body, headers)
 */
F1Resource.prototype._req = function (method, path, query, body, callback) {
  var config = this.f1.config

  var opts = {
    uri: config.apiURL + path,
    oauth: config.oauth_credentials,
    headers: {
      accept: this.mediaType,
      'content-type': this.mediaType
    },
    json: body,
    qs: query
  }
  debug('%s opts: %j', method, opts)

  // body is only necessary for PUT/POST
  if (_.includes(['PUT', 'POST'], method.toUpperCase())) {
    // query is optional...
    if (typeof body === 'function') {
      callback = body
      opts.json = query
      delete opts.qs
    }
  } else {
    callback = body
    opts.json = true
    if (typeof query === 'function') {
      callback = query
      delete opts.qs
    }
  }

  var handleErrorStatus = function (res, body, next) {
    debug('%s %s: %s %j', method.toUpperCase(), path, res.statusCode, body)
    if (res.statusCode > 299) {
      return next({
        statusCode: res.statusCode,
        headers: res.headers,
        message: body
      })
    } else return next(null, res, body)
  }

  async.waterfall([
    request[method.toLowerCase()].bind(request, opts),
    handleErrorStatus
  ], function (err, res, body) {
    callback(err, body, res && res.headers ? res.headers : undefined)
  })
}

/**
 * Generic HTTP GET for the resource
 *
 * @param  {String}   path      - the path (relative to the configured API URL)
 * of the resource
 * @param  {Object}   query    - optional query object
 * @param  {Function} callback - yields (err, response_body, headers)
 */
F1Resource.prototype._get = function (path, query, callback) {
  this._req('GET', path, query, callback)
}

/**
 * Generic HTTP POST for the resource
 *
 * @param  {String}   path     - the path (relative to the configured API URL)
 * of the resource
 * @param  {Object}   query    - optional query object
 * @param  {Object}   body     - the JSON data to POST
 * @param  {Function} callback - yields (err, response_body, headers)
 */
F1Resource.prototype._post = function (path, query, body, callback) {
  this._req('POST', path, query, body, callback)
}

/**
 * Note - unlike the API, this yields the actual array of objects.
 */
F1Resource.prototype.list = function (callback) {
  this._get(this.path, function (err, body, headers) {
    if (err) return callback(err)

    if (body && body[this.resourceNamePlural] && body[this.resourceNamePlural][this.resourceName]) {
      callback(null, body[this.resourceNamePlural][this.resourceName])
    } else {
      return callback({
        statusCode: 502,
        headers: headers,
        message: body
      })
    }
  }.bind(this))
}

/*
 * Note - unlike the API, this yields the unwrapped object.
 *
 * @param  {Number}   id - the object's ID
 */
F1Resource.prototype.show = function (id, callback) {
  var path = this.path + '/' + id
  this._get(path, function (err, body, headers) {
    if (err) return callback(err)

    if (body && body[this.resourceName]) {
      return callback(null, body[this.resourceName])
    } else {
      return callback({
        statusCode: 502,
        headers: headers,
        message: body
      })
    }
  }.bind(this))
}

/**
* Yields a valid (but empty) resource.
*
* @param  {Function} callback - called with (err, result)
*/
F1Resource.prototype.new = function (callback) {
  this.show('New', callback)
}

/**
 * @param  {Object} params - params to populate the resource with.
 * @param  {Function} callback - called with (err, result)
 */
F1Resource.prototype.create = function (params, callback) {
  this.new(function (err, template) {
    if (err) return callback(err)

    var resource = {}
    resource[this.resourceName] = _.merge(template, params)
    debug('new resource: %s', JSON.stringify(resource, null, 3))

    this._post(this.path, resource, callback)
  }.bind(this))
}

/**
 *  @param params - the search parameters. Will be merged with any resource defaults, if applicable.
 */
F1Resource.prototype.search = function (params, callback) {
  var query = _.merge(params, this.searchParams)

  this._get(this.path + '/Search', query, function (err, body) {
    if (err) {
      return callback(err)
    }

    // this probably won't (and shouldn't) return the raw response forever...
    callback(null, body)
  })
}

module.exports = F1Resource