Lightning Flow and Test Coverage

If you read on the same sentence Flow and Test, what do you think is the article about? My first thought was “how to test a flow in order to avoid issues.” Something like this one.

However, my entry would like to focus on Test Coverage as it is a key point that could be a pain if you need to deploy Flows in active mode. The reason, they also require to have a 75% of coverage at least, in order to get a success deploy.

Flow Test Coverage? I thought this is a #clicksnotcode tool …

Yes and it may not make sense if you are working with Screen Flow. In fact, we do not need to worry about test coverage on these kind of flows. However, you need to keep in mind those that work without screens such as autolaunched flows. You can have more information here.

And how can we do it? Like the documentation says, via a Trigger.

Let’s try to explain it with an example.

If you have read some others entries, you will already know my Flow examples are based on a Hotel Reservation App. So below example is related to Reservations. The scope of the autolaunched flow is to send an email to the guest once he pays the reservation.

And in order to make it work, I need to call it from my Trigger.

trigger ReservationTest on Reservation__c (before insert, before update)
{
    for(Reservation__c r : Trigger.new)
    {
        if(r.isPaid__c)
        {
            System.debug('Run the email flow');
            
            Map<String, Object&gt; params = new Map<String, Object&gt;();
            params.put('guestEmail', r.guestEmail__c);
                
            Flow.Interview.EmailFlow flowToSendEmail = new Flow.Interview.EmailFlow(params);
            flowToSendEmail.start();
        }
    }
}

Now, that we have the new flow, we need to check Flow Coverage status. But the first execution is to identify all Active elements we have in the org in order to be able to calculate the coverage.

Running this query:

Select MasterLabel,
	Definition.DeveloperName,
	VersionNumber,
	Processtype
 FROM Flow 
 WHERE Status = 'Active' 
    AND (ProcessType = 'AutolaunchedFlow' 
       OR ProcessType = 'Workflow' 
       OR ProcessType = 'CustomEvent' 
       OR ProcessType = 'InvocableProcess')

We get below result, and we can identify 2 Autolaunched flow. One of them is the EmailFlow, the one we are using on this example.

Run on Developer Console with Use Tooling API checked

On the other side, Salesforce provides an object in order to check coverage, FlowTestCoverage and we will use it in order to check EmailFlow one. However it only returns a record if there is already some coverage of the elements, so the easiest way to start checking it is modify previous query adding as a filter where the Id not in on the new FlowTestCoverage object.

Select MasterLabel,
		Definition.DeveloperName,
		VersionNumber,
		Processtype
 FROM Flow 
 WHERE Status = 'Active' 
    AND (ProcessType = 'AutolaunchedFlow' 
       OR ProcessType = 'Workflow' 
       OR ProcessType = 'CustomEvent' 
       OR ProcessType = 'InvocableProcess')
    AND Id NOT IN (SELECT FlowVersionId FROM FlowTestCoverage)

The query result is the same as before so we can start writing our first test in order to cover EmailFlow one.

