<commit_before>
var async = require("async"),
  _ = require("underscore"),
  fs = require("fs"),
  crypto = require("crypto"),
  RestJS = require("restjs"),
  Promise = require("bluebird"),
  path = require("path");

  if(RestJS.Rest)
    RestJS = RestJS.Rest;

// Azuqua Node.js Client Library
// -----------------------------

// This library provides an easy interface for interacting with your Azuqua flos.
// The Azuqua API is directly exposed to developers should you wish to write your own library.
// For full API documentation please visit developer.azuqua.com.

// In order to make API requests you will need both your accessKey and accessSecret.
// These can be found on your account information page.

// Since using an Azuqua client is the most secure way to invoke a flo
// any published flo can be invoked with the credentials provided by this client, regardless of the flo's security level.

// All asynchronous functions can return a promise if you prefer that pattern.
// By default when you call an asynchronous function and
// leave the callback undefined it will return a promise.
// By default Azuqua uses bluebird.

// Should you wish to write your own library that makes HTTP requests directly you will need to provide the following headers with each request:

// `x-api-accessKey: "your access key",`

// `x-api-hash: "the hexidecimal string output of a HMAC-SHA256 of the data and some metadata signed by your accessSecret (see the signData function)",`

// `x-api-timestamp: "an ISO timestamp of the current time"`

// Also note that the API rate limits all clients by the maximum number of requests per second
// as determined by your account plan. By default this is 3 requests per second.

// Flos are not directly invocable via their name. To invoke a flo you fi
// rst need to
// get the alias for the flo. These can be found at the /api/account/flos route. This client maintains
// an internal mapping between each flo name and its alias so you can invoke flos directly by their name.
// Aliases are designed to be disposable so the flo maintainer can obfuscate endpoints as needed.

var routes = {
  invoke: {
    path: "/flo/:id/invoke",
    method: "POST"
  },
  inject: {
    path: "/flo/:id/inject",
    method: "POST"
  },
  flos: {
    path: "/account/flos",
    method: "GET"
  },
  schedule: {
    path: "/flo/:id/schedule",
    method: "POST"
  },
  retry: {
    path: "/flo/:id/retry",
    method: "POST"
  },
  inputs: {
    path: "/flo/:id/inputs",
    method: "GET"
  }
};

function deepDataCopy(target) {
  return JSON.parse(JSON.stringify(target));
}

var wrapAsyncFunction = function(fn, args){
  args = Array.prototype.slice.call(args);
  var callback = args[args.length - 1];
  if(typeof callback !== "function"){
    var deferred = Promise.defer();
    args.push(function(error, data){
      if(error)
        deferred.reject(error);
      else
        deferred.resolve(data);
    });
    fn.apply(fn, args);
    return deferred.promise;
  }else{
    fn.apply(fn, args);
  }
};

var signData = function(accessSecret, data, verb, _path, timestamp){
  if(!data)
    data = "";
  else if(typeof data === "object")
    data = JSON.stringify(data);
  var meta = [verb.toLowerCase(), _path, timestamp].join(":");
  return crypto.createHmac("sha256", accessSecret).update(new Buffer(meta + data, 'utf-8')).digest("hex");
};

var addGetParameter = function(_path, key, value){
  var delimiter = _path.indexOf("?") > -1 ? "&" : "?";
  return _path + delimiter + encodeURIComponent(key) + "=" + encodeURIComponent(value);
};

var getAlias = function(map, str){
  return map[str] || _.find(map, function(v){ return v === str; });
};

// Constructor
// -----------

// Create a new Azuqua client.

// This function first attempts to read accessKey and accessSecret data from environment variables.
// These can be set with ACCESS_KEY and ACCESS_SECRET.
// It then attempts to read them from the function's arguments.
// You can overwrite the environment variables this way if necessary,
// although it's not recommended to hard-code your account credentials.
var Azuqua = function(accessKey, accessSecret, httpOptions){
  var self = this;

  self.account = {};
  if(process.env.ACCESS_KEY)
    self.account.accessKey = process.env.ACCESS_KEY;
  if(process.env.ACCESS_SECRET)
    self.account.accessSecret = process.env.ACCESS_SECRET;
  if(typeof accessKey === "string")
    self.account.accessKey = accessKey;
  if(typeof accessSecret === "string")
    self.account.accessSecret = accessSecret;

  var protocol = "https";

  self.httpOptions = {
    host: "api.azuqua.com",
    port: 443,
    headers: {
      "Content-Type": "application/json"
    }
  };

  if(typeof httpOptions === "object"){
    if(httpOptions.protocol){
      protocol = httpOptions.protocol;
      delete httpOptions.protocol;
    }
    _.extend(self.httpOptions, httpOptions);
  }
  Object.freeze(self.httpOptions)
  self.client = new RestJS({ protocol: protocol });

  self.signData = function(data, verb, path, timestamp) {
    if(!self.account.accessSecret)
      throw new Error("Account information not found");

    return signData(self.account.accessSecret, data, verb, path, timestamp);
  };

  self.makeRequest = function(externalOptions, params, callback){
    if(!self.account || !self.account.accessKey || !self.account.accessSecret)
      return callback(new Error("Account information not found"));

    var options = deepDataCopy(externalOptions);

    _.each(self.httpOptions, function(value, key){
      if (typeof value === "object") {
        options[key] = deepDataCopy(value);
      } else {
        options[key] = value;
      }
    });

    var timestamp = new Date().toISOString();

    if(!params || Object.keys(params).length < 1) {
      params = options.method === 'GET' ? "" : {};
    }
    else {
      params = JSON.parse(JSON.stringify(params));
    }

    var hash = signData(self.account.accessSecret, params, options.method, options.path, timestamp);
    if(options.method === "GET"){
      _.each(params, function(key, value){
        options.path = addGetParameter(options.path, key, value);
      });
    }
    else{
      params = JSON.stringify(params);
    }

    if(options.method === "POST") {
      options.headers["Content-Length"] = Buffer.byteLength(params);
    }

    options.headers["x-api-timestamp"] = timestamp;
    options.headers["x-api-hash"] = hash;
    options.headers["x-api-accessKey"] = self.account.accessKey;
    self.client.request(options, params, function(error, resp){
      if(error){
        callback(error);
      }
      else{
        try{
          resp.body = JSON.parse(resp.body);
        }
        catch(e){
          return callback(resp.body);
        }

        if(resp.body.error) {
          callback(new Error(resp.body.error.message ? resp.body.error.message : resp.body.error));
        }
        else {
          callback(null, resp.body);
        }
      }
    });
  };

};

// Account Configuration
// ---------------------

// loadConfig

// Read accessKey and accessSecret data from a .json file.

// Note: This function blocks. Use loadConfigAsync for the async variant.
Azuqua.prototype.loadConfig = function(_path){
  // Resolve the config path from the requiring parent's location if it's relative
  _path = path.resolve(path.dirname(module.parent.filename), _path);
  this.account = require(_path);
};

// loadConfigAsync

// Asynchronously read accessKey and accessSecret data from a .json file.
Azuqua.prototype.loadConfigAsync = function(_path, _callback){
  var self = this,
      args = Array.prototype.slice.call(arguments);

  // Resolve the config path from the requiring parent's location if it's relative
  args[0] = path.resolve(path.dirname(module.parent.filename), _path);

  return wrapAsyncFunction(function(_path, callback){
    fs.readFile(_path, { encoding: "utf8" }, function(error, data){
      if(error){
        callback(error);
      }else{
        try{
          data = JSON.parse(data);
        }catch(e){
          return callback(e);
        }
        if(data.accessKey && data.accessSecret){
          self.account = data;
          callback(null, true);
        }else{
          callback(new Error("Invalid account credentials"));
        }
      }
    });
  }, args);
};

// API Functions
// -------------

// flos

// List all flos avaliable to your access key and secret.
Azuqua.prototype.getFlos = function (_callback){
  var self = this;
  return wrapAsyncFunction(function (callback) {
    self.makeRequest(routes.flos, null, function(error, flos){
      if(error){
        callback(error);
      }else{
        async.map(flos, function(flo, next) {
          return next(null, {
            id : flo.id,
            alias: flo.alias,
            name: flo.name,
            description: flo.description,
            org_id : flo.org_id,
            active: flo.active,
            published: flo.published
          })
        }, callback);
      }
    });
  }, arguments);
};

