Oh my god. It's full of code!

Access Salesforce REST API with Javascript

So this has been a bit of a holy grail kind of endeavor for me. A way to access Salesforce org data using only javascript. Salesforce has a million ways to get at it’s data an interact with it, but it’s always required some kind of server side technology to create the requests and deal with authentication. Not anymore! Now before I get too far, this is just proof of concept code I’d say. It works, but I only just got it working in the last few hours and have done very minimal testing, but the concepts are solid.

The approach is create a Salesforce Apex REST service that can perform all the basic CRUD operations. You then host that REST service on a salesforce.com site. By using JSONP and callbacks you can hit the service from any javascript program, send requests and get data back. This idea was put forth by Pat Patterson via his awesome post at

http://blogs.developerforce.com/developer-relations/2012/02/quick-tip-public-restful-web-services-on-force-com-sites.html

I just took the idea an expanded upon it a bit. So first off, here is the code.

/* RestProxy
Author: Daniel Llewellyn (Kenji776@gmail.com, @Kenji776)

Description: This is a rest class that can be made available to client applications via salesforce.com
sites which will allow them to perform basic CRUD operations against the org. Before this, there was no
way I know of for pure javascript applications to 'integrate' with Salesforce. You had to deal with oAuth or 
SOAP or whatever. Stuff Javascript can't really do. I needed a way to get data in and out of a site.com hosted site
which only gives you access to javascript. So I wrote this. YOu can perform queries, updates and deletes. Very basic right
now, but could be expanded on as required. Inspired by the awesome post at

http://blogs.developerforce.com/developer-relations/2012/02/quick-tip-public-restful-web-services-on-force-com-sites.html

*/

