'use strict';
var inherits = require('inherits'),
events = require('events'),
_ = require('lodash/core'),
Promise = require('./promise');
/**
* HTTP based API class with authorization hook
*
* @constructor
* @extends events.EventEmitter
* @param {Connection} conn - Connection object
* @param {Object} [options] - Http API Options
* @param {String} [options.responseType] - Overriding content mime-type in response
* @param {Transport} [options.transport] - Transport for http api
* @param {Object} [options.noContentResponse] - Alternative response when no content returned in response (= HTTP 204)
*/
var HttpApi = function(conn, options) {
options = options || {};
this._conn = conn;
this.on('resume', function(err) { conn.emit('resume', err); });
this._responseType = options.responseType;
this._transport = options.transport || conn._transport;
this._noContentResponse = options.noContentResponse;
};
inherits(HttpApi, events.EventEmitter);
/**
* Callout to API endpoint using http
*
* @param {Object} request - Http Request object
* @param {String} request.url - Endpoint URL to request
* @param {String} request.method - Http method for request
* @param {Object} [request.headers] - Http request headers in hash object
* @param {Callback.<Object>} callback - Callback function
* @returns {Promise.<Object>} -
*/
HttpApi.prototype.request = function(request, callback) {
var self = this;
var conn = this._conn;
var logger = conn._logger;
var refreshDelegate = this.getRefreshDelegate();
// remember previous instance url in case it changes after a refresh
var lastInstanceUrl = conn.instanceUrl;
var deferred = Promise.defer();
var onResume = function(err) {
if (err) {
deferred.reject(err);
return;
}
// check to see if the token refresh has changed the instance url
if(lastInstanceUrl !== conn.instanceUrl){
// if the instance url has changed
// then replace the current request urls instance url fragment
// with the updated instance url
request.url = request.url.replace(lastInstanceUrl,conn.instanceUrl);
}
self.request(request).then(function(response) {
deferred.resolve(response);
}, function(err) {
deferred.reject(err);
});
};
if (refreshDelegate && refreshDelegate._refreshing) {
refreshDelegate.once('resume', onResume);
return deferred.promise.thenCall(callback);
}
// hook before sending
self.beforeSend(request);
self.emit('request', request);
logger.debug("<request> method=" + request.method + ", url=" + request.url);
var requestTime = Date.now();
return this._transport.httpRequest(request).then(function(response) {
var responseTime = Date.now();
logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
logger.debug("<response> status=" + response.statusCode + ", url=" + request.url);
self.emit('response', response);
// Refresh token if session has been expired and requires authentication
// when session refresh delegate is available
if (self.isSessionExpired(response) && refreshDelegate) {
refreshDelegate.refresh(requestTime, onResume);
return deferred.promise;
}
if (self.isErrorResponse(response)) {
var err = self.getError(response);
throw err;
}
return self.getResponseBody(response);
}, function(err) {
var responseTime = Date.now();
logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
logger.error(err);
throw err;
})
.thenCall(callback);
};
/**
* @protected
*/
HttpApi.prototype.getRefreshDelegate = function() {
return this._conn._refreshDelegate;
};
/**
*
* @protected
*/
HttpApi.prototype.beforeSend = function(request) {
request.headers = request.headers || {};
if (this._conn.accessToken) {
request.headers.Authorization = "Bearer " + this._conn.accessToken;
}
if (this._conn.callOptions) {
var callOptions = [];
for (var name in this._conn.callOptions) {
callOptions.push(name + "=" + this._conn.callOptions[name]);
}
request.headers["Sforce-Call-Options"] = callOptions.join(', ');
}
};
/**
* Detect response content mime-type
* @protected
*/
HttpApi.prototype.getResponseContentType = function(response) {
return this._responseType || response.headers && response.headers["content-type"];
};
/**
*
*/
HttpApi.prototype.parseResponseBody = function(response) {
var contentType = this.getResponseContentType(response);
var parseBody = /^(text|application)\/xml(;|$)/.test(contentType) ? parseXML :
/^application\/json(;|$)/.test(contentType) ? parseJSON :
/^text\/csv(;|$)/.test(contentType) ? parseCSV :
parseText;
try {
return parseBody(response.body);
} catch(e) {
return response.body;
}
};
/**
* Get response body
* @protected
*/
HttpApi.prototype.getResponseBody = function(response) {
if (response.statusCode === 204) { // No Content
return this._noContentResponse;
}
var body = this.parseResponseBody(response);
var err;
if (this.hasErrorInResponseBody(body)) {
err = this.getError(response, body);
throw err;
}
if (response.statusCode === 300) { // Multiple Choices
err = new Error('Multiple records found');
err.name = "MULTIPLE_CHOICES";
err.content = body;
throw err;
}
return body;
};
/** @private */
function parseJSON(str) {
return JSON.parse(str);
}
/** @private */
function parseXML(str) {
var ret = {};
require('xml2js').parseString(str, { explicitArray: false }, function(err, result) {
ret = { error: err, result : result };
});
if (ret.error) { throw ret.error; }
return ret.result;
}
/** @private */
function parseCSV(str) {
return require('./csv').parseCSV(str);
}
/** @private */
function parseText(str) { return str; }
/**
* Detect session expiry
* @protected
*/
HttpApi.prototype.isSessionExpired = function(response) {
return response.statusCode === 401;
};
/**
* Detect error response
* @protected
*/
HttpApi.prototype.isErrorResponse = function(response) {
return response.statusCode >= 400;
};
/**
* Detect error in response body
* @protected
*/
HttpApi.prototype.hasErrorInResponseBody = function(body) {
return false;
};
/**
* Parsing error message in response
* @protected
*/
HttpApi.prototype.parseError = function(body) {
var errors = body;
return _.isArray(errors) ? errors[0] : errors;
};
/**
* Get error message in response
* @protected
*/
HttpApi.prototype.getError = function(response, body) {
var error;
try {
error = this.parseError(body || this.parseResponseBody(response));
} catch(e) {}
error = _.isObject(error) && _.isString(error.message) ? error : {
errorCode: 'ERROR_HTTP_' + response.statusCode,
message : response.body
};
var err = new Error(error.message);
err.name = error.errorCode;
for (var key in error) { err[key] = error[key]; }
return err;
};
/*-------------------------------------------------------------------------*/
/**
* @protected
*/
var SessionRefreshDelegate = function(conn, refreshFn) {
this._conn = conn;
this._refreshFn = refreshFn;
this._refreshing = false;
};
inherits(SessionRefreshDelegate, events.EventEmitter);
/**
* Refresh access token
* @private
*/
SessionRefreshDelegate.prototype.refresh = function(since, callback) {
// Callback immediately When refreshed after designated time
if (this._lastRefreshedAt > since) { return callback(); }
var self = this;
var conn = this._conn;
var logger = conn._logger;
self.once('resume', callback);
if (self._refreshing) { return; }
logger.debug("<refresh token>");
self._refreshing = true;
return self._refreshFn(conn, function(err, accessToken, res) {
if (!err) {
logger.debug("Connection refresh completed. Refreshed access token = " + accessToken);
conn.accessToken = accessToken;
conn.emit("refresh", accessToken, res);
}
self._lastRefreshedAt = Date.now();
self._refreshing = false;
self.emit('resume', err);
});
};
/**
*
*/
HttpApi.SessionRefreshDelegate = SessionRefreshDelegate;
module.exports = HttpApi;