// API Functions
// -------------

// flos

// List all flos for your organization.

// Note: This caches the flos locally. To refresh them provide a truthy first parameter.
Azuqua.prototype.flos = function(_refresh, _callback){
  var self = this;
  return wrapAsyncFunction(function(refresh, callback){
    if(typeof refresh === "function"){
      callback = refresh;
      refresh = false;
    }
    if(self.floMap && !refresh){
      callback(null, Object.keys(self.floMap));
    }else{
      self.makeRequest(routes.flos, null, function(error, flos){
        if(error){
          callback(error);
        }else{
          self.floMap = {};
          var out = [];
          async.each(flos, function(flo, cb){
            if(!self.floMap[flo.name])
              out.push(flo.name);
            self.floMap[flo.name] = flo.alias;
            cb();
          }, function(){
            callback(null, out);
          });
        }
      });
    }
  }, arguments);
};

// Retry

// Retry a Flo identified by @instance_id
// @instance_id is a string representing a particula execution of a flo.
// @data is expected to be an object with the org and flo properties set to the
// original parent flo's id as well as the owner's org id.
Azuqua.prototype.retry = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var options = _.extend({}, routes.retry);
    options.path = options.path.replace(":id", flo);
    self.makeRequest(options, data, callback);
  }, arguments);
};

// invoke

// Invoke a Flo identified by @flo with @data.
// @flo is a string representing the flo name.
// If you try to invoke a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to invoke the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.invoke = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.invoke);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.invoke(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

/**
 *
 * @param _flo - flo alias or flo name
 * @param _force - If true then it will attempt to retrieve the inputs of the flo without using the cache
 * @param _callback
 */
Azuqua.prototype.inputs = function (_flo, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function (flo, force, callback) {
    if (typeof force === "function" && !callback) {
      callback = force;
      force = false;
    }

    var alias = null;
    try {
      alias = getAlias(self.floMap || {}, flo);
    } catch (e) {
      return callback(e);
    }

    if (!alias && force) {
      alias = flo;
    }

    if (alias) {
      var options = _.extend({}, routes.inputs);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, "", callback);
    }
    else {
      self.flos(true)
        .then(function () {
          alias = getAlias(self.floMap, flo);
          if (alias) {
            self.inputs(alias, force, callback);
          }
          else {
            callback(new Error("Flo not found"));
          }
        })
        .catch(callback);
    }
  }, arguments);
};

// inject

// Inject and run a Flo identified by @flo with @data mocking the first step.
// @flo is a string representing the flo name.
// If you try to inject a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to inject the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.inject = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.inject);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.inject(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

Azuqua.prototype.schedule = function(_flo, _data, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;

    if(alias){
      var options = _.extend({}, routes.schedule);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }
    else {
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.schedule(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

module.exports = Azuqua;
<commit_msg>add metadata<commit_after>
var async = require("async"),
  _ = require("underscore"),
  fs = require("fs"),
  crypto = require("crypto"),
  RestJS = require("restjs"),
  Promise = require("bluebird"),
  path = require("path");

  if(RestJS.Rest)
    RestJS = RestJS.Rest;

// Azuqua Node.js Client Library
// -----------------------------

// This library provides an easy interface for interacting with your Azuqua flos.
// The Azuqua API is directly exposed to developers should you wish to write your own library.
// For full API documentation please visit developer.azuqua.com.

// In order to make API requests you will need both your accessKey and accessSecret.
// These can be found on your account information page.

// Since using an Azuqua client is the most secure way to invoke a flo
// any published flo can be invoked with the credentials provided by this client, regardless of the flo's security level.

// All asynchronous functions can return a promise if you prefer that pattern.
// By default when you call an asynchronous function and
// leave the callback undefined it will return a promise.
// By default Azuqua uses bluebird.

// Should you wish to write your own library that makes HTTP requests directly you will need to provide the following headers with each request:

// `x-api-accessKey: "your access key",`

// `x-api-hash: "the hexidecimal string output of a HMAC-SHA256 of the data and some metadata signed by your accessSecret (see the signData function)",`

// `x-api-timestamp: "an ISO timestamp of the current time"`

// Also note that the API rate limits all clients by the maximum number of requests per second
// as determined by your account plan. By default this is 3 requests per second.

// Flos are not directly invocable via their name. To invoke a flo you fi
// rst need to
// get the alias for the flo. These can be found at the /api/account/flos route. This client maintains
// an internal mapping between each flo name and its alias so you can invoke flos directly by their name.
// Aliases are designed to be disposable so the flo maintainer can obfuscate endpoints as needed.

var routes = {
  invoke: {
    path: "/flo/:id/invoke",
    method: "POST"
  },
  inject: {
    path: "/flo/:id/inject",
    method: "POST"
  },
  flos: {
    path: "/account/flos",
    method: "GET"
  },
  schedule: {
    path: "/flo/:id/schedule",
    method: "POST"
  },
  retry: {
    path: "/flo/:id/retry",
    method: "POST"
  },
  inputs: {
    path: "/flo/:id/inputs",
    method: "GET"
  }
};

function deepDataCopy(target) {
  return JSON.parse(JSON.stringify(target));
}

var wrapAsyncFunction = function(fn, args){
  args = Array.prototype.slice.call(args);
  var callback = args[args.length - 1];
  if(typeof callback !== "function"){
    var deferred = Promise.defer();
    args.push(function(error, data){
      if(error)
        deferred.reject(error);
      else
        deferred.resolve(data);
    });
    fn.apply(fn, args);
    return deferred.promise;
  }else{
    fn.apply(fn, args);
  }
};

var signData = function(accessSecret, data, verb, _path, timestamp){
  if(!data)
    data = "";
  else if(typeof data === "object")
    data = JSON.stringify(data);
  var meta = [verb.toLowerCase(), _path, timestamp].join(":");
  return crypto.createHmac("sha256", accessSecret).update(new Buffer(meta + data, 'utf-8')).digest("hex");
};

var addGetParameter = function(_path, key, value){
  var delimiter = _path.indexOf("?") > -1 ? "&" : "?";
  return _path + delimiter + encodeURIComponent(key) + "=" + encodeURIComponent(value);
};

var getAlias = function(map, str){
  return map[str] || _.find(map, function(v){ return v === str; });
};

// Constructor
// -----------

// Create a new Azuqua client.

// This function first attempts to read accessKey and accessSecret data from environment variables.
// These can be set with ACCESS_KEY and ACCESS_SECRET.
// It then attempts to read them from the function's arguments.
// You can overwrite the environment variables this way if necessary,
// although it's not recommended to hard-code your account credentials.
var Azuqua = function(accessKey, accessSecret, httpOptions){
  var self = this;

  self.account = {};
  if(process.env.ACCESS_KEY)
    self.account.accessKey = process.env.ACCESS_KEY;
  if(process.env.ACCESS_SECRET)
    self.account.accessSecret = process.env.ACCESS_SECRET;
  if(typeof accessKey === "string")
    self.account.accessKey = accessKey;
  if(typeof accessSecret === "string")
    self.account.accessSecret = accessSecret;

  var protocol = "https";

  self.httpOptions = {
    host: "api.azuqua.com",
    port: 443,
    headers: {
      "Content-Type": "application/json"
    }
  };

  if(typeof httpOptions === "object"){
    if(httpOptions.protocol){
      protocol = httpOptions.protocol;
      delete httpOptions.protocol;
    }
    _.extend(self.httpOptions, httpOptions);
  }
  Object.freeze(self.httpOptions)
  self.client = new RestJS({ protocol: protocol });

  self.signData = function(data, verb, path, timestamp) {
    if(!self.account.accessSecret)
      throw new Error("Account information not found");

    return signData(self.account.accessSecret, data, verb, path, timestamp);
  };

  self.makeRequest = function(externalOptions, params, callback){
    if(!self.account || !self.account.accessKey || !self.account.accessSecret)
      return callback(new Error("Account information not found"));

    var options = deepDataCopy(externalOptions);

    _.each(self.httpOptions, function(value, key){
      if (typeof value === "object") {
        options[key] = deepDataCopy(value);
      } else {
        options[key] = value;
      }
    });

    var timestamp = new Date().toISOString();

    if(!params || Object.keys(params).length < 1) {
      params = options.method === 'GET' ? "" : {};
    }
    else {
      params = JSON.parse(JSON.stringify(params));
    }

    var hash = signData(self.account.accessSecret, params, options.method, options.path, timestamp);
    if(options.method === "GET"){
      _.each(params, function(key, value){
        options.path = addGetParameter(options.path, key, value);
      });
    }
    else{
      params = JSON.stringify(params);
    }

    if(options.method === "POST") {
      options.headers["Content-Length"] = Buffer.byteLength(params);
    }

    options.headers["x-api-timestamp"] = timestamp;
    options.headers["x-api-hash"] = hash;
    options.headers["x-api-accessKey"] = self.account.accessKey;
    self.client.request(options, params, function(error, resp){
      if(error){
        return callback(error);
      }
      var metadata = {
        execution: resp.headers["x-flo-instance"],
        statusCode: resp.statusCode
      };
      try{
        resp.body = JSON.parse(resp.body);
      }
      catch(e){
        return callback(resp.body, null, metadata);
      }

      if(resp.body.error) {
        callback(new Error(resp.body.error.message ? resp.body.error.message : resp.body.error), null, metadata);
      }
      else {
        callback(null, resp.body, metadata);
      }
    });
  };

};

// Account Configuration
// ---------------------

// loadConfig

// Read accessKey and accessSecret data from a .json file.

// Note: This function blocks. Use loadConfigAsync for the async variant.
Azuqua.prototype.loadConfig = function(_path){
  // Resolve the config path from the requiring parent's location if it's relative
  _path = path.resolve(path.dirname(module.parent.filename), _path);
  this.account = require(_path);
};

// loadConfigAsync

// Asynchronously read accessKey and accessSecret data from a .json file.
Azuqua.prototype.loadConfigAsync = function(_path, _callback){
  var self = this,
      args = Array.prototype.slice.call(arguments);

  // Resolve the config path from the requiring parent's location if it's relative
  args[0] = path.resolve(path.dirname(module.parent.filename), _path);

  return wrapAsyncFunction(function(_path, callback){
    fs.readFile(_path, { encoding: "utf8" }, function(error, data){
      if(error){
        callback(error);
      }else{
        try{
          data = JSON.parse(data);
        }catch(e){
          return callback(e);
        }
        if(data.accessKey && data.accessSecret){
          self.account = data;
          callback(null, true);
        }else{
          callback(new Error("Invalid account credentials"));
        }
      }
    });
  }, args);
};

// API Functions
// -------------

// flos

// List all flos avaliable to your access key and secret.
Azuqua.prototype.getFlos = function (_callback){
  var self = this;
  return wrapAsyncFunction(function (callback) {
    self.makeRequest(routes.flos, null, function(error, flos){
      if(error){
        callback(error);
      }else{
        async.map(flos, function(flo, next) {
          return next(null, {
            id : flo.id,
            alias: flo.alias,
            name: flo.name,
            description: flo.description,
            org_id : flo.org_id,
            active: flo.active,
            published: flo.published
          })
        }, callback);
      }
    });
  }, arguments);
};

// API Functions
// -------------

// flos

// List all flos for your organization.

// Note: This caches the flos locally. To refresh them provide a truthy first parameter.
Azuqua.prototype.flos = function(_refresh, _callback){
  var self = this;
  return wrapAsyncFunction(function(refresh, callback){
    if(typeof refresh === "function"){
      callback = refresh;
      refresh = false;
    }
    if(self.floMap && !refresh){
      callback(null, Object.keys(self.floMap));
    }else{
      self.makeRequest(routes.flos, null, function(error, flos){
        if(error){
          callback(error);
        }else{
          self.floMap = {};
          var out = [];
          async.each(flos, function(flo, cb){
            if(!self.floMap[flo.name])
              out.push(flo.name);
            self.floMap[flo.name] = flo.alias;
            cb();
          }, function(){
            callback(null, out);
          });
        }
      });
    }
  }, arguments);
};

// Retry

// Retry a Flo identified by @instance_id
// @instance_id is a string representing a particula execution of a flo.
// @data is expected to be an object with the org and flo properties set to the
// original parent flo's id as well as the owner's org id.
Azuqua.prototype.retry = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var options = _.extend({}, routes.retry);
    options.path = options.path.replace(":id", flo);
    self.makeRequest(options, data, callback);
  }, arguments);
};