@RestResource(urlMapping='/respondentPortal/*')
global class respondentPortal 
{
    
    
    //simple wrapper response class. Makes it so all replies via this API have the same basic structure
    //including a boolean success flag, a message, any sObjects affected and some debugging params.
    global class restResponseWrapper
    {
        string message = 'run successful';
        boolean success = true;
        list<string> params = new list<string>();       
        list<sobject> sObjects = new list<sobject>();
        string requestURI = RestContext.request.requestURI;
    }
    
    class applicationException extends Exception {}
    
    //can run queries. This one doesn't except an /objectType/Id as the other methods do. This one just takes a query as specified in the
    //?query= param of the url. It will return all the objects in the sObjects property of the returned object.
    @HttpGet
    global static void doGet() 
    {                     
        restResponseWrapper thisResponse = new restResponseWrapper();
        
        //since all requests to this service will be get, since we are operating using JSONP, we first need to figure out what they actually want to do.
        thisResponse.params = getRestParams(thisResponse.requestURI);     
        string httpVerb = thisResponse.params[1].toLowerCase();

        try
        {      
              
            if(RestContext.request.params.get('authToken') == null)
            {
                throw new applicationException('No auth token provided. Please provide one with ?authToken= in the request url');
            }
            long authToken = getAuthToken();
            long providedToken = long.valueOf(RestContext.request.params.get('authToken'));
            
            //check the auth token, provide a 20 second range where it is valid (+10 seconds, -10 seconds)
            if(providedToken == null || providedToken < authToken - 10000 || providedToken > authToken + 10000)
            {
                throw new applicationException('Invalid auth token');
            }
                     
            if(httpVerb == 'get')
            {               
                String query = RestContext.request.params.get('query');
                thisResponse.sObjects = runQuery(EncodingUtil.urlDecode(query,'UTF-8'));  
                 
            }
            else if(httpVerb == 'post')
            {
                thisResponse.sObjects.add(saveSObject(thisResponse.params[2],null,RestContext.request.params));
            }
            else if(httpVerb == 'put')
            {
                thisResponse.sObjects.add(saveSObject(thisResponse.params[2],thisResponse.params[3],RestContext.request.params));
            } 
            else if(httpVerb == 'delete')
            {
                Schema.sObjectType objectDef = Schema.getGlobalDescribe().get(thisResponse.params[2]).getDescribe().getSObjectType();     
                sObject obj = objectDef.newSobject(thisResponse.params[3]);     
                database.delete(obj);  
            }   
            else
            {
                thisResponse.success = false;
                thisResponse.message = 'method ' + httpVerb + ' not implimented';                
            }          
        } 
        catch(exception e)
        {
            thisResponse.success = false;
            thisResponse.message = e.getMessage();
        }     
        

        RestContext.response.addHeader('Content-Type', 'text/plain');
        RestContext.response.responseBody = formatResponse(thisResponse);                    

    }

    
    
    //take a query string and run it. Yeah, I know SOQL injection possible here, but whatever. This
    //whole thing is inherently insecure. It's like worrying about wet floors on the titanic.
    public static list<sobject> runQuery(string queryString)
    {
        list<sobject> objs = new list<sobject>();
        objs = database.query(queryString);
        return objs;
    }
    
    //returns rest params (things after the main url, separated by /)
    //in an easy to use list. The name of the class should be element 0, the 
    //type of object should be element 1 and the id of the object should be element 2
    global static list<string> getRestParams(string url)
    {
        list<string> returnParams = new list<string>();
        integer endOfUrl = url.indexOf('?');
        if(endOfUrl == -1)
        {
            endOfUrl = url.length();
        }
        //clean up the url, make sure we are only dealing with the section we want to. After the host name, before the ? mark.
        //i've seen in come in with the full host name, and also just the relative portion, so we gotta make sure it's exactly the same
        //from here on in, or else the array it returns could get all messed up.
        if(url.indexOf('/apexrest/') > 0)
        {   
            url = url.substring(url.indexOf('/apexrest/')+10,endOfUrl);
        }  
        else
        {
            url = url.substring(1,endOfUrl);
        }            
        list<String> URLParams = url.split('/'); 
         
        for(integer i =0; i < 4; i++)
        {
            if(i<urlParams.size())
            {
                returnParams.add(EncodingUtil.urlDecode(urlParams[i], 'UTF-8'));
            }
            else
            {
                returnParams.add(' ');
            }
        }   
        
        return returnParams;           
    }
    
    
    //so for the record, I really dislike this method. I wish I could just take the pure json data and deserialize it into
    //an sobject type defined at runtime, but as far as I know, you cannot do that (at least I couldn't find the syntax for it).
    //so instead this iterates over the keys of the sobject known from the global describe, finds matching keys in the JSON data
    //and assignes the value from the JSON to the sObject.
    public static sObject saveSObject(string objectType, string recordid, Map<String, Object> sObjectData)
    {
        //create a generic sObject to contain the update values.
        sObject updateObj;      
                
        //get the describe object for the type of object passed in (its just passed in as a string from the URL)
        Schema.sObjectType objectDef = Schema.getGlobalDescribe().get(objectType).getDescribe().getSObjectType();
        
        //find all the fields for this object type
        Map<String, Schema.SobjectField> ObjectFieldsMap = objectDef.getDescribe().fields.getMap();
        
 
        //this method can handle updates or inserts. If a record ID was passed in, 
        //cast the object as the type represented by the ID. If not, just create a
        //new object of the type found in the object describe.
        if(recordId != null)
        {
            updateObj = objectDef.newSobject(recordid);
        }
        else
        {
            updateObj = objectDef.newSobject();
        }    
        // populate the object's fields by looping over all the params in the rest request.
        for (String key : sObjectData.keySet())
        {
            // only add params if they are valid field on the object
            if (ObjectFieldsMap.containsKey(key))
            {
                //figure out the type of this field so we can cast it to the correct type
                string fieldType = ObjectFieldsMap.get(key).getDescribe().getType().name().ToLowerCase();
                
                //since I don't know how to do, or if it's even possible to do dynamic casting we need a 
                //series of if statments to handle the casting to numeric types. I think all the others should
                //be fine if left as a string. Dates might explode, not sure.
                
                
                if(fieldType == 'currency' || fieldType == 'double' || fieldType == 'percent' || fieldType == 'decimal' )
                {
                    updateObj.put(key, decimal.valueOf(string.valueOf(sObjectData.get(key)).trim())); 
                }
                else if(fieldType == 'boolean')
                {
                    updateObj.put(key, Boolean.valueOf(sObjectData.get(key))); 
                }                   
                else if(fieldType == 'date')
                {
                    updateObj.put(key, date.valueOf(sObjectData.get(key))); 
                }                
                else
                {
                    updateObj.put(key, sObjectData.get(key));
                }
            }
            else
            {
                system.debug('Invalid field: '+ key + ' for object type ' + objectType);
            }
        }
        //update/insert the object
        upsert updateObj;
        
        //return the saved object.
        return updateObj;
        
    }
        
    //take one of those wrapper objects and format it by wrapping it in a callback if needed, and serializing 
    //the result into json.    
    public static blob formatResponse(restResponseWrapper responseData)
    {
        string response;
        String callback = RestContext.request.params.get('callback');
        if(callback != null)
        {
            response = callback + '(' + JSON.serialize(responseData) + ');';
        }    
        else
        {
            response = JSON.serialize(responseData);
        }
        return blob.valueOf(response);
    }
    
    //just put some random mathemtical expression here that will only be known by the client application and 
    //this class. I know it's not super secure, but it's better than nothing. Will at least stop super novice
    //script kiddies.
    public static long getAuthToken()
    {
        return DateTime.Now().getTime() - 42131 / 3 * 8;
    }
    
    @isTest
    global static void testRespondentPortal()
    {
        // set up the request object
        System.RestContext.request = new RestRequest();
        System.RestContext.response = new RestResponse();
        
        //First lets try and create a contact.
        RestContext.request.requestURI = '/respondentPortal/post/contact';
        RestContext.request.params.put('firstname','test');
        RestContext.request.params.put('lastname','guy');
        RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
        //send the request
        respondentPortal.doGet();
        respondentPortal.restResponseWrapper createResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class); 
        
       
        //make sure the create was successful       
        system.assertEquals(true,createResponse.success,createResponse.message);
              
        //Okay, now lets try and get that contact we created
        System.RestContext.request = new RestRequest();
        string query = 'select name, id from contact where id = \''+createResponse.sObjects[0].Id+'\'';
        RestContext.request.requestURI = '/respondentPortal/get';
        RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
        RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
        respondentPortal.doGet();
        
        respondentPortal.restResponseWrapper queryResponse = (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);        

        system.assertEquals(true,queryResponse.success,queryResponse.message);
        system.assertEquals(1,queryResponse.sObjects.size());
        
        //cool, now lets do an update on that contact
        System.RestContext.request = new RestRequest();
        RestContext.request.requestURI = '/respondentPortal/put/contact/'+createResponse.sObjects[0].Id;
        RestContext.request.params.put('firstname','frank');
        RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
        respondentPortal.doGet();
        respondentPortal.restResponseWrapper updateResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);

        system.assertEquals(true,updateResponse.success,updateResponse.message);
        system.assertEquals(1,updateResponse.sObjects.size());   
        system.assertEquals('frank',updateResponse.sObjects[0].get('firstname'));
        
        //and finally we'll delete them 
        System.RestContext.request = new RestRequest();
        RestContext.request.requestURI = '/respondentPortal/delete/contact/'+createResponse.sObjects[0].Id;    
        RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
        respondentPortal.doGet();
        respondentPortal.restResponseWrapper deleteResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class); 
        system.assertEquals(true,deleteResponse.success,deleteResponse.message);  
        
        //now try to make a call without an auth token. it should bomb 
        System.RestContext.request = new RestRequest();
        RestContext.request.requestURI = '/respondentPortal/get';
        RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
        respondentPortal.doGet();
        respondentPortal.restResponseWrapper noAuthTokenResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);

        system.assertEquals(false,noAuthTokenResponse.success,noAuthTokenResponse.message);   


        //now try to make a call with a bad auth token. it should bomb 
        System.RestContext.request = new RestRequest();
        RestContext.request.requestURI = '/respondentPortal/get';
        RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
        RestContext.request.params.put('authToken','12345678');
        respondentPortal.doGet();
        respondentPortal.restResponseWrapper badAuthTokenResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);

        system.assertEquals(false,badAuthTokenResponse.success,badAuthTokenResponse.message); 
                
        //try to make a call to a non existing http verb   
        System.RestContext.request = new RestRequest();
        RestContext.request.requestURI = '/respondentPortal/herpDerp';
        RestContext.request.params.put('query',EncodingUtil.urlEncode(query,'UTF-8'));
        RestContext.request.params.put('authToken',string.valueOf(getAuthToken()));
        respondentPortal.doGet();
        respondentPortal.restResponseWrapper badVerbResponse= (restResponseWrapper) JSON.deserialize(RestContext.response.responseBody.toString(),restResponseWrapper.class);

        system.assertEquals(false,badVerbResponse.success,badVerbResponse.message); 
        
        blob jsonResponse = formatResponse(badVerbResponse);   
      
             
    }
}