@isTest
public class EmailFlowApexTest
{
	static testMethod void validateEmailFlow()
    {
        Reservation__c r = new Reservation__c();
        r.isPaid__c = true;
        r.guestEmail__c = 'a@ff.com';
        insert r;
        
	    List<Reservation__c&gt; rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
}

Now, this test covers the trigger that starts EmailFlow, so if we run previous query again, now, EmailFlow will not appear as part of the result, and running a query against FlowTestCoverage, we can start getting some information about our job.

SELECT Id, 
 	ApexTestClassId, 
 	TestMethodName, 
 	FlowVersionId, 
 	NumElementsCovered, 
 	NumElementsNotCovered 
 FROM FlowTestCoverage

Where we can see how match the FlowVersionId with ours, and also the TestMethodName we defined above, validateEmailFlow.

Now I wonder: What about if our test has more than one step? What does happen with above result? So let’s go for it. After the email is sent, we will have a decision element that will check the total amount of the reservation. After that, do an assignment to a certain object. We could continue adding more complex to the flow but let’s keep it simple so far.

The first step is to modify the test in order to have an amount higher or less than the decision filter.

@isTest
public class EmailFlowApexTest
{
    static testMethod void validateEmailFlow()
    {
        Reservation__c r = new Reservation__c ();
        r.isPaid__c = true;
        r.guestEmail__c = 'agarcia@financialforce.com';
        r.TotalAmount__c = 100;
        insert r;
        
	    List<Reservation__c&gt; rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
}

With this, if we run below query,

SELECT Id, 
     ApexTestClassId, 
     TestMethodName, 
     FlowVersionId, 
     NumElementsCovered, 
     NumElementsNotCovered 
 FROM FlowTestCoverage

We get a similar result as the previous one. The only difference is that NumElementsCovered is 3 while NumElementsNotCovered is 1 . The reason is because now, our flow has 4 elements, the core one that sends the email, the decision and the 2 assignments. But our test only covers one of the assignments.

So next step is to add a second method that covers the second decision path.

@isTest
public class EmailFlowApexTest
{
    static testMethod void validateEmailFlow()
    {
        Reservation__c r = new Reservation__c ();
        r.isPaid__c = true;
        r.guestEmail__c = 'agarcia@financialforce.com';
        r.TotalAmount__c = 100;
        insert r;
        
	List<Reservation__c&gt; rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
    
    static testMethod void validateEmailFlowWithAmountLessThan50()
    {
        Reservation__c r = new Reservation__c();
        r.isPaid__c = true;
        r.guestEmail__c = 'agarcia@financialforce.com';
        r.TotalAmount__c = 25;
        insert r;
        
	List<Reservation__c&gt; rList = [Select Id, isPaid__c, guestEmail__c from Reservation__c where Id = :r.Id];
        System.assert(true, rList.get(0).isPaid__c);
    }
}

And this time, the query execution returns below result where we can see the second testMethod.

Also we can check both have same values on NumElementsCovered and NumElementsNotCovered because each one cover a single path.

Note: Whenever I made a modification on my trigger, the query took a while (even a day) to get the results. It’s like Salesforce needed some time to process new test coverages.

Finally I would like to talk about how we know we achieve the 75% required in order to be able to deploy my flow. Unfortunately Salesforce does not provide the information yet but explain a way to calculate it manually.

For example, if you have 10 active autolaunched flows and processes, and 8 of them have test coverage, your org’s flow test coverage is 80%.

Salesforce help: https://sforce.co/2PqALZw

 

Having that, if I only have a single flow in my org with some test coverage, per above info, my flow coverage is 100%. However I would not be surprised if they look into the test flow and check the number of elements. So if I have 4 elements and 2 are covered, my coverage is 50% and if I cover 3, I achieve the 75% required. Keep this in mind just in case.

Update 20190726

While I was writing this post, I found out some weird behaviours with tests. If I removed a test method that covered a flow and run the query again, results were same as if I had the test in place. Because of that, I decided to raise a case with Salesforce in order to know more, and I would really like to share the final answer with you.

When we first run a test class it will add a record for each method to the FlowTestCoverage object. If we modify the class to add a new method and run it again, then, it will add a new record for the new method. But if we remove a method it will not delete the record. The record will only be removed if a change is made to the associated flow version.

If we delete the test class completely it will remove all FlowTestCoverage records for the related class since it no longer exists.

If you would like the FlowTestCoverage object to reflect changes made to test classes then, the best way to do this would be to create a new version of the flow and run the test classes against the new version.

Hope it helps others.

New Lightning Component for Maps

New Lightning Component for Maps

20180126 UPDATE!! I got some new and very useful information about maps. Please, go to Latest Update section if you have already read this entry

A couple of years ago, I wrote a post about how to visualise maps and locations on Visualforce pages, and without having that in mind, Winter ’19 helped me to deliver a second part of that one. Now, Salesforce has released a new Map Lightning Component, so I will give it a try before moving to Spring ’19 version.

You will also find a similar example on Winter ’19 maintenance exam challenge. My code is much simpler than the maintenance one, so maybe it is a good start point before making it more complex.

In a similar way as the old post, I need an object with the information to show. In my case, Doctor object with a record about my Dentist, including a Geolocation field, the Location__c one.

Having that, I need to write some code in order to deliver a Lightning Component that call the standard Map component.

I will start with the Controller in Apex in order to retrieve the information from the database. Remember that Geolocation fields is a compound field with 2 pieces, Latitude__s and Longitude__s.

public with sharing class DoctorMapController
{
   @AuraEnabled
   public static List<Doctor__c> getDoctors()
   {
      List<Doctor__c> doctors = [Select Id,
                                   Name, 
                                   Address__c,
	                           Email__c, 
                                   Phone__c,
	                           SurgeryName__c, 
                                   Location__Latitude__s,
                                   Location__Longitude__s
                                 FROM Doctor__c];
      return doctors;
   }
}

We will continue with the component.

First of all, we implement usual interfaces in order to be able to visualise the component on different areas/views of our environment.

Secondly, I will add the tag <lightning:map> and its properties in order to show map information. The most important is mapMarkers. It is an array of markers that will help us to set the usual “pointer globe” icon that google maps provides. After that, we can add others like markerTitle or zoomLevel but they are not required, they just add more value to the component and the visualisation itself.

So having to provide information to mapMarkers and others, we need attributes in order to retrieve the data from our component Controller. That’s why we have 2 <aura:attribute> They are like variables on an apex class. Define the type and name of the variables that you will use among the component to get and set the data you need.

Finally <aura:handler> that will execute the action that will initialise the component via the javascript Controller. It is like the start point.

<aura:component implements="force:appHostable,
                            flexipage:availableForAllPageTypes,
                            flexipage:availableForRecordHome" 
                controller="DoctorMapController" 
                access="global" >
   <aura:attribute name="mapMarkers" type="Object" access="PRIVATE" />
   <aura:attribute name="markersTitle" type="String" access="PRIVATE" />
   <aura:handler name="init" value="{!this}" action="{!c.init}"/>
   <aura:if isTrue="{!!empty(v.mapMarkers)}" >
      <lightning:map 
		     mapMarkers="{!v.mapMarkers}" 
		     markersTitle="{!v.markersTitle}"
                     zoomLevel="5"
      />
   </aura:if>
</aura:component>

And the init, points to here, the javascript Controller. It depends on the way you develop. Actually you can add all method logics to here, but it is a good practice to have it on the Helper, and on the Controller, just a call to there.

({
   init : function(component, event, helper) {
	helper.initHelper(component, event, helper);
   }
})

Finally a Helper where we will find main methods. If you are familizarize with Javascript, it will not be very difficult to understand.

Basically the variable action, access our Apex Controller getDoctors method in order to retrieve all Doctor__c records.

With this information, we can iterate over there and show all doctors on our map. But my case is much simpler, I get the first one, and populate another variable with its information. Finally I will set the values to the component attributes, markTitle and mapMarkers.

But let me focus for a second on the variable that is assigned to mapMarkers attribute. The variable markers is an array because, as I mentioned at the beginning, mapMarkers parameter is an array with locations. But what do I need to specify there? The main one is Location that could be populated with the Geolocation field data or with a direction itself. After that we only have some more descriptive data like the title or the description. They add value to the map but the required one is Location.

({
   initHelper : function(component, event, helper) {
        helper.defineMarkers(component, event, helper);
   },
   defineMarkers : function(component, event, helper) {
	let action = component.get("c.getDoctors");
	action.setCallback(this, function(response) {
	   const data = response.getReturnValue();
	   const dataSize = data.length;
	   let markers = [];
        
           //I will only show my single record
	   const dentist = data[0]; 
	   markers.push({
                'location': {
                    'Latitude':dentist.Location__Latitude__s,
                    'Longitude':dentist.Location__Longitude__s
                },
                'title' : dentist.SurgeryName__c,
                'description' : 
                      dentist.SurgeryName__c + 
                      ' dentist Location at ' + 
                      dentist.Address__c
	   });
	   component.set('v.markersTitle', 
                         'Out and About Communications Dentist 
                         Locations');
	   component.set('v.mapMarkers', markers);
	});
	$A.enqueueAction(action);
   }
})

And here you can find the result, a map with the “Moyua Square” with a pointer to my dentist place.

Reading this entry you can realise that using <lightning:map> is very simple and that actually, I focused on explain how to build the component itself, but if you would like to know more or create more complex pieces of code, take a look at the documentation and do some searches on internet. I’m sure people share their experiences with the new <lightning:map>.

Latest Update:

After reading this entry, my friend Alba Rivas told me she was doing some researches on maps too, and found out some useful information that shared with me:

If an address is specified for a marker, instead of its latitude and longitude, the component makes API requests to the google geolocation, and per Google documentation, it seems it has a cost. Also I would like to know if there is an event that I can listen when a marker is selected.

The API key is from Salesforce, and there are no current limitations on its use. We are going to add a click handler for selecting a marker in an upcoming release

Alba Rivas question and Salesforce response

From Attachment to File via Flow?

Some days ago I got a message asking me if it was possible to move from Attachments to Files in Salesforce using the #clicksnotcode tool VisualFlow and after some researches I would say no, this is not possible without code. But if you find the way, please, please, please, leave me a message so that I can update this post.

So, what is this post about? My researches and how I got that the answer is no.

First of all, the use case. As you know we cannot create Attachments in Lightning Experience, only Files are available in LEX, so anything that we want to keep should be also migrated.

But how?

  • Manually? That could be a really expensive option, and tired, to be honest.
  • Via code? Yes!! I like this option. At the end, a File is a Salesforce Object, ContentVersion , and as you can find on this blog, with a really simple piece of code you can create a new File record. Below piece adds the query to an attachment. After that, find the image with the new file and also the result of a query done in  Workbench.
List<Attachment> attList = [SELECT 
                               Body,BodyLength,Name,ParentId 
                            FROM Attachment limit 1];
Attachment att = attList.get(0);

String fileContent = att.body.toString();
ContentVersion conVer = new ContentVersion();
conVer.PathOnClient = 'MigratedFile.txt';
conVer.Title = 'MigratedFile.txt';
conVer.Description = att.Name + ' migrated';
conVer.VersionData = EncodingUtil.base64Decode(fileContent);
conVer.FirstPublishLocationId = '001B000000hYespIAC'; 
insert conVer;

Captura de pantalla 2018-06-28 a las 17.15.33

Captura de pantalla 2018-06-28 a las 17.20.22

And what about VisualFlow?

I created something really simple, 4 steps flow. But it has a welcome and a thanks screen, so actually the job is done in 2 steps.

Captura de pantalla 2018-06-28 a las 17.30.46

First one is Record Lookup where I retrieve the single attachment that I have in my organization. Please, keep in mind that if you have a list of Attachments, and use a Record Lookup, it will return a single record, but if your idea is to retrieve all attachments, you need to use a Fast Record Lookup step.

Captura de pantalla 2018-06-28 a las 17.31.10

This step is going to store data into the variable that I created, and all of them are similar, Text variables.

Captura de pantalla 2018-06-28 a las 17.31.48

The second step is to create the ContentVersion record, assigning the variable data that I got on the previous step.

Captura de pantalla 2018-06-28 a las 17.32.00

And that is all. Yes, so simple.

However, when I run it I got an error and this email where I can see that there is an issue on VersionData field.

Captura de pantalla 2018-06-28 a las 17.42.18

The reason is simple. Attachment Body returns a Blob that I need to convert into String in order to be able to encode and pass to the Content file.

....
String fileContent = att.body.toString();
....
conVer.VersionData = EncodingUtil.base64Decode(fileContent);
....

Is there a way to fix it? No as far as I could find. My first idea was to check what the variable was returning, adding a middle step to show the body value

Captura de pantalla 2018-06-28 a las 17.55.06

And as I expected that is not my body text

Captura de pantalla 2018-06-28 a las 17.54.46

So the next thought was to try to create a formula in flow or make any transform, something that could help me, but I couldn’t find any.

Captura de pantalla 2018-06-28 a las 17.51.16

So I think that the only way to do it is via some Apex code. We can create a class that has InvocableMethod annotation and do the transform there and call the class as it was an step, but, to be honest, why do I want to do it when I can run the whole transform from an apex class as I explained before?

At the end, I would advice to create your own ApexClass that execute the process to do the migration if you do not want to use the tool that Salesforce provides.

Good luck!!

BigObjects and Triggers

Some months ago I wrote an entry about BigObjects and since that moment I go lot of comments with questions and some useful information that I missed in my post and I would like to include here because sometimes, we do not read comments.

Not all SOQL statements are valid. For now, the !=, LIKE, NOT IN, EXCLUDES, and INCLUDES operators are not valid in any query.

There are already some Standard Big Objects like FieldHistoryArchive, so do not try to create one directly, and check before if it already exist.

Unfortunately SOSL is not supported by BigObjects.

But maybe the most important one was the fact that people was not able to create BigObject records from Triggers. It was a big surprise for me because documentation said that you could not create triggers related to a Big Object, but nothing about not be able to create Big Object records from triggers. So today I decided to give it a try.

If you have taken a look at my blog, the use case was to archive Code Review records so that we could end up with Code Review History records. Although it doesn’t make too much sense, create History records in a trigger, I decided to make it simple and continue with these objects.

So the first idea was to create a Trigger related to CodeReview__c:

trigger CodeReviewTrigger on CodeReview__c (after insert)
{
   List<CodeReviewHistoric__b> crhToInsert = new List<CodeReviewHistoric__b>(); 
   for (CodeReview__c cr: Trigger.new)    
   { 
      CodeReviewHistoric__b crh = new CodeReviewHistoric__b();        
      crh.CanBeMerged__c = cr.CanBeMerged__c ? 'true' : 'false';        
      crh.CodeReviewDate__c = cr.CodeReviewDate__c;         
      crh.Comments__c = cr.Comments__c;        
      crh.Employee__c = cr.Employee__c;        
      crh.Score__c = cr.Score__c;        
      crh.Story__c = cr.Story__c;        
      crhToInsert.add(crh);    
   }
   Database.insertImmediate(crhToInsert);
}

However, when I tried to create a Code Review record via the UI, I got below error

Captura de pantalla 2018-02-18 a las 15.16.26

and if I checked log, I also got this one.

Captura de pantalla 2018-02-18 a las 15.16.54

So came back to BigObjects product manager suggestion:

However a workaround could be to put the insertImmediate() call into a queueable method

But why queuable method? I think the key point is that we need a different context, a new transaction, so it doesn’t matter the way to get it, and I decided to keep things simple using a @future method in order to perform the Database.insertInmediate action. Then, this method will be called from the trigger.

But this annotation has several restrictions, like argument types. It doesn’t support a list of SObjects, but it allows a List of Ids.

So Trigger will look like this:

trigger CodeReviewTrigger on CodeReview__c (after insert)
{    
   List<Id> codeReviewIds = new List<Id>(); 
   for (CodeReview__c cr: Trigger.new)    
   {        
      codeReviewIds.add(cr.Id);    
   }
   AsyncProcess.createBigObjectRecords(codeReviewIds);
}

And @future one like this:

public with sharing class AsyncProcess
{
   @future
   public static void createBigObjectRecords(List<Id> crIds)
   {
      List<CodeReviewHistory__b> crhToInsert = new List<CodeReviewHistory__b>();
      for (CodeReview__c cr: [Select Id, 
                                     CanBeMerged__c, 
                                     CodeReviewDate__c, 
                                     Comments__c, 
                                     Employee__c, 
                                     Score__c, 
                                     Story__c 
                               From CodeReview__c 
                               Where Id In :crIds])    
      {
         CodeReviewHistory__b crh = new CodeReviewHistory__b();
         crh.CanBeMerged__c = cr.CanBeMerged__c ? 'true' : 'false';            
         crh.CodeReviewDate__c = cr.CodeReviewDate__c;             
         crh.Comments__c = cr.Comments__c;            
         crh.Employee__c = cr.Employee__c;            
         crh.Score__c = cr.Score__c;            
         crh.Story__c = cr.Story__c;            
         crhToInsert.add(crh);    
      }     
      Database.insertImmediate(crhToInsert);    
   }
}

And now it works sucessfuly

Captura de pantalla 2018-02-18 a las 16.37.46

Captura de pantalla 2018-02-18 a las 16.39.40.png

Any other options? Batch Apex, Queueable, even, you can use Async SOQL via a http call.

I’m sure that I still miss things about BigObjects but another amazing resource to check is the Salesforce MVP Alba A. Rivas blogs about this technology.

 

Process billions of records with Async SOQL

Christmas time is closer but I would like to deliver this last post before end of the year.

Some months ago, I wrote an entry talking about BigObjects, a feature that was General Available by Winter ’18 and via a use case, I tried to explain it. Now, I would like to follow that post with a new way to create records, Async SOQL.

Async SOQL is half GA by Winter ’18 and half still in Pilot 

The use case talked about moving Code Review custom object records, into Code Review History big objects records, and release storage of custom objects one. We want to Archive records, and Async SOQL will help us to deal with big amounts of data.

What is Async SOQL?

Basically Async SOQL allows you to run SOQL in the background so that, you will get a response after a period of time, but at the same time, helps you to deal with millions or even billions of records without hitting time outs or governor limits.

How can I execute Async SOQL?

Async SOQL is implemented as REST full API and in order to execute this SOQL in the background we need to run a post request:

Captura de pantalla 2017-11-29 a las 21.51.22

And provide a body in JSON format in order to make the call. Find bellow a really easy example that helps you to create Vendor__c custom object records based on Accounts.

Captura de pantalla 2017-12-19 a las 9.40.12What can we highlight?

query: allows you to define a SOQL. On our case, the object I’m going to read, Account and the field I want to recover, Name.

operation: help us to define what we want to do, insert or upsert (please check big objects entry to understand Primary Key and upsert, as it works in the same way)

Basically that is the main difference between simple SOQL and Async SOQL. With this new feature we can read and create or update records in one go.

targetobject: object we want to use to create new records. In our case, Vendor__c.

Then, we need to specify from where we want to get record field values and where we want to store this info. For that we have 2 keys in this JSON code.

targetFieldMapping: allows you to define source field and target fields

and

targetValueMap: allows you to map a target field with a literal value, for instance, we want all records have Spain as Country__c field value.

How is the response?

When we do the post call, we get a similar response:

Captura de pantalla 2017-11-29 a las 22.01.12

It is also in JSON format and basically it provides similar information that you passed in the post call. Just highlight 3 keys:

jobId: remember this is an asynchronous call so there is an Id related to the background job. You can also use it in the post call via the global variable $Job_Id as part of key value on targetValueMap.

status: helps you to know where is the job. Moving from New to Running, Complete, Failed etc.

message: before executing the call, the code is analyzed. If it realizes about a possible issue, it doesn’t start the call and provides an error message in that field. For instance, below message. My VendorName__c field size was too short to store all Account Names.

Captura de pantalla 2017-11-29 a las 22.11.03.png

How can I stop the execution?

Async SOQL doesn’t provide a UI in Salesforce like other background jobs like Batch Apex or Queueable. But we can make an http delete call passing the jobId as part of the url.

Captura de pantalla 2017-11-29 a las 22.22.02

How can I check the progress?

Similar as before, as we do not have UI, we need to look for another way in order to get this information. For that, we have 2 options:

1º – Make a get post call: Similar to the cancel action, if we pass the JobId as part of the url, and do a get call, the response shows you information abut the background execution that is running in the system.

Captura de pantalla 2017-11-29 a las 22.32.33

2º – Make a SOQL against BatckgroundOperation object: This is a new object where we can see extra information about the job execution.

Captura de pantalla 2017-11-29 a las 22.32.42

But this is not the only new object that we can get in the system. We also have BackgroundOperationResult that will help you to identify any issue during the execution if the result is not the expected one. For instance. Bellow image shows an issue related to a field that is required but the source value is empty. In that case, a new error is logged on this table, but the execution doesn’t stop, it continues till the end.

Just keep in mind that this information would be removed after 7 days.

Captura de pantalla 2017-11-29 a las 22.32.55

And what about Archiving?

Yes, you are right. We promised to show you how to archive Code Review records but tried to explain the whole functionality with a really simple use case. Now it’s time to move to Code Review History use case.

As I mentioned before, we would like to create Code Review History records reading Code Review records. Similar as before we will do a post call but the body would be like this one:

Captura de pantalla 2017-11-29 a las 22.51.40

On BigObjects post, the object was called Test1CodeReviewHistory__b instead of CodeReviewHistory__b

And … that’s all. Simple, isn’t it?

Can I integrate this functionality into Apex?

Yes, of course, at the end we are just making http calls, so we only need to set the proper url, add it on my remote settings and create a string in JSON format for the body.

Captura de pantalla 2017-11-29 a las 22.42.57

Summary

Finally I would like to sum up some key concepts we have talked about.

  1. Async SOQL allows you to run SOQL in the background
  2. It takes some times but allows you to process millions or even billions of records
  3. You do not need to worry about governor or time out limitations
  4. It is implemented as REST full API
  5. You can make as many calls as you want per day but just a single one at a time
  6. You can read and create records in one go. Delete is out of scope
  7. This feature is part GA and part still in Pilot:
    1. Read Standard or Custom objects and create Standard, Custom or Big Objects is in Pilot
    2. Read Big Objects and create Standard, Custom or Big Objects is GA

Captura de pantalla 2017-11-29 a las 22.43.44

Platform Encryption

Summer time, but I do not want to miss the opportunity so share something even if this post is sorter than usual.

A year ago I completed Platform Encryption Trailhead  but I have started working with it few months ago.

What is Platform Encryption and Why do we need it?

You can find more information here, but basically it would allow us to protect some data, so that, it would not be visible. However this does not mean that the data is stored in database with a mask “****”. It is going to be saved as usual, but if your user doesn’t have certain rights, it will not be visible, and he would have a mask “****” as view effect .

Ok, let’s explain a bit better. If I need a field encrypted, because it is a password, I will use the existing Text (encrypted) field type. If I need to have my data visible in my organization, but if this is stoles, for instance, it should not be visible, then, I need to use existing field types, but enable Platform Encryption on those that have sensible data.

How to enable Platform Encryption?

First of all, you need to have rights, to manage encrypted data. For that, the first thing to do is to create a Permission Set, under System Mode, select Manage Encryption Keys.  Remember to assign it to your user.

If you need to see also encrypted data, you need to add to the same Permission Set, or a new one, View Encrypted Data right.

Then, you need to go to Setup, under Administration Setup you can find Platform Encryption

Captura de pantalla 2017-08-11 a las 18.36.28

The very first time you also need to generate a Tenant Secret just clicking on the button.

Captura de pantalla 2017-08-14 a las 11.40.38

Captura de pantalla 2017-08-14 a las 11.44.24

 

The last step is to decide what you need to encrypt. Right now Salesforce allows you to encrypt certain fields on certain Standard Objects.

Leads fields are in beta

And you can also encrypt custom fields on Standard and Custom objects as long as their field type are: Email, Phone, Text, Text Area, Long Text Area, URL, Date or Date/Time.

But before encrypt any field you need to be aware that there are certain limitations. Maybe one of the most important one is that you cannot filter, order or group by an encrypted field, so that your code can fail once you enable this feature in your organization.

 

Reviewing the code

Now it is time to review your current code to check if there is something failing now. The easiest example of failure is this SOQL in your code.

Select Id, Name From Account Where Name = :accName;

But now it fails at compilation time. And if you turn Encryption off, it continues failing … so what should you do at this point?

If your next step is to move to dynamic SOQL, it’s a good try. Now it saves, but it would fail at run time.

You need to find another way to get the information. Salesforce suggests to use SOSL, and this is an easy solution, as we only need to move to this piece of code. So let’s go for it and solve this issue.

Account acc = new Account();Account acc = new Account();        
acc.Name = 'PE Blog Test';        
insert acc;         

List<List<Account>> accList = [FIND 'PE Blog Test' 
                               IN ALL FIELDS 
                               RETURNING Account(Id, Name)];        

System.debug('Testing: ' + accList);

Captura de pantalla 2017-08-14 a las 12.08.35

Reviewing Unit Tests

Ok, this section sound similar to the previous one. Yes, you are right, but I wanted to highlight it here, because I was not used to work with SOSL and I found something interesting to share.

If I have below method:

public static testMethod void testAcountsWithSOSL() 
{ 
   List<Account> accList = [Select Id from Account]; 
   System.assertEquals(0, accList.size(), 'Error: There should not be any account.');
   
   Account acc = new Account(Name = 'Test'); 
   insert acc;
   accList = [Select Id from Account]; 
   System.assertEquals(1, accList.size(), 'Error: There should be an account.'); 

   List<List<Account>> accList2 = [FIND 'Test' IN ALL FIELDS RETURNING Account];
   System.assertEquals(1, accList2.get(0).size(), 'Error: There should be an account. And result is = ' + accList2); 
}

And run the test, the result is the below one:

Captura de pantalla 2017-08-14 a las 12.14.31

Exactly, it fails because accList2 is empty although I was expecting to get an element.

Doing some researches, I found this article about unit test. So it seems they provide a Test method that allows you to insert the Id of the record you need to look for and get it back. So that, the solution would be this:

public static testMethod void testAcountsWithSOSLFollowingGuide() 
{ 
   List<Account> accList = [Select Id from Account]; 
   System.assertEquals(0, accList.size(), 'Error: There should not be any account.');

   Account acc = new Account(Name = 'Test'); 
   insert acc;
   accList = [Select Id from Account]; 
   System.assertEquals(1, accList.size(), 'Error: There should be an account.'); 

   Id [] fixedSearchResults= new Id[1]; 
   fixedSearchResults[0] = acc.Id;
   Test.setFixedSearchResults(fixedSearchResults); 
   List<List<SObject>> searchList = [FIND 'Test' IN ALL FIELDS RETURNING Account(Id, Name)]; 
   System.assertEquals(1, searchList.get(0).size(), 'Error: There should be an account. And result is = ' + searchList);    
}

Captura de pantalla 2017-08-14 a las 12.19.46

Ok, so doing in this way, you are saved. However, be aware that your tests have the SeeAllData=false and you do not expect to get data from your org, other wise, it would fail too.

I hope this post help you to be closer to understand what you need to do before enabling Platform Encryption in your org.

Picklist dinámicos en Lightning Components

Mis últimos post técnicos en Español eran bastante básicos. Explicaba cómo crear páginas visualforce, controllers, etc. Sin embargo, desde entonces, Lightning Experience y Lightning Components han cogido el protagonismo en Salesforce.

Así que en esta entrada voy a explicar como crear una lista desplegable y poder determinar su valor por defecto desde App Builder, de forma que cada usuario puede decidir qué valor le conviene.

Caso de Uso:

Como usuario, me gustaría tener un lightning component que me muestre los eventos de Salesforce a partir de una ciudad elegida y decidir el valor por defecto de esa lista desplegable.

Captura de pantalla 2017-08-02 a las 18.28.06

Pasos:

Lista desplegable con valores fijos

Comencemos creando un componente con determinadas ciudades pero sus valores son fijos. Para poder testearlo, lo mostraré en la pestaña Home de mi App Salesforce Events.

Para no alargar el post pondré una breve explicación de lo más importante en el código

Captura de pantalla 2017-08-03 a las 16.58.42

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" 
      implements="flexipage:availableForAllPageTypes" 
      access="global">
    
   <!-- Lista de ciudades sobre la que iterar -->
   <aura:attribute name="cities" type="List" />  
   
   <!-- Valor seleccionado para poder usarlo más adelante.-->
   <!-- Indicamos que el valor por defecto es "Madrid" mediante hardcode -->
   <aura:attribute name="selectedValue" type="String" default="Madrid"/> 
   
   <!-- handler donde llamamos al método que me devuelve los valores de la lista -->
   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />    
   
   <!-- Usando Lightning Design System, creamos la lista desplegable -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="{!v.selectedValue}">    
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration> 
   </lightning:select>
   
</aura:component>

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)    
   {
      //asigno a la variable cities de mi component los nombres de las ciudades
      component.set('v.cities',['London','Madrid','Paris','San Francisco']);
   }
})

Lista desplegable con valores dinámicos

El siguiente paso es recuperar de la base de datos los valores de la ciudades que tenemos. Según la imagen de más arriba, de la lista desplegable desaparecería San Francisco.

Captura de pantalla 2017-08-03 a las 17.10.17

Para ello modificaría el Controlador JavaScript que llamaría a un Controlador en Apex dónde hacemos la búsqueda en base de datos.

SalesforceEventsController.cls

//Esta clase puede ser también global
public with sharing class SalesforceEventsController
{
   //Recuerda añadir la anotación @AuraEnabled 
   //para el que método sea visible en el componente
   @AuraEnabled public static List<String> getCityNames()
   {        
      List<String> cityNames = new List<String>();               
      
     //TODO: Recuerda SOC por lo que una SOQL debería hacerse en otra clase 
     //destinada a ello.
     for(City__c c : [Select Id, Name From City__c])        
      {            
         cityNames.add(c.Name);        
      }                
      return cityNames;    
   }
}

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)
   {        
      var action = component.get("c.getCityNames");
      
      //asigno a la variable cities de mi component 
      //los nombres de las ciudades que he recuperado del controlador apex
      action.setCallback(this, 
                         function(a){
                            component.set("v.cities", a.getReturnValue());
                         });
      $A.enqueueAction(action);    
   }
})