// invoke

// Invoke a Flo identified by @flo with @data.
// @flo is a string representing the flo name.
// If you try to invoke a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to invoke the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.invoke = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.invoke);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.invoke(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

/**
 *
 * @param _flo - flo alias or flo name
 * @param _force - If true then it will attempt to retrieve the inputs of the flo without using the cache
 * @param _callback
 */
Azuqua.prototype.inputs = function (_flo, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function (flo, force, callback) {
    if (typeof force === "function" && !callback) {
      callback = force;
      force = false;
    }

    var alias = null;
    try {
      alias = getAlias(self.floMap || {}, flo);
    } catch (e) {
      return callback(e);
    }

    if (!alias && force) {
      alias = flo;
    }

    if (alias) {
      var options = _.extend({}, routes.inputs);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, "", callback);
    }
    else {
      self.flos(true)
        .then(function () {
          alias = getAlias(self.floMap, flo);
          if (alias) {
            self.inputs(alias, force, callback);
          }
          else {
            callback(new Error("Flo not found"));
          }
        })
        .catch(callback);
    }
  }, arguments);
};

// inject

// Inject and run a Flo identified by @flo with @data mocking the first step.
// @flo is a string representing the flo name.
// If you try to inject a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to inject the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.inject = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.inject);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.inject(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

