jsforce2
Version:
Salesforce API Library for JavaScript
1,613 lines (1,476 loc) • 1.98 MB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.jsforce = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
'use strict';
var jsforce = require('../core');
jsforce.browser = require('./client');
module.exports = jsforce;
},{"../core":10,"./client":5}],2:[function(require,module,exports){
'use strict';
module.exports = '1.10.0';
},{}],3:[function(require,module,exports){
// This file content is dynamically created in build script
"use strict";
module.exports = {
'inherits': require('inherits'),
'util': require('util'),
'events': require('events'),
'lodash/core': require('lodash/core'),
'readable-stream': require('readable-stream'),
'multistream': require('multistream'),
'./VERSION': require('./VERSION'),
'./cache': require('./cache'),
'./connection': require('./connection'),
'./core': require('./core'),
'./csv': require('./csv'),
'./date': require('./date'),
'./http-api': require('./http-api'),
'./logger': require('./logger'),
'./oauth2': require('./oauth2'),
'./process': require('./process'),
'./promise': require('./promise'),
'./query': require('./query'),
'./quick-action': require('./quick-action'),
'./record-stream': require('./record-stream'),
'./record': require('./record'),
'./soap': require('./soap'),
'./sobject': require('./sobject'),
'./soql-builder': require('./soql-builder'),
'./transport': require('./transport')
};
},{"./VERSION":2,"./cache":8,"./connection":9,"./core":10,"./csv":11,"./date":12,"./http-api":13,"./logger":14,"./oauth2":15,"./process":16,"./promise":17,"./query":18,"./quick-action":19,"./record":21,"./record-stream":20,"./soap":23,"./sobject":24,"./soql-builder":25,"./transport":26,"events":37,"inherits":39,"lodash/core":43,"multistream":44,"readable-stream":62,"util":72}],4:[function(require,module,exports){
/*global Sfdc */
'use strict';
var Duplex = require('readable-stream').Duplex,
_ = require('lodash/core');
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 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;
};
}
};
},{"lodash/core":43,"readable-stream":62}],5:[function(require,module,exports){
/**
* @file Browser client connection management class
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
'use strict';
var events = require('events'),
inherits = require('inherits'),
qs = require('querystring'),
_ = require('lodash/core'),
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;
};
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":9,"../oauth2":15,"events":37,"inherits":39,"lodash/core":43,"querystring":51}],6:[function(require,module,exports){
/*global window, document */
'use strict';
var _index = 0;
module.exports = {
supported: typeof window !== 'undefined' && typeof document !== '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];
};
};
}
};
},{}],7:[function(require,module,exports){
'use strict';
var Duplex = require('readable-stream').Duplex;
var _ = require('lodash/core');
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 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 headerNames = getResponseHeaderNames(xhr);
var headers = {};
_.forEach(headerNames, function(headerName) {
if (headerName) {
headers[headerName] = xhr.getResponseHeader(headerName);
}
});
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;
};
function getResponseHeaderNames(xhr) {
var headerLines = (xhr.getAllResponseHeaders() || "").split(/[\r\n]+/);
return _.map(headerLines, function(headerLine) {
return headerLine.split(/\s*:/)[0].toLowerCase();
});
}
},{"lodash/core":43,"readable-stream":62}],8:[function(require,module,exports){
/**
* @file Manages asynchronous method response cache
* @author Shinichi Tomita <shinichi.tomita@gmail.com>
*/
'use strict';
var events = require('events'),
inherits = require('inherits'),
_ = require('lodash/core');
/**
* Class for managing cache entry
*
* @private
* @class
* @constructor
* @template T
*/
var CacheEntry = function() {
this.fetching = false;
};
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 keys = _.isString(options.key) ? options.key :
_.isFunction(options.key) ? options.key.apply(scope, args) :
createCacheKey(options.namespace, args);
if (!Array.isArray(keys)) { keys = [ keys ]; }
var entries = [];
keys.forEach(function (key) {
var entry = cache.get(key);
entry.fetching = true;
entries.push(entry);
})
if (callback) {
args.push(function(err, result) {
if (Array.isArray(result) && result.length == entries.length) {
entries.forEach(function (entry, index) {
entry.set({ error: err, result: result[index] });
})
} else {
entries.forEach(function (entry) {
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) {
if (Array.isArray(result) && result.length == entries.length) {
entries.forEach(function (entry, index) {
entry.set({ error: undefined, result: result[index] });
})
} else {
entries.forEach(function (entry) {
entry.set({ error: undefined, result: result });
});
}
return result;
}, function(err) {
if (Array.isArray(err) && err.length == entries.length) {
entries.forEach(function (entry, index) {
entry.set({ error: err[index], result: undefined });
})
} else {
entries.forEach(function (entry) {
entry.set({ error: err, result: undefined });
});
}
throw err;
});
} else {
return ret;
}
} else {
if (Array.isArray(ret) && ret.length == entries.length) {
entries.forEach(function (entry, index) {
entry.set({ error: error, result: ret[index] });
})
} else {
entries.forEach(function (entry) {
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":37,"inherits":39,"lodash/core":43}],9:[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>
*/
'use strict';
var events = require('events'),
inherits = require('inherits'),
_ = require('lodash/core'),
Promise = require('./promise'),
Logger = require('./logger'),
OAuth2 = require('./oauth2'),
Query = require('./query'),
SObject = require('./sobject'),
QuickAction = require('./quick-action'),
HttpApi = require('./http-api'),
Transport = require('./transport'),
Process = require('./process'),
Cache = require('./cache');
var defaults = {
loginUrl: "https://login.salesforce.com",
instanceUrl: "",
version: "42.0"
};
/*
* Constant of maximum records num in DML operation (update/delete)
*/
var MAX_DML_COUNT = 200;
/*
* Constant of maximum number of requests that can be batched
*/
var MAX_BATCH_REQUESTS = 25;
/**
* 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.
* @param {String} [options.httpProxy] - URL of HTTP proxy server, used in server client.
* @param {Object} [options.callOptions] - Call options used in each SOAP/REST API request. See manual.
*/
var Connection = module.exports = function(options) {
options = options || {};
this._logger = new Logger(options.logLevel);
var oauth2 = options.oauth2 || {
loginUrl : options.loginUrl,
clientId : options.clientId,
clientSecret : options.clientSecret,
redirectUri : options.redirectUri,
proxyUrl: options.proxyUrl,
httpProxy: options.httpProxy
};
/**
* OAuth2 object
* @member {OAuth2} Connection#oauth2
*/
this.oauth2 = 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 */
if (options.proxyUrl) {
this._transport = new Transport.ProxyTransport(options.proxyUrl);
} else if (options.httpProxy) {
this._transport = new Transport.HttpProxyTransport(options.httpProxy);
} else {
this._transport = new Transport();
}
this.callOptions = options.callOptions;
/*
* Fire connection:new event to notify jsforce plugin modules
*/
var jsforce = require('./core');
jsforce.emit('connection:new', this);
/**
* Streaming API object
* @member {Streaming} Connection#streaming
*/
// this.streaming = new Streaming(this);
/**
* Bulk API object
* @member {Bulk} Connection#bulk
*/
// this.bulk = new Bulk(this);
/**
* Tooling API object
* @member {Tooling} Connection#tooling
*/
// this.tooling = new Tooling(this);
/**
* Analytics API object
* @member {Analytics} Connection#analytics
*/
// this.analytics = new Analytics(this);
/**
* Chatter API object
* @member {Chatter} Connection#chatter
*/
// this.chatter = new Chatter(this);
/**
* Metadata API object
* @member {Metadata} Connection#metadata
*/
// this.metadata = new Metadata(this);
/**
* SOAP API object
* @member {SoapApi} Connection#soap
*/
// this.soap = new SoapApi(this);
/**
* Apex REST API object
* @member {Apex} Connection#apex
*/
// this.apex = new Apex(this);
/**
* @member {Process} Connection#process
*/
this.process = new Process(this);
/**
* Cache object for result
* @member {Cache} Connection#cache
*/
this.cache = new Cache();
// Allow to delegate connection refresh to outer function
var self = this;
var refreshFn = options.refreshFn;
if (!refreshFn && this.oauth2.clientId) {
refreshFn = oauthRefreshFn;
}
if (refreshFn) {
this._refreshDelegate = new HttpApi.SessionRefreshDelegate(this, refreshFn);
}
var cacheOptions = {
key: function(type) {
return type
? type.type ? "describe." + type.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;
var batchCacheOptions = {
key: function(options) {
var types = options.types;
var autofetch = options.autofetch || false;
var typesToFetch = types.length > MAX_BATCH_REQUESTS
? (autofetch ? types : types.slice(0, MAX_BATCH_REQUESTS))
: types;
var keys = [];
typesToFetch.forEach(function (type) { keys.push('describe.' + type); });
return keys;
}
};
this.batchDescribe = this.cache.makeResponseCacheable(this.batchDescribe, this, batchCacheOptions);
this.batchDescribeSObjects = this.batchDescribe;
cacheOptions = { key: 'describeGlobal' };
this.describeGlobal$ = this.cache.makeCacheable(this.describeGlobal, this, cacheOptions);
this.describeGlobal = this.cache.makeResponseCacheable(this.describeGlobal, this, cacheOptions);
this.initialize(options);
};
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.accessToken = options.sessionId || options.accessToken || this.accessToken;
this.refreshToken = options.refreshToken || this.refreshToken;
if (this.refreshToken && !this._refreshDelegate) {
throw new Error("Refresh token is specified without oauth2 client information or refresh function");
}
this.signedRequest = options.signedRequest && parseSignedRequest(options.signedRequest);
if (this.signedRequest) {
this.accessToken = this.signedRequest.client.oauthToken;
if (Transport.CanvasTransport.supported) {
this._transport = new Transport.CanvasTransport(this.signedRequest);
}
}
if (options.userInfo) {
this.userInfo = options.userInfo;
}
this.limitInfo = {};
this.sobjects = {};
this.cache.clear();
this.cache.get('describeGlobal').removeAllListeners('value');
this.cache.get('describeGlobal').on('value', _.bind(function(res) {
if (res.result) {
var types = _.map(res.result.sobjects, function(so) { return so.name; });
types.forEach(this.sobject, this);
}
}, this));
if (this.tooling) {
this.tooling.initialize();
}
this._sessionType = options.sessionId ? "soap" : "oauth2";
};
/** @private **/
function oauthRefreshFn(conn, callback) {
conn.oauth2.refreshToken(conn.refreshToken, function(err, res) {
if (err) { return callback(err); }
var userInfo = parseIdUrl(res.id);
conn.initialize({
instanceUrl : res.instance_url,
accessToken : res.access_token,
userInfo : userInfo
});
callback(null, res.access_token, res);
});
}
/** @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 = Buffer.from(msg, 'base64').toString('utf-8');
return JSON.parse(json);
}
return null;
}
return sr;
}
/** @private **/
Connection.prototype._baseUrl = function() {
return [ this.instanceUrl, "services/data", "v" + this.version ].join('/');
};
/**
* Convert path to absolute url
* @private
*/
Connection.prototype._normalizeUrl = function(url) {
if (url[0] === '/') {
if (url.indexOf('/services/') === 0) {
return this.instanceUrl + url;
} else {
return this._baseUrl() + url;
}
} else {
return url;
}
};
/**
* Send REST API request with given HTTP request info, with connected session information.
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*
* @param {String|Object} request - HTTP request object or URL to GET request
* @param {String} request.method - HTTP method URL to send HTTP request
* @param {String} request.url - URL to send HTTP request
* @param {Object} [request.headers] - HTTP request headers in hash object (key-value)
* @param {Object} [options] - HTTP API request options
* @param {Callback.<Object>} [callback] - Callback function
* @returns {Promise.<Object>}
*/
Connection.prototype.request = function(request, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
var self = this;
// if request is simple string, regard it as url in GET method
if (_.isString(request)) {
request = { method: 'GET', url: request };
}
// if url is given in relative path, prepend base url or instance url before.
request.url = this._normalizeUrl(request.url);
var httpApi = new HttpApi(this, options);
// log api usage and its quota
httpApi.on('response', function(response) {
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)
}
};
}
}
});
return httpApi.request(request).thenCall(callback);
};
/**
* Send HTTP GET request
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*
* @param {String} url - Endpoint URL to request HTTP GET
* @param {Object} [options] - HTTP API request options
* @param {Callback.<Object>} [callback] - Callback function
* @returns {Promise.<Object>}
*/
Connection.prototype.requestGet = function(url, options, callback) {
var request = {
method: "GET",
url: url
};
return this.request(request, options, callback);
};
/**
* Send HTTP POST request with JSON body, with connected session information
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*
* @param {String} url - Endpoint URL to request HTTP POST
* @param {Object} body - Any JS object which can be serialized to JSON
* @param {Object} [options] - HTTP API request options
* @param {Callback.<Object>} [callback] - Callback function
* @returns {Promise.<Object>}
*/
Connection.prototype.requestPost = function(url, body, options, callback) {
var request = {
method: "POST",
url: url,
body: JSON.stringify(body),
headers: { "content-type": "application/json" }
};
return this.request(request, options, callback);
};
/**
* Send HTTP PUT request with JSON body, with connected session information
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*
* @param {String} url - Endpoint URL to request HTTP PUT
* @param {Object} body - Any JS object which can be serialized to JSON
* @param {Object} [options] - HTTP API request options
* @param {Callback.<Object>} [callback] - Callback function
* @returns {Promise.<Object>}
*/
Connection.prototype.requestPut = function(url, body, options, callback) {
var request = {
method: "PUT",
url: url,
body: JSON.stringify(body),
headers: { "content-type": "application/json" }
};
return this.request(request, options, callback);
};
/**
* Send HTTP PATCH request with JSON body
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*
* @param {String} url - Endpoint URL to request HTTP PATCH
* @param {Object} body - Any JS object which can be serialized to JSON
* @param {Object} [options] - HTTP API request options
* @param {Callback.<Object>} [callback] - Callback function
* @returns {Promise.<Object>}
*/
Connection.prototype.requestPatch = function(url, body, options, callback) {
var request = {
method: "PATCH",
url: url,
body: JSON.stringify(body),
headers: { "content-type": "application/json" }
};
return this.request(request, options, callback);
};
/**
* Send HTTP DELETE request
*
* Endpoint URL can be absolute URL ('https://na1.salesforce.com/services/data/v32.0/sobjects/Account/describe')
* , relative path from root ('/services/data/v32.0/sobjects/Account/describe')
* , or relative path from version root ('/sobjects/Account/describe').
*
* @param {String} url - Endpoint URL to request HTTP DELETE
* @param {Object} [options] - HTTP API request options
* @param {Callback.<Object>} [callback] - Callback function
* @returns {Promise.<Object>}
*/
Connection.prototype.requestDelete = function(url, options, callback) {
var request = {
method: "DELETE",
url: url
};
return this.request(request, options, callback);
};
/** @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';
}
/** @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 {Object} [options] - Query options
* @param {Object} [options.headers] - Additional HTTP request headers sent in query request
* @param {Callback.<QueryResult>} [callback] - Callback function
* @returns {Query.<QueryResult>}
*/
Connection.prototype.query = function(soql, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
var query = new Query(this, soql, options);
if (callback) {
query.run(callback);
}
return query;
};
/**
* Execute query by using SOQL, including deleted records
*
* @param {String} soql - SOQL string
* @param {Object} [options] - Query options
* @param {Object} [options.headers] - Additional HTTP request headers sent in query request
* @param {Callback.<QueryResult>} [callback] - Callback function
* @returns {Query.<QueryResult>}
*/
Connection.prototype.queryAll = function(soql, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
var query = new Query(this, soql, options);
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 {Object} [options] - Query options
* @param {Object} [options.headers] - Additional HTTP request headers sent in query request
* @param {Callback.<QueryResult>} [callback] - Callback function
* @returns {Query.<QueryResult>}
*/
Connection.prototype.queryMore = function(locator, options, callback) {
if (typeof options === 'function') {
callback = options;
options = undefined;
}
var query = new Query(this, { locator: locator }, options);
if (callback) {
query.run(callback);
}
return query;
};
/** @private */
Connection.prototype._ensureVersion = function(majorVersion) {
var versions = this.version.split('.');
return parseInt(versions[0], 10) >= majorVersion;
}
/** @private */
Connection.prototype._supports = function(feature) {
switch (feature) {
case 'sobject-collection':
return this._ensureVersion(42);
default:
return false;
}
}
/**
* 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 {Array.<String>} [options.fields] - Fetching field names in retrieving record
* @param {Object} [options.headers] - Additional HTTP request headers sent in retrieve request
* @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 = {};
}
options = options || {};
return (
_.isArray(ids) ?
(this._supports('sobject-collection') ? // check whether SObject collection API is supported
this._retrieveMany(type, ids, options) :
this._retrieveParallel(type, ids, options)) :
this._retrieveSingle(type, ids, options)
).thenCall(callback);
};
/** @private */
Connection.prototype._retrieveSingle = function(type, id, options) {
if (!id) {
return Promise.reject(new Error('Invalid record ID. Specify valid record ID value'));
}
var url = [ this._baseUrl(), "sobjects", type, id ].join('/');
if (options.fields) {
url += '?fields=' + options.fields.join(',');
}
return this.request({
method: 'GET',
url: url,
headers: options.headers,
});
};
/** @private */
Connection.prototype._retrieveParallel = function(type, ids, options) {
if (ids.length > this.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call"));
}
var self = this;
return Promise.all(
ids.map(function(id) {
return self._retrieveSingle(type, id, options).catch(function(err) {
if (options.allOrNone || err.errorCode !== 'NOT_FOUND') {
throw err;
}
return null;
});
})
);
};
/** @private */
Connection.prototype._retrieveMany = function(type, ids, options) {
if (ids.length === 0) {
return Promise.resolve([]);
}
var url = [ this._baseUrl(), "composite", "sobjects", type ].join('/');
var self = this;
return (
options.fields ?
Promise.resolve(options.fields) :
new Promise(function(resolve, reject) {
self.describe$(type, function(err, so) {
if (err) {
reject(err);
} else {
var fields = so.fields.map(function(field) {
return field.name;
});
resolve(fields);
}
});
})
).then(function(fields) {
return self.request({
method : 'POST',
url : url,
body : JSON.stringify({
ids : ids,
fields : fields
}),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
});
});
};
/**
* @typedef RecordResult
* @prop {Boolean} success - The result is succeessful or not
* @prop {String} [id] - Record ID
* @prop {Array.<Object>} [errors] - Errors (available when success = false)
*/
/** @private */
Connection.prototype._toRecordResult = function(id, err) {
var error = {
statusCode: err.errorCode,
message: err.message
};
if (err.content) { error.content = err.content; } // preserve External id duplication message
if (err.fields) { error.fields = err.fields; } // preserve DML exception occurred fields
var result = {
success: false,
errors: [error]
};
if (id) { result.id = id; }
return result;
};
/**
* 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 {Object} [options] - Options for rest api.
* @param {Boolean} [options.allOrNone] - If true, any failed records in a call cause all changes for the call to be rolled back
* @param {Boolean} [options.allowRecursive] - If true, when records goes over the max num of collection API (=200), records are divided into several chunks and requested recursively.
* @param {Object} [options.headers] - Additional HTTP request headers sent in retrieve request
* @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 {Boolean} [options.allOrNone] - If true, any failed records in a call cause all changes for the call to be rolled back
* @param {Boolean} [options.allowRecursive] - If true, when records goes over the max num of collection API (=200), records are divided into several chunks and requested recursively.
* @param {Object} [options.headers] - Additional HTTP request headers sent in retrieve request
* @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 || {};
return (
_.isArray(records) ?
(this._supports('sobject-collection') ? // check whether SObject collection API is supported
this._createMany(type, records, options) :
this._createParallel(type, records, options)) :
this._createSingle(type, records, options)
).thenCall(callback);
};
/** @private */
Connection.prototype._createSingle = function(type, record, options) {
var sobjectType = type || (record.attributes && record.attributes.type) || record.type;
if (!sobjectType) {
return Promise.reject(new Error('No SObject Type defined in record'));
}
record = _.clone(record);
delete record.Id;
delete record.type;
delete record.attributes;
var url = [ this._baseUrl(), "sobjects", sobjectType ].join('/');
return this.request({
method : 'POST',
url : url,
body : JSON.stringify(record),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
});
};
/** @private */
Connection.prototype._createParallel = function(type, records, options) {
if (records.length > this.maxRequest) {
return Promise.reject(new Error("Exceeded max limit of concurrent call"));
}
var self = this;
return Promise.all(
records.map(function(record) {
return self._createSingle(type, record, options).catch(function(err) {
// be aware that allOrNone in parallel mode will not revert the other successful requests
// it only raises error when met at least one failed request.
if (options.allOrNone || !err.errorCode) {
throw err;
}
return this._toRecordResult(null, err);
});
})
);
};
/** @private */
Connection.prototype._createMany = function(type, records, options) {
if (records.length === 0) {
return Promise.resolve([]);
}
if (records.length > MAX_DML_COUNT && options.allowRecursive) {
var self = this;
return self._createMany(type, records.slice(0, MAX_DML_COUNT), options).then(function(rets1) {
return self._createMany(type, records.slice(MAX_DML_COUNT), options).then(function(rets2) {
return rets1.concat(rets2);
});
});
}
records = _.map(records, function(record) {
var sobjectType = type || (record.attributes && record.attributes.type) || record.type;
if (!sobjectType) {
return Promise.reject(new Error('No SObject Type defined in record'));
}
record = _.clone(record);
delete record.Id;
delete record.type;
record.attributes = { type : sobjectType };
return record;
});
var url = [ this._baseUrl(), "composite", "sobjects" ].join('/');
return this.request({
method : 'POST',
url : url,
body : JSON.stringify({
allOrNone : options.allOrNone || false,
records : records
}),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
});
};
/**
* 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 {Boolean} [options.allOrNone] - If true, any failed records in a call cause all changes for the call to be rolled back
* @param {Boolean} [options.allowRecursive] - If true, when records goes over the max num of collection API (=200), records are divided into several chunks and requested recursively.
* @param {Object} [options.headers] - Additional HTTP request headers sent in retrieve request
* @param {Callback.<RecordResult|Array.<RecordResult>>} [callback] - Callback function
* @returns {Promise.<RecordResult|Array.<RecordResult>>}
*/
Connection.prototype