/*
* Copyright (c) 2011, salesforce.com, inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are permitted provided
* that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this list of conditions and the
* following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
* the following disclaimer in the documentation and/or other materials provided with the distribution.
*
* Neither the name of salesforce.com, inc. nor the names of its contributors may be used to endorse or
* promote products derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/* JavaScript library to wrap REST API on Visualforce. Leverages Ajax Proxy
* (see http://bit.ly/sforce_ajax_proxy for details).
*
* Note that you must add the REST endpoint hostname for your instance (i.e.
* https://na1.salesforce.com/ or similar) as a remote site - in the admin
* console, go to Your Name | Setup | Security Controls | Remote Site Settings
*/
/*jslint browser: true*/
/*global alert, Blob, $, jQuery*/
var forcetk = window.forcetk;
if (forcetk === undefined) {
forcetk = {};
}
if (forcetk.Client === undefined) {
/**
* The Client provides a convenient wrapper for the Force.com REST API,
* allowing JavaScript in Visualforce pages to use the API via the Ajax
* Proxy.
* @param [clientId=null] 'Consumer Key' in the Remote Access app settings
* @param [loginUrl='https://login.salesforce.com/'] Login endpoint
* @param [proxyUrl=null] Proxy URL. Omit if running on Visualforce or
* PhoneGap etc
* @constructor
*/
forcetk.Client = function (clientId, loginUrl, proxyUrl) {
'use strict';
this.clientId = clientId;
this.loginUrl = loginUrl || 'https://login.salesforce.com/';
if (proxyUrl === undefined || proxyUrl === null) {
if (location.protocol === 'file:' || location.protocol === 'ms-appx:') {
// In PhoneGap
this.proxyUrl = null;
} else {
// In Visualforce - still need proxyUrl for Apex REST methods
this.proxyUrl = location.protocol + "//" + location.hostname
+ "/services/proxy";
}
this.authzHeader = "Authorization";
} else {
// On a server outside VF
this.proxyUrl = proxyUrl;
this.authzHeader = "X-Authorization";
}
this.refreshToken = null;
this.sessionId = null;
this.apiVersion = null;
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) {
'use strict';
this.refreshToken = refreshToken;
};
/**
* 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) {
'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);
}
}
});
};
/**
* 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 [instanceUrl] Omit this if running on Visualforce; otherwise
* use the value from the OAuth token.
*/
forcetk.Client.prototype.setSessionToken = function (sessionId, apiVersion, instanceUrl) {
'use strict';
this.sessionId = sessionId;
this.apiVersion = (apiVersion === undefined || apiVersion === null)
? 'v29.0' : apiVersion;
if (instanceUrl === undefined || instanceUrl === null) {
this.visualforce = true;
// location.hostname can be of the form 'abc.na1.visual.force.com',
// 'na1.salesforce.com' or 'abc.my.salesforce.com' (custom domains).
// Split on '.', and take the [1] or [0] element as appropriate
var elements = location.hostname.split("."),
instance = null;
if (elements.length === 4 && elements[1] === 'my') {
instance = elements[0] + '.' + elements[1];
} else if (elements.length === 3) {
instance = elements[0];
} else {
instance = elements[1];
}
this.instanceUrl = "https://" + instance + ".salesforce.com";
} else {
this.instanceUrl = instanceUrl;
}
};
/*
* 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
*/
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);
}
xhr.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId);
xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion);
}
});
};
/**
* Utility function to query the Chatter API and download a file
* Note, raw XMLHttpRequest because JQuery mangles the arraybuffer
* This should work on any browser that supports XMLHttpRequest 2 because arraybuffer is required.
* For mobile, that means iOS >= 5 and Android >= Honeycomb
* @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) {
'use strict';
var that = this,
url = (this.visualforce ? '' : this.instanceUrl) + path,
request = new XMLHttpRequest();
request.open("GET", (this.proxyUrl !== null && !this.visualforce) ? this.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.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());
}
} 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();
};
// 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
* @param fields an object containing initial field names and values for
* the record, e.g. {ContentDocumentId: "069D00000000so2",
* PathOnClient: "Q1 Sales Brochure.pdf"}
* @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) {
'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);
}
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);
}
}
};
}
request.send(blob);
return this.asyncAjax ? null : JSON.parse(request.response);
};
/*
* Create a record with blob data
* @param objtype object type; e.g. "ContentVersion"
* @param fields an object containing initial field names and values for
* the record, e.g. {ContentDocumentId: "069D00000000so2",
* PathOnClient: "Q1 Sales Brochure.pdf"}
* @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) {
'use strict';
return this.blob('/' + this.apiVersion + '/sobjects/' + objtype + '/',
fields, filename, payloadField, payload, callback, error, retry);
};
/*
* Update a record with blob data
* @param objtype object type; e.g. "ContentVersion"
* @param id the record's object ID
* @param fields an object containing initial field names and values for
* the record, e.g. {ContentDocumentId: "069D00000000so2",
* PathOnClient: "Q1 Sales Brochure.pdf"}
* @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) {
'use strict';
return this.blob('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id +
'?_HttpMethod=PATCH', fields, filename, payloadField, payload, callback, error, retry);
};
/*
* 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) {
'use strict';
var that = this,
url = this.instanceUrl + '/services/apexrest' + path;
method = method || "GET";
if (method === "GET") {
// Handle proxied query params correctly
if (this.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);
}
}
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]);
}
}
xhr.setRequestHeader(that.authzHeader, "Bearer " + that.sessionId);
xhr.setRequestHeader('X-User-Agent', 'salesforce-toolkit-rest-javascript/' + that.apiVersion);
}
});
};
/*
* 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) {
'use strict';
return this.ajax('/', callback, error);
};
/*
* 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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/', callback, error);
};
/*
* 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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/', callback, error);
};
/*
* 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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/',
callback, error);
};
/*
* 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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype
+ '/describe/', callback, error);
};
/*
* Creates a new record of the given type.
* @param objtype object type; e.g. "Account"
* @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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/',
callback, error, "POST", JSON.stringify(fields));
};
/*
* Retrieves field values for a record of the given type.
* @param objtype object type; e.g. "Account"
* @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) {
'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);
};
/*
* Upsert - creates or updates record of the given type, based on the
* given external Id.
* @param objtype object type; e.g. "Account"
* @param externalIdField external ID field name; e.g. "accountMaster__c"
* @param externalId the record's external ID value
* @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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + externalIdField + '/' + externalId
+ '?_HttpMethod=PATCH', callback, error, "POST", JSON.stringify(fields));
};
/*
* Updates field values on a record of the given type.
* @param objtype object type; e.g. "Account"
* @param id the record's object ID
* @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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id
+ '?_HttpMethod=PATCH', callback, error, "POST", JSON.stringify(fields));
};
/*
* Deletes a record of the given type. Unfortunately, 'delete' is a
* 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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/sobjects/' + objtype + '/' + id,
callback, error, "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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/query?q=' + encodeURIComponent(soql),
callback, error);
};
/*
* 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 )
*
* @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) {
'use strict';
//-- ajax call adds on services/data to the url call, so only send the url after
var serviceData = "services/data",
index = url.indexOf(serviceData);
if (index > -1) {
url = url.substr(index + serviceData.length);
}
return this.ajax(url, callback, error);
};
/*
* 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) {
'use strict';
return this.ajax('/' + this.apiVersion + '/search?q=' + encodeURIComponent(sosl),
callback, error);
};
}