And here is a basic test page to show you how to invoke it with javascript and handle the results, along with how to generate a matching auth token.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="core.js"></script>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Respondent Portal JS Library Test</title>

<script>

    // JavaScript Document
    var portal = new Object();
    
    portal.restEndPoint = 'https://yourendpoint.testbed.cs7.force.com/yoursite/services/apexrest/respondentPortal/';
    
    portal.runQuery = function(queryString,callback)
    {
        var jqxhr = $.getJSON(portal.restEndPoint+'get/?callback=?', {authToken:portal.generateAuthToken(),query: encodeURI(queryString)},  function(response) {
          console.log('queryRan');
          callback(response);
        })        
    }
    
    portal.generateAuthToken = function generateAuthToken()
    {
        return Math.round(new Date().getTime() - 42131 / 3 * 8);
    }
    
    portal.runQuery('select name, id from contact',function(result){
        console.log('Callback ran');
        console.log(result);
    });
</script>
</head>

<body>
</body>
</html>

The basic structure of a call being

https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/%5BhttpVerb%5D/%5BobjectType%5D/%5Bid%5D?sObjectfield=value&othersObjectfield=otherValue
So to perform an update you might call.

https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/put/contact/003M000000DSvWT?email=Kenji776@gmail.com

To do a create it would be

