UNPKG

jsforce2

Version:

Salesforce API Library for JavaScript

1,613 lines (1,476 loc) 1.98 MB
(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