function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
rsoesemannrsoesemann 

ExtJS Grid with REST Proxy always gets a 302 Found when connect to Apex REST service

I'm trying to use the ExtJS 4 Grid widget to display multipe SObjects on a page in an editable grid.

 

I have a visualforce page which just calls a custom component.

 

<apex:page sidebar="false" showHeader="false">

	<c:sobjectGrid objectName="Opportunity" />
	
	<c:sobjectGrid objectName="Contact" />
	
	<div id="pmgrid">
		<c:sobjectGrid objectName="Milestone__c" width="600" height="600" title="Milestones" />
	</div>
	
</apex:page>

 

This visualforce component initializes the ExtJS Grid

<apex:component >
	<apex:attribute name="objectName" 	description=" " type="String" 	required="true"/>
	<apex:attribute name="fields" 		description=" " type="String" 	required="false" default="Id, Name" />
	<apex:attribute name="limit" 		description=" " type="String" 	required="false" default="100" />
	<apex:attribute name="orderby" 		description=" " type="String" 	required="false"/>
	<apex:attribute name="search" 		description=" " type="String" 	required="false"/>
	<apex:attribute name="width" 		description=" " type="Integer" 	required="false" default="400" />
	<apex:attribute name="height" 		description=" " type="Integer" 	required="false" default="300" />
	<apex:attribute name="title" 		description=" " type="String" 	required="false" default="" />

	<!-- Visualforce takes care that this is only loaded once when having more than one component in a page  -->
	<apex:includeScript value="http://cdn.sencha.io/ext-4.0.7-gpl/ext-all.js" />
	<apex:stylesheet value="http://cdn.sencha.io/ext-4.0.7-gpl/resources/css/ext-all.css" />

	<script type="text/javascript">

		Ext.require(['Ext.data.*', 'Ext.grid.*']);

		Ext.define('CurrentSObject', {
			extend: 'Ext.data.Model',
			fields: ['id', 'name']
		});

		Ext.onReady(function(){
			var store = Ext.create('Ext.data.Store', {
				autoLoad: true,
				autoSync: true,
				model: 'CurrentSObject',
				proxy: {
					type: 'rest',
					url: '../services/apexrest/sobject/{!objectName}',
					params: {
		  				sessionId: '{!$Api.Session_ID}'
			  		},
					reader: {
						type: 'json'
					},
					writer: {
						type: 'json'
					}
				},
				listeners: {
					write: function(store, operation){
						var record = operation.getRecords()[0],
							name = Ext.String.capitalize(operation.action),
							verb;
							
						if (name == 'Destroy') {
							record = operation.records[0];
							verb = 'Destroyed';
						} else {
							verb = name + 'd';
						}
						Ext.example.msg(name, Ext.String.format("{0} {!objectName}: {1}", verb, record.getId()));
					}
				}
			});
			
			var rowEditing = Ext.create('Ext.grid.plugin.RowEditing');
			
			var grid = Ext.create('Ext.grid.Panel', {
				renderTo: parent.Ext.getBody(),
				plugins: [rowEditing],
				width: {!width},
				height: {!height},
				title: '{!title}',
				store: store,
				columns: [{
					text: 'ID',
					width: 40,
					sortable: true,
					dataIndex: 'id'
				}, {
					text: 'Name',
					flex: 1,
					sortable: true,
					dataIndex: 'name',
					field: {
						xtype: 'textfield'
					}
				}],
				dockedItems: [{
					xtype: 'toolbar',
					items: [{
						text: 'Add',
						iconCls: 'icon-add',
						handler: function(){
							// empty record
							store.insert(0, new CurrentSObject());
							rowEditing.startEdit(0, 0);
						}
					}, '-', {
						itemId: 'delete',
						text: 'Delete',
						iconCls: 'icon-delete',
						disabled: true,
						handler: function(){
							var selection = grid.getView().getSelectionModel().getSelection()[0];
							if (selection) {
								store.remove(selection);
							}
						}
					}]
				}]
			});
			grid.getSelectionModel().on('selectionchange', function(selModel, selections){
				grid.down('#delete').setDisabled(selections.length === 0);
			});
		});
	</script>