https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/post/contact?firstname=Dan&lastname=Llewellyn

A delete would be

https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/delete/contact/003M000000DSvWT

and finally a query.

https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/get/?query=select id, name from contact

Also, a nice feature of the update and create, you don’t have to worry about only passing in valid sObject fields. It will use everything it can, and discard the rest. So if you arn’t sure if a field exists, or if you have rights to it, no worries. The program won’t exlode. It might not perfectly support dates or relationships yet though.

Any of those calls can accept a ?callback=functionName param which will cause the service to return the data in a callback which can be used for doing JSONP style transactions. EX

<script>
$.getJSON("https://yourorg.cs7.force.com/yoursite/services/apexrest/respondentPortal/get/?query=select id, name from contact&callback=?",
 function(data) {
    $('#data').html(data);
 });
</script>

Of course this service is a big ass security risk, so you’d likely want to at least add some kind of password to your requests, or something. Maybe a key that cycles daily that both your class and your javascript can calculate? I dunno. At least now you can get at your org data using javascript, which to me, is a big win.

EDIT: The new version of the code posted above includes a super basic authToken system. All it requires is that the client application and the REST service have a pre agreed upon method for generating a one time use key per request. I just used epoch time with some totally random changes to it, and then added a 10 second buffer. It’s pretty lame I know, but it’s better than nothing at least. Try and protect the formula you use to generate the key.

There ya go. Still a little rough around the edges, but should be enough to get you started.

One response

  1. Ravi

    can you provide core.js file.

    August 30, 2013 at 7:51 am

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s