Azuqua.prototype.schedule = function(_flo, _data, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;

    if(alias){
      var options = _.extend({}, routes.schedule);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }
    else {
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.schedule(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

module.exports = Azuqua;
<|endoftext|>


<commit_before>
var async = require("async"),
  _ = require("underscore"),
  fs = require("fs"),
  crypto = require("crypto"),
  RestJS = require("restjs"),
  Promise = require("bluebird"),
  path = require("path");

  if(RestJS.Rest)
    RestJS = RestJS.Rest;

// Azuqua Node.js Client Library
// -----------------------------

// This library provides an easy interface for interacting with your Azuqua flos.
// The Azuqua API is directly exposed to developers should you wish to write your own library.
// For full API documentation please visit developer.azuqua.com.

// In order to make API requests you will need both your accessKey and accessSecret.
// These can be found on your account information page.

// Since using an Azuqua client is the most secure way to invoke a flo
// any published flo can be invoked with the credentials provided by this client, regardless of the flo's security level.

// All asynchronous functions can return a promise if you prefer that pattern.
// By default when you call an asynchronous function and
// leave the callback undefined it will return a promise.
// By default Azuqua uses bluebird.

// Should you wish to write your own library that makes HTTP requests directly you will need to provide the following headers with each request:

// `x-api-accessKey: "your access key",`

// `x-api-hash: "the hexidecimal string output of a HMAC-SHA256 of the data and some metadata signed by your accessSecret (see the signData function)",`

// `x-api-timestamp: "an ISO timestamp of the current time"`

// Also note that the API rate limits all clients by the maximum number of requests per second
// as determined by your account plan. By default this is 3 requests per second.

// Flos are not directly invocable via their name. To invoke a flo you fi
// rst need to
// get the alias for the flo. These can be found at the /api/account/flos route. This client maintains
// an internal mapping between each flo name and its alias so you can invoke flos directly by their name.
// Aliases are designed to be disposable so the flo maintainer can obfuscate endpoints as needed.

var routes = {
  invoke: {
    path: "/flo/:id/invoke",
    method: "POST"
  },
  inject: {
    path: "/flo/:id/inject",
    method: "POST"
  },
  flos: {
    path: "/account/flos",
    method: "GET"
  },
  schedule: {
    path: "/flo/:id/schedule",
    method: "POST"
  },
  retry: {
    path: "/flo/:id/retry",
    method: "POST"
  }
};

function deepDataCopy(target) {
  return JSON.parse(JSON.stringify(target));
}

var wrapAsyncFunction = function(fn, args){
  args = Array.prototype.slice.call(args);
  var callback = args[args.length - 1];
  if(typeof callback !== "function"){
    var deferred = Promise.defer();
    args.push(function(error, data){
      if(error)
        deferred.reject(error);
      else
        deferred.resolve(data);
    });
    fn.apply(fn, args);
    return deferred.promise;
  }else{
    fn.apply(fn, args);
  }
};

var signData = function(accessSecret, data, verb, _path, timestamp){
  if(!data)
    data = "";
  else if(typeof data === "object")
    data = JSON.stringify(data);
  var meta = [verb.toLowerCase(), _path, timestamp].join(":");
  return crypto.createHmac("sha256", accessSecret).update(new Buffer(meta + data, 'utf-8')).digest("hex");
};

var addGetParameter = function(_path, key, value){
  var delimiter = _path.indexOf("?") > -1 ? "&" : "?";
  return _path + delimiter + encodeURIComponent(key) + "=" + encodeURIComponent(value);
};

var getAlias = function(map, str){
  return map[str] || _.find(map, function(v){ return v === str; });
};

// Constructor
// -----------

// Create a new Azuqua client.

// This function first attempts to read accessKey and accessSecret data from environment variables.
// These can be set with ACCESS_KEY and ACCESS_SECRET.
// It then attempts to read them from the function's arguments.
// You can overwrite the environment variables this way if necessary,
// although it's not recommended to hard-code your account credentials.
var Azuqua = function(accessKey, accessSecret, httpOptions){
  var self = this;

  self.account = {};
  if(process.env.ACCESS_KEY)
    self.account.accessKey = process.env.ACCESS_KEY;
  if(process.env.ACCESS_SECRET)
    self.account.accessSecret = process.env.ACCESS_SECRET;
  if(typeof accessKey === "string")
    self.account.accessKey = accessKey;
  if(typeof accessSecret === "string")
    self.account.accessSecret = accessSecret;

  var protocol = "https";

  self.httpOptions = {
    host: "api.azuqua.com",
    port: 443,
    headers: {
      "Content-Type": "application/json"
    }
  };

  if(typeof httpOptions === "object"){
    if(httpOptions.protocol){
      protocol = httpOptions.protocol;
      delete httpOptions.protocol;
    }
    _.extend(self.httpOptions, httpOptions);
  }
  Object.freeze(self.httpOptions)
  self.client = new RestJS({ protocol: protocol });

  self.signData = function(data, verb, path, timestamp) {
    if(!self.account.accessSecret)
      throw new Error("Account information not found");

    return signData(self.account.accessSecret, data, verb, path, timestamp);
  };

  self.makeRequest = function(externalOptions, params, callback){
    if(!self.account || !self.account.accessKey || !self.account.accessSecret)
      return callback(new Error("Account information not found"));

    var options = deepDataCopy(externalOptions);

    _.each(self.httpOptions, function(value, key){
      if (typeof value === "object") {
        options[key] = deepDataCopy(value);
      } else {
        options[key] = value;
      }
    });

    var timestamp = new Date().toISOString();

    if(!params || Object.keys(params).length < 1) {
      params = options.method === 'GET' ? "" : {};
    }
    else {
      params = JSON.parse(JSON.stringify(params));
    }

    var hash = signData(self.account.accessSecret, params, options.method, options.path, timestamp);
    if(options.method === "GET"){
      _.each(params, function(key, value){
        options.path = addGetParameter(options.path, key, value);
      });
    }
    else{
      params = JSON.stringify(params);
    }

    if(options.method === "POST") {
      options.headers["Content-Length"] = Buffer.byteLength(params);
    }

    options.headers["x-api-timestamp"] = timestamp;
    options.headers["x-api-hash"] = hash;
    options.headers["x-api-accessKey"] = self.account.accessKey;
    self.client.request(options, params, function(error, resp){
      if(error){
        callback(error);
      }
      else{
        try{
          resp.body = JSON.parse(resp.body);
        }
        catch(e){
          return callback(resp.body);
        }

        if(resp.body.error) {
          callback(new Error(resp.body.error.message ? resp.body.error.message : resp.body.error));
        }
        else {
          callback(null, resp.body);
        }
      }
    });
  };

};

// Account Configuration
// ---------------------

// loadConfig

// Read accessKey and accessSecret data from a .json file.

// Note: This function blocks. Use loadConfigAsync for the async variant.
Azuqua.prototype.loadConfig = function(_path){
  // Resolve the config path from the requiring parent's location if it's relative
  _path = path.resolve(path.dirname(module.parent.filename), _path);
  this.account = require(_path);
};

// loadConfigAsync

// Asynchronously read accessKey and accessSecret data from a .json file.
Azuqua.prototype.loadConfigAsync = function(_path, _callback){
  var self = this,
      args = Array.prototype.slice.call(arguments);

  // Resolve the config path from the requiring parent's location if it's relative
  args[0] = path.resolve(path.dirname(module.parent.filename), _path);

  return wrapAsyncFunction(function(_path, callback){
    fs.readFile(_path, { encoding: "utf8" }, function(error, data){
      if(error){
        callback(error);
      }else{
        try{
          data = JSON.parse(data);
        }catch(e){
          return callback(e);
        }
        if(data.accessKey && data.accessSecret){
          self.account = data;
          callback(null, true);
        }else{
          callback(new Error("Invalid account credentials"));
        }
      }
    });
  }, args);
};

// API Functions
// -------------

// flos

// List all flos for your organization.

// Note: This caches the flos locally. To refresh them provide a truthy first parameter.
Azuqua.prototype.flos = function(_refresh, _callback){
  var self = this;
  return wrapAsyncFunction(function(refresh, callback){
    if(typeof refresh === "function"){
      callback = refresh;
      refresh = false;
    }
    if(self.floMap && !refresh){
      callback(null, Object.keys(self.floMap));
    }else{
      self.makeRequest(routes.flos, null, function(error, flos){
        if(error){
          callback(error);
        }else{
          self.floMap = {};
          var out = [];
          async.each(flos, function(flo, cb){
            if(!self.floMap[flo.name])
              out.push(flo.name);
            self.floMap[flo.name] = flo.alias;
            cb();
          }, function(){
            callback(null, out);
          });
        }
      });
    }
  }, arguments);
};

// Retry

// Retry a Flo identified by @instance_id
// @instance_id is a string representing a particula execution of a flo.
// @data is expected to be an object with the org and flo properties set to the
// original parent flo's id as well as the owner's org id.
Azuqua.prototype.retry = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var options = _.extend({}, routes.retry);
    options.path = options.path.replace(":id", flo);
    self.makeRequest(options, data, callback);
  }, arguments);
};

// invoke

// Invoke a Flo identified by @flo with @data.
// @flo is a string representing the flo name.
// If you try to invoke a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to invoke the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.invoke = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.invoke);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.invoke(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

// inject

// Inject and run a Flo identified by @flo with @data mocking the first step.
// @flo is a string representing the flo name.
// If you try to inject a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to inject the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.inject = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.inject);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.inject(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