</apex:component>

 and loads via this generic SObject CRUD Service that is implemented as Apex REST:

 

@RestResource(urlMapping='/sobject/*') 
global with sharing class SObjectRestService { 
	
	@HttpPost
	global static String doCreate(RestRequest request, RestResponse response) {
		String resourceType = getType(request);
		String resourceId = getId(request);

		if (resourceId == null) {
			return createResource(resourceType, request);
		} 
		else {	
			response.statusCode = 400;	// BAD REQUEST
			return 'Invalid operation';
		} 
	}

	@HttpGet
	global static List<SObject> doRead(RestRequest request, RestResponse response) {
		String resourceType = getType(request);
		String resourceId = getId(request);

		if (resourceId != null) {
			return getSpecificResource(resourceType, resourceId, request);
		} 
		else {	
			return getAllResources(resourceType, request);
		} 
	}		
	
	@HttpPut
	global static String doUpdate(RestRequest request, RestResponse response) {
		String resourceType = getType(request);
		String resourceId = getId(request);
		Map<String, String> params = request.params;

		if (resourceId != null) {
			return updateResource(resourceType, resourceId, request);
		} 
		else {	
			return 'Invalid operation';
		} 
	}
	
	@HttpDelete
	global static String doDelete(RestRequest request, RestResponse response) {
		String resourceType = getType(request);
		String resourceId = getId(request);

		if (resourceId != null) {
			return deleteResource(resourceType, resourceId);
		} 
		else {	
			return 'Invalid operation';
		} 
	}
  
  	private static String getType(RestRequest request) {
  		String resourceType = null;
  		
  		Integer firstSlash = request.requestURI.indexOf('/sobject/') + 8;
  		Integer lastSlash = request.requestURI.lastIndexOf('/');
  		
  		if(firstSlash == lastSlash) {
  			resourceType = request.requestURI.substring(firstSlash + 1);
  		}
  		else {
  			resourceType = request.requestURI.substring(firstSlash + 1, lastSlash);
  		}
  		return resourceType;
  	}
  
	private static String getId(RestRequest request) {
  		String resourceId = null;
  		  	
  		Integer firstSlash = request.requestURI.indexOf('/sobject/') + 8;
  		Integer lastSlash = request.requestURI.lastIndexOf('/');
  		
  		if(firstSlash != lastSlash) {
  			resourceId = request.requestURI.substring(lastSlash + 1);
  		}
  		return resourceId;  	
  	}
	
  	private static List<SObject> getSpecificResource(String resourceType, String resourceId, RestRequest request) {
    	String qryFields = 'id, name';

    	if (request.params.containsKey('fields')) {
    		qryFields = request.params.get('fields');
    	}
   		return Database.query('select ' + qryFields + ' from ' + resourceType + ' where Id = \'' + resourceId +'\'');
  	}
  
  	private static List<SObject> getAllResources(String resourceType, RestRequest request) { 
	    String qryFields = 'id, name';
	    String qryLimit = 'limit 100';   
	    String qryOrderby = '';      
	    String qryWhere = '';  
	      
	    if (request.params.containsKey('fields')) qryFields = request.params.get('fields');
	    if (request.params.containsKey('limit')) qryLimit = 'limit ' + request.params.get('limit'); 
	    if (request.params.containsKey('orderby')) qryOrderby = 'order by ' + request.params.get('orderby');
	    if (request.params.containsKey('search')) qryWhere = 'where Name LIKE \'' + request.params.get('search') +'%\'';
	      
	    return Database.query('select ' + qryFields + ' from ' + resourceType + ' ' + qryWhere + ' ' + qryOrderby + ' ' + qryLimit);
	}

  	private static String updateResource(String resourceType, String resourceId, RestRequest request) {  	
  		SObject resource;
	    Map<String, Schema.SObjectField> sObjectFieldsMap = Schema.getGlobalDescribe().get(resourceType).getDescribe().fields.getMap();
	  	
	  	try {
			// fetch the member by username if it exists
			resource = Database.query('select Id from ' + resourceType + ' where Id = \'' + resourceId +'\'');
		  	
			// populate the object's fields
			for (String key : request.params.keySet()) {
		    	if (sObjectFieldsMap.containsKey(key) && sObjectFieldsMap.get(key).getDescribe().isUpdateable()) {
					resource.put(key, request.params.get(key)); 
				}
			}	  
			update resource;
		}
		catch (QueryException qe) {
			return resourceType + ' with Id ' + resourceId + ' not found.';		  
		} 
		catch (DMLException de) {
		   	return de.getDmlMessage(0);   
		} 
		catch (Exception e) {
	    	return e.getMessage();
  		}   
	  	return resource.id;
  	}
  	
  	private static String createResource(String resourceType, RestRequest request) {  	
  		SObject resource;
  		Map<String, Schema.SObjectType> gd = Schema.getGlobalDescribe(); 
	    Map<String, Schema.SObjectField> sObjectFieldsMap = gd.get(resourceType).getDescribe().fields.getMap();
	    
	  	try {
			Schema.SObjectType st = gd.get(resourceType);
		        
	        resource = st.newSObject();
	        
			// populate the object's fields
			for (String key : request.params.keySet()) {
		    	if (sObjectFieldsMap.containsKey(key) && sObjectFieldsMap.get(key).getDescribe().isUpdateable()) {
					resource.put(key, request.params.get(key)); 
				}
			}	  
			insert resource;
		}
		catch (DMLException de) {
		   	return de.getDmlMessage(0);   
		} 
		catch (Exception e) {
	    	return e.getMessage();
  		}   
	  	return resource.id;
  	}
  	
  	private static String deleteResource(String resourceType, String resourceId) {  	
	  	try {
			SObject resource = Database.query('select Id from ' + resourceType + ' where Id = \'' + resourceId +'\'');
			delete resource;
			return 'DELETED';
		}
		catch (QueryException qe) {
			return resourceType + ' with Id ' + resourceId + ' not found.';		  
		} 
		catch (DMLException de) {
		   	return de.getDmlMessage(0);   
		} 
		catch (Exception e) {
	    	return e.getMessage();
  		}   
  	}
}