Lista desplegable con valores dinámicos y Tabla con información

El siguiente paso es mostrar los valores de los eventos de Salesforce a partir de una selección en la lista desplegable.

SalesforceEventsController.cls

@AuraEnabled  
public static List<SalesforceEvent__c> getEventsByCity(String cityName)
{
    City__c theCity = [Select Id From City__c Where Name = :cityName Limit 1];        
    Id theCityId = theCity.Id;                
    List<SalesforceEvent__c> sEvents = new List<SalesforceEvent__c>();        
    
    //TODO: Recuerda SOC por lo que una SOQL debería hacerse en otra clase 
    //destinada a ello.
    for(SalesforceEvent__c e : [Select Id,EventName__c,EventDate__c From SalesforceEvent__c Where City__c = :theCityId])        
    {            
        sEvents.add(e);        
    }        
    return sEvents;    
}

En este punto me di cuenta que el refresco iba a ser más complicado de lo que tenía pensado ya que tuve que crear un componente hijo para la parte de los Salesforce Events, y un evento para poder hacer el refresco. Os dejo el enlace que usé como referencia.

SalesforceEventsEvent.evt

<aura:event type="APPLICATION">
   <!-- A partir de una ciudad seleccionada, calculo los eventos -->
   <aura:attribute name="citySelected" type="String" />