Azuqua.prototype.schedule = function(_flo, _data, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;

    if(alias){
      var options = _.extend({}, routes.schedule);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }
    else {
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.schedule(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

module.exports = Azuqua;

<commit_msg>added inputs route<commit_after>
var async = require("async"),
  _ = require("underscore"),
  fs = require("fs"),
  crypto = require("crypto"),
  RestJS = require("restjs"),
  Promise = require("bluebird"),
  path = require("path");

  if(RestJS.Rest)
    RestJS = RestJS.Rest;

// Azuqua Node.js Client Library
// -----------------------------

// This library provides an easy interface for interacting with your Azuqua flos.
// The Azuqua API is directly exposed to developers should you wish to write your own library.
// For full API documentation please visit developer.azuqua.com.

// In order to make API requests you will need both your accessKey and accessSecret.
// These can be found on your account information page.

// Since using an Azuqua client is the most secure way to invoke a flo
// any published flo can be invoked with the credentials provided by this client, regardless of the flo's security level.

// All asynchronous functions can return a promise if you prefer that pattern.
// By default when you call an asynchronous function and
// leave the callback undefined it will return a promise.
// By default Azuqua uses bluebird.

// Should you wish to write your own library that makes HTTP requests directly you will need to provide the following headers with each request:

// `x-api-accessKey: "your access key",`

// `x-api-hash: "the hexidecimal string output of a HMAC-SHA256 of the data and some metadata signed by your accessSecret (see the signData function)",`

// `x-api-timestamp: "an ISO timestamp of the current time"`

// Also note that the API rate limits all clients by the maximum number of requests per second
// as determined by your account plan. By default this is 3 requests per second.

// Flos are not directly invocable via their name. To invoke a flo you fi
// rst need to
// get the alias for the flo. These can be found at the /api/account/flos route. This client maintains
// an internal mapping between each flo name and its alias so you can invoke flos directly by their name.
// Aliases are designed to be disposable so the flo maintainer can obfuscate endpoints as needed.

var routes = {
  invoke: {
    path: "/flo/:id/invoke",
    method: "POST"
  },
  inject: {
    path: "/flo/:id/inject",
    method: "POST"
  },
  flos: {
    path: "/account/flos",
    method: "GET"
  },
  schedule: {
    path: "/flo/:id/schedule",
    method: "POST"
  },
  retry: {
    path: "/flo/:id/retry",
    method: "POST"
  },
  inputs: {
    path: "/flo/:id/inputs",
    method: "GET"
  }
};

function deepDataCopy(target) {
  return JSON.parse(JSON.stringify(target));
}

var wrapAsyncFunction = function(fn, args){
  args = Array.prototype.slice.call(args);
  var callback = args[args.length - 1];
  if(typeof callback !== "function"){
    var deferred = Promise.defer();
    args.push(function(error, data){
      if(error)
        deferred.reject(error);
      else
        deferred.resolve(data);
    });
    fn.apply(fn, args);
    return deferred.promise;
  }else{
    fn.apply(fn, args);
  }
};

var signData = function(accessSecret, data, verb, _path, timestamp){
  if(!data)
    data = "";
  else if(typeof data === "object")
    data = JSON.stringify(data);
  var meta = [verb.toLowerCase(), _path, timestamp].join(":");
  return crypto.createHmac("sha256", accessSecret).update(new Buffer(meta + data, 'utf-8')).digest("hex");
};

var addGetParameter = function(_path, key, value){
  var delimiter = _path.indexOf("?") > -1 ? "&" : "?";
  return _path + delimiter + encodeURIComponent(key) + "=" + encodeURIComponent(value);
};

var getAlias = function(map, str){
  return map[str] || _.find(map, function(v){ return v === str; });
};

// Constructor
// -----------

// Create a new Azuqua client.

// This function first attempts to read accessKey and accessSecret data from environment variables.
// These can be set with ACCESS_KEY and ACCESS_SECRET.
// It then attempts to read them from the function's arguments.
// You can overwrite the environment variables this way if necessary,
// although it's not recommended to hard-code your account credentials.
var Azuqua = function(accessKey, accessSecret, httpOptions){
  var self = this;

  self.account = {};
  if(process.env.ACCESS_KEY)
    self.account.accessKey = process.env.ACCESS_KEY;
  if(process.env.ACCESS_SECRET)
    self.account.accessSecret = process.env.ACCESS_SECRET;
  if(typeof accessKey === "string")
    self.account.accessKey = accessKey;
  if(typeof accessSecret === "string")
    self.account.accessSecret = accessSecret;

  var protocol = "https";

  self.httpOptions = {
    host: "api.azuqua.com",
    port: 443,
    headers: {
      "Content-Type": "application/json"
    }
  };

  if(typeof httpOptions === "object"){
    if(httpOptions.protocol){
      protocol = httpOptions.protocol;
      delete httpOptions.protocol;
    }
    _.extend(self.httpOptions, httpOptions);
  }
  Object.freeze(self.httpOptions)
  self.client = new RestJS({ protocol: protocol });

  self.signData = function(data, verb, path, timestamp) {
    if(!self.account.accessSecret)
      throw new Error("Account information not found");

    return signData(self.account.accessSecret, data, verb, path, timestamp);
  };

  self.makeRequest = function(externalOptions, params, callback){
    if(!self.account || !self.account.accessKey || !self.account.accessSecret)
      return callback(new Error("Account information not found"));

    var options = deepDataCopy(externalOptions);

    _.each(self.httpOptions, function(value, key){
      if (typeof value === "object") {
        options[key] = deepDataCopy(value);
      } else {
        options[key] = value;
      }
    });

    var timestamp = new Date().toISOString();

    if(!params || Object.keys(params).length < 1) {
      params = options.method === 'GET' ? "" : {};
    }
    else {
      params = JSON.parse(JSON.stringify(params));
    }

    var hash = signData(self.account.accessSecret, params, options.method, options.path, timestamp);
    if(options.method === "GET"){
      _.each(params, function(key, value){
        options.path = addGetParameter(options.path, key, value);
      });
    }
    else{
      params = JSON.stringify(params);
    }

    if(options.method === "POST") {
      options.headers["Content-Length"] = Buffer.byteLength(params);
    }

    options.headers["x-api-timestamp"] = timestamp;
    options.headers["x-api-hash"] = hash;
    options.headers["x-api-accessKey"] = self.account.accessKey;
    self.client.request(options, params, function(error, resp){
      if(error){
        callback(error);
      }
      else{
        try{
          resp.body = JSON.parse(resp.body);
        }
        catch(e){
          return callback(resp.body);
        }

        if(resp.body.error) {
          callback(new Error(resp.body.error.message ? resp.body.error.message : resp.body.error));
        }
        else {
          callback(null, resp.body);
        }
      }
    });
  };

};

// Account Configuration
// ---------------------

// loadConfig

// Read accessKey and accessSecret data from a .json file.

// Note: This function blocks. Use loadConfigAsync for the async variant.
Azuqua.prototype.loadConfig = function(_path){
  // Resolve the config path from the requiring parent's location if it's relative
  _path = path.resolve(path.dirname(module.parent.filename), _path);
  this.account = require(_path);
};

// loadConfigAsync

// Asynchronously read accessKey and accessSecret data from a .json file.
Azuqua.prototype.loadConfigAsync = function(_path, _callback){
  var self = this,
      args = Array.prototype.slice.call(arguments);

  // Resolve the config path from the requiring parent's location if it's relative
  args[0] = path.resolve(path.dirname(module.parent.filename), _path);

  return wrapAsyncFunction(function(_path, callback){
    fs.readFile(_path, { encoding: "utf8" }, function(error, data){
      if(error){
        callback(error);
      }else{
        try{
          data = JSON.parse(data);
        }catch(e){
          return callback(e);
        }
        if(data.accessKey && data.accessSecret){
          self.account = data;
          callback(null, true);
        }else{
          callback(new Error("Invalid account credentials"));
        }
      }
    });
  }, args);
};

// API Functions
// -------------

// flos

// List all flos for your organization.

// Note: This caches the flos locally. To refresh them provide a truthy first parameter.
Azuqua.prototype.flos = function(_refresh, _callback){
  var self = this;
  return wrapAsyncFunction(function(refresh, callback){
    if(typeof refresh === "function"){
      callback = refresh;
      refresh = false;
    }
    if(self.floMap && !refresh){
      callback(null, Object.keys(self.floMap));
    }else{
      self.makeRequest(routes.flos, null, function(error, flos){
        if(error){
          callback(error);
        }else{
          self.floMap = {};
          var out = [];
          async.each(flos, function(flo, cb){
            if(!self.floMap[flo.name])
              out.push(flo.name);
            self.floMap[flo.name] = flo.alias;
            cb();
          }, function(){
            callback(null, out);
          });
        }
      });
    }
  }, arguments);
};

// Retry

// Retry a Flo identified by @instance_id
// @instance_id is a string representing a particula execution of a flo.
// @data is expected to be an object with the org and flo properties set to the
// original parent flo's id as well as the owner's org id.
Azuqua.prototype.retry = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var options = _.extend({}, routes.retry);
    options.path = options.path.replace(":id", flo);
    self.makeRequest(options, data, callback);
  }, arguments);
};

// invoke

