jsforce
Version:
Salesforce API Library for JavaScript
1,699 lines (1,548 loc) • 541 kB
JavaScript
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.jsforce=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/*global Sfdc */
var stream = require('stream'),
_ = require('underscore');
function parseHeaders(hs) {
var headers = {};
hs.split(/\n/).forEach(function(line) {
var pair = line.split(/\s*:\s*/);
var name = pair[0].toLowerCase();
var value = pair[1];
headers[name] = value;
});
return headers;
}
module.exports = {
supported: typeof Sfdc === 'object' && typeof Sfdc.canvas !== 'undefined',
createRequest: function(signedRequest) {
return function(params, callback) {
var response;
var str = new stream.Duplex();
str._read = function(size) {
if (response) {
str.push(response.body);
}
};
var bufs = [];
var sent = false;
str._write = function(chunk, encoding, callback) {
bufs.push(chunk.toString(encoding));
callback();
};
str.on('finish', function() {
if (!sent) {
send(bufs.join(''));
sent = true;
}
});
if (params.body || params.body === "" || !/^(put|post|patch)$/i.test(params.method)) {
send(params.body);
sent = true;
}
function send(body) {
var settings = {
client: signedRequest.client,
method: params.method,
data: body
};
if (params.headers) {
settings.headers = {};
for (var name in params.headers) {
if (name.toLowerCase() === 'content-type') {
settings.contentType = params.headers[name];
} else {
settings.headers[name] = params.headers[name];
}
}
}
settings.success = function(data) {
var headers = parseHeaders(data.responseHeaders);
var body = data.payload;
if (!_.isString(body)) {
body = JSON.stringify(body);
}
response = {
statusCode : data.status,
headers: headers,
body: body
};
if (callback) {
callback(null, response, response.body);
}
str.end();
};
settings.failure = function(err) {
if (callback) {
callback(err);
}
};
Sfdc.canvas.client.ajax(params.url, settings);
}
return str;
};
}
};
},{"stream":35,"underscore":54}],2:[function(require,module,exports){
/**
* @file Browser client connection management class
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
var events = require('events'),
util = require('util'),
qs = require('querystring'),
_ = require('underscore'),
Connection = require('../connection'),
OAuth2 = require('../oauth2');
/**
* @private
*/
function popupWin(url, w, h) {
var left = (screen.width/2)-(w/2);
var top = (screen.height/2)-(h/2);
return window.open(url, null, 'location=yes,toolbar=no,status=no,menubar=no,width='+w+',height='+h+',top='+top+',left='+left);
}
function handleCallbackResponse() {
var res = checkCallbackResponse();
var state = localStorage.getItem('jsforce_state');
if (res && state && res.body.state === state) {
localStorage.removeItem('jsforce_state');
var states = state.split('.');
var prefix = states[0], promptType = states[1];
var cli = new Client(prefix);
if (res.success) {
cli._storeTokens(res.body);
location.hash = '';
} else {
cli._storeError(res.body);
}
if (promptType === 'popup') { window.close(); }
return true;
}
}
/**
* @private
*/
function checkCallbackResponse() {
var params;
if (window.location.hash) {
params = qs.parse(window.location.hash.substring(1));
if (params.access_token) {
return { success: true, body: params };
}
} else if (window.location.search) {
params = qs.parse(window.location.search.substring(1));
if (params.error) {
return { success: false, body: params };
}
}
}
/** @private **/
var clientIdx = 0;
/**
* @class
* @todo add document
*/
var Client = function(prefix) {
this._prefix = prefix || 'jsforce' + clientIdx++;
this.connection = null;
};
util.inherits(Client, events.EventEmitter);
/**
*
*/
Client.prototype.init = function(config) {
if (handleCallbackResponse()) { return; }
this.config = config;
this.connection = new Connection(config);
var tokens = this._getTokens();
if (tokens) {
this.connection.initialize(tokens);
var self = this;
setTimeout(function() {
self.emit('connect', self.connection);
}, 10);
}
};
/**
*
*/
Client.prototype.login = function(options, callback) {
if (_.isFunction(options)) {
callback = options;
options = {};
}
options = options || {};
callback = callback || function(){ };
_.extend(options, this.config);
var self = this;
this._prompt(options, callback);
};
Client.prototype._prompt = function(options, callback) {
var self = this;
var oauth2 = new OAuth2(options);
var rand = Math.random().toString(36).substring(2);
var state = [ this._prefix, "popup", rand ].join('.');
localStorage.setItem("jsforce_state", state);
var authzUrl = oauth2.getAuthorizationUrl({
response_type: 'token',
scope : options.scope,
state: state
});
var size = options.size || {};
var pw = popupWin(authzUrl, size.width || 912, size.height || 513);
if (!pw) {
state = [ this._prefix, "redirect", rand ].join('.');
localStorage.setItem("jsforce_state", state);
authzUrl = oauth2.getAuthorizationUrl({
response_type: 'token',
scope : options.scope,
state: state
});
location.href = authzUrl;
return;
}
self._removeTokens();
var pid = setInterval(function() {
try {
if (!pw || pw.closed) {
clearInterval(pid);
var tokens = self._getTokens();
if (tokens) {
self.connection.initialize(tokens);
self.emit('connect', self.connection);
callback(null, { status: 'connect' });
} else {
var err = self._getError();
if (err) {
callback(new Error(err.error + ": " + err.error_description));
} else {
callback(null, { status: 'cancel' });
}
}
}
} catch(e) {}
}, 1000);
};
/**
*
*/
Client.prototype.isLoggedIn = function() {
return !!(this.connection && this.connection.accessToken);
};
/**
*
*/
Client.prototype.logout = function() {
this.connection.logout();
this._removeTokens();
this.emit('disconnect');
};
/**
* @private
*/
Client.prototype._getTokens = function() {
var regexp = new RegExp("(^|;\\s*)"+this._prefix+"_loggedin=true(;|$)");
if (document.cookie.match(regexp)) {
var issuedAt = Number(localStorage.getItem(this._prefix+'_issued_at'));
if (Date.now() < issuedAt + 2 * 60 * 60 * 1000) { // 2 hours
var userInfo;
var idUrl = localStorage.getItem(this._prefix + '_id');
if (idUrl) {
var ids = idUrl.split('/');
userInfo = { id: ids.pop(), organizationId: ids.pop(), url: idUrl };
}
return {
accessToken: localStorage.getItem(this._prefix + '_access_token'),
instanceUrl: localStorage.getItem(this._prefix + '_instance_url'),
userInfo: userInfo
};
}
}
return null;
};
/**
* @private
*/
Client.prototype._storeTokens = function(params) {
localStorage.setItem(this._prefix + '_access_token', params.access_token);
localStorage.setItem(this._prefix + '_instance_url', params.instance_url);
localStorage.setItem(this._prefix + '_issued_at', params.issued_at);
localStorage.setItem(this._prefix + '_id', params.id);
document.cookie = this._prefix + '_loggedin=true;';
};
/**
* @private
*/
Client.prototype._removeTokens = function() {
localStorage.removeItem(this._prefix + '_access_token');
localStorage.removeItem(this._prefix + '_instance_url');
localStorage.removeItem(this._prefix + '_issued_at');
localStorage.removeItem(this._prefix + '_id');
document.cookie = this._prefix + '_loggedin=';
};
/**
* @private
*/
Client.prototype._getError = function() {
try {
var err = JSON.parse(localStorage.getItem(this._prefix + '_error'));
localStorage.removeItem(this._prefix + '_error');
return err;
} catch(e) {}
};
/**
* @private
*/
Client.prototype._storeError = function(err) {
localStorage.setItem(this._prefix + '_error', JSON.stringify(err));
};
/**
*
*/
module.exports = new Client();
module.exports.Client = Client;
},{"../connection":7,"../oauth2":13,"events":29,"querystring":34,"underscore":54,"util":38}],3:[function(require,module,exports){
var jsforce = require('../jsforce');
var crequire;
// core-require code will be automatically generated in grunt process before making browser build
try {
crequire = require('../core-require');
} catch(e) {}
jsforce.browser = require('./client');
jsforce.modules = jsforce.modules || {};
jsforce.require = function(name) {
try {
return crequire(name);
} catch(e) {
if (name === "jsforce" || name === "./jsforce") { return jsforce; }
if (name.substring(0, 2) === "./") {
var paths = name.substring(2).split("/");
var p = paths.pop();
paths.push(p.replace(/(^|\-)([a-zA-Z])/g, function(_0, _1, _2) {
return _2.toUpperCase();
}));
var o = jsforce.modules;
while (o && paths.length > 0) {
o = o[paths.shift()];
}
return o;
}
}
};
module.exports = jsforce;
},{"../core-require":8,"../jsforce":11,"./client":2}],4:[function(require,module,exports){
/*global window, document */
var _index = 0;
module.exports = {
supported: typeof window !== 'undefined',
createRequest: function(jsonpParam, timeout) {
jsonpParam = jsonpParam || 'callback';
timeout = timeout || 10000;
return function(params, callback) {
if (params.method.toUpperCase() !== 'GET') {
return callback(new Error('JSONP only supports GET request.'));
}
var cbFuncName = '_jsforce_jsonpCallback_' + (++_index);
var callbacks = window;
var url = params.url;
url += url.indexOf('?')>0 ? '&' : '?';
url += jsonpParam + '=' + cbFuncName;
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
document.documentElement.appendChild(script);
var pid = setTimeout(function() {
cleanup();
callback(new Error("JSONP call time out."));
}, timeout);
callbacks[cbFuncName] = function(res) {
cleanup();
callback(null, {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify(res)
});
};
var cleanup = function() {
clearTimeout(pid);
document.documentElement.removeChild(script);
delete callbacks[cbFuncName];
};
};
}
};
},{}],5:[function(require,module,exports){
var stream = require('stream');
module.exports = function(params, callback) {
var xhr = new XMLHttpRequest();
xhr.open(params.method, params.url);
if (params.headers) {
for (var header in params.headers) {
xhr.setRequestHeader(header, params.headers[header]);
}
}
xhr.setRequestHeader("Accept", "*/*");
var response;
var str = new stream.Duplex();
str._read = function(size) {
if (response) {
str.push(response.body);
}
};
var bufs = [];
var sent = false;
str._write = function(chunk, encoding, callback) {
bufs.push(chunk.toString(encoding === "buffer" ? "binary" : encoding));
callback();
};
str.on('finish', function() {
if (!sent) {
xhr.send(bufs.join(''));
sent = true;
}
});
if (params.body || params.body === "" || !/^(put|post|patch)$/i.test(params.method)) {
xhr.send(params.body);
sent = true;
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
var headers = {
"content-type": xhr.getResponseHeader("content-type")
};
response = {
statusCode: xhr.status,
headers: headers,
body: xhr.response
};
if (!response.statusCode) {
response.statusCode = 400;
response.body = "Access Declined";
}
if (callback) {
callback(null, response, response.body);
}
str.end();
}
};
return str;
};
},{"stream":35}],6:[function(require,module,exports){
/**
* @file Manages asynchronous method response cache
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
var events = require('events'),
util = require('util'),
_ = require('underscore')._;
/**
* Class for managing cache entry
*
* @private
* @class
* @constructor
* @template T
*/
var CacheEntry = function() {
this.fetching = false;
};
util.inherits(CacheEntry, events.EventEmitter);
/**
* Get value in the cache entry
*
* @param {Callback.<T>} [callback] - Callback function callbacked the cache entry updated
* @returns {T|undefined}
*/
CacheEntry.prototype.get = function(callback) {
if (!callback) {
return this._value;
} else {
this.once('value', callback);
if (!_.isUndefined(this._value)) {
this.emit('value', this._value);
}
}
};
/**
* Set value in the cache entry
*
* @param {T} [value] - A value for caching
*/
CacheEntry.prototype.set = function(value) {
this._value = value;
this.emit('value', this._value);
};
/**
* Clear cached value
*/
CacheEntry.prototype.clear = function() {
this.fetching = false;
delete this._value;
};
/**
* Caching manager for async methods
*
* @class
* @constructor
*/
var Cache = function() {
this._entries = {};
};
/**
* retrive cache entry, or create if not exists.
*
* @param {String} [key] - Key of cache entry
* @returns {CacheEntry}
*/
Cache.prototype.get = function(key) {
if (key && this._entries[key]) {
return this._entries[key];
} else {
var entry = new CacheEntry();
this._entries[key] = entry;
return entry;
}
};
/**
* clear cache entries prefix matching given key
* @param {String} [key] - Key prefix of cache entry to clear
*/
Cache.prototype.clear = function(key) {
for (var k in this._entries) {
if (!key || k.indexOf(key) === 0) {
this._entries[k].clear();
}
}
};
/**
* create and return cache key from namespace and serialized arguments.
* @private
*/
function createCacheKey(namespace, args) {
args = Array.prototype.slice.apply(args);
return namespace + '(' + _.map(args, function(a){ return JSON.stringify(a); }).join(',') + ')';
}
/**
* Enable caching for async call fn to intercept the response and store it to cache.
* The original async calll fn is always invoked.
*
* @protected
* @param {Function} fn - Function to covert cacheable
* @param {Object} [scope] - Scope of function call
* @param {Object} [options] - Options
* @return {Function} - Cached version of function
*/
Cache.prototype.makeResponseCacheable = function(fn, scope, options) {
var cache = this;
options = options || {};
return function() {
var args = Array.prototype.slice.apply(arguments);
var callback = args.pop();
if (!_.isFunction(callback)) {
args.push(callback);
callback = null;
}
var key = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, args) :
createCacheKey(options.namespace, args);
var entry = cache.get(key);
entry.fetching = true;
if (callback) {
args.push(function(err, result) {
entry.set({ error: err, result: result });
callback(err, result);
});
}
var ret, error;
try {
ret = fn.apply(scope || this, args);
} catch(e) {
error = e;
}
if (ret && _.isFunction(ret.then)) { // if the returned value is promise
if (!callback) {
return ret.then(function(result) {
entry.set({ error: undefined, result: result });
return result;
}, function(err) {
entry.set({ error: err, result: undefined });
throw err;
});
} else {
return ret;
}
} else {
entry.set({ error: error, result: ret });
if (error) { throw error; }
return ret;
}
};
};
/**
* Enable caching for async call fn to lookup the response cache first, then invoke original if no cached value.
*
* @protected
* @param {Function} fn - Function to covert cacheable
* @param {Object} [scope] - Scope of function call
* @param {Object} [options] - Options
* @return {Function} - Cached version of function
*/
Cache.prototype.makeCacheable = function(fn, scope, options) {
var cache = this;
options = options || {};
var $fn = function() {
var args = Array.prototype.slice.apply(arguments);
var callback = args.pop();
if (!_.isFunction(callback)) {
args.push(callback);
}
var key = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, args) :
createCacheKey(options.namespace, args);
var entry = cache.get(key);
if (!_.isFunction(callback)) { // if callback is not given in last arg, return cached result (immediate).
var value = entry.get();
if (!value) { throw new Error('Function call result is not cached yet.'); }
if (value.error) { throw value.error; }
return value.result;
}
entry.get(function(value) {
callback(value.error, value.result);
});
if (!entry.fetching) { // only when no other client is calling function
entry.fetching = true;
args.push(function(err, result) {
entry.set({ error: err, result: result });
});
fn.apply(scope || this, args);
}
};
$fn.clear = function() {
var key = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, arguments) :
createCacheKey(options.namespace, arguments);
cache.clear(key);
};
return $fn;
};
module.exports = Cache;
},{"events":29,"underscore":54,"util":38}],7:[function(require,module,exports){
(function (Buffer){
/*global Buffer */
/**
* @file Connection class to keep the API session information and manage requests
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
var events = require('events'),
util = require('util'),
async = require('async'),
_ = require('underscore')._,
Promise = require('./promise'),
Logger = require('./logger'),
OAuth2 = require('./oauth2'),
Query = require('./query'),
SObject = require('./sobject'),
Transport = require('./transport'),
Cache = require('./cache');
var defaults = {
loginUrl: "https://login.salesforce.com",
instanceUrl: "",
version: "31.0"
};
/**
* Connection class to keep the API session information and manage requests
*
* @constructor
* @extends events.EventEmitter
* @param {Object} [options] - Connection options
* @param {OAuth2|Object} [options.oauth2] - OAuth2 instance or options to be passed to OAuth2 constructor
* @param {String} [options.logLevel] - Output logging level (DEBUG|INFO|WARN|ERROR|FATAL)
* @param {String} [options.version] - Salesforce API Version (without "v" prefix)
* @param {Number} [options.maxRequest] - Max number of requests allowed in parallel call
* @param {String} [options.loginUrl] - Salesforce Login Server URL (e.g. https://login.salesforce.com/)
* @param {String} [options.instanceUrl] - Salesforce Instance URL (e.g. https://na1.salesforce.com/)
* @param {String} [options.serverUrl] - Salesforce SOAP service endpoint URL (e.g. https://na1.salesforce.com/services/Soap/u/28.0)
* @param {String} [options.accessToken] - Salesforce OAuth2 access token
* @param {String} [options.sessionId] - Salesforce session ID
* @param {String} [options.refreshToken] - Salesforce OAuth2 refresh token
* @param {String|Object} [options.signedRequest] - Salesforce Canvas signed request (Raw Base64 string, JSON string, or deserialized JSON)
* @param {String} [options.proxyUrl] - Cross-domain proxy server URL, used in browser client, non Visualforce app.
*/
var Connection = module.exports = function(options) {
var Bulk = jsforce.require('./api/bulk'),
Streaming = jsforce.require('./api/streaming'),
Tooling = jsforce.require('./api/tooling'),
Analytics = jsforce.require('./api/analytics'),
Chatter = jsforce.require('./api/chatter'),
Apex = jsforce.require('./api/apex'),
Metadata = jsforce.require('./api/metadata');
options = options || {};
this._logger = new Logger(options.logLevel);
var oauth2 = options.oauth2 || {
loginUrl : options.loginUrl,
clientId : options.clientId,
clientSecret : options.clientSecret,
redirectUri : options.redirectUri
};
/**
* OAuth2 object
* @member {OAuth2} Connection#oauth2
*/
this.oauth2 = oauth2 instanceof OAuth2 ? oauth2 : new OAuth2(oauth2);
this.loginUrl = options.loginUrl || oauth2.loginUrl || defaults.loginUrl;
this.version = options.version || defaults.version;
this.maxRequest = options.maxRequest || this.maxRequest || 10;
/** @private */
this._transport =
options.proxyUrl ? new Transport.ProxyTransport(options.proxyUrl) : new Transport();
/**
* Streaming API object
* @member {Streaming} Connection#streaming
*/
if (Streaming) {
this.streaming = new Streaming(this);
}
/**
* Bulk API object
* @member {Bulk} Connection#bulk
*/
if (Bulk) {
this.bulk = new Bulk(this);
}
/**
* Tooling API object
* @member {Tooling} Connection#tooling
*/
if (Tooling) {
this.tooling = new Tooling(this);
}
/**
* Analytics API object
* @member {Analytics} Connection#analytics
*/
if (Analytics) {
this.analytics = new Analytics(this);
}
/**
* Chatter API object
* @member {Chatter} Connection#chatter
*/
if (Chatter) {
this.chatter = new Chatter(this);
}
/**
* Metadata API object
* @member {Metadata} Connection#metadata
*/
if (Metadata) {
this.metadata = new Metadata(this);
}
/**
* Apex REST API object
* @member {Apex} Connection#apex
*/
if (Apex) {
this.apex = new Apex(this);
}
/**
* Cache object for result
* @member {Cache} Connection#cache
*/
this.cache = new Cache();
// Allow to delegate connection refresh to outer function
if (options.refreshFn) {
this._refreshDelegate = {
refreshToken: options.refreshFn
};
}
var cacheOptions = {
key: function(type) { return type ? "describe." + type : "describe"; }
};
this.describe$ = this.cache.makeCacheable(this.describe, this, cacheOptions);
this.describe = this.cache.makeResponseCacheable(this.describe, this, cacheOptions);
this.describeSObject$ = this.describe$;
this.describeSObject = this.describe;
cacheOptions = { key: 'describeGlobal' };
this.describeGlobal$ = this.cache.makeCacheable(this.describeGlobal, this, cacheOptions);
this.describeGlobal = this.cache.makeResponseCacheable(this.describeGlobal, this, cacheOptions);
this.initialize(options);
};
util.inherits(Connection, events.EventEmitter);
/**
* Initialize connection.
*
* @protected
* @param {Object} options - Initialization options
* @param {String} [options.instanceUrl] - Salesforce Instance URL (e.g. https://na1.salesforce.com/)
* @param {String} [options.serverUrl] - Salesforce SOAP service endpoint URL (e.g. https://na1.salesforce.com/services/Soap/u/28.0)
* @param {String} [options.accessToken] - Salesforce OAuth2 access token
* @param {String} [options.sessionId] - Salesforce session ID
* @param {String} [options.refreshToken] - Salesforce OAuth2 refresh token
* @param {String|Object} [options.signedRequest] - Salesforce Canvas signed request (Raw Base64 string, JSON string, or deserialized JSON)
* @param {UserInfo} [options.userInfo] - Logged in user information
*/
Connection.prototype.initialize = function(options) {
if (!options.instanceUrl && options.serverUrl) {
options.instanceUrl = options.serverUrl.split('/').slice(0, 3).join('/');
}
this.instanceUrl = options.instanceUrl || options.serverUrl || this.instanceUrl || defaults.instanceUrl;
this.urls = {
soap : {
login : [ this.loginUrl, "services/Soap/u", this.version ].join('/'),
service : [ this.instanceUrl, "services/Soap/u", this.version ].join('/')
},
rest : {
base : [ this.instanceUrl, "services/data", "v" + this.version ].join('/')
}
};
this.accessToken = options.sessionId || options.accessToken || this.accessToken;
this.refreshToken = options.refreshToken || this.refreshToken;
if (this.refreshToken && !this.oauth2 && !this._refreshDelegate) {
throw new Error("Refresh token is specified without oauth2 client information");
}
this.signedRequest = options.signedRequest && parseSignedRequest(options.signedRequest);
if (this.signedRequest) {
this.accessToken = this.signedRequest.client.oauthToken;
}
if (options.userInfo) {
this.userInfo = options.userInfo;
}
this.limitInfo = {};
this.sobjects = {};
this.cache.clear();
this.cache.get('describeGlobal').on('value', _.bind(function(res) {
if (res.result) {
var types = _.map(res.result.sobjects, function(so) { return so.name; });
_.each(types, this.sobject, this);
}
}, this));
if (this.tooling) {
this.tooling.initialize();
}
this._sessionType = options.sessionId ? "soap" : "oauth2";
this._initializedAt = Date.now();
};
/** @private **/
function parseSignedRequest(sr) {
if (_.isString(sr)) {
if (sr[0] === '{') { // might be JSON
return JSON.parse(sr);
} else { // might be original base64-encoded signed request
var msg = sr.split('.').pop(); // retrieve latter part
var json = new Buffer(msg, 'base64').toString('utf-8');
return JSON.parse(json);
}
return null;
}
return sr;
}
/**
* @private
*/
Connection.prototype._baseUrl = function() {
return this.urls.rest.base;
};
/**
* Sending request using given HTTP request info
* @private
*/
Connection.prototype._request = function(params, callback, options) {
options = options || {};
// if params is simple string, regard it as url in GET method
if (_.isString(params)) {
params = { method: 'GET', url: params };
}
// if url is given in site root relative path, prepend instance url before.
if (params.url[0] === '/') {
params.url = this.instanceUrl + params.url;
}
var self = this;
var logger = this._logger;
var deferred = Promise.defer();
var onResume = function(err) {
if (err) {
deferred.reject(err);
return;
}
self._request(params, null, options).then(function(response) {
deferred.resolve(response);
}, function(err) {
deferred.reject(err);
});
};
if (self._suspended) {
self.once('resume', onResume);
return deferred.promise.thenCall(callback);
}
params.headers = params.headers || {};
if (this.accessToken) {
params.headers.Authorization = "Bearer " + this.accessToken;
}
// hook in sending
if (options.beforesend) { options.beforesend(this, params); }
// for connection in canvas with signed request
if (this.signedRequest) { options.signedRequest = this.signedRequest; }
self.emit('request', params.method, params.url, params);
logger.debug("<request> method=" + params.method + ", url=" + params.url);
var requestTime = Date.now();
var onFailure = function(err) {
var responseTime = Date.now();
logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
logger.error(err);
throw err;
};
var onResponse = function(response) {
var responseTime = Date.now();
logger.debug("elappsed time : " + (responseTime - requestTime) + "msec");
logger.debug("<response> status=" + response.statusCode + ", url=" + params.url);
self.emit('response', response.statusCode, response.body, response);
// log api usage and its quota
if (response.headers && response.headers["sforce-limit-info"]) {
var apiUsage = response.headers["sforce-limit-info"].match(/api\-usage=(\d+)\/(\d+)/)
if (apiUsage) {
self.limitInfo = {
apiUsage: {
used: parseInt(apiUsage[1], 10),
limit: parseInt(apiUsage[2], 10)
}
};
}
}
// Refresh token if status code requires authentication
// when oauth2 info and refresh token is available.
if (response.statusCode === 401 &&
(self._refreshDelegate || (self.oauth2 && self.refreshToken))) {
// Access token may be refreshed before the response
if (self._initializedAt > requestTime) {
onResume();
} else {
self.once('resume', onResume);
if (!self._suspended) {
self._suspended = true;
self._refresh();
}
}
return deferred.promise;
}
// check response content type to choose parser
var contentType = options.responseContentType ||
(response.headers && response.headers["content-type"]);
var parseBody = /^application\/xml(;|$)/.test(contentType) ? parseXML :
/^application\/json(;|$)/.test(contentType) ? parseJSON :
/^text\/csv(;|$)/.test(contentType) ? parseCSV :
parseText;
var err;
if (response.statusCode >= 400) {
var error;
try {
var parseError = options.parseError || function(errs) {
var err = _.isArray(errs) ? errs[0] : errs;
if (_.isObject(err) && _.isString(err.message)) { return err; }
};
error = parseError(parseBody(response.body));
} catch(e) {}
if (!error) {
error = { message : response.body, errorCode: 'ERROR_HTTP_' + response.statusCode };
}
err = new Error(error.message);
err.name = error.errorCode;
for (var key in error) { err[key] = error[key]; }
throw err;
} else if (response.statusCode === 204) {
return options.noContentResponse;
} else {
var res = parseBody(response.body);
if (response.statusCode === 300) { // Multiple Choices
err = new Error('Multiple records found');
err.name = "MULTIPLE_CHOICES";
err.content = res;
throw err;
}
return res;
}
};
return this._transport.httpRequest(params, null, options).then(onResponse, onFailure).thenCall(callback);
};
/** @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; }
/** @private */
function formatDate(date) {
function pad(number) {
if (number < 10) {
return '0' + number;
}
return number;
}
return date.getUTCFullYear() +
'-' + pad(date.getUTCMonth() + 1) +
'-' + pad(date.getUTCDate()) +
'T' + pad(date.getUTCHours()) +
':' + pad(date.getUTCMinutes()) +
':' + pad(date.getUTCSeconds()) +
'+00:00';
}
/**
* Refresh access token
* @private
*/
Connection.prototype._refresh = function() {
var self = this;
var logger = this._logger;
logger.debug("<refresh token>");
var delegate = this._refreshDelegate || this.oauth2;
return delegate.refreshToken(this.refreshToken, function(err, res) {
if (!err) {
var userInfo = parseIdUrl(res.id);
self.initialize({
instanceUrl : res.instance_url,
accessToken : res.access_token,
userInfo : userInfo
});
logger.debug("token refresh completed. result = " + JSON.stringify(res));
self.emit("refresh", res.access_token, res);
}
self._suspended = false;
self.emit('resume', err);
});
};
/** @private **/
function parseIdUrl(idUrl) {
var idUrls = idUrl.split("/");
var userId = idUrls.pop(), orgId = idUrls.pop();
return {
id: userId,
organizationId: orgId,
url: idUrl
};
}
/**
* @callback Callback
* @type {Function}
* @param {Error} err - Callback error
* @param {T} response - Callback response
* @template T
*/
/**
* @typedef {Object} QueryResult
* @prop {Boolean} done - Flag if the query is fetched all records or not
* @prop {String} [nextRecordsUrl] - URL locator for next record set, (available when done = false)
* @prop {Number} totalSize - Total size for query
* @prop {Array.<Record>} [records] - Array of records fetched
*/
/**
* Execute query by using SOQL
*
* @param {String} soql - SOQL string
* @param {Callback.<QueryResult>} [callback] - Callback function
* @returns {Query.<QueryResult>}
*/
Connection.prototype.query = function(soql, callback) {
var query = new Query(this, soql);
if (callback) {
query.run(callback);
}
return query;
};
/**
* Execute query by using SOQL, including deleted records
*
* @param {String} soql - SOQL string
* @param {Callback.<QueryResult>} [callback] - Callback function
* @returns {Query.<QueryResult>}
*/
Connection.prototype.queryAll = function(soql, callback) {
var query = new Query(this, soql);
query.scanAll(true);
if (callback) {
query.run(callback);
}
return query;
};
/**
* Query next record set by using query locator
*
* @param {String} locator - Next record set locator
* @param {Callback.<QueryResult>} [callback] - Callback function
* @returns {Query.<QueryResult>}
*/
Connection.prototype.queryMore = function(locator, callback) {
var query = new Query(this, null, locator);
if (callback) {
query.run(callback);
}
return query;
};
/**
* Retrieve specified records
*
* @param {String} type - SObject Type
* @param {String|Array.<String>} ids - A record ID or array of record IDs
* @param {Object} [options] - Options for rest api.
* @param {Callback.<Record|Array.<Record>>} [callback] - Callback function
* @returns {Promise.<Record|Array.<Record>>}
*/
Connection.prototype.retrieve = function(type, ids, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
var self = this;
var isArray = _.isArray(ids);
ids = isArray ? ids : [ ids ];
if (ids.length > self.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call")).thenCall(callback);
}
return Promise.all(
_.map(ids, function(id) {
var url = [ self._baseUrl(), "sobjects", type, id ].join('/');
return self._request(url);
})
).then(function(results) {
return !isArray && _.isArray(results) ? results[0] : results;
}).thenCall(callback);
};
/**
* @typedef RecordResult
* @prop {Boolean} success - The result is succeessful or not
* @prop {String} [id] - Record ID
* @prop {Array.<String>} [errors] - Errors (available when success = false)
*/
/**
* Synonym of Connection#create()
*
* @method Connection#insert
* @param {String} type - SObject Type
* @param {Object|Array.<Object>} records - A record or array of records to create
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback function
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
/**
* Create records
*
* @method Connection#create
* @param {String} type - SObject Type
* @param {Record|Array.<Record>} records - A record or array of records to create
* @param {Object} [options] - Options for rest api.
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback function
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
Connection.prototype.insert =
Connection.prototype.create = function(type, records, options, callback) {
if (!_.isString(type)) {
// reverse order
callback = options;
options = records;
records = type;
type = null;
}
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
var self = this;
var isArray = _.isArray(records);
records = isArray ? records : [ records ];
if (records.length > self.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call")).thenCall(callback);
}
return Promise.all(
_.map(records, function(record) {
var sobjectType = type || (record.attributes && record.attributes.type) || record.type;
if (!sobjectType) {
throw new Error('No SObject Type defined in record');
}
record = _.clone(record);
delete record.Id;
delete record.type;
delete record.attributes;
var url = [ self._baseUrl(), "sobjects", sobjectType ].join('/');
return self._request({
method : 'POST',
url : url,
body : JSON.stringify(record),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
});
})
).then(function(results) {
return !isArray && _.isArray(results) ? results[0] : results;
}).thenCall(callback);
};
/**
* Update records
*
* @param {String} type - SObject Type
* @param {Record|Array.<Record>} records - A record or array of records to update
* @param {Object} [options] - Options for rest api.
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback function
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
Connection.prototype.update = function(type, records, options, callback) {
if (!_.isString(type)) {
// reverse order
callback = options;
options = records;
records = type;
type = null;
}
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
var self = this;
var isArray = _.isArray(records);
records = isArray ? records : [ records ];
if (records.length > self.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call")).thenCall(callback);
}
return Promise.all(
_.map(records, function(record) {
var id = record.Id;
if (!id) {
throw new Error('Record id is not found in record.');
}
var sobjectType = type || (record.attributes && record.attributes.type) || record.type;
if (!sobjectType) {
throw new Error('No SObject Type defined in record');
}
record = _.clone(record);
delete record.Id;
delete record.type;
delete record.attributes;
var url = [ self._baseUrl(), "sobjects", sobjectType, id ].join('/');
return self._request({
method : 'PATCH',
url : url,
body : JSON.stringify(record),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
}, null, {
noContentResponse: { id : id, success : true, errors : [] }
});
})
).then(function(results) {
return !isArray && _.isArray(results) ? results[0] : results;
}).thenCall(callback);
};
/**
* Upsert records
*
* @param {String} type - SObject Type
* @param {Record|Array.<Record>} records - Record or array of records to upsert
* @param {String} extIdField - External ID field name
* @param {Object} [options] - Options for rest api.
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
Connection.prototype.upsert = function(type, records, extIdField, options, callback) {
// You can omit "type" argument, when the record includes type information.
if (!_.isString(type)) {
// reverse order
callback = options;
options = extIdField;
extIdField = records;
records = type;
type = null;
}
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
var self = this;
var isArray = _.isArray(records);
records = isArray ? records : [ records ];
if (records.length > self.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call")).thenCall(callback);
}
return Promise.all(
_.map(records, function(record) {
var sobjectType = type || (record.attributes && record.attributes.type) || record.type;
var extId = record[extIdField];
if (!extId) {
return Promise.reject(new Error('External ID is not defined in the record'));
}
record = _.clone(record);
delete record[extIdField];
delete record.type;
delete record.attributes;
var url = [ self._baseUrl(), "sobjects", sobjectType, extIdField, extId ].join('/');
return self._request({
method : 'PATCH',
url : url,
body : JSON.stringify(record),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
}, null, {
noContentResponse: { success : true, errors : [] }
});
})
).then(function(results) {
return !isArray && _.isArray(results) ? results[0] : results;
}).thenCall(callback);
};
/**
* Synonym of Connection#destroy()
*
* @method Connection#delete
* @param {String} type - SObject Type
* @param {String|Array.<String>} ids - A ID or array of IDs to delete
* @param {Object} [options] - Options for rest api.
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
/**
* Synonym of Connection#destroy()
*
* @method Connection#del
* @param {String} type - SObject Type
* @param {String|Array.<String>} ids - A ID or array of IDs to delete
* @param {Object} [options] - Options for rest api.
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
/**
* Delete records
*
* @method Connection#destroy
* @param {String} type - SObject Type
* @param {String|Array.<String>} ids - A ID or array of IDs to delete
* @param {Object} [options] - Options for rest api.
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
Connection.prototype["delete"] =
Connection.prototype.del =
Connection.prototype.destroy = function(type, ids, options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
var self = this;
var isArray = _.isArray(ids);
ids = isArray ? ids : [ ids ];
if (ids.length > self.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call")).thenCall(callback);
}
return Promise.all(
_.map(ids, function(id) {
var url = [ self._baseUrl(), "sobjects", type, id ].join('/');
return self._request({
method : 'DELETE',
url : url,
headers: options.headers || null
}, null, {
noContentResponse: { id : id, success : true, errors : [] }
});
})
).then(function(results) {
return !isArray && _.isArray(results) ? results[0] : results;
}).thenCall(callback);
};
/**
* Execute search by SOSL
*
* @param {String} sosl - SOSL string
* @param {Callback.<Array.<RecordResult>>} [callback] - Callback function
* @returns {Promise.<Array.<RecordResult>>}
*/
Connection.prototype.search = function(sosl, callback) {
var url = this._baseUrl() + "/search?q=" + encodeURIComponent(sosl);
return this._request(url).thenCall(callback);
};
/**
* Result returned by describeSObject call
*
* @typedef {Object} DescribeSObjectResult
*/
/**
* Synonym of Connection#describe()
*
* @method Connection#describeSObject
* @param {String} type - SObject Type
* @param {Callback.<DescribeSObjectResult>} [callback] - Callback function
* @returns {Promise.<DescribeSObjectResult>}
*/
/**
* Describe SObject metadata
*
* @method Connection#describe
* @param {String} type - SObject Type
* @param {Callback.<DescribeSObjectResult>} [callback] - Callback function
* @returns {Promise.<DescribeSObjectResult>}
*/
Connection.prototype.describe =
Connection.prototype.describeSObject = function(type, callback) {
var url = [ this._baseUrl(), "sobjects", type, "describe" ].join('/');
return this._request(url).thenCall(callback);
};
/**
* Result returned by describeGlobal call
*
* @typedef {Object} DescribeGlobalResult
*/
/**
* Describe global SObjects
*
* @param {Callback.<DescribeGlobalResult>} [callback] - Callback function
* @returns {Promise.<DescribeGlobalResult>}
*/
Connection.prototype.describeGlobal = function(callback) {
var url = this._baseUrl() + "/sobjects";
return this._request(url).thenCall(callback);
};
/**
* Get SObject instance
*
* @param {String} type - SObject Type
* @returns {SObject}
*/
Connection.prototype.sobject = function(type) {
this.sobjects = this.sobjects || {};
var sobject = this.sobjects[type] =
this.sobjects[type] || new SObject(this, type);
return sobject;
};
/**
* Get identity information of current user
*
* @param {Callback.<IdentityInfo>} [callback] - Callback function
* @returns {Promise.<IdentityInfo>}
*/
Connection.prototype.identity = function(callback) {
var self = this;
var idUrl = this.userInfo && this.userInfo.url;
return new Promise(
idUrl ?
{ identity: idUrl } :
this._request(this._baseUrl())
).then(function(res) {
var url = res.identity;
url += '?format=json&oauth_token=' + encodeURIComponent(self.accessToken);
return self._request(url, null, { jsonp : 'callback' });
}).then(function(res) {
self.userInfo = {
id: res.user_id,
organizationId: res.organization_id,
url: res.id
};
return res;
}).thenCall(callback);
};
/**
* @typedef UserInfo
* @prop {String} id - User ID
* @prop {String} organizationId - Organization ID
* @prop {String} url - Identity URL of the user
*/
/**
* Authorize (using oauth2 web server flow)
*
* @param {String} code - Authorization code
* @param {Callback.<UserInfo>} [callback] - Callback function
* @returns {Promise.<UserInfo>}
*/
Connection.prototype.authorize = function(code, callback) {
var self = this;
var logger = this._logger;
return this.oauth2.requestToken(code).then(function(res) {
logger.debug("OAuth2 token response = " + JSON.stringify(res));
var userInfo = parseIdUrl(res.id);
self.initialize({
instanceUrl : res.instance_url,
accessToken : res.access_token,
refreshToken : res.refresh_token,
userInfo: userInfo
});
logger.debug("<login> completed. user id = " + userInfo.id + ", org id = " + userInfo.organizationId);
return userInfo;
}).thenCall(callback);
};
/**
* Login to Salesforce
*
* @param {String} username - Salesforce username
* @param {String} password - Salesforce password (and security token, if required)
* @param {Callback.<UserInfo>} [callback] - Callback function
* @returns {Promise.<UserInfo>}
*/
Connection.prototype.login = function(username, password, callback) {
if (this.oauth2 && this.oauth2.clientId && this.oauth2.clientSecret) {
return this.loginByOAuth2(username, password, callback);
} else {
return this.loginBySoap(username, password, callback);
}
};
/**
* Login by OAuth2 username & password flow
*
* @param {String} username - Salesforce username
* @param {String} password - Salesforce password (and security token, if required)
* @param {Callback.<UserInfo>} [callback] - Callback function
* @returns {Promise.<UserInfo>}
*/
Connection.prototype.loginByOAuth2 = function(username, password, callback) {
var self = this;
var logger = this._logger;
return this.oauth2.authenticate(username, password).then(function(res) {
logger.debug("OAuth2 token response = " + JSON.stringify(res));
var userInfo = parseIdUrl(res.id);
self.initialize({
instanceUrl : res.instance_url,
accessToken : res.access_token,
userInfo: userInfo
});
logger.debug("<login> completed. user id = " + userInfo.id + ", org id = " + userInfo.organizationId);
return userInfo;
}).thenCall(callback);
};
/**
* @private
*/
function esc(str) {
return str && String(str).replace(/&/g, '&').replace(/</g, '<')
.replace(/>/g, '>').replace(/"/g, '"');
}
/**
* Login by SOAP web service API
*
* @param {String} username - Salesforce username
* @param {String} password - Salesforce password (and security token, if required)
* @param {Callback.<UserInfo>} [callback] - Callback function
* @returns {Promise.<UserInfo>}
*/
Connection.prototype.loginBySoap = function(username, password, callback) {
var self = this;
var logger = this._logger;
var body = [
'<se:Envelope xmlns:se="http://schemas.xmlsoap.org/soap/envelope/">',
'<se:Header/>',
'<se:Body>',
'<login xmlns="urn:partner.soap.sforce.com">',
'<username>' + esc(username) + '</username>',
'<password>' + esc(password) + '</password>',
'</login>',
'</se:Body>