</aura:event>

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" implements="flexipage:availableForAllPageTypes" access="global">
   <!-- Attributes set on Design Page-->    
   <aura:attribute access="global" name="headerText" type="String" /> 
   <aura:attribute access="global" name="defaultCity" type="String" />        
   
   <aura:attribute name="cities" type="List" />    
   <aura:attribute name="selectedValue" type="String" default="Madrid"/>    

   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />

   <!-- onchange no funciona sin un handler relacionado con value el tag option -->
   <aura:handler name="change" value="{!v.value}" action="{!c.cityChange}"/>        

   <!-- Usaremos onChange para llamar a la función js que refrescará la lista -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="selectedValue" onchange="{!c.cityChange}"> 
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration> 
   </lightning:select>        

   <h1>Salesforce Events:</h1>    
   <!-- Registro el evento y llamo al nuevo componente hijo para listar los eventos de Salesforce -->
   <aura:registerEvent name="appEvent" type="c:SalesforceEventsEvent"/>    
   <c:SalesforEventList />

</aura:component>

SalesforceEventsComponentController.js

({
   loadCities: function (component, event, helper)    
   { 
      var action = component.get("c.getCityNames"); 
      action.setCallback(this, function(a) 
      { 
         component.set("v.cities", a.getReturnValue());        
      });        
      $A.enqueueAction(action);    
   },        
   cityChange : function(component, event)    
   {        
      //Determino el nuevo valor por defecto que es el elegido
      component.set('v.selectedValue', component.find("mySelect").get("v.value"));        
      component.find("mySelect").get("v.value");
      
      //Paso al atributo citySelected del event el nuevo valor seleccionado
      var appEvent = $A.get("e.c:SalesforceEventsEvent");        
      appEvent.setParams({"citySelected" : component.get("v.selectedValue")});        
      appEvent.fire();    
   }
})

