I know it has been around for a long time, but recently found this useful feature, where I can request the logs of all emails that has been sent. It can be super useful for troubleshooting.
It would be nice to have some heroku app which can download this periodically and create cases for emails that are not delivered.
Sunday, October 20, 2019
Mock Framework
As Salesforce has introduced powerful mock framework for testing, we wrote thin layer on top of it to take full advantage of the dynamic mock class that you can create.
It is always a good practice to break down your classes, and methods into small chunks, and mostly take further reusable methods in services. However, at the time of testing, it gets hard to test your code if it relies on too many services method. You have to have to knowledge of the service.
In code, it may look like
And caller
Now when we write the test class for MockDemoServiceCaller, it would be good to have full control over the output of mock demo service. We should be able to return various result including the exception. Salesforce mock framework allows you to mock the service so you have full control when you write the test class for service caller.
Here is example of the code. You can see in line 19 and 20, we create dynamic service instance using mock framework, as well as send mock individual method and return the response.
On line 34, we can see, we can even return the exception in the dynamic mock instance that we create, and hence we have full control on how to test the service caller.
With wrapper around salesforce framework, we don't need to write individual mock classes, but we can declarative use what to mock and what it would return.
Web Service Mock
Web service mock framework has been around for years, however we have to write mock class for each web service we are trying to call. With this framework, we can declarative create dynamic instance of the mock class and test the class which is calling web service. e.g. below
Actual Class
Test Class
As you can see, we don't have to write the mock class for each web service, we simply just need to use the http mock callout and set the response as mentioned in line 25 and 33.
You can download the code from : https://github.com/springsoa/springsoa-salesforce
or from unmanaged package here
It is always a good practice to break down your classes, and methods into small chunks, and mostly take further reusable methods in services. However, at the time of testing, it gets hard to test your code if it relies on too many services method. You have to have to knowledge of the service.
In code, it may look like
/** * Created by Chintan Shah on 10/18/2019. */ /** * this is the service that will be called by caller * it would be faked out during the testing framework. */ public with sharing class MockDemoService { public List<String> toUpper(List<String> inputs) { List<String> outputs = new List<String>(); for(String input : inputs ) { outputs.add( input.toUpperCase() ); } return outputs; } public String concat(List<String> inputs) { String output = null; for(String input : inputs ) { output = ( output == null ? '' : ( output + ' ' ) ) + input; } return output; } }
And caller
/** * Created by Chintan Shah on 10/18/2019. */ /** * demo of how to use this framework. * * MockDemoServiceCaller -> MockDemoService * * During testing, we will just mock the entire MockDemoService * */ public with sharing class MockDemoServiceCaller { /** * instance of mockDemoService */ public MockDemoService mockDemoService; /** * regular constructor */ public MockDemoServiceCaller() { mockDemoService = new MockDemoService(); } /** * entry point for fake service * * @param */ public MockDemoServiceCaller(MockDemoService mockDemoService) { this.mockDemoService = mockDemoService; } /** * sample routine that we are going to test. */ public String sampleRoutine(List<String> inputs) { try { System.debug(' MockDemoServiceCaller.sampleRoutine inputs ' + JSON.serialize(inputs) ); List<String> outputs = mockDemoService.toUpper( new List<String> { 'John', 'Doe' } ); System.debug(' MockDemoServiceCaller.sampleRoutine outputs ' + JSON.serialize(outputs) ); String output = mockDemoService.concat( outputs ); System.debug(' MockDemoServiceCaller.sampleRoutine output ' + output ); return output; } catch(Exception e) { throw new MockException('What\' up? ' + e.getStackTraceString() ); } } }
Now when we write the test class for MockDemoServiceCaller, it would be good to have full control over the output of mock demo service. We should be able to return various result including the exception. Salesforce mock framework allows you to mock the service so you have full control when you write the test class for service caller.
Here is example of the code. You can see in line 19 and 20, we create dynamic service instance using mock framework, as well as send mock individual method and return the response.
On line 34, we can see, we can even return the exception in the dynamic mock instance that we create, and hence we have full control on how to test the service caller.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /** * Created by Chintan Shah on 10/18/2019. */ /** * test class for MockDemoServiceCaller */ @isTest public with sharing class MockDemoServiceCallerTest { @testSetup public static void testSetup() { } @isTest public static void testSampleRoutine1() { MockProvider mockDeMockServiceProvider = new MockProvider( MockDemoService.class ); MockDemoService mockDemoService = (MockDemoService) mockDeMockServiceProvider.getMock(); mockDeMockServiceProvider.addReturnValue( 'toUpper', new List<String> { 'HELLO', 'WORLD' } ); mockDeMockServiceProvider.addReturnValue( 'concat', 'HELLO TEST' ); // initialize MockDemoServiceCaller with mock MockDemoService MockDemoServiceCaller mockDemoServiceCaller = new MockDemoServiceCaller( mockDemoService ); String output = mockDemoServiceCaller.sampleRoutine( new List<String> { 'John', 'Doe'} ); System.assertEquals( output, 'HELLO TEST', 'It should be HELLO TEST based on mock '); } @isTest public static void testSampleRoutine2() { MockProvider mockDeMockServiceProvider = new MockProvider( MockDemoService.class ); MockDemoService mockDemoService = (MockDemoService) mockDeMockServiceProvider.getMock(); mockDeMockServiceProvider.addReturnValue( 'toUpper', new List<String> { 'HELLO', 'WORLD' } ); mockDeMockServiceProvider.addReturnValue( 'concat', new MockException('Test Exception') ); // initialize MockDemoServiceCaller with mock MockDemoService MockDemoServiceCaller mockDemoServiceCaller = new MockDemoServiceCaller( mockDemoService ); try { String output = mockDemoServiceCaller.sampleRoutine( new List<String> { 'John', 'Doe'} ); System.assert( false, 'Exception was expected, success was not expected '); } catch(Exception e) { System.assert( true, 'Exception was expected. '); } } } |
With wrapper around salesforce framework, we don't need to write individual mock classes, but we can declarative use what to mock and what it would return.
Web Service Mock
Web service mock framework has been around for years, however we have to write mock class for each web service we are trying to call. With this framework, we can declarative create dynamic instance of the mock class and test the class which is calling web service. e.g. below
Actual Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | /** * Created by Chintan Shah on 10/19/2019. */ public with sharing class MockDemoWebServiceCaller { public List<Map<String,Object>> httpGetCallout(String url) { List<Map<String,Object>> returnEmployees = new List<Map<String,Object>>(); Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint( url ); request.setMethod('GET'); System.debug(' MockDemoWebServiceCaller.httpGetCallout url ' + url ); HttpResponse response = http.send(request); if (response.getStatusCode() == 200) { System.debug(' response.getBody() ' + response.getBody() ); List<Object> employees = (List<Object>) JSON.deserializeUntyped( response.getBody() ); for(Object employee : employees ) { Map<String, Object> currentEmployee = ( Map<String, Object> ) ( employee ); returnEmployees.add( currentEmployee ); } } else { throw new MockException( response.getStatusCode() + ' ' + response.getBody() ); } system.debug( ' MockDemoWebServiceCaller.httpGetCallout returnEmployees ' + returnEmployees ); return returnEmployees; } public List<Map<String,Object>> getAllEmployees() { try { return httpGetCallout('https://basic-authentication-ws.herokuapp.com/hr/employees'); } catch(Exception e) { throw new MockException(e); } } public List<Map<String,Object>> getEmployee(String column, String value) { try { return httpGetCallout('https://basic-authentication-ws.herokuapp.com/hr/employee/' + column + '/' + value ); } catch(Exception e) { throw new MockException(e); } } } |
Test Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | /** * Created by Chintan Shah on 10/19/2019. */ @isTest public with sharing class MockDemoWebServiceCallerTest { @testSetup public static void testSetup() { } @isTest public static void testGetAllEmployees() { MockHttpCallout mockHttpCallout = new MockHttpCallout(); mockHttpCallout.setMock(); mockHttpCallout.addJsonHttpResponse('https://basic-authentication-ws.herokuapp.com/hr/employees', '[{"ExternalId":"E-001","Name":"Chintan Shah","DisplayUrl":"https://external.com/E-001","EmployeeAccountKey":"Account-001"},{"ExternalId":"E-002","Name":"Chuck Summer","DisplayUrl":"https://external.com/E-002","EmployeeAccountKey":"Account-001"},{"ExternalId":"E-003","Name":"Chris Walker","DisplayUrl":"https://external.com/E-003","EmployeeAccountKey":"Account-001"},{"ExternalId":"E-004","Name":"Dan Topper","DisplayUrl":"https://external.com/E-004","EmployeeAccountKey":"Account-002"},{"ExternalId":"E-005","Name":"Drew Berry","DisplayUrl":"https://external.com/E-005","EmployeeAccountKey":"Account-002"},{"ExternalId":"E-006","Name":"Don Aiken","DisplayUrl":"https://external.com/E-006","EmployeeAccountKey":"Account-002"}]'); System.assertEquals( 6, new MockDemoWebServiceCaller().getAllEmployees().size(), 'Should be 6'); } @isTest public static void testGetEmployee() { // MockHttpCallout mockHttpCallout = new MockHttpCallout(); mockHttpCallout.setMock(); mockHttpCallout.addJsonHttpResponse('https://basic-authentication-ws.herokuapp.com/hr/employee/Name/Chintan', '[{"ExternalId":"E-001","Name":"Chintan Shah","DisplayUrl":"https://external.com/E-001","EmployeeAccountKey":"Account-001"}]'); System.assertEquals( 1, new MockDemoWebServiceCaller().getEmployee('Name','Chintan').size() , 'Should be 1'); } @isTest public static void testGetAllEmployees2() { MockHttpCallout mockHttpCallout = new MockHttpCallout(); mockHttpCallout.setMock(); mockHttpCallout.addErrorResponse('https://basic-authentication-ws.herokuapp.com/hr/employees', 401 ); try { new MockDemoWebServiceCaller().getAllEmployees().size(); System.assert( false, 'Exception was expected, success was not expected '); } catch(Exception e) { System.assert( true, 'Exception was expected. '); } } @isTest public static void testGetEmployee2() { // MockHttpCallout mockHttpCallout = new MockHttpCallout(); mockHttpCallout.setMock(); mockHttpCallout.addErrorResponse('https://basic-authentication-ws.herokuapp.com/hr/employee/Name/Chintan', 401); try { new MockDemoWebServiceCaller().getEmployee('Name','Chintan').size(); System.assert( false, 'Exception was expected, success was not expected '); } catch(Exception e) { System.assert( true, 'Exception was expected. '); } } } |
As you can see, we don't have to write the mock class for each web service, we simply just need to use the http mock callout and set the response as mentioned in line 25 and 33.
You can download the code from : https://github.com/springsoa/springsoa-salesforce
or from unmanaged package here
Thursday, October 10, 2019
External Data Service
As mentioned in my previous blog : https://chintanblog.blogspot.com/2017/05/odataheroku-with-salesforce-integrate.html , if we can expose the external data as OData 2.0 or 4.0, we can directly consume them in salesforce as External Objects. The second option would be to directly consume those web service using External Data Service and consume them as external objects.
As you can see above, either you can put thin layer against external web service to do protocol conversion to support Odata (as mentioned in https://chintanblog.blogspot.com/2017/05/odataheroku-with-salesforce-integrate.html) or write the plugin in Apex.
Here we will discuss how to write External Data Service in Apex.
1) I created a sample external web service to just for the demo - It is HR service, which return list of employees, and also employees by name, account, etc..
https://github.com/c-shah/basic-authentication
2) As next step, we need to write
ExternalDataSourceProvider -> ExternalDataSourceConnection -> ExternalDataService
a) ExternalDataSourceProvider
this class basically extends DataSource.Provider, and provides what capability are supported from both authentication and database support (e.g. query, update)
b) ExternalDataSourceConnection
This class extends DataSource.Connection, and responsible for providing table structures and as well as data results which will be consumed by query associated with External Objects
3) ExternalDataService, which takes care of calling web service and returning the data needed for ExternalDataSourceConnection.
This basically :
We can see external Objects now:
Once it is done, it is ready to be tested on Account page layout, due to indirect lookup relationship, Employee external object would be available in Account Page layout.
This way we can consume external service as external object without external Odata layer.
Source Code :
https://github.com/springsoa/springsoa-salesforce/tree/master/classes
https://github.com/c-shah/basic-authentication
As you can see above, either you can put thin layer against external web service to do protocol conversion to support Odata (as mentioned in https://chintanblog.blogspot.com/2017/05/odataheroku-with-salesforce-integrate.html) or write the plugin in Apex.
Here we will discuss how to write External Data Service in Apex.
1) I created a sample external web service to just for the demo - It is HR service, which return list of employees, and also employees by name, account, etc..
https://github.com/c-shah/basic-authentication
2) As next step, we need to write
ExternalDataSourceProvider -> ExternalDataSourceConnection -> ExternalDataService
a) ExternalDataSourceProvider
this class basically extends DataSource.Provider, and provides what capability are supported from both authentication and database support (e.g. query, update)
/** * Created by chint on 10/4/2019. */ global without sharing class ExternalDataSourceProvider extends DataSource.Provider { override global List<DataSource.AuthenticationCapability> getAuthenticationCapabilities() { System.debug(' ExternalDataSourceProvider.getAuthenticationCapabilities '); List<DataSource.AuthenticationCapability> capabilities = new List<DataSource.AuthenticationCapability> { DataSource.AuthenticationCapability.ANONYMOUS }; System.debug(' ExternalDataSourceProvider.getAuthenticationCapabilities ' + JSON.serialize(capabilities) ); return capabilities; } override global List<DataSource.Capability> getCapabilities() { System.debug(' ExternalDataSourceProvider.getCapabilities '); List<DataSource.Capability> capabilities = new List<DataSource.Capability> { DataSource.Capability.ROW_QUERY }; System.debug(' ExternalDataSourceProvider.getCapabilities ' + JSON.serialize(capabilities) ); return capabilities; } override global DataSource.Connection getConnection(DataSource.ConnectionParams connectionParams) { System.debug(' ExternalDataSourceProvider.getConnection connectionParams: ' + connectionParams); DataSource.Connection connection = new ExternalDataSourceConnection(connectionParams); System.debug(' ExternalDataSourceProvider.getConnection connection: ' + connection); return connection; } }
b) ExternalDataSourceConnection
This class extends DataSource.Connection, and responsible for providing table structures and as well as data results which will be consumed by query associated with External Objects
/** * Created by chint on 10/4/2019. */ global without sharing class ExternalDataSourceConnection extends DataSource.Connection { private DataSource.ConnectionParams connectionInfo ; global ExternalDataSourceConnection(DataSource.ConnectionParams connectionInfo) { System.debug(' ExternalDataSourceConnection.ExternalDataSourceConnection connectionInfo: ' + JSON.serialize(connectionInfo)); this.connectionInfo = connectionInfo; } override global List<DataSource.Table> sync() { System.debug(' ExternalDataSourceConnection.sync '); List<DataSource.Table> tables = ExternalDataService.getInstance().getTables(); System.debug(' ExternalDataSourceConnection.sync tables ' + JSON.serialize(tables) ); return tables; } override global DataSource.TableResult query(DataSource.QueryContext context) { try { printContent(context); DataSource.TableResult tableResult = DataSource.TableResult.get(context, ExternalDataService.getInstance().getData(context) ); System.debug(' ExternalDataSourceConnection.query tableResult ' + JSON.serialize(tableResult)); return tableResult; } catch (Exception currentException) { String message = currentException.getMessage() + currentException.getStackTraceString() ; System.debug(' ExternalDataSourceConnection.query exception : ' + message ); throw new DataSource.DataSourceException(message); } } public static void printContent(DataSource.QueryContext context) { System.debug(' ExternalDataSourceConnection.printContent queryMoreToken ' + context.queryMoreToken + ' tableSelected ' + context.tableSelection.tableSelected ); DataSource.Filter filter = context.tableSelection.filter; List<DataSource.Order> orders = context.tableSelection.order; List<DataSource.ColumnSelection> columnsSelected = context.tableSelection.columnsSelected; if( filter != null ) { System.debug('ExternalDataSourceConnection.printContent filter columnName ' + filter.columnName + ' columnValue ' + filter.columnValue + ' subfilters ' + filter.subfilters + ' tableName ' + filter.tableName + ' type ' + filter.type ); } if( orders != null ) { for(DataSource.Order order : orders ) { System.debug('ExternalDataSourceConnection.printContent order columnName ' + order.columnName + ' tableName ' + order.tableName + ' direction ' + order.direction ); } } if( columnsSelected != null ) { for(DataSource.ColumnSelection columnSelected : columnsSelected ) { System.debug('ExternalDataSourceConnection.printContent columnSelected columnName ' + columnSelected.columnName + ' tableName ' + columnSelected.tableName + ' aggregation ' + columnSelected.aggregation ); } } } }
3) ExternalDataService, which takes care of calling web service and returning the data needed for ExternalDataSourceConnection.
/** * Created by chint on 10/4/2019. */ public without sharing class ExternalDataService { private static ExternalDataService instance; private ExternalDataService() { System.debug(' ExternalDataService.ExternalDataService '); } public static ExternalDataService getInstance() { System.debug(' ExternalDataService.getInstance '); if( instance == null ) { instance = new ExternalDataService(); } return instance; } public List<DataSource.Table> getTables() { System.debug(' ExternalDataService.getTables '); List<DataSource.Table> tables = new List<DataSource.Table> { getEmployeeTable(), getAddressTable() }; System.debug(' ExternalDataService.getTables tables ' + JSON.serialize(tables) ); return tables; } public List<Map<String, Object>> getData(DataSource.QueryContext context) { System.debug(' ExternalDataService.getData context ' + context ); return getEmployeeData(context); } private DataSource.Table getEmployeeTable() { List<DataSource.Column> columns; columns = new List<DataSource.Column>(); columns.add(DataSource.Column.text('ExternalId', 255)); columns.add(DataSource.Column.text('Name', 255)); columns.add(DataSource.Column.url('DisplayUrl')); columns.add(DataSource.Column.indirectLookup('EmployeeAccountKey', 'Account', 'Account_Key__c')); return DataSource.Table.get('Employee', 'ExternalId', columns); } private List<Map<String, Object>> getDummyData() { Map<String,Object> row1 = new Map<String,Object> { 'ExternalId' => 'Emp1', 'Name' => 'Chintan Shah', 'EmployeeAccountKey' => 'ACN1' }; Map<String,Object> row2 = new Map<String,Object> { 'ExternalId' => 'Emp2', 'Name' => 'Mark Twain', 'EmployeeAccountKey' => 'ACN1' }; Map<String,Object> row3 = new Map<String,Object> { 'ExternalId' => 'Emp3', 'Name' => 'John Doe', 'EmployeeAccountKey' => 'ACN2' }; List<Map<String,Object>> dataRows = new List<Map<String,Object>> { row1, row2, row3 }; return dataRows; } private List<Map<String, Object>> getEmployeeData(DataSource.QueryContext context) { if( context.tableSelection != null && context.tableSelection.tableSelected == 'Employee' ) { DataSource.Filter filter = context.tableSelection.filter; String url = '/hr/employees'; if( filter != null ) { url = '/hr/employee/' + filter.columnName + '/' + filter.columnValue; } List<Map<String,Object>> response = httpGetCallout('HerokuBasicAuth', url); return response; } return new List<Map<String,Object>>(); } public static List<Map<String,Object>> httpGetCallout(String namedCredentials, String url) { List<Map<String,Object>> returnEmployees = new List<Map<String,Object>>(); Http http = new Http(); HttpRequest request = new HttpRequest(); request.setEndpoint('callout:' + namedCredentials + url ); request.setMethod('GET'); HttpResponse response = http.send(request); if (response.getStatusCode() == 200) { System.debug(' response.getBody() ' + response.getBody() ); List<Object> employees = (List<Object>) JSON.deserializeUntyped( response.getBody() ); for(Object employee : employees ) { Map<String, Object> currentEmployee = ( Map<String, Object> ) ( employee ); returnEmployees.add( currentEmployee ); } } system.debug( ' ExternalDataService.httpGetCallout returnEmployees ' + returnEmployees ); return returnEmployees; } private DataSource.Table getAddressTable() { DataSource.Table table = new DataSource.Table(); List<DataSource.Column> columns; columns = new List<DataSource.Column>(); columns.add(DataSource.Column.text('ExternalId', 255)); columns.add(DataSource.Column.text('Street', 255)); columns.add(DataSource.Column.text('ZipCode', 255)); columns.add(DataSource.Column.url('DisplayUrl')); columns.add(DataSource.Column.indirectLookup('AddressAccountKey', 'Account', 'Account_Key__c')); return DataSource.Table.get('Address', 'ExternalId', columns); } private List<Map<String, Object>> getAddressData(DataSource.QueryContext context) { List<Map<String,Object>> dataRows = new List<Map<String,Object>>(); return dataRows; } }
This basically :
- understand the context (name of the table, query criteria
- calls the webservice
- returns the data in List
We can see external Objects now:
Once it is done, it is ready to be tested on Account page layout, due to indirect lookup relationship, Employee external object would be available in Account Page layout.
This way we can consume external service as external object without external Odata layer.
Source Code :
https://github.com/springsoa/springsoa-salesforce/tree/master/classes
https://github.com/c-shah/basic-authentication
Tuesday, October 8, 2019
Lightning Component Web Service Integration
When calling web service from lightning component, we can either call it from apex using @AuraEnabled method, or we can call from lightning component client side controller. This is specially important when you want to avoid governor limits on Apex side, e.g. uploading file bigger than 7MB or calling web service which could potentially take more than 120s. We can directly use browser to call the web service.
Calling web service from client side controller (browser integration)
1) We need to make sure that web service we call, allows cross origin and it is https enabled
We wrote simple web service in Heroku
https://basic-authentication-ws.herokuapp.com/echo
2) We need to make sure the web service URL is added to CSP trusted site for locker service
https://basic-authentication-ws.herokuapp.com
3) Now, lightning component (client side controller)
We can make call to web service
Source Code
Heroku
https://github.com/c-shah/basic-authentication
Salesforce
https://github.com/springsoa/springsoa-salesforce/tree/master/aura/webserviceTest
Calling web service from client side controller (browser integration)
1) We need to make sure that web service we call, allows cross origin and it is https enabled
We wrote simple web service in Heroku
https://basic-authentication-ws.herokuapp.com/echo
2) We need to make sure the web service URL is added to CSP trusted site for locker service
https://basic-authentication-ws.herokuapp.com
3) Now, lightning component (client side controller)
We can make call to web service
({
doInit: function (c, e, h) {
},
postData: function (c, e, h) {
c.set('v.isProcessing', true);
var xhr = new XMLHttpRequest();
var url = 'https://basic-authentication-ws.herokuapp.com/echo';
xhr.open('POST', url, true);
xhr.onload = function () {
if (this.status === 200) {
c.set('v.isProcessing', false);
c.set('v.response', this.response);
}
};
xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
xhr.send(JSON.stringify({message: c.get('v.message')}));
}
});
Source Code
Heroku
https://github.com/c-shah/basic-authentication
Salesforce
https://github.com/springsoa/springsoa-salesforce/tree/master/aura/webserviceTest
Monday, December 11, 2017
SOSL - platform encryption
Salesforce platform encryption encrypts the data at rest, hence we can not run SOQL against it. SOSL is good work around but have its own limitation. (side note: SOSL also has limitation of 2000 rows returned)
Enable Platform Encryption
Encrypt Fields
Create Acount
SOQL
SOSL
Enable Platform Encryption
a) create permission set
b) assign system permission for this permission set
c) Assign permission set to current user
Encrypt Fields
Fields to encrypt (Setup -> Platform Encryption)
Create Account
Created account with name : My Account
SOQL
If we run SOQL against account, now it would return error.
SOSL
If we run below SOSL, we can access our account:

However issues comes when we want to narrow down the result. If we apply the where clause to the SOSL, it breaks. I believe it is because behind the scene SOSL still runs as SOQL.
Error Message
So, we need to be careful when we write SOSL or SOQL where clause (especially in managed package) given that fields could be encrypted.
Enable Platform Encryption
Encrypt Fields
Create Acount
SOQL
SOSL
a) create permission set
b) assign system permission for this permission set
c) Assign permission set to current user
Fields to encrypt (Setup -> Platform Encryption)
Create Account
Created account with name : My Account
If we run SOQL against account, now it would return error.
select id, name, description from Account where name like '%My%'
[object Object]: description from Account where name like '%My%' ^ ERROR at Row:1:Column:49 encrypted field 'Account.name' cannot be filtered in a query call
If we run below SOSL, we can access our account:
FIND {My} IN ALL FIELDS RETURNING Account(Name, Description, AnnualRevenue)
FIND {M*} IN ALL FIELDS RETURNING Account(Name, Description, AnnualRevenue)

FIND {M*} IN ALL FIELDS RETURNING Account(Name, Description, AnnualRevenue WHERE Name LIKE 'M%')
Error Message
Description, AnnualRevenue WHERE Name LIKE 'M%')
^
ERROR at Row:1:Column:82
encrypted field 'Account.Name' cannot be filtered in a query call
So, we need to be careful when we write SOSL or SOQL where clause (especially in managed package) given that fields could be encrypted.
Saturday, November 11, 2017
Salesforce Canvas : what and how?
When we are integrating third party UI application, there are quite a few options
Step 3) Configure Canvas App Settings (Note: check Publisher and Create Actions, if we plan to publish canvas app there)
Step 4)
Step 5) Grab the consumer secret and key
Step 6) Assign canvas app the users or profiles
Step 7) Canvas app viewer
Security
Two way event
- Events can be raised from Visual Force and can be sent to Third Party App, and Third party app can send event back to Salesforce
Resize
- Third party App can use resize API to resize the canvas size in Salesforce. When we put canvas in visualforce and then in layout, we are restricted by iFrame size and height, but if canvas is put in directly in page layout we can have much better control over the size.
Api calls (e.g. chatter)
Third party application can use OAuth token provided in JSON request and make any API call. Below is example of chatter post
Where canvas can be used
This is very advantageous as we can resize and don't have to use iframe
Lightning Component
We can add canvas in lightning component
Chatter / Publish Action
Heroku code
- Including iframe inside Visualforce or Lightning component
- mostly one way integration (salesforce to UI application)
- security issues
- sizing problems
- Canvas
- lightning and classic
- seamless and two way integration
- Lightning container component (Winter'18 - for lightning)
- only for lightning
Canvas by far the most feature rich and seamless integration between salesforce and third party UI application, especially we have two way interaction. E.g. salesforce passing data to third party application and UI application updating/creating data back in salesforce.
How to configure it
- Create Connected application
Step 1) Connected App -> Allow users to install canvas personal apps
Step 2) Create New App
Step 3) Configure Canvas App Settings (Note: check Publisher and Create Actions, if we plan to publish canvas app there)