// Invoke a Flo identified by @flo with @data.
// @flo is a string representing the flo name.
// If you try to invoke a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to invoke the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.invoke = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }
    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.invoke);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.invoke(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

/**
 *
 * @param _flo - flo alias or flo name
 * @param _force - If true then it will attempt to retrieve the inputs of the flo without using the cache
 * @param _callback
 */
Azuqua.prototype.inputs = function (_flo, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function (flo, force, callback) {
    if (typeof force === "function" && !callback) {
      callback = force;
      force = false;
    }

    var alias = null;
    try {
      alias = getAlias(self.floMap || {}, flo);
    } catch (e) {
      return callback(e);
    }

    if (!alias && force) {
      alias = flo;
    }

    if (alias) {
      var options = _.extend({}, routes.inputs);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }
    else {
      self.flos(true)
        .then(function () {
          alias = getAlias(self.floMap, flo);
          if (alias) {
            self.inputs(alias, force, callback);
          }
          else {
            callback(new Error("Flo not found"));
          }
        })
        .catch(callback);
    }
  }, arguments);
};

// inject

// Inject and run a Flo identified by @flo with @data mocking the first step.
// @flo is a string representing the flo name.
// If you try to inject a flo whose alias cannot be found the client will first attempt to refresh
// the flos cache once. Failing that it will call the callback or reject the promise with an error.
// If the optional parameter @force is true then it will attempt to inject the flo without
// using the cache. Use this to avoid an extra network request if you already know the alias.
Azuqua.prototype.inject = function(_flo, _data, _force, _callback){
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;
    if(alias){
      var options = _.extend({}, routes.inject);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }else{
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.inject(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

Azuqua.prototype.schedule = function(_flo, _data, _force, _callback) {
  var self = this;
  return wrapAsyncFunction(function(flo, data, force, callback){
    if(typeof force === "function" && !callback){
      callback = force;
      force = false;
    }

    var alias = getAlias(self.floMap || {}, flo);
    if(!alias && force)
      alias = flo;

    if(alias){
      var options = _.extend({}, routes.schedule);
      options.path = options.path.replace(":id", alias);
      self.makeRequest(options, data, callback);
    }
    else {
      self.flos(true).then(function(){
        alias = getAlias(self.floMap, flo);
        if(alias)
          self.schedule(alias, data, callback);
        else
          callback(new Error("Flo not found"));
      }, callback);
    }
  }, arguments);
};

module.exports = Azuqua;

<|endoftext|>


<commit_before>'use strict';

var jwt    = require('jsonwebtoken');
var utils  = require('../../utils');
var config = require('../index');

var User           = require('mongoose').model('user');
var Session        = require('mongoose').model('session');
var BearerStrategy = require('passport-http-bearer').Strategy;

/**
 * Authenticate the requestee as a 'user' based on the passed 'Bearer' token.
 */
module.exports = new BearerStrategy(function(token, done) {

	Session.findOne({token: token}, function(err, session) {

		if(err) {
			return done(utils.error(500, err));
		}

		if(!session) {
			return done(null, null, 'Session not valid');
		}

		User.findOne({ '_id': session.user }, function(err, user) {
			if(err) {
				return done(utils.error(500, err));
			}

			if(!user) {
				return done(null, null, 'User not found');
			}

			jwt.verify(token, config.token.secret, function(err, decoded) {
				if(err) {
					return done(null, null, err);
				}

				return done(null, {
					id:        user.id,
					type:      user.account_type,
					username:  user.name,
					providers: user.providers,
					boards:    user.boards,
					edited_at: user.edited_at,
					avatar:    user.avatar
				});
			});
		});

	});
});
<commit_msg>Changed user strategy to return provider names instead of object<commit_after>'use strict';

var jwt    = require('jsonwebtoken');
var utils  = require('../../utils');
var config = require('../index');

var User           = require('mongoose').model('user');
var Session        = require('mongoose').model('session');
var BearerStrategy = require('passport-http-bearer').Strategy;

/**
 * Authenticate the requestee as a 'user' based on the passed 'Bearer' token.
 */
module.exports = new BearerStrategy(function(token, done) {

	Session.findOne({token: token}, function(err, session) {

		if(err) {
			return done(utils.error(500, err));
		}

		if(!session) {
			return done(null, null, 'Session not valid');
		}

		User.findOne({ '_id': session.user }, function(err, user) {
			if(err) {
				return done(utils.error(500, err));
			}

			if(!user) {
				return done(null, null, 'User not found');
			}

			jwt.verify(token, config.token.secret, function(err, decoded) {
				if(err) {
					return done(null, null, err);
				}

				return done(null, {
					id:        user.id,
					type:      user.account_type,
					username:  user.name,
					providers: Object.keys(user.providers),
					boards:    user.boards,
					edited_at: user.edited_at,
					avatar:    user.avatar
				});
			});
		});

	});
});
<|endoftext|>


<commit_before>var config = require('./config')
var engine = require('./engine')
var hooks = require('./hooks')
var log = require('./log').logger('beacon')

var request = require('request')
var Q = require('q');

var Beacon = function(options) {
	var options = options || {}
	this.url = options.url
	if(!this.url) { throw new Error('Beacon needs a URL.'); }
	this.interval = options.interval || 1*60*60*1000;
	this._timer = null;
	this.running = false;
	this.consent_for_beacon = "false";
	this.localAddresses = []
}

function setURL(url) { this.set('url', url); }
function setInterval(interval) { this.set('interval', interval); }

// Changing any of the options provokes a new beacon report
Beacon.prototype.set = function(key, value) {
	this[key] = value;
	var wasRunning = this.running;
	this.stop();
	if(wasRunning) { this.run('config'); }
}

// Start reporting with the beacon
Beacon.prototype.start = function(reason) {
	this.running = true;
	this.run(reason);
}

// Function called at intervals to report to the beacon
Beacon.prototype.run = function(reason) {
	if (this.consent_for_beacon === "true"){
		this.report(reason)
			.catch(function(err) {
				log.warn('Could not send a beacon message: ' + err);
			})
			.finally(function() {
				this._timer = setTimeout(this.run.bind(this), this.interval);
			}.bind(this));
	} else {
		log.warn('Beacon is not enabled');
	}
}

// Stops further beacon reports. Reports can be restarted with start()
Beacon.prototype.stop = function() {
	this.running = false;
	if(this._timer) {
		clearTimeout(this._timer);
	}
}

Beacon.prototype.once = function(reason) {
	var wasRunning = this.running;
	this.stop();
	this.start(reason);
}

// Return a promise that fulfills with the beacon message to be sent
Beacon.prototype.createMessage = function(reason) {
	var msg = {
		id : config.updater.get('id'),
		name : config.updater.get('name'),
		os : config.platform,
		platform : config.updater.get('platform'),
		os_version : config.updater.get('os_version'),
		reason : reason || 'interval',
		local_ips : []
	}

	this.localAddresses.forEach(function(idx, addr) {
		msg.local_ips.push({'address':addr});
	});

	var deferred = Q.defer()
	try {
		engine.getVersion(function(err, version) {
			if(err) {
				msg.engine_version = {};
				log.warn("Engine version could not be determined");
				log.warn(err);
			} else {
				msg.engine_version = version;
			}
			require('./updater').getVersion(function(err, version) {
				if(err) {
					msg.updater_version = {};
					log.warn("Updater version could not be determined");
					log.warn(err);
				} else {
					msg.updater_version = version
				}
				deferred.resolve(msg);
			})
		})
	} catch(e) {
		deferred.reject(e)
	}
	return deferred.promise
}

// Return a promise that resolves when the beacon server has been contacted
Beacon.prototype.report = function(reason) {
	deferred = Q.defer()
	if(this.url) {
		log.info('Sending beacon report (' + (reason || 'interval') + ') to ' + this.url);
		return this.createMessage(reason)
		.then(function(message) {
			request({uri : this.url, json : true,body : message, method : 'POST'}, function(err, response, body) {
				if(err) {
					log.warn('Could not send message to beacon server: ' + err);
					deferred.reject(err);
				} else if(response.statusCode != 200) {
					if(response.statusCode === 301) {
						if(response.headers.location) {
							log.warn('Beacon URL has changed.  Updating configuration to new URL: ' + response.headers.location)
							config.updater.set('beacon_url', response.headers.location);
							deferred.resolve();
							return;
						}
					}
					var err = new Error("Beacon server responded with status code " + response.statusCode);
					log.warn(err);
					deferred.reject(err);
				} else {
					//log.debug('Beacon response code: ' + response.statusCode)
					//log.debug('Beacon response body: ' + body);
					log.info('Post to beacon server successful.');
					deferred.resolve();
				}
			});
		}.bind(this)).catch(function(err){
			log.error(err)
			deferred.reject(err);
		}.bind(this))
	} else {
		deferred.resolve();
	}

	return deferred.promise()
}

Beacon.prototype.setLocalAddresses = function(localAddresses) {
	this.localAddresses = localAddresses;
}

module.exports = Beacon
<commit_msg>Fix address iteration<commit_after>var config = require('./config')
var engine = require('./engine')
var hooks = require('./hooks')
var log = require('./log').logger('beacon')

var request = require('request')
var Q = require('q');

var Beacon = function(options) {
	var options = options || {}
	this.url = options.url
	if(!this.url) { throw new Error('Beacon needs a URL.'); }
	this.interval = options.interval || 1*60*60*1000;
	this._timer = null;
	this.running = false;
	this.consent_for_beacon = "false";
	this.localAddresses = []
}

function setURL(url) { this.set('url', url); }
function setInterval(interval) { this.set('interval', interval); }

// Changing any of the options provokes a new beacon report
Beacon.prototype.set = function(key, value) {
	this[key] = value;
	var wasRunning = this.running;
	this.stop();
	if(wasRunning) { this.run('config'); }
}

// Start reporting with the beacon
Beacon.prototype.start = function(reason) {
	this.running = true;
	this.run(reason);
}

// Function called at intervals to report to the beacon
Beacon.prototype.run = function(reason) {
	if (this.consent_for_beacon === "true"){
		this.report(reason)
			.catch(function(err) {
				log.warn('Could not send a beacon message: ' + err);
			})
			.finally(function() {
				this._timer = setTimeout(this.run.bind(this), this.interval);
			}.bind(this));
	} else {
		log.warn('Beacon is not enabled');
	}
}

// Stops further beacon reports. Reports can be restarted with start()
Beacon.prototype.stop = function() {
	this.running = false;
	if(this._timer) {
		clearTimeout(this._timer);
	}
}

Beacon.prototype.once = function(reason) {
	var wasRunning = this.running;
	this.stop();
	this.start(reason);
}

// Return a promise that fulfills with the beacon message to be sent
Beacon.prototype.createMessage = function(reason) {
	var msg = {
		id : config.updater.get('id'),
		name : config.updater.get('name'),
		os : config.platform,
		platform : config.updater.get('platform'),
		os_version : config.updater.get('os_version'),
		reason : reason || 'interval',
		local_ips : []
	}

	this.localAddresses.forEach(function(addr, idx) {
		msg.local_ips.push({'address':addr});
	});

	var deferred = Q.defer()
	try {
		engine.getVersion(function(err, version) {
			if(err) {
				msg.engine_version = {};
				log.warn("Engine version could not be determined");
				log.warn(err);
			} else {
				msg.engine_version = version;
			}
			require('./updater').getVersion(function(err, version) {
				if(err) {
					msg.updater_version = {};
					log.warn("Updater version could not be determined");
					log.warn(err);
				} else {
					msg.updater_version = version
				}
				deferred.resolve(msg);
			})
		})
	} catch(e) {
		deferred.reject(e)
	}
	return deferred.promise
}

// Return a promise that resolves when the beacon server has been contacted
Beacon.prototype.report = function(reason) {
	deferred = Q.defer()
	if(this.url) {
		log.info('Sending beacon report (' + (reason || 'interval') + ') to ' + this.url);
		return this.createMessage(reason)
		.then(function(message) {
			//console.log(message)
			request({uri : this.url, json : true,body : message, method : 'POST'}, function(err, response, body) {
				if(err) {
					log.warn('Could not send message to beacon server: ' + err);
					deferred.reject(err);
				} else if(response.statusCode != 200) {
					if(response.statusCode === 301) {
						if(response.headers.location) {
							log.warn('Beacon URL has changed.  Updating configuration to new URL: ' + response.headers.location)
							config.updater.set('beacon_url', response.headers.location);
							deferred.resolve();
							return;
						}
					}
					var err = new Error("Beacon server responded with status code " + response.statusCode);
					log.warn(err);
					deferred.reject(err);
				} else {
					//log.debug('Beacon response code: ' + response.statusCode)
					//log.debug('Beacon response body: ' + body);
					log.info('Post to beacon server successful.');
					deferred.resolve();
				}
			});
		}.bind(this)).catch(function(err){
			log.error(err)
			deferred.reject(err);
		}.bind(this))
	} else {
		deferred.resolve();
	}

	return deferred.promise()
}

Beacon.prototype.setLocalAddresses = function(localAddresses) {
	this.localAddresses = localAddresses;
}

module.exports = Beacon
<|endoftext|>


<commit_before>var http = require('http');
var fs = require('fs');
var util = require('util');
// var qs = require('querystring');
// var child = require('child_process');
var sutil = require('./lib/client-utils');
var serialport = require("serialport");
var SerialPort = serialport.SerialPort;

// base config for beagle
var config =  {
    cloudHost: 'ninj.herokuapp.com',
    cloudPort: 80,
    devtty: "/dev/ttyO1",
    serialfile: "/utilities/etc/serial.conf",
    heartbeat_interval: 500
}
// commandline config
if (process.argv[2] == 'local') {
    config.cloudHost = 'localhost';
    config.cloudPort = '3000';
}
if (process.argv[3] == 'fddi') {
    config.devtty = "/dev/tty.usbserial-AE01AAE3";
    config.serialfile = "/Users/pete/work/ninj/client/serialnumber";
}
console.log(config);

var tty = new SerialPort(config.devtty, {
    parser: serialport.parsers.readline("\n")
});

var nodedetails = {}
fs.readFile(config.serialfile,'ascii',function(err,data){
    nodedetails["id"] = data.replace(/\n/g,"");
    // Set here because of a race condition
    cmdOptions.path += nodedetails.id;
    console.log(cmdOptions);
    longpoll()
})
nodedetails["token"] = "1234123412341234"; // TODO

var readings = {};
tty.on('data',function(data){
    // console.log(data); // the almost raw serial data
    var nm = sutil.getJSON(data) || false;
    if (nm) {
        // only keep latest reading per device between heartbeats
        for (var x=0; x<nm.DEVICE.length; x++) {
            nm.DEVICE[x].GUID = nodedetails.id+'_'+nm.DEVICE[x].G+'_'+nm.DEVICE[x].V+'_'+nm.DEVICE[x].D;
            readings[nm.DEVICE[x].GUID] = nm.DEVICE[x];
        }
    }
});

var getHeartbeat = function(){
    hb = {  "NODE_ID":nodedetails.id,
            "TOKEN": nodedetails.token,
            "TIMESTAMP": null,
            "DEVICE":[] }
    hb.TIMESTAMP = new Date().getTime();
    for (r in readings) {
        hb.DEVICE.unshift(readings[r]);
        delete readings[r];
    }
    return JSON.stringify(hb);
}

var postOptions = {
    host: config.cloudHost,
    port: config.cloudPort,
    path: '/heartbeats',
    method: 'POST',
    headers: { 'content-type': 'application/json'}
}
var emptyBeats = 0;
var beatThrottle = {
    isGoodToGo : function() {
        if (!sutil.isEmpty(readings) || beatThrottle.counter>beatThrottle.rate) {
            beatThrottle.counter = 0;
            return true;
        } else {
            beatThrottle.counter++;
            return false;
        }
    },
    rate : 10000/config.heartbeat_interval,
    counter: 0
}
var longPost = function(){
    var request = http.request(postOptions, function(res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            console.log('LongPost received a body: '+chunk);
        });
        res.on('end', function(){
            setTimeout(longPost,100);
        })
    });
    var sendBeats = setInterval(function(){
        // Only send empty heartbeats every 10s
        // TODO reduce frequency of identical heartbeats
        if (beatThrottle.isGoodToGo()) {
            request.write(getHeartbeat());
        }
    },config.heartbeat_interval);
    setTimeout(function(){
        // restart connection after 120s
        clearInterval(sendBeats);
        request.end();
    },120000)
    // console.log('connection');
    request.on('error', function(e){
        console.log('Longpost Error: '+e);
        setTimeout(longPost,1000);
    });
}
longPost();

var cmdOptions = {
    host: config.cloudHost,
    port: config.cloudPort,
    path: '/commands/',
    method: 'GET'
}
var longpoll = function(){
    http.get(cmdOptions, function (http_res) {
        http_res.on("data", function (data) {
            executeCommand(data);
        });
        http_res.on("end", function () {
            longpoll();
        });
        http_res.on("close", function () {
            longpoll();
        });
        http_res.on("error", function () {
            setTimeout(longpoll,5000)
        });

    }).on('error',function(err){
        console.log('Error in longpoll request: '+err);
        setTimeout(longpoll,5000)
    });
}

var executeCommand = function(data){
    var ds = sutil.getJSON(data).DEVICE;
    if (ds && ds.length>0) {
        for (d in ds) {
            delete ds[d].GUID;
            if (ds[d].G == 0 ) ds[d].G = ''; //TODO get JP to fix for 0
            sutil.writeTTY(tty,'{"DEVICE":['+JSON.stringify(ds[d])+']}');
        }
    } else {
        console.log(data.toString());
    }
}

process.on('uncaughtException', function (err) {
  // console.log('Caught Uncaught Exception: ' + util.inspect(err,null,true,true));
});
<commit_msg>Chart fun<commit_after>var http = require('http');
var fs = require('fs');
var util = require('util');
// var qs = require('querystring');
// var child = require('child_process');
var sutil = require('./lib/client-utils');
var serialport = require("serialport");
var SerialPort = serialport.SerialPort;

// base config for beagle
var config =  {
    cloudHost: 'ninj.herokuapp.com',
    cloudPort: 80,
    devtty: "/dev/ttyO1",
    serialfile: "/utilities/etc/serial.conf",
    heartbeat_interval: 500
}
// commandline config
if (process.argv[2] == 'local') {
    config.cloudHost = '10.10.0.65';
    config.cloudPort = '3000';
}
if (process.argv[3] == 'ftdi') {
    config.devtty = "/dev/tty.usbserial-AE01AAE3";
    config.serialfile = "/Users/pete/work/ninj/client/serialnumber";
}
console.log(config);

var tty = new SerialPort(config.devtty, {
    parser: serialport.parsers.readline("\n")
});

var nodedetails = {}
fs.readFile(config.serialfile,'ascii',function(err,data){
    nodedetails["id"] = data.replace(/\n/g,"");
    // Set here because of a race condition
    cmdOptions.path += nodedetails.id;
    console.log(cmdOptions);
    longpoll()
})
nodedetails["token"] = "1234123412341234"; // TODO

var readings = {};
tty.on('data',function(data){
    // console.log(data); // the almost raw serial data
    var nm = sutil.getJSON(data) || false;
    if (nm) {
        // only keep latest reading per device between heartbeats
        for (var x=0; x<nm.DEVICE.length; x++) {
            nm.DEVICE[x].GUID = nodedetails.id+'_'+nm.DEVICE[x].G+'_'+nm.DEVICE[x].V+'_'+nm.DEVICE[x].D;
            readings[nm.DEVICE[x].GUID] = nm.DEVICE[x];
        }
    }
});

var getHeartbeat = function(){
    hb = {  "NODE_ID":nodedetails.id,
            "TOKEN": nodedetails.token,
            "TIMESTAMP": null,
            "DEVICE":[] }
    hb.TIMESTAMP = new Date().getTime();
    for (r in readings) {
        hb.DEVICE.unshift(readings[r]);
        delete readings[r];
    }
    return JSON.stringify(hb);
}

var postOptions = {
    host: config.cloudHost,
    port: config.cloudPort,
    path: '/heartbeats',
    method: 'POST',
    headers: { 'content-type': 'application/json'}
}
var emptyBeats = 0;
var beatThrottle = {
    isGoodToGo : function() {
        if (!sutil.isEmpty(readings) || beatThrottle.counter>beatThrottle.rate) {
            beatThrottle.counter = 0;
            return true;
        } else {
            beatThrottle.counter++;
            return false;
        }
    },
    rate : 10000/config.heartbeat_interval,
    counter: 0
}
var longPost = function(){
    var request = http.request(postOptions, function(res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            console.log('LongPost received a body: '+chunk);
        });
        res.on('end', function(){
            setTimeout(longPost,100);
        })
    });
    var sendBeats = setInterval(function(){
        // Only send empty heartbeats every 10s
        // TODO reduce frequency of identical heartbeats
        if (beatThrottle.isGoodToGo()) {
            request.write(getHeartbeat());
        }
    },config.heartbeat_interval);
    setTimeout(function(){
        // restart connection after 120s
        clearInterval(sendBeats);
        request.end();
    },120000)
    // console.log('connection');
    request.on('error', function(e){
        console.log('Longpost Error: '+e);
        setTimeout(longPost,1000);
    });
}
longPost();