SalesforEventList.cmp

<aura:component controller="SalesforceEventsController" implements="flexipage:availableForAllPageTypes" access="global">

   <aura:handler event="c:SalesforceEventsEvent" action="{!c.loadEvents}"/>
   <aura:attribute name="cityToFilter" type="String" />    
   <aura:attribute name="sevents" type="SalesforceEvent__c[]" />         

   <aura:iteration items="{!v.sevents}" var="theEvent">        
      <li>{!theEvent.EventDate__c} - {!theEvent.EventName__c }</li>    
   </aura:iteration> 
</aura:component>

SalesforEventList.cmp

({ 
   loadEvents : function(component, event)    
   {        
      var citySentFromEvent = event.getParam("citySelected");
      component.set("v.cityToFilter", citySentFromEvent);            

      var action = component.get("c.getEventsByCity");        
      action.setParams({ cityName : component.get("v.cityToFilter") }); 
      action.setCallback(this, function(a) 
      { 
         component.set("v.sevents", a.getReturnValue());        
      });        
      $A.enqueueAction(action);                
   }
})

Determinar un valor por defecto desde App Builder

La idea aquí es dar la opción de poner el valor por defecto cómo parte de la configuración. Para ello usaremos la funcionalidad Dynamic PickList for Lightning Components.

