UNPKG

jsforce2

Version:

Salesforce API Library for JavaScript

301 lines (267 loc) 8.15 kB
'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("elapsed 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("elapsed 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."); 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;