var cmdOptions = {
    host: config.cloudHost,
    port: config.cloudPort,
    path: '/commands/',
    method: 'GET'
}
var longpoll = function(){
    http.get(cmdOptions, function (http_res) {
        http_res.on("data", function (data) {
            executeCommand(data);
        });
        http_res.on("end", function () {
            longpoll();
        });
        http_res.on("close", function () {
            longpoll();
        });
        http_res.on("error", function () {
            setTimeout(longpoll,5000)
        });

    }).on('error',function(err){
        console.log('Error in longpoll request: '+err);
        setTimeout(longpoll,5000)
    });
}

var executeCommand = function(data){
    var ds = sutil.getJSON(data).DEVICE;
    if (ds && ds.length>0) {
        for (d in ds) {
            delete ds[d].GUID;
            ds[d].G = ds[d].G.toString(); //TODO get JP to fix for 0
            sutil.writeTTY(tty,'{"DEVICE":['+JSON.stringify(ds[d])+']}');
        }
    } else {
        console.log(data.toString());
    }
}

process.on('uncaughtException', function (err) {
  // console.log('Caught Uncaught Exception: ' + util.inspect(err,null,true,true));
});
<|endoftext|>


<commit_before>var s3 = require('s3');
var conf = require('./aws-credentials.json')
var pkg = require('../package.json')