Añadiremos 2 atributos a nuestro componente. Uno sera un texto con información y el segundo nuestra lista desplegable.

SalesforceEventsComponent.cmp

<aura:component controller="SalesforceEventsController" 
                implements="flexipage:availableForAllPageTypes" 
                access="global">
   
   <!-- Atributos para la Design Page--> 
   <!-- Este primer atributo no es requerido para la lista desplegable -->
   <aura:attribute access="global" name="headerText" type="String" /> 
   <!-- En vez de tener el valor por defecto con hardcode, usamos el atributo defaultCity -->
   <aura:attribute name="selectedValue" type="String" default="{!v.defaultCity}"/>

   <aura:attribute name="cities" type="List" />     
   <aura:attribute name="selectedValue" type="String"/>    
   <aura:handler name="init" value="{!this}" action="{!c.loadCities}" />    

   <!-- Utilizo como valor para value el atributo defaultCity -->
   <lightning:select name="mySelect" label="Select a city:" aura:id="mySelect" value="selectedValue">
      <aura:iteration items="{!v.cities}" var="theCity">        
         <option text="{!theCity}" value="{!theCity}" selected="{!theCity}"/>    
      </aura:iteration>
   </lightning:select>
...
</aura:component>

A continuación debemos crear nuestro design para añadir los nuevos campos a la Design Page.

