From f2562ca9f2ba839e595880b5489f6d77a43666ce Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Wed, 20 Jan 2016 14:14:33 -0800 Subject: [PATCH 1/5] Fix broken edit --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 7ce5297..b919832 100644 --- a/app.js +++ b/app.js @@ -156,7 +156,7 @@ && !response.Website.startsWith('http://')) { response.Website = 'http://'+response.Website; } - $dialog.TrimPath.processDOMTemplate("detail_jst" + $dialog.html(TrimPath.processDOMTemplate("detail_jst" ,response)); $dialog.find('#industry').click(function(e) { e.preventDefault(); From 0c1e81f050aa9f50b23111c2b40c5df238d73650 Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Wed, 20 Jan 2016 22:41:40 -0800 Subject: [PATCH 2/5] Remove jQuery dependencies --- forcetk.js | 252 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 160 insertions(+), 92 deletions(-) diff --git a/forcetk.js b/forcetk.js index 7fd2918..959594e 100644 --- a/forcetk.js +++ b/forcetk.js @@ -32,8 +32,8 @@ * console, go to Your Name | Setup | Security Controls | Remote Site Settings */ -/*jslint browser: true*/ -/*global alert, Blob, $, jQuery*/ +/*jslint browser: true, plusplus: true*/ +/*global alert, Blob*/ var forcetk = window.forcetk; @@ -96,30 +96,36 @@ if (forcetk.Client === undefined) { */ forcetk.Client.prototype.refreshAccessToken = function (callback, error) { 'use strict'; - var that = this, - url = this.loginUrl + '/services/oauth2/token'; - return $.ajax({ - type: 'POST', - url: (this.proxyUrl !== null && !this.visualforce) ? this.proxyUrl : url, - cache: false, - processData: false, - data: 'grant_type=refresh_token&client_id=' + this.clientId + '&refresh_token=' + this.refreshToken, - success: callback, - error: error, - dataType: "json", - beforeSend: function (xhr) { - if (that.proxyUrl !== null && !this.visualforce) { - xhr.setRequestHeader('SalesforceProxy-Endpoint', url); + var xhr = new XMLHttpRequest(), + url = this.loginUrl + '/services/oauth2/token', + payload = 'grant_type=refresh_token&client_id=' + this.clientId + '&refresh_token=' + this.refreshToken; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status > 199 && xhr.status < 300) { + if (callback) { + callback(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); + } + } else { + console.error(xhr.responseText); + if (error) { + error(xhr); + } } } - }); + }; + + xhr.open('POST', url, true); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); + xhr.send(payload); }; /** * Set a session token and the associated metadata in the client. * @param sessionId a salesforce.com session ID. In a Visualforce page, * use '{!$Api.sessionId}' to obtain a session ID. - * @param [apiVersion="v29.0"] Force.com API version + * @param [apiVersion="v36.0"] Force.com API version * @param [instanceUrl] Omit this if running on Visualforce; otherwise * use the value from the OAuth token. */ @@ -127,7 +133,7 @@ if (forcetk.Client === undefined) { 'use strict'; this.sessionId = sessionId; this.apiVersion = (apiVersion === undefined || apiVersion === null) - ? 'v29.0' : apiVersion; + ? 'v36.0' : apiVersion; if (instanceUrl === undefined || instanceUrl === null) { this.visualforce = true; @@ -150,49 +156,73 @@ if (forcetk.Client === undefined) { } }; + var nonce = +(new Date()); + var rquery = (/\?/); + /* * Low level utility function to call the Salesforce endpoint. * @param path resource path relative to /services/data * @param callback function to which response will be passed * @param [error=null] function to which jqXHR will be passed in case of error * @param [method="GET"] HTTP method for call - * @param [payload=null] payload for POST/PATCH etc + * @param [payload=null] string payload for POST/PATCH etc */ forcetk.Client.prototype.ajax = function (path, callback, error, method, payload, retry) { 'use strict'; - var that = this, - url = (this.visualforce ? '' : this.instanceUrl) + '/services/data' + path; - - return $.ajax({ - type: method || "GET", - async: this.asyncAjax, - url: (this.proxyUrl !== null && !this.visualforce) ? this.proxyUrl : url, - contentType: method === "DELETE" ? null : 'application/json', - cache: false, - processData: false, - data: payload, - success: callback, - error: (!this.refreshToken || retry) ? error : function (jqXHR, textStatus, errorThrown) { - if (jqXHR.status === 401) { - that.refreshAccessToken(function (oauthResponse) { - that.setSessionToken(oauthResponse.access_token, null, - oauthResponse.instance_url); - that.ajax(path, callback, error, method, payload, true); - }, - error); - } else { - error(jqXHR, textStatus, errorThrown); - } - }, - dataType: "json", - beforeSend: function (xhr) { - if (that.proxyUrl !== null && !that.visualforce) { - xhr.setRequestHeader('SalesforceProxy-Endpoint', url); + + // dev friendly API: Add leading '/' if missing so url + path concat always works + if (path.charAt(0) !== '/') { + path = '/' + path; + } + + var xhr = new XMLHttpRequest(), + url = (this.visualforce ? '' : this.instanceUrl) + '/services/data' + path, + that = this; + + method = method || 'GET'; + + // Cache-busting logic inspired by jQuery + url = url + (rquery.test(url) ? "&" : "?") + "_=" + nonce++; + + if (this.asyncAjax) { + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status > 199 && xhr.status < 300) { + if (callback) { + callback(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); + } + } else if (xhr.status === 401 && that.refresh_token) { + if (retry) { + console.error(xhr.responseText); + error(xhr); + } else { + that.refreshAccessToken(function (oauthResponse) { + that.setSessionToken(oauthResponse.access_token, null, + oauthResponse.instance_url); + that.ajax(path, callback, error, method, payload, true); + }, + error); + } + } else { + console.error(xhr.responseText); + if (error) { + error(xhr); + } + } } - xhr.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId); - xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); - } - }); + }; + } + + xhr.open(method, url, this.asyncAjax); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader(this.authzHeader, "Bearer " + this.sessionId); + xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); + if (method !== "DELETE") { + xhr.setRequestHeader("Content-Type", 'application/json'); + } + xhr.send(payload); + + return this.asyncAjax ? null : JSON.parse(xhr.responseText); }; /** @@ -374,6 +404,19 @@ if (forcetk.Client === undefined) { '?_HttpMethod=PATCH', fields, filename, payloadField, payload, callback, error, retry); }; + var param = function (data) { + 'use strict'; + var r20 = /%20/g, + s = [], + key; + for (key in data) { + if (data.hasOwnProperty(key)) { + s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(data[key]); + } + } + return s.join("&").replace(r20, "+"); + }; + /* * Low level utility function to call the Salesforce endpoint specific for Apex REST API. * @param path resource path relative to /services/apexrest @@ -386,16 +429,24 @@ if (forcetk.Client === undefined) { */ forcetk.Client.prototype.apexrest = function (path, callback, error, method, payload, paramMap, retry) { 'use strict'; - var that = this, - url = this.instanceUrl + '/services/apexrest' + path; - method = method || "GET"; + // dev friendly API: Add leading '/' if missing so url + path concat always works + if (path.charAt(0) !== '/') { + path = '/' + path; + } + + var xhr = new XMLHttpRequest(), + that = this, + url = this.instanceUrl + '/services/apexrest' + path, + paramName; + + method = method || 'GET'; if (method === "GET") { // Handle proxied query params correctly if (this.proxyUrl && payload) { if (typeof payload !== 'string') { - payload = $.param(payload); + payload = param(payload); } url += "?" + payload; payload = null; @@ -407,47 +458,64 @@ if (forcetk.Client === undefined) { } } - return $.ajax({ - type: method, - async: this.asyncAjax, - url: this.proxyUrl || url, - contentType: 'application/json', - cache: false, - processData: false, - data: payload, - success: callback, - error: (!this.refreshToken || retry) ? error : function (jqXHR, textStatus, errorThrown) { - if (jqXHR.status === 401) { - that.refreshAccessToken(function (oauthResponse) { - that.setSessionToken(oauthResponse.access_token, null, - oauthResponse.instance_url); - that.apexrest(path, callback, error, method, payload, paramMap, true); - }, error); - } else { - error(jqXHR, textStatus, errorThrown); - } - }, - dataType: "json", - beforeSend: function (xhr) { - var paramName; - if (that.proxyUrl !== null) { - xhr.setRequestHeader('SalesforceProxy-Endpoint', url); - } - //Add any custom headers - if (paramMap === null) { - paramMap = {}; - } - for (paramName in paramMap) { - if (paramMap.hasOwnProperty(paramName)) { - xhr.setRequestHeader(paramName, paramMap[paramName]); + // Cache-busting logic inspired by jQuery + url = url + (rquery.test(url) ? "&" : "?") + "_=" + nonce++; + + if (this.asyncAjax) { + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status > 199 && xhr.status < 300) { + if (callback) { + callback(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); + } + } else if (xhr.status === 401 && that.refresh_token) { + if (retry) { + console.error(xhr.responseText); + error(xhr); + } else { + that.refreshAccessToken(function (oauthResponse) { + that.setSessionToken(oauthResponse.access_token, null, + oauthResponse.instance_url); + that.apexrest(path, callback, error, method, payload, paramMap, true); + }, + error); + } + } else { + console.error(xhr.responseText); + if (error) { + error(xhr); + } } } - xhr.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId); - xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); + }; + } + + xhr.open(method, this.proxyUrl || url, this.asyncAjax); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader(this.authzHeader, "Bearer " + this.sessionId); + xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); + xhr.setRequestHeader("Content-Type", 'application/json'); + + //Add any custom headers + if (paramMap === null) { + paramMap = {}; + } + for (paramName in paramMap) { + if (paramMap.hasOwnProperty(paramName)) { + xhr.setRequestHeader(paramName, paramMap[paramName]); } - }); + } + + if (that.proxyUrl !== null) { + xhr.setRequestHeader('SalesforceProxy-Endpoint', url); + } + + xhr.send(payload); + + return this.asyncAjax ? null : JSON.parse(xhr.responseText); }; + /* * Lists summary information about each Salesforce.com version currently * available, including the version, label, and a link to each version's From ded55d62a1330a091efc45b97505f0b6f7041e6c Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Thu, 21 Jan 2016 12:33:32 -0800 Subject: [PATCH 3/5] Move from callbacks to ES6 promises. --- OlderUpdates.md | 43 ++++ README.markdown | 79 +++---- app.js | 31 ++- example.page | 52 ++--- forcetk.js | 587 +++++++++++++++++++++++------------------------- 5 files changed, 395 insertions(+), 397 deletions(-) create mode 100644 OlderUpdates.md diff --git a/OlderUpdates.md b/OlderUpdates.md new file mode 100644 index 0000000..16a8458 --- /dev/null +++ b/OlderUpdates.md @@ -0,0 +1,43 @@ +Older Updates +============= + +* [Visualforce Remote Objects](https://www.salesforce.com/us/developer/docs/pages/index_Left.htm#CSHID=pages_remote_objects.htm|StartTopic=Content%2Fpages_remote_objects.htm|SkinName=webhelp) are proxy objects that enable basic DML operations on sObjects directly from JavaScript. Behind the scenes, the Remote Objects controller handles sharing rules, field level security, and other data accessibility concerns. Pages that use Remote Objects are subject to all the standard Visualforce limits, but like JavaScript remoting, Remote Objects calls don’t count toward API request limits. + + + Since Remote Objects are more secure than RemoteTK (which does not respect sharing rules, FLS etc since system-level access is proxied via the RemoteTK controller), and similarly do not consume API calls (the main motivation for RemoteTK), RemoteTK has been removed from the toolkit. + +* Since the Summer '13 release, the `/services/data` endpoint has been exposed on Visualforce hosts, so no proxy is now required for REST API calls in JavaScript served via Visualforce (although the proxy **is** still required for calls to `/services/apexrest`). `forcetk.js` has been updated to reflect this. + +* Inserting or updating blob data using the `create` or `update` functions (passing base64-encoded binary data in JSON) is limited by the REST API to 50 MB of text data or 37.5 MB of base64–encoded data. New functions, `createBlob` and `updateBlob`, allow creation and update of ContentVersion and Document records with binary ('blob') content with a size of up to 500 MB. Here is a minimal sample that shows how to upload a file to Chatter Files: + + +

+ Select a file to upload as a new Chatter File. +

+ +

+ + + +
+ + Under the covers, `createBlob` sends a multipart message. See the REST API doc page [Insert or Update Blob Data](https://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_insert_update_blob.htm) for more details. \ No newline at end of file diff --git a/README.markdown b/README.markdown index e3c0368..399bcc5 100644 --- a/README.markdown +++ b/README.markdown @@ -13,73 +13,42 @@ Due to the [same origin policy](http://en.wikipedia.org/wiki/Same_origin_policy) Recent Updates -------------- -* [Visualforce Remote Objects](https://www.salesforce.com/us/developer/docs/pages/index_Left.htm#CSHID=pages_remote_objects.htm|StartTopic=Content%2Fpages_remote_objects.htm|SkinName=webhelp) are proxy objects that enable basic DML operations on sObjects directly from JavaScript. Behind the scenes, the Remote Objects controller handles sharing rules, field level security, and other data accessibility concerns. Pages that use Remote Objects are subject to all the standard Visualforce limits, but like JavaScript remoting, Remote Objects calls don’t count toward API request limits. - - - Since Remote Objects are more secure than RemoteTK (which does not respect sharing rules, FLS etc since system-level access is proxied via the RemoteTK controller), and similarly do not consume API calls (the main motivation for RemoteTK), RemoteTK has been removed from the toolkit. - -* Since the Summer '13 release, the `/services/data` endpoint has been exposed on Visualforce hosts, so no proxy is now required for REST API calls in JavaScript served via Visualforce (although the proxy **is** still required for calls to `/services/apexrest`). `forcetk.js` has been updated to reflect this. - -* Inserting or updating blob data using the `create` or `update` functions (passing base64-encoded binary data in JSON) is limited by the REST API to 50 MB of text data or 37.5 MB of base64–encoded data. New functions, `createBlob` and `updateBlob`, allow creation and update of ContentVersion and Document records with binary ('blob') content with a size of up to 500 MB. Here is a minimal sample that shows how to upload a file to Chatter Files: - - -

- Select a file to upload as a new Chatter File. -

- -

- - - -
- - Under the covers, `createBlob` sends a multipart message. See the REST API doc page [Insert or Update Blob Data](https://www.salesforce.com/us/developer/docs/api_rest/Content/dome_sobject_insert_update_blob.htm) for more details. +[Older Updates](OlderUpdates.md) Dependencies ------------ -The toolkit uses [jQuery](http://jquery.com/). It has been tested on jQuery 1.4.4 and 1.5.2, but other versions may also work. +The toolkit uses [ECMAScript 6 Promises](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise). + +TBD - Babel Configuration ------------- -ForceTK requires that you add the correct REST endpoint hostname for your instance (i.e. https://na1.salesforce.com/ or similar) as a remote site in *Your Name > Administration Setup > Security Controls > Remote Site Settings*. +If you are using ForceTK from JavaScript hosted in an 'off platform' web page (for example, in a Heroku app), you will need to add the app's origin (protocol, host and port, for example https://myapp.herokuapp.com/) to your org's [CORS configuration](https://help.salesforce.com/htviewhelpdoc?err=1&id=extend_code_cors.htm&siteLang=en_US). Using ForceTK in a Visualforce page ----------------------------------- -Create a zip file containing app.js, forcetk.js, jquery.js, and any other static resources your project may need. Upload the zip via *Your Name > App Setup > Develop > Static Resources*. +Create a zip file containing app.js, forcetk.js and any other static resources your project may need. Upload the zip via *Your Name > App Setup > Develop > Static Resources*. Your Visualforce page will need to include jQuery and the toolkit, then create a client object, passing a session ID to the constructor. An absolutely minimal sample is: - - +

The first account I see is .

@@ -89,9 +58,9 @@ More fully featured samples are provided in [example.page](Force.com-JavaScript- Using the Toolkit in an HTML page outside the Force.com platform ---------------------------------------------------------------- -You will need to deploy proxy.php to your server, configuring CORS support (see comments in proxy.php) if your JavaScript is to be hosted on a different server. +As mentioned above, you will need to [configure CORS](https://help.salesforce.com/htviewhelpdoc?err=1&id=extend_code_cors.htm&siteLang=en_US) to call the Force.com REST API from JavaScript hosted 'off-platform'. -Your HTML page will need to include jQuery and the toolkit, then create a client object, passing a session ID to the constructor. An absolutely minimal sample using OAuth to obtain a session ID is: +Your HTML page will need to include the toolkit, then create a client object, passing a session ID to the constructor. An absolutely minimal sample using OAuth to obtain a session ID is: @@ -148,13 +117,17 @@ Your HTML page will need to include jQuery and the toolkit, then create a client

Click here.

+ +TBD - new OAuth! More fully featured samples are provided in [example.html](Force.com-JavaScript-REST-Toolkit/blob/master/example.html) and [mobile.html](Force.com-JavaScript-REST-Toolkit/blob/master/mobile.html). Using the Toolkit in a Cordova app ---------------------------------- -Your HTML page will need to include jQuery, the toolkit and Cordova. You will also need to install the [InAppBrowser](http://plugins.cordova.io/#/package/org.apache.cordova.inappbrowser) plugin to be able to pop up a browser window for authentication. Create a client object, passing a session ID to the constructor. You can use __https://login.salesforce.com/services/oauth2/success__ as the redirect URI and catch the page load in InAppBrowser. +TBD - update for current Cordova! + +Your HTML page will need to include the toolkit and Cordova. You will also need to install the [InAppBrowser](http://plugins.cordova.io/#/package/org.apache.cordova.inappbrowser) plugin to be able to pop up a browser window for authentication. Create a client object, passing a session ID to the constructor. You can use __https://login.salesforce.com/services/oauth2/success__ as the redirect URI and catch the page load in InAppBrowser. An absolutely minimal sample using OAuth to obtain a session ID is: diff --git a/app.js b/app.js index b919832..d679b2c 100644 --- a/app.js +++ b/app.js @@ -61,14 +61,16 @@ "WHERE "+$("#field").val()+" LIKE '%"+request.term+"%' "+ "ORDER BY Name LIMIT 20"; - client.query(query, function( data ) { + client.query(query) + .then(function( data ) { response( $.map( data.records, function( record ) { return { label: record.Name, value: record.Id } })); - }, errorCallback); + }) + .catch(errorCallback); }, minLength: 2, delay: 1000, @@ -115,7 +117,9 @@ $('#list').html(ajaxgif+" creating account..."); - client.create('Account', fields, createCallback, errorCallback); + client.create('Account', fields) + .then(createCallback) + .catch(errorCallback); }); $dialog.dialog('option', 'title', 'New Account'); $dialog.dialog('open'); @@ -167,7 +171,9 @@ e.preventDefault(); $dialog.dialog('close'); $('#list').html(ajaxgif+" deleting account..."); - client.del('Account', $dialog.find('#id').val(), deleteCallback, errorCallback); + client.del('Account', $dialog.find('#id').val()) + .then(deleteCallback) + .catch(errorCallback); }); $dialog.find('#edit').click(function(e) { e.preventDefault(); @@ -187,7 +193,9 @@ $('#list').html(ajaxgif+" updating account..."); - client.update('Account', $dialog.find('#id').val(), fields, updateCallback, errorCallback); + client.update('Account', $dialog.find('#id').val(), fields) + .then(updateCallback) + .catch(errorCallback); }); }); } @@ -217,8 +225,9 @@ $dialog.html(ajaxgif+" retrieving..."); // Get account details and populate the dialog - client.retrieve('Account', id, 'Name,Industry,TickerSymbol,Website' - , detailCallback, errorCallback); + client.retrieve('Account', id, 'Name,Industry,TickerSymbol,Website') + .then(detailCallback) + .catch(errorCallback); } function filterIndustry(industry) { @@ -227,7 +236,9 @@ var query = "SELECT Id, Name FROM Account WHERE Industry = '"+industry +"' ORDER BY Name LIMIT 20"; - client.query(query, queryCallback, errorCallback); + client.query(query) + .then(queryCallback) + .catch(errorCallback); } function filterAccounts(field, value) { @@ -238,6 +249,8 @@ +"%' ORDER BY Name LIMIT 20" : "SELECT Id, Name FROM Account ORDER BY Name LIMIT 20"; - client.query(query, queryCallback, errorCallback); + client.query(query) + .then(queryCallback) + .catch(errorCallback); } \ No newline at end of file diff --git a/example.page b/example.page index 7ce51a7..c6292ce 100644 --- a/example.page +++ b/example.page @@ -42,33 +42,35 @@ Sample Visualforce page for Force.com JavaScript REST Toolkit - + // Kick things off by doing a describe on Account... + $('#prompt').html(ajaxgif+" loading metadata..."); + client.describe('Account') + .then(metadataCallback) + .catch(errorCallback); + }); +
@@ -103,7 +105,7 @@ Sample Visualforce page for Force.com JavaScript REST Toolkit {/for}
- Logout + Logout

Running jQuery 0.0.0, jQuery UI 0.0.0

@@ -144,4 +146,4 @@ Sample Visualforce page for Force.com JavaScript REST Toolkit - + \ No newline at end of file diff --git a/forcetk.js b/forcetk.js index 959594e..4d12519 100644 --- a/forcetk.js +++ b/forcetk.js @@ -33,7 +33,7 @@ */ /*jslint browser: true, plusplus: true*/ -/*global alert, Blob*/ +/*global alert, Blob, Promise*/ var forcetk = window.forcetk; @@ -91,41 +91,40 @@ if (forcetk.Client === undefined) { /** * Refresh the access token. - * @param callback function to call on success - * @param error function to call on failure */ - forcetk.Client.prototype.refreshAccessToken = function (callback, error) { + forcetk.Client.prototype.refreshAccessToken = function () { 'use strict'; - var xhr = new XMLHttpRequest(), - url = this.loginUrl + '/services/oauth2/token', - payload = 'grant_type=refresh_token&client_id=' + this.clientId + '&refresh_token=' + this.refreshToken; - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status > 199 && xhr.status < 300) { - if (callback) { - callback(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); - } - } else { - console.error(xhr.responseText); - if (error) { - error(xhr); + var that = this, + promise = new Promise(function (resolve, reject) { + var xhr = new XMLHttpRequest(), + url = this.loginUrl + '/services/oauth2/token', + payload = 'grant_type=refresh_token&client_id=' + that.clientId + '&refresh_token=' + that.refreshToken; + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status > 199 && xhr.status < 300) { + resolve(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); + } else { + console.error(xhr.responseText); + reject(xhr, xhr.statusText, xhr.response); + } } - } - } - }; + }; + + xhr.open('POST', url, true); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); + xhr.send(payload); + }); - xhr.open('POST', url, true); - xhr.setRequestHeader("Accept", "application/json"); - xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); - xhr.send(payload); + return promise; }; /** * Set a session token and the associated metadata in the client. * @param sessionId a salesforce.com session ID. In a Visualforce page, * use '{!$Api.sessionId}' to obtain a session ID. - * @param [apiVersion="v36.0"] Force.com API version + * @param [apiVersion="v35.0"] Force.com API version * @param [instanceUrl] Omit this if running on Visualforce; otherwise * use the value from the OAuth token. */ @@ -133,7 +132,7 @@ if (forcetk.Client === undefined) { 'use strict'; this.sessionId = sessionId; this.apiVersion = (apiVersion === undefined || apiVersion === null) - ? 'v36.0' : apiVersion; + ? 'v35.0' : apiVersion; if (instanceUrl === undefined || instanceUrl === null) { this.visualforce = true; @@ -162,67 +161,69 @@ if (forcetk.Client === undefined) { /* * Low level utility function to call the Salesforce endpoint. * @param path resource path relative to /services/data - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error * @param [method="GET"] HTTP method for call * @param [payload=null] string payload for POST/PATCH etc */ - forcetk.Client.prototype.ajax = function (path, callback, error, method, payload, retry) { + forcetk.Client.prototype.ajax = function (path, method, payload, retry) { 'use strict'; - // dev friendly API: Add leading '/' if missing so url + path concat always works - if (path.charAt(0) !== '/') { - path = '/' + path; - } - - var xhr = new XMLHttpRequest(), - url = (this.visualforce ? '' : this.instanceUrl) + '/services/data' + path, - that = this; - - method = method || 'GET'; + var that = this, + promise = new Promise(function (resolve, reject) { - // Cache-busting logic inspired by jQuery - url = url + (rquery.test(url) ? "&" : "?") + "_=" + nonce++; + // dev friendly API: Add leading '/' if missing so url + path concat always works + if (path.charAt(0) !== '/') { + path = '/' + path; + } - if (this.asyncAjax) { - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status > 199 && xhr.status < 300) { - if (callback) { - callback(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); - } - } else if (xhr.status === 401 && that.refresh_token) { - if (retry) { - console.error(xhr.responseText); - error(xhr); - } else { - that.refreshAccessToken(function (oauthResponse) { - that.setSessionToken(oauthResponse.access_token, null, - oauthResponse.instance_url); - that.ajax(path, callback, error, method, payload, true); - }, - error); + var xhr = new XMLHttpRequest(), + url = (that.visualforce ? '' : that.instanceUrl) + '/services/data' + path; + + method = method || 'GET'; + + // Cache-busting logic inspired by jQuery + url = url + (rquery.test(url) ? "&" : "?") + "_=" + nonce++; + + if (that.asyncAjax) { + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status > 199 && xhr.status < 300) { + resolve(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); + } else if (xhr.status === 401 && that.refresh_token) { + if (retry) { + console.error(xhr.responseText); + reject(xhr, xhr.statusText, xhr.response); + } else { + // ATTN Christophe - does this look right? + return that.refreshAccessToken() + .then(function (oauthResponse) { + that.setSessionToken(oauthResponse.access_token, null, + oauthResponse.instance_url); + return that.ajax(path, method, payload, true); + }); + } + } else { + console.error(xhr.responseText); + reject(xhr, xhr.statusText, xhr.response); + } } - } else { - console.error(xhr.responseText); - if (error) { - error(xhr); - } - } + }; } - }; - } - xhr.open(method, url, this.asyncAjax); - xhr.setRequestHeader("Accept", "application/json"); - xhr.setRequestHeader(this.authzHeader, "Bearer " + this.sessionId); - xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); - if (method !== "DELETE") { - xhr.setRequestHeader("Content-Type", 'application/json'); - } - xhr.send(payload); + xhr.open(method, url, that.asyncAjax); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId); + xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); + if (method !== "DELETE") { + xhr.setRequestHeader("Content-Type", 'application/json'); + } + xhr.send(payload); - return this.asyncAjax ? null : JSON.parse(xhr.responseText); + if (!that.asyncAjax) { + resolve(JSON.parse(xhr.responseText)); + } + }); + + return promise; }; /** @@ -233,52 +234,54 @@ if (forcetk.Client === undefined) { * @author Tom Gersic * @param path resource path relative to /services/data * @param mimetype of the file - * @param callback function to which response will be passed - * @param [error=null] function to which request will be passed in case of error * @param retry true if we've already tried refresh token flow once */ - forcetk.Client.prototype.getChatterFile = function (path, mimeType, callback, error, retry) { + forcetk.Client.prototype.getChatterFile = function (path, mimeType, retry) { 'use strict'; var that = this, url = (this.visualforce ? '' : this.instanceUrl) + path, - request = new XMLHttpRequest(); + promise = new Promise(function (resolve, reject) { + var request = new XMLHttpRequest(); - request.open("GET", (this.proxyUrl !== null && !this.visualforce) ? this.proxyUrl : url, true); - request.responseType = "arraybuffer"; + request.open("GET", (that.proxyUrl !== null && !that.visualforce) ? that.proxyUrl : url, true); + request.responseType = "arraybuffer"; - request.setRequestHeader(this.authzHeader, "Bearer " + this.sessionId); - request.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); - if (this.proxyUrl !== null && !this.visualforce) { - request.setRequestHeader('SalesforceProxy-Endpoint', url); - } + request.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId); + request.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); + if (that.proxyUrl !== null && !that.visualforce) { + request.setRequestHeader('SalesforceProxy-Endpoint', url); + } - request.onreadystatechange = function () { - // continue if the process is completed - if (request.readyState === 4) { - // continue only if HTTP status is "OK" - if (request.status === 200) { - try { - // retrieve the response - callback(request.response); - } catch (e) { - // display error message - alert("Error reading the response: " + e.toString()); + request.onreadystatechange = function () { + // continue if the process is completed + if (request.readyState === 4) { + // continue only if HTTP status is "OK" + if (request.status === 200) { + try { + // retrieve the response + resolve(request.response); + } catch (e) { + // display error message + alert("Error reading the response: " + e.toString()); + } + } else if (request.status === 401 && !retry) { + //refresh token in 401 + return that.refreshAccessToken() + .then(function (oauthResponse) { + that.setSessionToken(oauthResponse.access_token, null, + oauthResponse.instance_url); + return that.getChatterFile(path, mimeType, true); + }); + } + + reject(request, request.statusText, request.response); } - } else if (request.status === 401 && !retry) { - //refresh token in 401 - that.refreshAccessToken(function (oauthResponse) { - that.setSessionToken(oauthResponse.access_token, null, oauthResponse.instance_url); - that.getChatterFile(path, mimeType, callback, error, true); - }, error); - } else { - // display status message - error(request, request.statusText, request.response); - } - } - }; + }; - request.send(); + request.send(); + }); + return promise; }; // Local utility to create a random string for multipart boundary @@ -300,65 +303,70 @@ if (forcetk.Client === undefined) { * @param filename filename for blob data; e.g. "Q1 Sales Brochure.pdf" * @param payloadField 'VersionData' for ContentVersion, 'Body' for Document * @param payload Blob, File, ArrayBuffer (Typed Array), or String payload - * @param callback function to which response will be passed - * @param [error=null] function to which response will be passed in case of error * @param retry true if we've already tried refresh token flow once */ - forcetk.Client.prototype.blob = function (path, fields, filename, payloadField, payload, callback, error, retry) { + forcetk.Client.prototype.blob = function (path, fields, filename, payloadField, payload, retry) { 'use strict'; var that = this, - url = (this.visualforce ? '' : this.instanceUrl) + '/services/data' + path, - boundary = randomString(), - blob = new Blob([ - "--boundary_" + boundary + '\n' - + "Content-Disposition: form-data; name=\"entity_content\";" + "\n" - + "Content-Type: application/json" + "\n\n" - + JSON.stringify(fields) - + "\n\n" - + "--boundary_" + boundary + "\n" - + "Content-Type: application/octet-stream" + "\n" - + "Content-Disposition: form-data; name=\"" + payloadField - + "\"; filename=\"" + filename + "\"\n\n", - payload, - "\n\n" - + "--boundary_" + boundary + "--" - ], {type : 'multipart/form-data; boundary=\"boundary_' + boundary + '\"'}), - request = new XMLHttpRequest(); - - request.open("POST", (this.proxyUrl !== null && !this.visualforce) ? this.proxyUrl : url, this.asyncAjax); - - request.setRequestHeader('Accept', 'application/json'); - request.setRequestHeader(this.authzHeader, "Bearer " + this.sessionId); - request.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); - request.setRequestHeader('Content-Type', 'multipart/form-data; boundary=\"boundary_' + boundary + '\"'); - if (this.proxyUrl !== null && !this.visualforce) { - request.setRequestHeader('SalesforceProxy-Endpoint', url); - } + promise = new Promise(function (resolve, reject) { + var url = (that.visualforce ? '' : that.instanceUrl) + '/services/data' + path, + boundary = randomString(), + blob = new Blob([ + "--boundary_" + boundary + '\n' + + "Content-Disposition: form-data; name=\"entity_content\";" + "\n" + + "Content-Type: application/json" + "\n\n" + + JSON.stringify(fields) + + "\n\n" + + "--boundary_" + boundary + "\n" + + "Content-Type: application/octet-stream" + "\n" + + "Content-Disposition: form-data; name=\"" + payloadField + + "\"; filename=\"" + filename + "\"\n\n", + payload, + "\n\n" + + "--boundary_" + boundary + "--" + ], {type : 'multipart/form-data; boundary=\"boundary_' + boundary + '\"'}), + request = new XMLHttpRequest(); + + request.open("POST", (that.proxyUrl !== null && !that.visualforce) ? that.proxyUrl : url, that.asyncAjax); + + request.setRequestHeader('Accept', 'application/json'); + request.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId); + request.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); + request.setRequestHeader('Content-Type', 'multipart/form-data; boundary=\"boundary_' + boundary + '\"'); + if (that.proxyUrl !== null && !that.visualforce) { + request.setRequestHeader('SalesforceProxy-Endpoint', url); + } - if (this.asyncAjax) { - request.onreadystatechange = function () { - // continue if the process is completed - if (request.readyState === 4) { - // continue only if HTTP status is good - if (request.status >= 200 && request.status < 300) { - // retrieve the response - callback(request.response ? JSON.parse(request.response) : null); - } else if (request.status === 401 && !retry) { - that.refreshAccessToken(function (oauthResponse) { - that.setSessionToken(oauthResponse.access_token, null, oauthResponse.instance_url); - that.blob(path, fields, filename, payloadField, payload, callback, error, true); - }, error); - } else { - // return status message - error(request, request.statusText, request.response); - } + if (that.asyncAjax) { + request.onreadystatechange = function () { + // continue if the process is completed + if (request.readyState === 4) { + // continue only if HTTP status is good + if (request.status >= 200 && request.status < 300) { + // retrieve the response + resolve(request.response ? JSON.parse(request.response) : null); + } else if (request.status === 401 && !retry) { + return that.refreshAccessToken() + .then(function (oauthResponse) { + that.setSessionToken(oauthResponse.access_token, null, + oauthResponse.instance_url); + return that.blob(path, fields, filename, payloadField, payload, true); + }); + } + // return status message + reject(request, request.statusText, request.response); + } + }; } - }; - } - request.send(blob); + request.send(blob); - return this.asyncAjax ? null : JSON.parse(request.response); + if (!that.asyncAjax) { + resolve(JSON.parse(request.responseText)); + } + }); + + return promise; }; /* @@ -370,16 +378,13 @@ if (forcetk.Client === undefined) { * @param filename filename for blob data; e.g. "Q1 Sales Brochure.pdf" * @param payloadField 'VersionData' for ContentVersion, 'Body' for Document * @param payload Blob, File, ArrayBuffer (Typed Array), or String payload - * @param callback function to which response will be passed - * @param [error=null] function to which response will be passed in case of error * @param retry true if we've already tried refresh token flow once */ forcetk.Client.prototype.createBlob = function (objtype, fields, filename, - payloadField, payload, callback, - error, retry) { + payloadField, payload, retry) { 'use strict'; return this.blob('/' + this.apiVersion + '/sobjects/' + objtype + '/', - fields, filename, payloadField, payload, callback, error, retry); + fields, filename, payloadField, payload, retry); }; /* @@ -392,16 +397,13 @@ if (forcetk.Client === undefined) { * @param filename filename for blob data; e.g. "Q1 Sales Brochure.pdf" * @param payloadField 'VersionData' for ContentVersion, 'Body' for Document * @param payload Blob, File, ArrayBuffer (Typed Array), or String payload - * @param callback function to which response will be passed - * @param [error=null] function to which response will be passed in case of error * @param retry true if we've already tried refresh token flow once */ forcetk.Client.prototype.updateBlob = function (objtype, id, fields, filename, - payloadField, payload, callback, - error, retry) { + payloadField, payload, retry) { 'use strict'; return this.blob('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id + - '?_HttpMethod=PATCH', fields, filename, payloadField, payload, callback, error, retry); + '?_HttpMethod=PATCH', fields, filename, payloadField, payload, retry); }; var param = function (data) { @@ -420,99 +422,100 @@ if (forcetk.Client === undefined) { /* * Low level utility function to call the Salesforce endpoint specific for Apex REST API. * @param path resource path relative to /services/apexrest - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error * @param [method="GET"] HTTP method for call * @param [payload=null] string or object with payload for POST/PATCH etc or params for GET * @param [paramMap={}] parameters to send as header values for POST/PATCH etc * @param [retry] specifies whether to retry on error */ - forcetk.Client.prototype.apexrest = function (path, callback, error, method, payload, paramMap, retry) { + forcetk.Client.prototype.apexrest = function (path, method, payload, paramMap, retry) { 'use strict'; - // dev friendly API: Add leading '/' if missing so url + path concat always works - if (path.charAt(0) !== '/') { - path = '/' + path; - } - - var xhr = new XMLHttpRequest(), - that = this, - url = this.instanceUrl + '/services/apexrest' + path, - paramName; - - method = method || 'GET'; + var that = this, + promise = new Promise(function (resolve, reject) { - if (method === "GET") { - // Handle proxied query params correctly - if (this.proxyUrl && payload) { - if (typeof payload !== 'string') { - payload = param(payload); + // dev friendly API: Add leading '/' if missing so url + path concat always works + if (path.charAt(0) !== '/') { + path = '/' + path; } - url += "?" + payload; - payload = null; - } - } else { - // Allow object payload for POST etc - if (payload && typeof payload !== 'string') { - payload = JSON.stringify(payload); - } - } - // Cache-busting logic inspired by jQuery - url = url + (rquery.test(url) ? "&" : "?") + "_=" + nonce++; + var xhr = new XMLHttpRequest(), + url = that.instanceUrl + '/services/apexrest' + path, + paramName; - if (this.asyncAjax) { - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status > 199 && xhr.status < 300) { - if (callback) { - callback(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); - } - } else if (xhr.status === 401 && that.refresh_token) { - if (retry) { - console.error(xhr.responseText); - error(xhr); - } else { - that.refreshAccessToken(function (oauthResponse) { - that.setSessionToken(oauthResponse.access_token, null, - oauthResponse.instance_url); - that.apexrest(path, callback, error, method, payload, paramMap, true); - }, - error); - } - } else { - console.error(xhr.responseText); - if (error) { - error(xhr); + method = method || 'GET'; + + if (method === "GET") { + // Handle proxied query params correctly + if (that.proxyUrl && payload) { + if (typeof payload !== 'string') { + payload = param(payload); } + url += "?" + payload; + payload = null; + } + } else { + // Allow object payload for POST etc + if (payload && typeof payload !== 'string') { + payload = JSON.stringify(payload); } } - }; - } - xhr.open(method, this.proxyUrl || url, this.asyncAjax); - xhr.setRequestHeader("Accept", "application/json"); - xhr.setRequestHeader(this.authzHeader, "Bearer " + this.sessionId); - xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + this.apiVersion); - xhr.setRequestHeader("Content-Type", 'application/json'); + // Cache-busting logic inspired by jQuery + url = url + (rquery.test(url) ? "&" : "?") + "_=" + nonce++; + + if (that.asyncAjax) { + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status > 199 && xhr.status < 300) { + resolve(xhr.responseText ? JSON.parse(xhr.responseText) : undefined); + } else if (xhr.status === 401 && that.refresh_token) { + if (retry) { + console.error(xhr.responseText); + reject(xhr, xhr.statusText, xhr.response); + } else { + return that.refreshAccessToken() + .then(function (oauthResponse) { + that.setSessionToken(oauthResponse.access_token, null, + oauthResponse.instance_url); + return that.apexrest(path, method, payload, paramMap, true); + }); + } + } else { + console.error(xhr.responseText); + reject(xhr, xhr.statusText, xhr.response); + } + } + }; + } - //Add any custom headers - if (paramMap === null) { - paramMap = {}; - } - for (paramName in paramMap) { - if (paramMap.hasOwnProperty(paramName)) { - xhr.setRequestHeader(paramName, paramMap[paramName]); - } - } + xhr.open(method, that.proxyUrl || url, that.asyncAjax); + xhr.setRequestHeader("Accept", "application/json"); + xhr.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId); + xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion); + xhr.setRequestHeader("Content-Type", 'application/json'); - if (that.proxyUrl !== null) { - xhr.setRequestHeader('SalesforceProxy-Endpoint', url); - } + //Add any custom headers + if (paramMap === null) { + paramMap = {}; + } + for (paramName in paramMap) { + if (paramMap.hasOwnProperty(paramName)) { + xhr.setRequestHeader(paramName, paramMap[paramName]); + } + } + + if (that.proxyUrl !== null) { + xhr.setRequestHeader('SalesforceProxy-Endpoint', url); + } + + xhr.send(payload); - xhr.send(payload); + if (!that.asyncAjax) { + resolve(JSON.parse(xhr.responseText)); + } + }); - return this.asyncAjax ? null : JSON.parse(xhr.responseText); + return promise; }; @@ -520,59 +523,48 @@ if (forcetk.Client === undefined) { * Lists summary information about each Salesforce.com version currently * available, including the version, label, and a link to each version's * root. - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.versions = function (callback, error) { + forcetk.Client.prototype.versions = function () { 'use strict'; - return this.ajax('/', callback, error); + return this.ajax('/'); }; /* * Lists available resources for the client's API version, including * resource name and URI. - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.resources = function (callback, error) { + forcetk.Client.prototype.resources = function () { 'use strict'; - return this.ajax('/' + this.apiVersion + '/', callback, error); + return this.ajax('/' + this.apiVersion + '/'); }; /* * Lists the available objects and their metadata for your organization's * data. - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.describeGlobal = function (callback, error) { + forcetk.Client.prototype.describeGlobal = function () { 'use strict'; - return this.ajax('/' + this.apiVersion + '/sobjects/', callback, error); + return this.ajax('/' + this.apiVersion + '/sobjects/'); }; /* * Describes the individual metadata for the specified object. * @param objtype object type; e.g. "Account" - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.metadata = function (objtype, callback, error) { + forcetk.Client.prototype.metadata = function (objtype) { 'use strict'; - return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/', - callback, error); + return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/'); }; /* * Completely describes the individual metadata at all levels for the * specified object. * @param objtype object type; e.g. "Account" - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.describe = function (objtype, callback, error) { + forcetk.Client.prototype.describe = function (objtype) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype - + '/describe/', callback, error); + + '/describe/'); }; /* @@ -581,13 +573,10 @@ if (forcetk.Client === undefined) { * @param fields an object containing initial field names and values for * the record, e.g. {:Name "salesforce.com", :TickerSymbol * "CRM"} - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.create = function (objtype, fields, callback, error) { + forcetk.Client.prototype.create = function (objtype, fields) { 'use strict'; - return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/', - callback, error, "POST", JSON.stringify(fields)); + return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/', "POST", JSON.stringify(fields)); }; /* @@ -596,19 +585,12 @@ if (forcetk.Client === undefined) { * @param id the record's object ID * @param [fields=null] optional comma-separated list of fields for which * to return values; e.g. Name,Industry,TickerSymbol - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.retrieve = function (objtype, id, fieldlist, callback, error) { + forcetk.Client.prototype.retrieve = function (objtype, id, fieldlist) { 'use strict'; - if (arguments.length === 4) { - error = callback; - callback = fieldlist; - fieldlist = null; - } var fields = fieldlist ? '?fields=' + fieldlist : ''; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id - + fields, callback, error); + + fields); }; /* @@ -620,13 +602,11 @@ if (forcetk.Client === undefined) { * @param fields an object containing field names and values for * the record, e.g. {:Name "salesforce.com", :TickerSymbol * "CRM"} - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.upsert = function (objtype, externalIdField, externalId, fields, callback, error) { + forcetk.Client.prototype.upsert = function (objtype, externalIdField, externalId, fields) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + externalIdField + '/' + externalId - + '?_HttpMethod=PATCH', callback, error, "POST", JSON.stringify(fields)); + + '?_HttpMethod=PATCH', "POST", JSON.stringify(fields)); }; /* @@ -636,13 +616,11 @@ if (forcetk.Client === undefined) { * @param fields an object containing initial field names and values for * the record, e.g. {:Name "salesforce.com", :TickerSymbol * "CRM"} - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.update = function (objtype, id, fields, callback, error) { + forcetk.Client.prototype.update = function (objtype, id, fields) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id - + '?_HttpMethod=PATCH', callback, error, "POST", JSON.stringify(fields)); + + '?_HttpMethod=PATCH', "POST", JSON.stringify(fields)); }; /* @@ -650,39 +628,31 @@ if (forcetk.Client === undefined) { * reserved word in JavaScript. * @param objtype object type; e.g. "Account" * @param id the record's object ID - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.del = function (objtype, id, callback, error) { + forcetk.Client.prototype.del = function (objtype, id) { 'use strict'; - return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id, - callback, error, "DELETE"); + return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id, "DELETE"); }; /* * Executes the specified SOQL query. * @param soql a string containing the query to execute - e.g. "SELECT Id, * Name from Account ORDER BY Name LIMIT 20" - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.query = function (soql, callback, error) { + forcetk.Client.prototype.query = function (soql) { 'use strict'; - return this.ajax('/' + this.apiVersion + '/query?q=' + encodeURIComponent(soql), - callback, error); + return this.ajax('/' + this.apiVersion + '/query?q=' + encodeURIComponent(soql)); }; /* * Queries the next set of records based on pagination. *

This should be used if performing a query that retrieves more than can be returned * in accordance with http://www.salesforce.com/us/developer/docs/api_rest/Content/dome_query.htm

- *

Ex: forcetkClient.queryMore( successResponse.nextRecordsUrl, successHandler, failureHandler )

+ *

Ex: forcetkClient.queryMore(successResponse.nextRecordsUrl, successHandler, failureHandler)

* * @param url - the url retrieved from nextRecordsUrl or prevRecordsUrl - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.queryMore = function (url, callback, error) { + forcetk.Client.prototype.queryMore = function (url) { 'use strict'; //-- ajax call adds on services/data to the url call, so only send the url after var serviceData = "services/data", @@ -692,19 +662,16 @@ if (forcetk.Client === undefined) { url = url.substr(index + serviceData.length); } - return this.ajax(url, callback, error); + return this.ajax(url); }; /* * Executes the specified SOSL search. * @param sosl a string containing the search to execute - e.g. "FIND * {needle}" - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error */ - forcetk.Client.prototype.search = function (sosl, callback, error) { + forcetk.Client.prototype.search = function (sosl) { 'use strict'; - return this.ajax('/' + this.apiVersion + '/search?q=' + encodeURIComponent(sosl), - callback, error); + return this.ajax('/' + this.apiVersion + '/search?q=' + encodeURIComponent(sosl)); }; } From 29176db5a8453e0c3560b06401f9b85accda381f Mon Sep 17 00:00:00 2001 From: Pat Patterson Date: Thu, 21 Jan 2016 13:37:20 -0800 Subject: [PATCH 4/5] Refactored BulkTK for promises. --- BulkAPISampleFiles/update.xml | 2 +- BulkTK.md | 48 +++++--- bulk.page | 55 ++++++--- bulktk.js | 226 ++++++++++++++++++---------------- 4 files changed, 190 insertions(+), 141 deletions(-) diff --git a/BulkAPISampleFiles/update.xml b/BulkAPISampleFiles/update.xml index 8574c72..7e4593c 100644 --- a/BulkAPISampleFiles/update.xml +++ b/BulkAPISampleFiles/update.xml @@ -2,7 +2,7 @@ - 003E000001CbzBi + 003E000001VB8F6IAL Pirate \ No newline at end of file diff --git a/BulkTK.md b/BulkTK.md index a5eb38f..681a64f 100644 --- a/BulkTK.md +++ b/BulkTK.md @@ -53,10 +53,12 @@ Create a job contentType : 'CSV' }; - client.createJob(job, function(response) { + client.createJob(job) + .then(function(response) { jobId = response.jobInfo.id; console.log('Job created with id '+jobId+'\n'); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error creating job', jqXHR.responseText); }); @@ -69,10 +71,11 @@ You can add multiple batches to the job; each batch can contain up to 10,000 rec "Tom,Jones,Marketing,1940-06-07Z,"Self-described as ""the top"" branding guru on the West Coast\n"+ "Ian,Dury,R&D,,"World-renowned expert in fuzzy logic design. Influential in technology purchases."\n"; - client.addBatch(jobId, "text/csv; charset=UTF-8", csvData, - function(response){ + client.addBatch(jobId, "text/csv; charset=UTF-8", csvData) + .then(function(response){ console.log('Added batch '+response.batchInfo.id+'. State: '+response.batchInfo.state+'\n'); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error adding batch', jqXHR.responseText); }); @@ -83,18 +86,22 @@ Close the job You must close the job to inform Salesforce that no more batches will be submitted for the job. - client.closeJob(jobId, function(response){ + client.closeJob(jobId) + .then(function(response){ console.log('Job closed. State: '+response.jobInfo.state+'\n'); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error closing job', jqXHR.responseText); }); Check batch status ------------------ - client.getBatchDetails(jobId, batchId, function(response){ + client.getBatchDetails(jobId, batchId) + .then(function(response){ console.log('Batch state: '+response.batchInfo.state+'\n'); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error getting batch details', jqXHR.responseText); }); @@ -103,9 +110,10 @@ Get batch results Pass `true` as the `parseXML` parameter to get batch results for a query, false otherwise. - client.getBatchResult(jobId, batchId, false, function(response){ + client.getBatchResult(jobId, batchId, false) + .then(function(response){ console.log('Batch result: '+response); - }, function(jqXHR, textStatus, errorThrown) { + }).then(function(jqXHR, textStatus, errorThrown) { console.log('Error getting batch result', jqXHR.responseText); }); @@ -116,22 +124,28 @@ When adding a batch to a bulk query job, the `contentType` for the request must var soql = 'SELECT Id, FirstName, LastName, Email FROM Contact'; - client.addBatch(jobId, 'text/csv', soql, function(response){ + client.addBatch(jobId, 'text/csv', soql) + .then(function(response){ console.log('Batch state: '+response.batchInfo.state+'\n'); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error getting batch result', jqXHR.responseText); }); Getting bulk query results is a two step process. Call `getBatchResult()` with `parseXML` set to `true` to get a set of result IDs, then call `getBulkQueryResult()` to get the actual records for each result - client.getBatchResult(jobId, batchId, true, function(response){ + client.getBatchResult(jobId, batchId, true) + .then(function(response){ response['result-list'].result.forEach(function(resultId){ - client.getBulkQueryResult(jobId, batchId, resultId, function(response){ + client.getBulkQueryResult(jobId, batchId, resultId) + .then(function(response){ console.log('Batch result: '+response); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error getting bulk query results', jqXHR.responseText); }); }); - }, function(jqXHR, textStatus, errorThrown) { + }) + .catch(function(jqXHR, textStatus, errorThrown) { console.log('Error getting batch result', jqXHR.responseText); }); diff --git a/bulk.page b/bulk.page index e9e7504..91260b6 100644 --- a/bulk.page +++ b/bulk.page @@ -148,7 +148,8 @@ Sample Visualforce page for Force.com Bulk API JavaScript Toolkit } job.contentType = contentType; - client.createJob(job, function(response) { + client.createJob(job) + .then(function(response) { batches = 0; jobId = response.jobInfo.id; $message.text('Job created with id '+jobId+'\n'); @@ -156,7 +157,8 @@ Sample Visualforce page for Force.com Bulk API JavaScript Toolkit if (operation === 'query') { $soql.val("SELECT Id FROM "+object); } - }, showError); + }) + .catch(showError); } function setQuery() { @@ -165,47 +167,57 @@ Sample Visualforce page for Force.com Bulk API JavaScript Toolkit ? "text/csv; charset=UTF-8" : "application/xml; charset=UTF-8"; - client.addBatch(jobId, mimeType, soql, function(response){ + client.addBatch(jobId, mimeType, soql) + .then(function(response){ batches++; $message.append('Batch state: ',response.batchInfo.state+'\n'); $close.show(); - }, showError); + }) + .catch(showError); } function getJobDetails(client, jobId){ - client.getJobDetails(jobId, function(response){ + client.getJobDetails(jobId) + .then(function(response){ $message.append(response.jobInfo.numberRecordsProcessed+' records processed\n'); if ((response.jobInfo.numberBatchesCompleted + response.jobInfo.numberBatchesFailed) === batches) { $message.append('Done!\n'); - client.getJobBatchDetails(jobId, function(response){ + client.getJobBatchDetails(jobId) + .then(function(response){ response.batchInfoList.batchInfo.forEach(function(batch){ var batchId = batch.id; - client.getBatchResult(jobId, batchId, (operation === 'query'), function(response){ + client.getBatchResult(jobId, batchId, (operation === 'query')) + .then(function(response){ $message.append('Batch result:\n'); if (operation === 'query') { response['result-list'].result.forEach(function(result){ - client.getBulkQueryResult(jobId, batchId, result, function(response){ + client.getBulkQueryResult(jobId, batchId, result) + .then(function(response){ $message.append('Query result:\n'); var text = (contentType === 'CSV') ? response : vkbeautify.xml(response); $message.append(document.createTextNode(text)); reset(); - }, showError); + }) + .catch(showError); }); } else { var text = (contentType === 'CSV') ? response : vkbeautify.xml(response); $message.append(document.createTextNode(text)); reset(); } - }, showError); + }) + .catch(showError); }); - }, showError); + }). + catch(showError); } else { setTimeout(function(){ getJobDetails(client, jobId); }, 1000); } - }, showError); + }) + .catch(showError); } function upload() { @@ -215,29 +227,34 @@ Sample Visualforce page for Force.com Bulk API JavaScript Toolkit ? "text/csv; charset=UTF-8" : "application/xml; charset=UTF-8"; - client.addBatch(jobId, mimeType, - file, function(response){ + client.addBatch(jobId, mimeType, file) + .then(function(response){ batches++; $message.append('Batch state: ',response.batchInfo.state+'\n'); $close.show(); - }, showError); + }) + .catch(showError); } function closeJob() { - client.closeJob(jobId, function(response){ + client.closeJob(jobId) + .then(function(response){ $message.append('Job state: '+response.jobInfo.state+'\n'); getJobDetails(client, jobId); - }, showError); + }) + .catch(showError); } $(document).ready(function(){ - client.describeGlobal(function(response){ + client.describeGlobal() + .then(function(response){ $object.empty(); response.sobjects.forEach(function(sobject){ $object.append(''); }); $object.val('Contact'); - }, showError); + }) + .catch(showError); $message = $("#message"); $operation = $("#operation"); diff --git a/bulktk.js b/bulktk.js index 4540aea..337a353 100644 --- a/bulktk.js +++ b/bulktk.js @@ -24,6 +24,9 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/*jslint browser: true, plusplus: true*/ +/*global alert, Promise, forcetk, JXON*/ + /* * BulkTK: JavaScript library to wrap Force.com Bulk API. Extends ForceTK. * Dependencies: @@ -40,74 +43,88 @@ forcetk.Client.prototype.xmlHeader = " * Low level utility function to call the Bulk API. * @param path resource path * @param parseXML set to true to parse XML response - * @param callback function to which response will be passed - * @param [error=null] function to which jqXHR will be passed in case of error * @param [method="GET"] HTTP method for call * @param [contentType=null] Content type of payload - e.g. 'application/xml; charset=UTF-8' * @param [payload=null] payload for POST * @param [parseXML=false] set to true to parse XML response */ -forcetk.Client.prototype.bulkAjax = function(path, parseXML, callback, error, method, contentType, payload, retry) { - var that = this; - var url = this.instanceUrl + path; - +forcetk.Client.prototype.bulkAjax = function (path, parseXML, method, contentType, payload, retry) { + 'use strict'; + if (this.debug) { console.log('bulkAjax sending: ', payload); } - - return $.ajax({ - type: method || "GET", - async: this.asyncAjax, - url: (this.proxyUrl !== null) ? this.proxyUrl: url, - contentType: method == "DELETE" ? null : contentType, - cache: false, - processData: false, - data: payload, - success: function(data, textStatus, jqXHR) { - var respContentType = jqXHR.getResponseHeader('Content-Type'); - // Naughty Bulk API doesn't always set Content-Type! - if (parseXML && - ((respContentType && respContentType.indexOf('application/xml') === 0) || - data.indexOf(' 199 && xhr.status < 300) { + var respContentType = xhr.getResponseHeader('Content-Type'), + data = xhr.responseText; + // Naughty Bulk API doesn't always set Content-Type! + if (parseXML && + ((respContentType && respContentType.indexOf('application/xml') === 0) || + data.indexOf(' Date: Mon, 8 Feb 2016 10:51:11 -0500 Subject: [PATCH 5/5] adding forceoauth + modularizing forcetk --- .gitignore | 1 + forceoauth.js | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++ forcetk.js | 154 ++++++++++++++++++------------------- 3 files changed, 282 insertions(+), 80 deletions(-) create mode 100755 forceoauth.js diff --git a/.gitignore b/.gitignore index e43b0f9..4befed3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .DS_Store +.idea diff --git a/forceoauth.js b/forceoauth.js new file mode 100755 index 0000000..de3731f --- /dev/null +++ b/forceoauth.js @@ -0,0 +1,207 @@ +"use strict"; + +let // The login URL for the OAuth process + // To override default, pass loginURL in init(props) + loginURL = 'https://login.salesforce.com', + + // The Connected App client Id. Default app id provided - Not for production use. + // This application supports http://localhost:8200/oauthcallback.html as a valid callback URL + // To override default, pass appId in init(props) + appId = '3MVG9fMtCkV6eLheIEZplMqWfnGlf3Y.BcWdOf1qytXo9zxgbsrUbS.ExHTgUPJeb3jZeT8NYhc.hMyznKU92', + + // The force.com API version to use. + // To override default, pass apiVersion in init(props) + apiVersion = 'v35.0', + + // Keep track of OAuth data (access_token, refresh_token, and instance_url) + oauthData, + + // By default we store fbtoken in sessionStorage. This can be overridden in init() + tokenStore = {}, + + // if page URL is http://localhost:3000/myapp/index.html, context is /myapp + context = window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/")), + + // if page URL is http://localhost:3000/myapp/index.html, serverURL is http://localhost:3000 + serverURL = window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : ''), + + // if page URL is http://localhost:3000/myapp/index.html, baseURL is http://localhost:3000/myapp + baseURL = serverURL + context, + + // Only required when using REST APIs in an app hosted on your own server to avoid cross domain policy issues + // To override default, pass proxyURL in init(props) + proxyURL = baseURL, + + // if page URL is http://localhost:3000/myapp/index.html, oauthCallbackURL is http://localhost:3000/myapp/oauthcallback.html + // To override default, pass oauthCallbackURL in init(props) + oauthCallbackURL = baseURL + '/oauthcallback.html', + + // Whether or not to use a CORS proxy. Defaults to false if app running in Cordova, in a VF page, + // or using the Salesforce console. Can be overriden in init() + useProxy = (window.SfdcApp || window.sforce) ? false : true; + +let parseQueryString = queryString => { + let qs = decodeURIComponent(queryString), + obj = {}, + params = qs.split('&'); + params.forEach(param => { + let splitter = param.split('='); + obj[splitter[0]] = splitter[1]; + }); + return obj; +}; + +let toQueryString = obj => { + let parts = [], + i; + for (i in obj) { + if (obj.hasOwnProperty(i)) { + parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i])); + } + } + return parts.join("&"); +}; + +let refreshToken = () => new Promise((resolve, reject) => { + + if (!oauthData.refresh_token) { + console.log('ERROR: refresh token does not exist'); + reject(); + return; + } + + let xhr = new XMLHttpRequest(), + + params = { + 'grant_type': 'refresh_token', + 'refresh_token': oauthData.refresh_token, + 'client_id': appId + }, + + url = useProxy ? proxyURL : loginURL; + + url = url + '/services/oauth2/token?' + toQueryString(params); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + console.log('Token refreshed'); + let res = JSON.parse(xhr.responseText); + oauthData.access_token = res.access_token; + tokenStore.forceOAuth = JSON.stringify(oauthData); + resolve(); + } else { + console.log('Error while trying to refresh token: ' + xhr.responseText); + reject(); + } + } + }; + + xhr.open('POST', url, true); + if (!useProxy) { + xhr.setRequestHeader("Target-URL", loginURL); + } + xhr.send(); + +}); + +/** + * Initialize ForceJS + * @param params + * appId (optional) + * loginURL (optional) + * proxyURL (optional) + * oauthCallbackURL (optional) + * apiVersion (optional) + * accessToken (optional) + * instanceURL (optional) + * refreshToken (optional) + */ +export let init = params => { + + if (params) { + appId = params.appId || appId; + apiVersion = params.apiVersion || apiVersion; + loginURL = params.loginURL || loginURL; + oauthCallbackURL = params.oauthCallbackURL || oauthCallbackURL; + proxyURL = params.proxyURL || proxyURL; + useProxy = params.useProxy === undefined ? useProxy : params.useProxy; + + if (params.accessToken) { + if (!oauthData) oauthData = {}; + oauthData.access_token = params.accessToken; + } + + if (params.instanceURL) { + if (!oauthData) oauthData = {}; + oauthData.instance_url = params.instanceURL; + } + + if (params.refreshToken) { + if (!oauthData) oauthData = {}; + oauthData.refresh_token = params.refreshToken; + } + } + + console.log("useProxy: " + useProxy); + +}; + +/** + * Discard the OAuth access_token. Use this function to test the refresh token workflow. + */ +export let discardToken = () => { + delete oauthData.access_token; + tokenStore.forceOAuth = JSON.stringify(oauthData); +}; + +export let login = () => new Promise((resolve, reject) => { + + console.log('loginURL: ' + loginURL); + console.log('oauthCallbackURL: ' + oauthCallbackURL); + + let loginWindowURL = loginURL + '/services/oauth2/authorize?client_id=' + appId + '&redirect_uri=' + oauthCallbackURL + '&response_type=token'; + + document.addEventListener("oauthCallback", (event) => { + + // Parse the OAuth data received from Salesforce + let url = event.detail, + queryString, + obj; + + if (url.indexOf("access_token=") > 0) { + queryString = url.substr(url.indexOf('#') + 1); + obj = parseQueryString(queryString); + oauthData = obj; + tokenStore.forceOAuth = JSON.stringify(oauthData); + resolve(oauthData); + } else if (url.indexOf("error=") > 0) { + queryString = decodeURIComponent(url.substring(url.indexOf('?') + 1)); + obj = parseQueryString(queryString); + reject(obj); + } else { + reject({status: 'access_denied'}); + } + + }); + + window.open(loginWindowURL, '_blank', 'location=no'); + +}); + +/** + * Gets the user's ID (if logged in) + * @returns {string} | undefined + */ +export let getUserId = () => (typeof(oauthData) !== 'undefined') ? oauthData.id.split('/').pop() : undefined; + +/** + * Get the OAuth data returned by the Salesforce login process + */ +export let getOAuthData = () => oauthData; + +/** + * Check the login status + * @returns {boolean} + */ +export let isAuthenticated = () => (oauthData && oauthData.access_token) ? true : false; \ No newline at end of file diff --git a/forcetk.js b/forcetk.js index 4d12519..ad5302c 100644 --- a/forcetk.js +++ b/forcetk.js @@ -35,13 +35,34 @@ /*jslint browser: true, plusplus: true*/ /*global alert, Blob, Promise*/ -var forcetk = window.forcetk; - -if (forcetk === undefined) { - forcetk = {}; -} +var nonce = +(new Date()); +var rquery = (/\?/); + +// Local utility to create a random string for multipart boundary +var randomString = function () { + 'use strict'; + var str = '', + i; + for (i = 0; i < 4; i += 1) { + str += (Math.random().toString(16) + "000000000").substr(2, 8); + } + return str; +}; + +var param = function (data) { + 'use strict'; + var r20 = /%20/g, + s = [], + key; + for (key in data) { + if (data.hasOwnProperty(key)) { + s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(data[key]); + } + } + return s.join("&").replace(r20, "+"); +}; -if (forcetk.Client === undefined) { +export class Org { /** * The Client provides a convenient wrapper for the Force.com REST API, @@ -53,8 +74,7 @@ if (forcetk.Client === undefined) { * PhoneGap etc * @constructor */ - forcetk.Client = function (clientId, loginUrl, proxyUrl) { - 'use strict'; + constructor(clientId, loginUrl, proxyUrl) { this.clientId = clientId; this.loginUrl = loginUrl || 'https://login.salesforce.com/'; if (proxyUrl === undefined || proxyUrl === null) { @@ -78,21 +98,21 @@ if (forcetk.Client === undefined) { this.visualforce = false; this.instanceUrl = null; this.asyncAjax = true; - }; + } /** * Set a refresh token in the client. * @param refreshToken an OAuth refresh token */ - forcetk.Client.prototype.setRefreshToken = function (refreshToken) { + setRefreshToken(refreshToken) { 'use strict'; this.refreshToken = refreshToken; - }; + } /** * Refresh the access token. */ - forcetk.Client.prototype.refreshAccessToken = function () { + refreshAccessToken() { 'use strict'; var that = this, promise = new Promise(function (resolve, reject) { @@ -118,7 +138,7 @@ if (forcetk.Client === undefined) { }); return promise; - }; + } /** * Set a session token and the associated metadata in the client. @@ -128,7 +148,7 @@ if (forcetk.Client === undefined) { * @param [instanceUrl] Omit this if running on Visualforce; otherwise * use the value from the OAuth token. */ - forcetk.Client.prototype.setSessionToken = function (sessionId, apiVersion, instanceUrl) { + setSessionToken(sessionId, apiVersion, instanceUrl) { 'use strict'; this.sessionId = sessionId; this.apiVersion = (apiVersion === undefined || apiVersion === null) @@ -153,10 +173,7 @@ if (forcetk.Client === undefined) { } else { this.instanceUrl = instanceUrl; } - }; - - var nonce = +(new Date()); - var rquery = (/\?/); + } /* * Low level utility function to call the Salesforce endpoint. @@ -164,7 +181,7 @@ if (forcetk.Client === undefined) { * @param [method="GET"] HTTP method for call * @param [payload=null] string payload for POST/PATCH etc */ - forcetk.Client.prototype.ajax = function (path, method, payload, retry) { + ajax(path, method, payload, retry) { 'use strict'; var that = this, @@ -224,7 +241,7 @@ if (forcetk.Client === undefined) { }); return promise; - }; + } /** * Utility function to query the Chatter API and download a file @@ -236,7 +253,7 @@ if (forcetk.Client === undefined) { * @param mimetype of the file * @param retry true if we've already tried refresh token flow once */ - forcetk.Client.prototype.getChatterFile = function (path, mimeType, retry) { + getChatterFile(path, mimeType, retry) { 'use strict'; var that = this, url = (this.visualforce ? '' : this.instanceUrl) + path, @@ -282,18 +299,7 @@ if (forcetk.Client === undefined) { }); return promise; - }; - - // Local utility to create a random string for multipart boundary - var randomString = function () { - 'use strict'; - var str = '', - i; - for (i = 0; i < 4; i += 1) { - str += (Math.random().toString(16) + "000000000").substr(2, 8); - } - return str; - }; + } /* Low level function to create/update records with blob data * @param path resource path relative to /services/data @@ -305,7 +311,7 @@ if (forcetk.Client === undefined) { * @param payload Blob, File, ArrayBuffer (Typed Array), or String payload * @param retry true if we've already tried refresh token flow once */ - forcetk.Client.prototype.blob = function (path, fields, filename, payloadField, payload, retry) { + blob(path, fields, filename, payloadField, payload, retry) { 'use strict'; var that = this, promise = new Promise(function (resolve, reject) { @@ -367,7 +373,7 @@ if (forcetk.Client === undefined) { }); return promise; - }; + } /* * Create a record with blob data @@ -380,12 +386,12 @@ if (forcetk.Client === undefined) { * @param payload Blob, File, ArrayBuffer (Typed Array), or String payload * @param retry true if we've already tried refresh token flow once */ - forcetk.Client.prototype.createBlob = function (objtype, fields, filename, + createBlob(objtype, fields, filename, payloadField, payload, retry) { 'use strict'; return this.blob('/' + this.apiVersion + '/sobjects/' + objtype + '/', fields, filename, payloadField, payload, retry); - }; + } /* * Update a record with blob data @@ -399,25 +405,12 @@ if (forcetk.Client === undefined) { * @param payload Blob, File, ArrayBuffer (Typed Array), or String payload * @param retry true if we've already tried refresh token flow once */ - forcetk.Client.prototype.updateBlob = function (objtype, id, fields, filename, + updateBlob(objtype, id, fields, filename, payloadField, payload, retry) { 'use strict'; return this.blob('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id + '?_HttpMethod=PATCH', fields, filename, payloadField, payload, retry); - }; - - var param = function (data) { - 'use strict'; - var r20 = /%20/g, - s = [], - key; - for (key in data) { - if (data.hasOwnProperty(key)) { - s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(data[key]); - } - } - return s.join("&").replace(r20, "+"); - }; + } /* * Low level utility function to call the Salesforce endpoint specific for Apex REST API. @@ -427,7 +420,7 @@ if (forcetk.Client === undefined) { * @param [paramMap={}] parameters to send as header values for POST/PATCH etc * @param [retry] specifies whether to retry on error */ - forcetk.Client.prototype.apexrest = function (path, method, payload, paramMap, retry) { + apexrest(path, method, payload, paramMap, retry) { 'use strict'; var that = this, @@ -516,7 +509,7 @@ if (forcetk.Client === undefined) { }); return promise; - }; + } /* @@ -524,48 +517,48 @@ if (forcetk.Client === undefined) { * available, including the version, label, and a link to each version's * root. */ - forcetk.Client.prototype.versions = function () { + versions() { 'use strict'; return this.ajax('/'); - }; + } /* * Lists available resources for the client's API version, including * resource name and URI. */ - forcetk.Client.prototype.resources = function () { + resources() { 'use strict'; return this.ajax('/' + this.apiVersion + '/'); - }; + } /* * Lists the available objects and their metadata for your organization's * data. */ - forcetk.Client.prototype.describeGlobal = function () { + describeGlobal() { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/'); - }; + } /* * Describes the individual metadata for the specified object. * @param objtype object type; e.g. "Account" */ - forcetk.Client.prototype.metadata = function (objtype) { + metadata(objtype) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/'); - }; + } /* * Completely describes the individual metadata at all levels for the * specified object. * @param objtype object type; e.g. "Account" */ - forcetk.Client.prototype.describe = function (objtype) { + describe(objtype) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/describe/'); - }; + } /* * Creates a new record of the given type. @@ -574,10 +567,10 @@ if (forcetk.Client === undefined) { * the record, e.g. {:Name "salesforce.com", :TickerSymbol * "CRM"} */ - forcetk.Client.prototype.create = function (objtype, fields) { + create(objtype, fields) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/', "POST", JSON.stringify(fields)); - }; + } /* * Retrieves field values for a record of the given type. @@ -586,12 +579,12 @@ if (forcetk.Client === undefined) { * @param [fields=null] optional comma-separated list of fields for which * to return values; e.g. Name,Industry,TickerSymbol */ - forcetk.Client.prototype.retrieve = function (objtype, id, fieldlist) { + retrieve(objtype, id, fieldlist) { 'use strict'; var fields = fieldlist ? '?fields=' + fieldlist : ''; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id + fields); - }; + } /* * Upsert - creates or updates record of the given type, based on the @@ -603,11 +596,11 @@ if (forcetk.Client === undefined) { * the record, e.g. {:Name "salesforce.com", :TickerSymbol * "CRM"} */ - forcetk.Client.prototype.upsert = function (objtype, externalIdField, externalId, fields) { + upsert(objtype, externalIdField, externalId, fields) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + externalIdField + '/' + externalId + '?_HttpMethod=PATCH', "POST", JSON.stringify(fields)); - }; + } /* * Updates field values on a record of the given type. @@ -617,11 +610,11 @@ if (forcetk.Client === undefined) { * the record, e.g. {:Name "salesforce.com", :TickerSymbol * "CRM"} */ - forcetk.Client.prototype.update = function (objtype, id, fields) { + update(objtype, id, fields) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id + '?_HttpMethod=PATCH', "POST", JSON.stringify(fields)); - }; + } /* * Deletes a record of the given type. Unfortunately, 'delete' is a @@ -629,20 +622,20 @@ if (forcetk.Client === undefined) { * @param objtype object type; e.g. "Account" * @param id the record's object ID */ - forcetk.Client.prototype.del = function (objtype, id) { + del(objtype, id) { 'use strict'; return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id, "DELETE"); - }; + } /* * Executes the specified SOQL query. * @param soql a string containing the query to execute - e.g. "SELECT Id, * Name from Account ORDER BY Name LIMIT 20" */ - forcetk.Client.prototype.query = function (soql) { + query(soql) { 'use strict'; return this.ajax('/' + this.apiVersion + '/query?q=' + encodeURIComponent(soql)); - }; + } /* * Queries the next set of records based on pagination. @@ -652,7 +645,7 @@ if (forcetk.Client === undefined) { * * @param url - the url retrieved from nextRecordsUrl or prevRecordsUrl */ - forcetk.Client.prototype.queryMore = function (url) { + queryMore(url) { 'use strict'; //-- ajax call adds on services/data to the url call, so only send the url after var serviceData = "services/data", @@ -663,15 +656,16 @@ if (forcetk.Client === undefined) { } return this.ajax(url); - }; + } /* * Executes the specified SOSL search. * @param sosl a string containing the search to execute - e.g. "FIND * {needle}" */ - forcetk.Client.prototype.search = function (sosl) { + search(sosl) { 'use strict'; return this.ajax('/' + this.apiVersion + '/search?q=' + encodeURIComponent(sosl)); - }; + } + }