jsforce2
Version:
Salesforce API Library for JavaScript
1,467 lines (1,368 loc) • 61.8 kB
JavaScript
/*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.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 || {};
return (
_.isArray(records) ?
(this._supports('sobject-collection') ? // check whether SObject collection API is supported
this._updateMany(type, records, options) :
this._updateParallel(type, records, options)) :
this._updateSingle(type, records, options)
).thenCall(callback);
};
/** @private */
Connection.prototype._updateSingle = function(type, record, options) {
var id = record.Id;
if (!id) {
return Promise.reject(new Error('Record id is not found in 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;
delete record.attributes;
var url = [ this._baseUrl(), "sobjects", sobjectType, id ].join('/');
return this.request({
method : 'PATCH',
url : url,
body : JSON.stringify(record),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
}, {
noContentResponse: { id : id, success : true, errors : [] }
});
};
/** @private */
Connection.prototype._updateParallel = 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._updateSingle(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(record.Id, err);
});
})
);
};
/** @private */
Connection.prototype._updateMany = function(type, records, options) {
if (records.length === 0) {
return Promise.resolve([]);
}
if (records.length > MAX_DML_COUNT && options.allowRecursive) {
var self = this;
return self._updateMany(type, records.slice(0, MAX_DML_COUNT), options).then(function(rets1) {
return self._updateMany(type, records.slice(MAX_DML_COUNT), options).then(function(rets2) {
return rets1.concat(rets2);
});
});
}
records = _.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;
record.id = id;
delete record.type;
record.attributes = { type : sobjectType };
return record;
});
var url = [ this._baseUrl(), "composite", "sobjects" ].join('/');
return this.request({
method : 'PATCH',
url : url,
body : JSON.stringify({
allOrNone : options.allOrNone || false,
records : records
}),
headers : _.defaults(options.headers || {}, {
"Content-Type" : "application/json"
})
});
};
/**
* 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 {Boolean} [options.allOrNone] - If true, any failed records in a call cause all changes for the call to be rolled back
* @param {Object} [options.headers] - Additional HTTP request headers sent in retrieve request
* @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 > this.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];
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"
})
}, {
noContentResponse: { success : true, errors : [] }
})
.catch(function(err) {
// be aware that `allOrNone` option in upsert method will not revert the other successful requests
// it only raises error when met at least one failed request.
if (!isArray || options.allOrNone || !err.errorCode) { throw err; }
return self._toRecordResult(null, err);
})
})
).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 {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 ids goes over the max num of collection API (=200), ids 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
* @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 {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 ids goes over the max num of collection API (=200), ids 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
* @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 {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 ids goes over the max num of collection API (=200), ids 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
* @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 || {};
return (
_.isArray(ids) ?
(this._supports('sobject-collection') ? // check whether SObject collection API is supported
this._destroyMany(type, ids, options) :
this._destroyParallel(type, ids, options)) :
this._destroySingle(type, ids, options)
).thenCall(callback);
};
/** @private */
Connection.prototype._destroySingle = function(type, id, options) {
var url = [ this._baseUrl(), "sobjects", type, id ].join('/');
return this.request({
method : 'DELETE',
url : url,
headers: options.headers || null
}, {
noContentResponse: { id : id, success : true, errors : [] }
});
};
/** @private */
Connection.prototype._destroyParallel = 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._destroySingle(type, id, options).catch(function(err) {
// be aware that `allOrNone` option 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(id, err);
});
})
);
};
/** @private */
Connection.prototype._destroyMany = function(type, ids, options) {
if (ids.length === 0) {
return Promise.resolve([]);
}
if (ids.length > MAX_DML_COUNT && options.allowRecursive) {
var self = this;
return self._destroyMany(type, ids.slice(0, MAX_DML_COUNT), options).then(function(rets1) {
return self._destroyMany(type, ids.slice(MAX_DML_COUNT), options).then(function(rets2) {
return rets1.concat(rets2);
});
});
}
var url = [ this._baseUrl(), "composite", "sobjects?ids=" ].join('/') + ids.join(',');
if (options.allOrNone) {
url += '&allOrNone=true';
}
return this.request({
method : 'DELETE',
url : url,
headers: options.headers || null
});
};
/**
* 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
*/
/**
* Parameter for describeSObject call
*
* @typedef {Object} DescribeSObjectOptions
*/
/**
* Synonym of Connection#describe()
*
* @method Connection#describeSObject
* @param {String|DescribeSObjectOptions} type - SObject Type or options object
* @param {String} type.type - The name of the SObject
* @param {String} type.ifModifiedSince - Date value for If-Modified-Since header; undefined resolved if not modified after this date
* @param {Callback.<DescribeSObjectResult>} [callback] - Callback function
* @returns {Promise.<DescribeSObjectResult>}
*/
/**
* Describe SObject metadata
*
* @method Connection#describe
* @param {String|DescribeSObjectOptions} type - SObject Type or options object
* @param {String} type.type - The name of the SObject
* @param {String} type.ifModifiedSince - Date value for If-Modified-Since header; undefined resolved if not modified after this date
* @param {Callback.<DescribeSObjectResult>} [callback] - Callback function
* @returns {Promise.<DescribeSObjectResult>}
*/
Connection.prototype.describe =
Connection.prototype.describeSObject = function(type, callback) {
var name = type.type ? type.type : type;
var url = [ this._baseUrl(), "sobjects", name, "describe" ].join('/');
var headers = type.ifModifiedSince
? { 'If-Modified-Since': type.ifModifiedSince }
: {};
return this.request({
method: 'GET',
url: url,
headers: headers
}).then(function (resp) {
if (resp === '') {
return Promise.resolve(undefined);
} else {
return Promise.resolve(resp);
}
}).thenCall(callback);
};
/**
* Result returned by batchDescribeSObjects call
*
* @typedef {Object[]} DescribeSObjectResult
*/
/**
* Parameter for describeSObject call
*
* @typedef {Object} BatchDescribeSObjectOptions
*/
/**
* Synonym of Connection#batchDescribe()
*
* @method Connection#batchDescribeSObjects
* @param {BatchDescribeSObjectOptions} options - options for function
* @param {String[]} options.types - names of objects to fetch
* @param {Boolean} options.autofetch - whether to automatically fetch metadata for large numbers of
* types (one batch request returns a maximum of 25 results); when true, will make
* subsequent requests until all object metadata is fetched; when false (default),
* will make one batch request for maximum of 25 results
* @param {number} options.maxConcurrentRequests - maximum number of concurrent requests sent to the org;
* default and maximum is 15
* @param {Callback.<DescribeSObjectResult[]>} [callback] - Callback function
* @returns {Promise.<DescribeSObjectResult[]>}
*/
/**
* Batch describe SObject metadata
*
* @method Connection#batchDescribe
* @param {BatchDescribeSObjectOptions} options - options for function
* @param {String[]} options.types - names of objects to fetch
* @param {Boolean} options.autofetch - whether to automatically fetch metadata for large numbers of
* types (one batch request returns a maximum of 25 results); when true, will make
* subsequent requests until all object metadata is fetched; when false (default),
* will make one batch request for maximum of 25 results
* @param {number} options.maxConcurrentRequests - maximum number of concurrent requests sent to the org;
* default and maximum is 15
* @param {Callback.<DescribeSObjectResult[]>} [callback] - Callback function
* @returns {Promise.<DescribeSObjectResult[]>}
*/
Connection.prototype.batchDescribe = Connection.prototype.batchDescribeSObjects = function (
options,
callback
) {
var self = this;
var types = options.types;
var autofetch = options.autofetch || false;
var maxConcurrentRequests = Math.min((options.maxConcurrentRequests || 15), 15);
var batches = [];
do {
var batch = types.length > MAX_BATCH_REQUESTS ? types.slice(0, MAX_BATCH_REQUESTS) : types;
batches.push(batch);
types = types.length > MAX_BATCH_REQUESTS ? types.slice(MAX_BATCH_REQUESTS) : [];
} while (types.length > 0 && autofetch);
var requestBatches = [];
do {
var requestBatch = batches.length > maxConcurrentRequests ? batches.slice(0, maxConcurrentRequests) : batches;
requestBatches.push(requestBatch);
batches = batches.length > maxConcurrentRequests ? batches.slice(maxConcurrentRequests) : [];
} while (batches.length > 0);
return self.doBatchDescribeRequestBatches(requestBatches)
.thenCall(callback);
};
Connection.prototype.doBatchDescribeRequestBatches = function(requestBatches) {
// make each batch of requests sequentially to avoid org limits of max concurrent requests
var self = this;
var sobjects = [];
var firstBatch = requestBatches.shift();
return self.doBatchOfBatchDescribeRequests(firstBatch).then(
function (sobjectArray) {
sobjectArray.forEach(function (sobject) { sobjects.push(sobject); });
if (requestBatches.length > 0) {
return self.doBatchDescribeRequestBatches(requestBatches).then(
function (results) {
results.forEach(function (result) { sobjects.push(result); });
return Promise.resolve(sobjects);
}
)
} else {
return Promise.resolve(sobjects);
}
}
)
}
/** private */
Connection.prototype.doBatchOfBatchDescribeRequests = function(requestBatch) {
// make up to maxConcurrentRequest requests in parallel
var self = this;
return Promise.all(
requestBatch.map(function (batch) { return self.doBatchDescribeRequest(batch); } )
).then(function (results) {
var sobjects = [];
results.forEach(function (sobjectArray) {
sobjectArray.forEach(function (sobject) { sobjects.push(sobject); })
});
return Promise.resolve(sobjects);
});
}
/** private */
Connection.prototype.doBatchDescribeRequest = function(types) {
var self = this;
var sobjects = [];
var url = [self._baseUrl(), "composite/batch"].join("/");
var version = "v" + self.version;
var batchRequests = [];
types.forEach(function (type) {
batchRequests.push({
method: "GET",
url: [version, "sobjects", type, "describe"].join("/")
});
});
return this.request({
method: "POST",
url: url,
body: JSON.stringify({ batchRequests: batchRequests }),
headers: {
"Content-Type": "application/json"
}
}).then(function (response) {
if (response.results) {
var i = 0;
for (var i = 0; i < response.results.length; i++) {
var subResp = response.results[i];
if (Array.isArray(subResp.result)) {
if (subResp.result[0].errorCode && subResp.result[0].message) {
this._logger.error(
'Error: ' + subResp.result[0].errorCode + ' ' +
subResp.result[0].message + ' - ' + typesToFetch[i]
);
}
} else {
sobjects.push(subResp.result);
}
}
}
return Promise.resolve(sobjects);
});
}
/**
* 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 {Object} [options] - Identity call options
* @param {Object} [options.headers] - Additional HTTP request headers sent in identity request
* @param {Callback.<IdentityInfo>} [callback] - Callback function
* @returns {Promise.<IdentityInfo>}
*/
Connection.prototype.identity = function(options, callback) {
if (typeof options === 'function') {
callback = options;
options = {};
}
options = options || {};
var self = this;
var idUrl = this.userInfo && this.userInfo.url;
return Promise.resolve(
idUrl ?
{ identity: idUrl } :
this.request({ method: 'GET', url: this._baseUrl(), headers: options.headers })
).then(function(res) {
var url = res.identity;
return self.request({ method: 'GET', url: url });
}).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 {Object} [params] - Optional parameters to send in token retrieval
* @param {String} [params.code_verifier] - Code verifier value (RFC 7636 - Proof Key of Code Exchange)
* @param {Callback.<UserInfo>} [callback] - Callback function
* @returns {Promise.<UserInfo>}
*/
Connection.prototype.authorize = function(code, params, callback) {
if (typeof params === 'function') {
callback = params;
params = {};
}
var self = this;
var logger = this._logger;
return this.oauth2.requestToken(code, params).then(function(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) {
// register refreshDelegate for session expiration
this._refreshDelegate = new HttpApi.SessionRefreshDelegate(this, createUsernamePasswordRefreshFn(username, password));
if (this.oauth2 && this.oauth2.clientId && this.oauth2.clientSecret) {
return this.loginByOAuth2(username, password, callback);
} else {
return this.l