SalesforceEventsComponent.design

<design:component label="Salesforce Events Cities">
   <design:attribute name="headerText" 
                     label="Header Text" 
                     description="Text that will appear in the header when the component is displayed" 
                     default="City" />    
   <!-- datasource nos ayuda a lincar 
        el atributo con una clase apex de dónde extraeremos 
        la información que necesitamos -->
   <design:attribute name="defaultCity" 
                     label="Default City PickList" 
                     datasource="apex://SalesforceEventsCityValuePickListDefault" />
</design:component>

Por último, debemos crear la clase apex lincada a nuestro atributo.

Aunque en la documentación aparece como global, si la hacemos pública funciona igualmente.

Esta clase extiende VisualEditor.DynamicPickList y aunque tiene cuatro métodos para sobre escribir, sólo son requeridos dos. Uno determina el valor por defecto en la lista desplegable, y el otro nos muestra los valores.

SalesforceEventsCityValuePickListDefault.cls

public class SalesforceEventsCityValuePickListDefault extends VisualEditor.DynamicPickList
{ 
   public override VisualEditor.DataRow getDefaultValue()    
   {        
      VisualEditor.DataRow defaultValue = new VisualEditor.DataRow('defaultLabel', 'defaultValue');        
      return defaultValue; 
   }        

