UNPKG

jsforce

Version:

Salesforce API Library for JavaScript

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