console.log(pkg.version)

var client = s3.createClient({
  s3Options: conf
});

var destination = "files/calcite-web/" + "version-foo" + "/"

var versioned = {
  localDir: "./dist",
  s3Params: {
    ACL: "public-read",
    Bucket: "patterns.esri.com",
    Prefix: destination
  }
};

var latest = {
  localDir: "./dist",
  s3Params: {
    ACL: "public-read",
    Bucket: "patterns.esri.com",
    Prefix: "files/calcite-web/latest/"
  }
};

var uploader = client.uploadDir(versioned);

uploader.on('error', function(err) {
  console.error("unable to sync:", err.stack);
});

uploader.on('progress', function() {
  console.log("progress", uploader.progressAmount, uploader.progressTotal);
});

uploader.on('end', function() {
  console.log("done uploading");
});

<commit_msg>final s3 readyness<commit_after>var s3 = require('s3');
var conf = require('./aws-credentials.json')
var pkg = require('../package.json')

var client = s3.createClient({
  s3Options: conf
});

var destination = "files/calcite-web/" + pkg.version + "/"

var versioned = {
  localDir: "./dist",
  s3Params: {
    ACL: "public-read",
    Bucket: "patterns.esri.com",
    Prefix: destination
  }
};

var latest = {
  localDir: "./dist",
  s3Params: {
    ACL: "public-read",
    Bucket: "patterns.esri.com",
    Prefix: "files/calcite-web/latest/"
  }
};

var uploader = client.uploadDir(versioned);

uploader.on('error', function(err) {
  console.error("unable to sync:", err.stack);
});

uploader.on('progress', function() {
  console.log("progress", uploader.progressAmount, uploader.progressTotal);
});

uploader.on('end', function() {
  console.log("done uploading");
});

var latestUpload = client.uploadDir(latest);

uploader.on('error', function(err) {
  console.error("unable to sync:", err.stack);
});

uploader.on('progress', function() {
  console.log("progress", uploader.progressAmount, uploader.progressTotal);
});

uploader.on('end', function() {
  console.log("done uploading");
});<|endoftext|>