Everything was tested succesfully in separation. The service returns correct JSON. I tested it with the ApiGee Console. The Component renders perfectly with static JSON.

 

But when I load my page it just renders three empty grids and Firebug is showing me 3 empty XDR response with return code 302 Found.

 

I have absolutely no clue why. Maybe you can help me?

 

Ideas what I might have done wrong include:

 

- Do I need to pass the session id in the components javascript? Have I done it wrong?

- Is ExtJS expecting another JSON format? (This would produce different errors!)

 

Your ideas are very welcome.

 

Robert

Best Answer chosen by Admin (Salesforce Developers) 
SuperfellSuperfell

I can see you pass sessionId in, but don't see where you make that into the Authorization header. Also, you know the regular REST API includes access to sobjects, no need to write your own.

All Answers

SuperfellSuperfell

I can see you pass sessionId in, but don't see where you make that into the Authorization header. Also, you know the regular REST API includes access to sobjects, no need to write your own.

This was selected as the best answer
JeffTrullJeffTrull

Interesting work, Robert!  I haven't tried using an Apex REST approach - just the regular REST API.  I'm not sure what your 302 might indicate - I would carefully review the headers you're sending to verify they contain the sessionId.  Also check that your proxy is set up properly to allow you to use Apex REST from Visualforce, and finally, maybe check to see that you are hitting the right "endpoint".  For my regular REST experiments, I had to access the "wrong" url, but with a SalesforceProxy-Endpoint parameter set according to which of the CRUD verbs you need.  This is only required when you're going through Visualforce.

 