Step 4)
Step 5) Grab the consumer secret and key
Step 6) Assign canvas app the users or profiles
Step 7) Canvas app viewer
- Sample Third Party application
- From Canvas App viewer, we can create canned "heroku quick start" app, however I decided to use create my own so that I can test out all the features in controller environment
- Create heroku application, and make sure to sign the request for the URL provided in canvas salesforce app
Main Features
Security
- Salesforce sends signed request to third party application, and it can be decrypted using consumer key. Hence the request is secure.
- Also when json request payload is decrypted, we get session id and all the information of the page context
Two way event
- Events can be raised from Visual Force and can be sent to Third Party App, and Third party app can send event back to Salesforce
Resize
- Third party App can use resize API to resize the canvas size in Salesforce. When we put canvas in visualforce and then in layout, we are restricted by iFrame size and height, but if canvas is put in directly in page layout we can have much better control over the size.
Api calls (e.g. chatter)
Third party application can use OAuth token provided in JSON request and make any API call. Below is example of chatter post
Where canvas can be used
Once it is configured correctly, it can be used at many places:
Canvas App Previewer : this is just for testing your canvas app
Page layout via Visual Force
Canvas App Previewer : this is just for testing your canvas app
Page layout via Visual Force
Canvas can be added in visual force page, however if we go that route, when we add the page to layout, we will be restricted by iframe size.
VisualForce code:
Page Layout Directly
VisualForce code:
Page Layout Directly
Lightning Component
We can add canvas in lightning component
Chatter / Publish Action
Source Code
Heroku code
https://github.com/c-shah/canvasly
Salesforce code
Salesforce code
https://github.com/c-shah/sf.canvasly
Thursday, October 19, 2017
Community User Test Data
Always find a hassle to create community user for test data, so just thought putting it out here:
Normal user creation:
But we get below errors:
System.DmlException: Insert failed. First exception on row 0; first error: INVALID_CROSS_REFERENCE_KEY, Cannot create a portal user without contact: [ContactId]
System.DmlException: Insert failed. First exception on row 0; first error: UNKNOWN_EXCEPTION, portal account owner must have a role: []
System.DmlException: Insert failed. First exception on row 0; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): Account, original object: User: []
To solve it : use below approach :
1) Create Portal Owner
2) Create Community User
3) We can use below code in testSetup or test method.
Normal user creation:
User communityUser = new User( ProfileId = [SELECT Id FROM Profile WHERE Name = 'Interconnection Community User Plus User'].Id, FirstName = 'first', LastName = 'last', Email = 'test.test@test.com', Username = 'test.' + System.currentTimeMillis() + '@test.com', Title = 'Title', Alias = 'alias', TimeZoneSidKey = 'America/Los_Angeles', EmailEncodingKey = 'UTF-8', LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US' );
But we get below errors:
System.DmlException: Insert failed. First exception on row 0; first error: INVALID_CROSS_REFERENCE_KEY, Cannot create a portal user without contact: [ContactId]
System.DmlException: Insert failed. First exception on row 0; first error: UNKNOWN_EXCEPTION, portal account owner must have a role: []
System.DmlException: Insert failed. First exception on row 0; first error: MIXED_DML_OPERATION, DML operation on setup object is not permitted after you have updated a non-setup object (or vice versa): Account, original object: User: []
To solve it : use below approach :
1) Create Portal Owner
private static User createPortalAccountOwner() {
UserRole portalRole = new UserRole(DeveloperName = 'MyCustomRole', Name = 'My Role', PortalType='None' );
insert portalRole;
System.debug('portalRole is ' + portalRole);
Profile sysAdminProfile = [Select Id from Profile where name = 'System Administrator'];
User portalAccountOwner = new User(
UserRoleId = portalRole.Id,
ProfileId = sysAdminProfile.Id,
Username = 'portalOwner' + System.currentTimeMillis() + '@test.com',
Alias = 'Alias',
Email='portal.owner@test.com',
EmailEncodingKey='UTF-8',
Firstname='Portal',
Lastname='Owner',
LanguageLocaleKey='en_US',
LocaleSidKey='en_US',
TimeZoneSidKey = 'America/Los_Angeles'
);
Database.insert(portalAccountOwner);
return portalAccountOwner;
}
2) Create Community User
private static void createCommunityUser(User portalAccountOwner) {
System.runAs ( portalAccountOwner ) {
//Create account
Account portalAccount = new Account(
Name = 'portalAccount',
OwnerId = portalAccountOwner.Id
);
Database.insert(portalAccount);
//Create contact
Contact portalContact = new Contact(
FirstName = 'portalContactFirst',
Lastname = 'portalContactLast',
AccountId = portalAccount.Id,
Email = 'portalContact' + System.currentTimeMillis() + '@test.com'
);
Database.insert(portalContact);
User communityUser = new User(
ProfileId = [SELECT Id FROM Profile WHERE Name = 'Interconnection Community User Plus User'].Id,
FirstName = 'CommunityUserFirst',
LastName = 'CommunityUserLast',
Email = 'community.user@test.com',
Username = 'community.user.' + System.currentTimeMillis() + '@test.com',
Title = 'Title',
Alias = 'Alias',
TimeZoneSidKey = 'America/Los_Angeles',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
LocaleSidKey = 'en_US',
ContactId = portalContact.id
);
Database.insert(communityUser);
}
}
3) We can use below code in testSetup or test method.
User portalAccountOwner = createPortalAccountOwner();
createCommunityUser(portalAccountOwner);
Subscribe to:
Posts (Atom)






