   public override VisualEditor.DynamicPickListRows getValues()    
   { 
      VisualEditor.DataRow defaultRow = new VisualEditor.DataRow('defaultLabel', 'defaultValue'); 
      VisualEditor.DynamicPickListRows myRows = new VisualEditor.DynamicPickListRows(); 
      myRows.addRow(defaultRow);              

      List<String> cities = SalesforceEventsController.getCityNames();
      for(Integer i=0; i<cities.size(); i++)
      {
         VisualEditor.DataRow newRow = new VisualEditor.DataRow(cities.get(i), cities.get(i));
         myRows.addRow(newRow);
      }        
      return myRows; 
   }
}

Como muestro en la imagen de abajo, si abrimos el App Builder y seleccionamos nuestro Lightning Component, veremos los dos atributos que definimos en SalesforceEventsComponent.design siendo uno de ellos la lista desplegable con los valores que hemos recuperado de base de datos.

Captura de pantalla 2017-08-03 a las 20.03.46.png

Un último apunte antes de terminar el post. No estoy cubriendo error handling en mi código pero échale un vistazo a la ayuda de Salesforce para ello. Por ejemplo este y este.

Como has podido comprobar, es muy sencillo. Si eres nuevo en este tema, los pasos son fáciles de seguir. Si ya eres un experto, y has encontrado algo en el código que se puede mejorar. Dímelo y así otros podrán beneficiarse de tus conocimientos también.