An alternative plan, which I strongly endorse, is using the Remoting feature.  Some additional configuration of the proxy is required in that case, but it works pretty well.

 

Have a look at my code and see if any of it helps you:

 

Grid with standard REST (ExtJS 3.4)

Grid with Remoting (ExtJS 4.0.7)

Controller for Grid with Remoting

 

Cheers,

Jeff

 

rsoesemannrsoesemann

Hy Jeff, hy Simon,

 

thank you both so much for your hints. What I have changed after reading you comments was:

 

  • I threw awys my custom REST controller ( I love the fact that Salesforce REST api also provides schema infos ;-)
  • Added an Authorization header correctly

and tried to cope with the same origin policy thing bydoing what Jeff suggested

  • setting the Url to the salesforce proxy
  • adding a SalesforceProxy-Endpoint header

 

Here are my changes

var store = Ext.create('Ext.data.Store', {
	autoLoad: true,
	autoSync: true,
	model: 'CurrentSObject',
	proxy: {
		type: 'rest',
		url: 'https://' + location.hostname + '/services/proxy',
		headers: {
         		 'SalesforceProxy-Endpoint' : '{!URLFOR('/services')}/data/v20.0/sobjects/{!object}/',
         		 'Authorization' : 'OAuth {!GETSESSIONID()}'
         	 	},
		reader: {
			type: 'json'
		},
		writer: {
			type: 'json'
		}
	},

 With that I don' get any error but also schema data in XML. Here is the response body from firebug

 

<?xml version="1.0" encoding="UTF-8"?><Contact><objectDescribe><activateable>false</activateable><createable>true</createable><custom>false</custom><customSetting>false</customSetting><deletable>true</deletable><deprecatedAndHidden>false</deprecatedAndHidden><feedEnabled>false</feedEnabled><keyPrefix>003</keyPrefix><label>Contact</label><labelPlural>Contacts</labelPlural><layoutable>true</layoutable><mergeable>true</mergeable><name>Contact</name><queryable>true</queryable><replicateable>true</replicateable><retrieveable>true</retrieveable><searchable>true</searchable><triggerable>true</triggerable><undeletable>true</undeletable><updateable>true</updateable><urls><sobject>/services/data/v20.0/sobjects/Contact</sobject><describe>/services/data/v20.0/sobjects/Contact/describe</describe><rowTemplate>/services/data/v20.0/sobjects/Contact/{ID}</rowTemplate></urls></objectDescribe></Contact>

 

 

Any ideas how to fix that. I am to new to ExtJS to think of a solution.

 

Thanks in advance,

 

Robert

rsoesemannrsoesemann

It seems that the REST API isnt really REST.

 

The documentation says to query all one would have to use /services/data/v20.0/query. Puhhh! How would that work with the rest style that ExtJS is expecting. Seem like I will need my homegrows rest wrapper again.

 

What do you think?

 

R.

rsoesemannrsoesemann

I am getting really close.

I switched back to my REST Wrapper Class and fixed some minor errors and eureka I get data.

 

<apex:component >
	<apex:attribute name="object" 	description=" " type="String" 	required="true"/>
	<apex:attribute name="fields" 		description=" " type="String" 	required="false" default="Id, Name" />
	<apex:attribute name="limit" 		description=" " type="String" 	required="false" default="100" />
	<apex:attribute name="orderby" 		description=" " type="String" 	required="false"/>
	<apex:attribute name="search" 		description=" " type="String" 	required="false"/>
	<apex:attribute name="width" 		description=" " type="Integer" 	required="false" default="400" />
	<apex:attribute name="height" 		description=" " type="Integer" 	required="false" default="300" />
	<apex:attribute name="title" 		description=" " type="String" 	required="false" default="" />

	<!-- Visualforce takes care that this is only loaded once when having more than one component in a page  -->
	<apex:includeScript value="http://cdn.sencha.io/ext-4.0.7-gpl/ext-all.js" />
	<apex:stylesheet value="http://cdn.sencha.io/ext-4.0.7-gpl/resources/css/ext-all.css" />

	<script type="text/javascript">

		Ext.require(['Ext.data.*', 'Ext.grid.*']);
		
		Ext.define('CurrentSObject', {
			extend: 'Ext.data.Model',
			fields: ['Id', 'Name']
		});

		Ext.onReady(function(){
			var store = Ext.create('Ext.data.Store', {
				autoLoad: true,
				autoSync: true,
				model: 'CurrentSObject',
				proxy: {
					type: 'rest',
					url: 'https://' + location.hostname + '/services/proxy',
					headers: {
			         		 'SalesforceProxy-Endpoint' : '{!URLFOR('/services')}/apexrest/sobject/{!object}',
			         		 'Authorization' : 'OAuth {!GETSESSIONID()}',
			         		 'Accept' : 'application/json'
			         	 	},
					reader: {
						type: 'json'
					},
					writer: {
						type: 'json'
					}
				},
				listeners: {
					write: function(store, operation){
						var record = operation.getRecords()[0],
							name = Ext.String.capitalize(operation.action),
							verb;
							
						if (name == 'Destroy') {
							record = operation.records[0];
							verb = 'Destroyed';
						} else {
							verb = name + 'd';
						}
						Ext.example.msg(name, Ext.String.format("{0} {!object}: {1}", verb, record.getId()));
					}
				}
			});
			
			var rowEditing = Ext.create('Ext.grid.plugin.RowEditing');
			
			var grid = Ext.create('Ext.grid.Panel', {
				renderTo: parent.Ext.getBody(),
				plugins: [rowEditing],
				width: {!width},
				height: {!height},
				title: '{!title}',
				store: store,
				columns: [{
					text: 'Id',
					width: 40,
					sortable: true,
					dataIndex: 'Id'
				}, {
					text: 'Name',
					flex: 1,
					sortable: true,
					dataIndex: 'Name',
					field: {
						xtype: 'textfield'
					}
				}],
				dockedItems: [{
					xtype: 'toolbar',
					items: [{
						text: 'Add',
						iconCls: 'icon-add',
						handler: function(){
							// empty record
							store.insert(0, new CurrentSObject());
							rowEditing.startEdit(0, 0);
						}
					}, '-', {
						itemId: 'delete',
						text: 'Delete',
						iconCls: 'icon-delete',
						disabled: true,
						handler: function(){
							var selection = grid.getView().getSelectionModel().getSelection()[0];
							if (selection) {
								store.remove(selection);
							}
						}
					}]
				}]
			});
			grid.getSelectionModel().on('selectionchange', function(selModel, selections){
				grid.down('#delete').setDisabled(selections.length === 0);
			});
		});
	</script>
	
    <!-- End SFDCStore component definition;  begin code for grid page -->
    <!-- Icons.  Using those included with ExtJs. -->
    <style type="text/css">
        .icon-add
        {
            background:url({!$Resource.ExtJS_4}/ext-4.0.7-gpl/examples/shared/icons/fam/add.gif) 0 no-repeat !important
        }
        .icon-save
        {
            background:url({!$Resource.ExtJS_4}/ext-4.0.7-gpl/examples/shared/icons/save.gif) 0 no-repeat !important
        }
        .icon-delete
        {
            background:url({!$Resource.ExtJS_4}/ext-4.0.7-gpl/examples/shared/icons/fam/delete.gif) 0 no-repeat !important
        }
    </style>
</apex:component>

 

 

 

Update and Add doesn't work bet that seems doable.

 

R.