SFDC Stop - Always the latest about Salesforce


Full Tutorial Series with videos, free apps, live sessions, salesforce consulting and much more.


Telegram logo   Join our Telegram Channel

Friday, 13 May 2022

How to setup prettier for apex in VSCode?

Hello Trailblazers,


In this post, we're going to learn how to setup prettier for apex in VSCode. We're going to use a plugin named as prettier-plugin-apex. Once you've created a salesforce project, the first step is to install Prettier - Code formatter extension.

Click on the Extensions Tab in the left sidebar in your VSCode and search for prettier as shown below:

The first option that you'll see is for Prettier - Code Formatter open that and click on the Install button.

Once you have installed prettier, now it's time to install prettier-plugin-apex. You can install the plugin in your project only by running the command given below:

npm install --save-dev --save-exact prettier prettier-plugin-apex

The output of the above command is given below:

Please note that this will install prettier-plugin-apex locally in your project. If you want to install it globally so that you can use it in all the projects, you can use the below command:

npm install --global prettier prettier-plugin-apex

If you've installed it locally, it'll the package.json file in your salesforce project and you'll find the entry for prettier under scripts as shown below:

So, if you want to run prettier for your project, you can run it by executing the below command:
npm run prettier

I tried this command in my project, so you can see the sample output below:

You'll see that the apex files are also formatted. Let's have a look at the test class formatted using prettier, that we created for salesforce flow in the previous post:

As you've formatted the whole project once, a better option is to setup prettier so that it formats the file automatically as you save it. You can do that by following the below steps:

1. Open the command pallette (Ctrl + Shift + P for Windows, Command + Shift + P for Mac) and search for User settings.
2. Click on Preferences:Open User Settings as shown above and search for format on save
Click the Editor: Format On Save checkbox under the User tab as shown above. Also click the same checkbox under the Workspace tab as shown in the below screenshot:
Now your apex file will format automatically using prettier as you save it. Congratulations!! You've successfully setup prettier for apex in VSCode and it's ready for use.

That's all for this tutorial. I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!

Saturday, 7 May 2022

How to create test class for a flow? | Test coverage for flows | Salesforce Flow Test Class Basics

Hello Trailblazers,


In this post we're going to learn How to create test class for a flow? Most of the developers I've met have never written a test class for flow and some of them are not even aware that we can write a test class for flow and evaluate the test coverage. The question that I get is: Is it possible to create test class for a flow??


YES, THIS IS POSSIBLE!! LET'S SEE HOW?

Create a Flow

First of all, let's create a simple flow. I am sharing the details of the flow below:


Flow Type: Record Triggered Flow

Object: Lead

Trigger the Flow When: A record is created or updated

Entry Conditions: All Conditions Are Met (AND) -> AnnualRevenue -> Is Changed -> True

Optimize the Flow for: Fast Field Updates

Flow Label: Update Lead Rating based on Annual Revenue

Flow Description: This flow will update lead rating based on the value of annual revenue


What this flow is going to do?


Our flow will update the Lead Rating based on the value of Annual Revenue of lead. There are 3 conditions that it'll cover:


  1. Annual Revenue is less than 25,000 -> Lead Rating = Cold

  2. Annual Revenue is between 25,000 and 75,000 -> Lead Rating = Warm

  3. Annual Revenue is greater than 75,000 -> Lead Rating = Hot

So, we created a decision element named "Check Annual Revenue". It'll have 3 outcomes based on the above 3 conditions:

Less than 25,000


Between 25,000 and 75,000


Greater than 75,000 (Default Outcome)


Now, let's create 3 update elements as well:

Update Rating to Cold


Update Rating to Warm


Update Rating to Hot


Now that we have our update elements, we need to link these 3 update elements with our decision outcomes so that, when: 
  • Annual Revenue is less than 25,000 -> Lead Rating = Cold
  • Annual Revenue is between 25,000 and 75,000 -> Lead Rating = Warm
  • Annual Revenue is greater than 75,000 -> Lead Rating = Hot

This is how our final flow looks like:

Create a test class for the above flow

Now, it's time to create a test class for this flow. While creating a test class for flow, The intent is to cover all the flow elements, while testing various scenarios and verify the result. Let's have a look at the full code for the test class below covering all scenarios and then we'll see each method one by one in detail.
/*
* Created:- 07/05/2022
* Last Updated:- 07/05/2022
* Description:- This is the test class for Update_Lead_Rating_based_on_Annual_Revenue Flow
*/
@isTest
public with sharing class LeadUpdateFlowTest {

    // * Constants
    static final Integer NUMBER_OF_RECORDS = 200;
    static final String RATING_COLD = 'Cold';
    static final String RATING_WARM = 'Warm';
    static final String RATING_HOT = 'Hot';

    // * Description: This method is used to create initial data for the test methods
    @TestSetup
    static void makeData() {
        List<Lead> leads = new List<Lead>();
        for(Integer i=1; i<=NUMBER_OF_RECORDS; i++) {
            leads.add(new Lead(LastName = 'Test Lead ' + i, Company='Test Company'));
        }
        insert leads;
    }

    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is less than 25,000
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueLessThan25kTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 24999;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_COLD, lead.Rating, 'Lead rating should be: ' + RATING_COLD);
        }
        Test.stopTest();
    }

    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is between 25,000 and 75,000.
    *   We're keeping the value at floor i.e. 25,000 for this test
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueBetween25kAnd75kFloorTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 25000;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_WARM, lead.Rating, 'Lead rating should be: ' + RATING_WARM);
        }
        Test.stopTest();
    }

    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is between 25,000 and 75,000.
    *   We're keeping the value at ceiling i.e. 75,000 for this test
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueBetween25kAnd75kCeilingTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 75000;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_WARM, lead.Rating, 'Lead rating should be: ' + RATING_WARM);
        }
        Test.stopTest();
    }

    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is greater than 75,000
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueGreaterThan75kTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 75001;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_HOT, lead.Rating, 'Lead rating should be: ' + RATING_HOT);
        }
        Test.stopTest();
    }

    /*
    *   Description: This method is used to cover all low elements and verify lead rating
    *   when the annual revenue is less than 25k, between 25k and 75k and also greater than 75k
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueAllElementsTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 20000;
        }
        update leads;
        List<Lead> updatedLeadsWithRatingCold = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeadsWithRatingCold) {
            System.assertEquals(RATING_COLD, lead.Rating, 'Lead rating should be: ' + RATING_COLD);
        }
        for(Lead lead : leads) {
            lead.AnnualRevenue = 50000;
        }
        update leads;
        List<Lead> updatedLeadsWithRatingWarm = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeadsWithRatingWarm) {
            System.assertEquals(RATING_WARM, lead.Rating, 'Lead rating should be: ' + RATING_WARM);
        }
        for(Lead lead : leads) {
            lead.AnnualRevenue = 80000;
        }
        update leads;
        List<Lead> updatedLeadsWithRatingHot = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeadsWithRatingHot) {
            System.assertEquals(RATING_HOT, lead.Rating, 'Lead rating should be: ' + RATING_HOT);
        }
        Test.stopTest();
    }
}
Now let's understand each method one by one in detail:
    // * Constants
    static final Integer NUMBER_OF_RECORDS = 200;
    static final String RATING_COLD = 'Cold';
    static final String RATING_WARM = 'Warm';
    static final String RATING_HOT = 'Hot';
First of all I created some constants where I specified the total number of records my test class will create and also the 3 lead ratings that I need to verify once my flow is executed.
    // * Description: This method is used to create initial data for the test methods
    @TestSetup
    static void makeData() {
        List<Lead> leads = new List<Lead>();
        for(Integer i=1; i<=NUMBER_OF_RECORDS; i++) {
            leads.add(new Lead(LastName = 'Test Lead ' + i, Company='Test Company'));
        }
        insert leads;
    }
Next, I have the makeData() method which is going to create some lead records based on the number we specified in the NUMBER_OF_RECORDS constant.
    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is less than 25,000
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueLessThan25kTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 24999;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_COLD, lead.Rating, 'Lead rating should be: ' + RATING_COLD);
        }
        Test.stopTest();
    }
Once we have the records created, the first method that's specified above will test the scenario when we have the annual revenue of every lead less than 25,000. So, we updated all the leads with annual revenue as 24,999, we queried the updated leads and verified the lead rating. As per our flow, the lead rating should be Cold.

Let's run this test method once and see the results:

As you can see, the test executed successfully and passed. But Wait!! How can I check the code coverage? OR I should say the flow elements coverage?

We can do that using a simple soql query:
sfdx force:data:soql:query --query "SELECT Id, ApexTestClassId, TestMethodName, FlowVersionId, NumElementsCovered, NumElementsNotCovered FROM FlowTestCoverage WHERE FlowVersion.Definition.DeveloperName = 'Update_Lead_Rating_based_on_Annual_Revenue' AND TestMethodName = 'updateLeadBasedOnAnnualRevenueLessThan25kTest'" --usetoolingapi

As you can see above, I am querying the Id, ApexTestClassId, TestMethodName, FlowVersionId, NumElementsCovered and NumElementsNotCovered fields of FlowTestCoverage object and I have also added a where condition where I specified the API name of my flow as: FlowVersion.Definition.DeveloperName = 'Update_Lead_Rating_based_on_Annual_Revenue' and I also added a condition for my test method that I executed above as: TestMethodName = 'updateLeadBasedOnAnnualRevenueLessThan25kTest'. Note that I am using Tooling API for this query by adding a flag --usetoolingapi because we're performing a query on the flow metadata.

The output of the above query is provided below:
As you can see above, there are 2 elements of the flow which are covered and 2 elements which are not covered. As per the condition that we checked, we can say that the below two elements of the flow encircled in RED are covered:
and the other two elements are not covered. As we've verified that the flow is updating the Rating to Cold, therefore, we can say that this update element was covered. Let's jump onto the next method now!
    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is between 25,000 and 75,000.
    *   We're keeping the value at floor i.e. 25,000 for this test
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueBetween25kAnd75kFloorTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 25000;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_WARM, lead.Rating, 'Lead rating should be: ' + RATING_WARM);
        }
        Test.stopTest();
    }
In the next method that you can see above, we tested the scenario when the annual revenue is between 25,000 and 75,000. We can have 2 edge conditions here i.e. the annual revenue equal to 25,000 and the annual revenue equal to 75,000. We're testing the floor condition here i.e. the minimum value of AnnualRevenue which will satisfy this condition. Therefore, we've set the annual revenue to 25,000 and verified that the rating should be Warm.

The output when the above test method is executed is given below:
    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is between 25,000 and 75,000.
    *   We're keeping the value at ceiling i.e. 75,000 for this test
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueBetween25kAnd75kCeilingTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 75000;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_WARM, lead.Rating, 'Lead rating should be: ' + RATING_WARM);
        }
        Test.stopTest();
    }
Next we tested with the ceiling value or the second edge condition where the annual revenue is set to 75,000. As you can see above, we verified that the rating should be Warm in this case as well. 

The output when the above test method is executed is given below:

Let's see the test coverage for these methods as well. The updated query is given below:
sfdx force:data:soql:query --query "SELECT Id, ApexTestClassId, TestMethodName, FlowVersionId, NumElementsCovered, NumElementsNotCovered FROM FlowTestCoverage WHERE FlowVersion.Definition.DeveloperName = 'Update_Lead_Rating_based_on_Annual_Revenue' AND (TestMethodName = 'updateLeadBasedOnAnnualRevenueBetween25kAnd75kFloorTest' OR TestMethodName = 'updateLeadBasedOnAnnualRevenueBetween25kAnd75kCeilingTest')" --usetoolingapi
The result when the above query is executed is shown below:
As you can see above, for both the test method executions, two elements of the flow are covered and two are not covered. This time both the methods are dealing with the condition when annual revenue is between 25,000 and 75,000. Therefore, we can say that the below two elements of the flow encircled in RED are covered:
and the other two elements are not covered from this test method. As we've verified that the flow is updating the Rating to Warm, therefore, we can say that this update element was covered. Let's cover the last update element as well:
    /*
    *   Description: This method is used to verify lead rating
    *   when the annual revenue is greater than 75,000
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueGreaterThan75kTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 75001;
        }
        update leads;
        List<Lead> updatedLeads = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeads) {
            System.assertEquals(RATING_HOT, lead.Rating, 'Lead rating should be: ' + RATING_HOT);
        }
        Test.stopTest();
    }

Finally, the last test condition can be, when the annual revenue is greater than 75,000. So, we updated all the leads with annual revenue as 75,001. We queried the updated leads and verified the lead rating. As per our flow, the lead rating should be Hot.

The output when the above test method is executed is given below:

As you can see above, the method executed successfully, let's see the code coverage for this method as well. I am sharing the updated query below:
sfdx force:data:soql:query --query "SELECT Id, ApexTestClassId, TestMethodName, FlowVersionId, NumElementsCovered, NumElementsNotCovered FROM FlowTestCoverage WHERE FlowVersion.Definition.DeveloperName = 'Update_Lead_Rating_based_on_Annual_Revenue' AND TestMethodName = 'updateLeadBasedOnAnnualRevenueGreaterThan75kTest'" --usetoolingapi
The output when this query is executed is given below:

As you can see above, once again, two elements of the flow are covered and two elements are not covered. This time our method is dealing with the condition when annual revenue is greater than 75,000. Therefore, we can say that the below two elements of the flow encircled in RED are covered:
and the other two elements are not covered from this test method. Again we've verified that the flow is updating the Rating to Hot, therefore, we can say that this update element was covered.

We've now covered all the flow elements using different test methods and the results that we verified in our test methods signify that the right elements were covered. You may be thinking that it would be great if we can cover all the flow elements once, just to be sure that we've covered everything. Well, we can do that as well. Let's have a look at the below method:
    /*
    *   Description: This method is used to cover all low elements and verify lead rating
    *   when the annual revenue is less than 25k, between 25k and 75k and also greater than 75k
    */
    @isTest
    public static void updateLeadBasedOnAnnualRevenueAllElementsTest() {
        List<Lead> leads = [SELECT AnnualRevenue FROM Lead];
        Test.startTest();
        for(Lead lead : leads) {
            lead.AnnualRevenue = 20000;
        }
        update leads;
        List<Lead> updatedLeadsWithRatingCold = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeadsWithRatingCold) {
            System.assertEquals(RATING_COLD, lead.Rating, 'Lead rating should be: ' + RATING_COLD);
        }
        for(Lead lead : leads) {
            lead.AnnualRevenue = 50000;
        }
        update leads;
        List<Lead> updatedLeadsWithRatingWarm = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeadsWithRatingWarm) {
            System.assertEquals(RATING_WARM, lead.Rating, 'Lead rating should be: ' + RATING_WARM);
        }
        for(Lead lead : leads) {
            lead.AnnualRevenue = 80000;
        }
        update leads;
        List<Lead> updatedLeadsWithRatingHot = [SELECT Rating FROM Lead];
        for(Lead lead : updatedLeadsWithRatingHot) {
            System.assertEquals(RATING_HOT, lead.Rating, 'Lead rating should be: ' + RATING_HOT);
        }
        Test.stopTest();
    }
As you can see above, this method is used to cover all the scenarios one by one by updating the leads and will also verify the results of the flow updates once all the lead records are updated.

The output when the above test method is executed is given below:

So, our test passed. Now let's see the code coverage for this method using the updated query shared below:
sfdx force:data:soql:query --query "SELECT Id, ApexTestClassId, TestMethodName, FlowVersionId, NumElementsCovered, NumElementsNotCovered FROM FlowTestCoverage WHERE FlowVersion.Definition.DeveloperName = 'Update_Lead_Rating_based_on_Annual_Revenue' AND TestMethodName = 'updateLeadBasedOnAnnualRevenueAllElementsTest'" --usetoolingapi
The output when this query is executed is given below:

As you can see above, this time the total number of elements covered are 4 and the total number of elements not covered are 0. Therefore, we can say that all the 4 elements of the flow were covered this time which are encircled in RED below:
So, this time, YOU HAVE COVERED THE WHOLE FLOW. CONGRATULATIONS!!!

This is how you can create a test class of the flow and verify the test coverage as well. I am sharing the combined results of all the methods below:
You can get these results by executing the SOQL query as shown below:
sfdx force:data:soql:query --query "SELECT Id, ApexTestClassId, TestMethodName, FlowVersionId, NumElementsCovered, NumElementsNotCovered FROM FlowTestCoverage WHERE FlowVersion.Definition.DeveloperName = 'Update_Lead_Rating_based_on_Annual_Revenue'" --usetoolingapi
We just removed the TestMethodName AND condition here because we want to query the results of all test methods.

Note: If you want to make sure that you deploy all the flows to your production environment with good test coverage, you can go to Setup --> Process Automation Settings and select the Deploy processes and flows as active option. Please note that this option will not be available in non-production orgs. If you turn this on, salesforce will make sure that your apex tests have at least 75% coverage of the total number of active processes and active autolaunched flows in your org. If the required percentage isn't met, the deployment will be rolled back. You can learn more about that here.

Now you can make sure that your flows are also covered while performing the deployments.

So, that's all for this tutorial everyone, I hope you liked it and understood how you can create test class for a flow. Let me know your feedback in the comments down below. 

Happy Trailblazing!!

Monday, 11 April 2022

Lightning Datatable in LWC | How to create a lightning-datatable in LWC?

Hello Trailblazers,


In this post we're going to learn how we can create a lightning datatable in lwc. In order to create a lightning datatable we use the lightning-datatable tag where we need to specify 3 attributes:


key-field: To specify a unique identifier for each row of the table. It'll be a string specifying the key column in the data that we need to use to identify each row.

data: Information to be displayed in the datatable. The value passed to this attribute should be a javascript array.

columns: Information about columns of the datatable. This include column name and other metadata associated with it like: type, fieldName, icon details etc. The value passed to this attribute should be a javascript array as well.


Now let's see how we can create a very simple datatable to display employees information. Each employee will have some attributes including: Employee Id, First Name, Last Name, Phone Number and Email Address. We're going to consider these 5 attributes for now and will create a datatable showcasing the data with these columns. Let's create a lightning web component now!!


We're going to name our component: employeeDatatable. First things first, we're going to display this component at the homepage, so let's setup that:

employeeDatatable.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>54.0</apiVersion>
    <isExposed>true</isExposed>
    <masterLabel>Employee Datatable</masterLabel>
    <description>This datatable is used to display employee records.</description>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

As you can see in the above code snippet, I have specified a target as lighting__Homepage as we want to include this datatable in homepage and the isExposed tag has a value as true because we want to search that component in the lightning app builder. The masterLabel and description attributes are optional but nice to have. You can search the component by it's masterLabel if it's provided, otherwise, you'll search it by it's actual name. Let's move onto html now.

employeeDatatable.html

<template>
    <div style="height: 250px;">
        <lightning-datatable
            key-field="employeeId"
            data={employeeData}
            columns={employeeColumns}>
        </lightning-datatable>
    </div>
</template>
Above you can see the full html code for our datatable. You don't need anything else to display the data. No tr, td, thead or any more tags. As you can see above, key-field attribute has a value as employeeId because our employeeId will be unique and we can use it to uniquely identify each row. Next our data attribute has a value as employeeData and the columns attribute has the value as employeeColumns. Remember, employeeData will be a javascript array and employeeColumns will also be a javascript array.

Note: You may have seen code specifying data={data} and columns={columns}. Don't get confused by this. In data={data} the data before the "=" sign is the attribute name and the {data} after the "=" sign is the attribute value which is nothing but an array defined in our js file which will store the data. Similarly, in columns={columns}, the columns before the "=" sign is the attribute name and the {columns} after the "=" sign is the attribute value which is again an array defined in our js file which will store the columns information. This is the reason I am using different names for my variables above.

Now, let's see the js part!

employeeDatatable.js

import { LightningElement } from 'lwc';

export default class EmployeeDatatable extends LightningElement {

    employeeColumns = [
        { label: 'Employee Id', fieldName: 'employeeId' },
        { label: 'First Name', fieldName: 'firstName' },
        { label: 'Last Name', fieldName: 'lastName' },
        { label: 'Phone Number', fieldName: 'employeePhone', type: 'phone' },
        { label: 'Email Address', fieldName: 'employeEemail', type: 'email' }
    ];

}

As you can see above, I've defined a javascript array named employeeColumns which we have referred in our html. This is an array of objects where each object is going to have a label and a fieldName. The value of the label will be displayed as the column heading and the value of the fieldName is used to identify what information should be displayed under this column, we'll see this in detail as we define employeeData. For now, let's focus on the columns.

By default, each column will have a datatype as text. You can also specify a type attribute to specify the datatype of a particular column. Lightning Datatable will automatically apply the formatting according to the type of the column defined. Isn't it great?

Note: The fieldName can be anything and doesn't depend on the label or type of the data.

In our employeeColumns array above, we've kept the first 3 columns as text and the next two columns have a datatype of phone and email respectively. You can now go to your salesforce homepage. Click on Setup -> Edit Page and you can search for the Employee Datatable component in the lightning appbuilder as shown below: 


This is coming because we've exposed our lwc component: <isExposed>true</isExposed> tag in the .js-meta.xml file. You just need to drag and drop this component into the homepage and save it (activate as org default if necessary).

Once you've embedded this component in the homepage. It'll look as shown below:


As you can see above, we've defined the columns of our datatable here. The labels are the column names and we don't have any data because we haven't defined the employeeData variable yet in our js file. We have defined a height for this datatable as we've specified <div style="height: 250px;"> in our html code. div is the parent of our lightning-datatable here and is used to restrict the datatable expansion to a specified height.

Now, let's add the data to our datatable as well. For this, we'll define another js array named employeeData as shown below:
    employeeData = [
        {
            employeeId: '1',
            firstName: 'Richard',
            lastName: 'Hendricks',
            employeePhone: '(158) 389-2794',
            employeeEmail: 'richard@piedpiper.com'
        },
        {
            employeeId: '2',
            firstName: 'Jared',
            lastName: 'Dunn',
            employeePhone: '(518) 390-2749',
            employeeEmail: 'jared@piedpiper.com'
        },
        {
            employeeId: '3',
            firstName: 'Erlich',
            lastName: 'Bachman',
            employeePhone: '(815) 391-2974',
            employeeEmail: 'erlich.bachman@piedpiper.com'
        }
    ];
As you can see above, I have added 3 employee records to my employeeData array. Notice that In each record, the key is exactly the "fieldName" that we mentioned in employeeColumns array before, namely: employeeId, firstName, lastName, employeePhone and employeeEmail. This is most important to understand how the data is mapped to columns. Sharing the below image which should clarify the mappings in a better way:


The output of the above code is given below:


As you can see we have 3 entries in our datatable based on the 3 objects (records) in our employeeData array. Notice that the phone number and email address values are formatted automatically as we have mentioned the correct type for those. I am sharing the full js code below for your convinience:
import { LightningElement } from 'lwc';

export default class EmployeeDatatable extends LightningElement {

    employeeColumns = [
        { label: 'Employee Id', fieldName: 'employeeId' },
        { label: 'First Name', fieldName: 'firstName' },
        { label: 'Last Name', fieldName: 'lastName' },
        { label: 'Phone Number', fieldName: 'employeePhone', type: 'phone' },
        { label: 'Email Address', fieldName: 'employeeEmail', type: 'email' }
    ];

    employeeData = [
        {
            employeeId: '1',
            firstName: 'Richard',
            lastName: 'Hendricks',
            employeePhone: '(158) 389-2794',
            employeeEmail: 'richard@piedpiper.com'
        },
        {
            employeeId: '2',
            firstName: 'Jared',
            lastName: 'Dunn',
            employeePhone: '(518) 390-2749',
            employeeEmail: 'jared@piedpiper.com'
        },
        {
            employeeId: '3',
            firstName: 'Erlich',
            lastName: 'Bachman',
            employeePhone: '(815) 391-2974',
            employeeEmail: 'erlich.bachman@piedpiper.com'
        }
    ];
}
Once, we add a lot of data to our datatable by filling up employeeData array, it'll look as shown below:


In this tutorial, we learned about the basics of lightning datatable and how we can implement our own lightning datatable in lwc. I hope you liked the post, let me know your feedback in the comments down below.

Happy Trailblazing!!

Thursday, 20 January 2022

The importance of Data Structures in Salesforce Development | Live Session at Cactusforce 2022

Hello Trailblazers,


This post consist of the presentation, problem statement and the solution code that was used in the live session on "The importance of Data Structures in Salesforce Development" at Cactusforce 2022. The full session abstract can be viewed here.


In this session, we solved a real life project requirement using an advanced data structure and we also had a look at the performance impact we had by using it and learned why it is better than a brute force solution. So, the next time when you face such a requirement, you'll think at the first place - Which data structure should I use to solve it? and then you'll design and implement the most efficient solution.

Session Video

Slides

Problem Statement

We have a custom field on account named as: UltimateParentAccount__c, this field is a lookup to account and will specify the ultimate parent of any account in the hierarchy. For example, in the below accounts hierarchy:
In every node: 
  • The letter in black is the current account's name. 
  • The letter in red represents the parent account (value in ParentId) of the current account
  • The letter in blue represents the ultimate parent account for the current account i.e. the value in UltimateParentAccount__c field.

We have to implement a solution for the event when the current account is updated, to make sure that the UltimateParentAccount__c field for the current account, as well as for the accounts in the hierarchy below have the correct value. We need to take into account, below scenarios for this:
  1. Current account parent is null and is updated to a new value
  2. Current account parent is populated and is updated to null
  3. Current account parent is populated and is updated to a new value

In all these cases, the UltimateParentAccount__c field should be update to a new value on the current account as well as, on the accounts below the current account.

Solution code snippet

Please find all the solution code snippets related to this problem statement below. We've implemented the solution to this problem using N-Ary Trees:


N-Ary Tree

/*
*	Author:- Rahul Malhotra
*	Description:- NAry Tree implementation in apex
*	Created Date:- 14-01-2022
*	Last Modified:- 19-01-2022
*       Code Origin:- SFDC Stop (https://www.sfdcstop.com)
*/
public class NAryTree {

    // * Node class
    public class Node {

        // * Data members
        sObject data;
        Map<Id, Node> childNodesMap;

        // * Member functions
        public Node(sObject data) {
            this.data = data;
        }

        public sObject getData() {
            return data;
        }

        public Map<Id, Node> getChildNodesMap() {
            return childNodesMap;
        }
    }

    // * Data members
    Node root;

    /*
    *   Description: This method is used to return root of the tree
    */
    public Node getRoot() {
        return root;
    }

    /*
    *   Description: This method is used to insert a new node in a tree
    */
    public Boolean insertData(sObject currentRecord, sObject parentRecord, Comparator comparator) {

        /*
        *   If root node is NULL, creating a new root node
        *   and adding assigning it as the root node
        */
        if(root==NULL) {
            root = new Node(currentRecord);
            return true;
        }

        // * Otherwise, adding the current record to the subtree under the correct parent node
        return insertDataRecursive(root, currentRecord, parentRecord, comparator);
    }

    /*
    *   Description: This method is used to insert the new node in the tree
    *   by recursively traversing the tree to find the correct position
    */
    private Boolean insertDataRecursive(
        Node current,
        sObject currentRecord,
        sObject parentRecord,
        Comparator comparator
    ) {

        /*
        *   If current node's data is equal to parentRecord,
        *   adding new node to the current node's child
        */
        if(comparator.compare(current.data, parentRecord)) {
            if(current.childNodesMap == null) {
                current.childNodesMap = new Map<Id, Node>();
            }
            Node newNode = new Node(currentRecord);
            current.childNodesMap.put((Id) currentRecord.get('Id'), newNode);
            return true;
        }

        // * If current node's data is not equal to parentRecord, check in the current node's child subtrees
        if(current.childNodesMap!=null) {
            List<Node> childNodes = current.childNodesMap.values();
            for(Node childNode : childNodes) {
                if(insertDataRecursive(childNode, currentRecord, parentRecord, comparator)) {
                    return true;
                }
            }
        }

        // * Otherwise, return false as the node cannot be inserted
        return false;
    }

    /*
    *   Description: This method is used to delete a tree based on the root node
    *   provided in the parameter
    */
    public void deleteTree(Node root) {

        // * If root node is not NULL, proceed ahead
        if(root!=NULL) {

            // * Get the child nodes for the current node
            Map<Id, Node> childNodesMap = root.getChildNodesMap();
            // * If there are child nodes present
            if(childNodesMap!=NULL) {
                /*
                *   Recursively call the deleteTree() method to delete current child
                *   node as well as subtrees under these child nodes
                */
                for(Node currentNode : childNodesMap.values()) {
                    deleteTree(currentNode);
                }
            }

            // * Remove the root node from the tree
            root = NULL;
        }
    }

    /*
    *   Description: This method is used to perform a level order traversal
    *   of the whole tree and display the nodes information
    */
    public void levelOrderPrint() {

        // * Checking if root is not NULL
        if(root!=NULL) {
            String result = '\n';

            // * Initializing a queue
            List<Node> nodesQueue = new List<Node>();
            // * Adding root node to the queue
            nodesQueue.add(root);

            // * Looping while queue is not empty
            while(!nodesQueue.isEmpty()) {

                // * Getting nodes at current level
                Integer nodesAtCurrentLevel = nodesQueue.size();
                // * Looping while nodes at current level is more than 0
                while(nodesAtCurrentLevel>0) {

                    // * Getting the current node and removing it from the queue
                    Node current = nodesQueue.get(0);
                    nodesQueue.remove(0);
                    // * Adding current node data to the result
                    result += current.data.get('Id') + ' ' + current.data.get('Name') + ' -> ' + current.data.get('ParentId') + ' ';
                    // * Adding the child nodes for the current node to the queue
                    if(current.childNodesMap!=NULL) {
                        List<Node> childNodes = current.childNodesMap.values();
                        for(Node childNode : childNodes) {
                            nodesQueue.add(childNode);
                        }
                    }
                    // * Decrementing nodes at current level by 1
                    nodesAtCurrentLevel--;
                }

                // * Adding a new line to the result for next level
                result += '\n';
            }
            // * Displaying the result
            System.debug(result);
        }
        // * If root is NULL, display error message
        else {
            System.debug('Tree not found');
        }
    }
}

IdCompare Class

/*
*       Author:- Rahul Malhotra
*       Description:- This helper class is used to compare two records based on their ids.
*	Created Date:- 14-01-2022
*	Last Modified:- 19-01-2022
*       Code Origin:- SFDC Stop (https://www.sfdcstop.com)
*/
public class IdCompare extends Comparator {
    public override Boolean compare(sObject o1, sObject o2) {
        return (Id) o1.get('Id') == (Id) o2.get('Id');
    }
}

Comparator Class

/*
*	Author:- Rahul Malhotra
*	Description:- Comparator class to be used in NAry Tree
*	Created Date:- 14-01-2022
*	Last Modified:- 14-01-2022
*       Code Origin:- SFDC Stop (https://www.sfdcstop.com)
*/
public abstract class Comparator {
    public abstract Boolean compare(sObject o1, sObject o2);
}

Accounts Trigger

/*
*       Author:- Rahul Malhotra
*       Description: This is the trigger on account object
*	Created Date:- 14-01-2022
*	Last Modified:- 19-01-2022
*       Code Origin:- SFDC Stop (https://www.sfdcstop.com)
*/
trigger AccountsTrigger on Account (after update) {

    // * Marking accounts whose parent is updated
    List<Account> accountsWithUltimateParentUpdate = new List<Account>();
    if(trigger.isAfter && trigger.isUpdate) {
        for(Account account : trigger.new) {
            if(account.ParentId!=trigger.oldMap.get(account.Id).ParentId) {
                accountsWithUltimateParentUpdate.add(account);
            }
        }
    }

    /*
    *   If the parent is updated on some accounts,
    *   update the ultimate parent on these as well as
    *   on the child accounts in the hierarchy
    */
    if(!accountsWithUltimateParentUpdate.isEmpty()) {
        AccountTriggerHandler.updateUltimateParentOnChildAccounts(accountsWithUltimateParentUpdate);
    }
}

AccountTriggerHandler

/*
*       Author: Rahul Malhotra
*       Description: Trigger handler for Account trigger
*	Created Date:- 14-01-2022
*	Last Modified:- 19-01-2022
*       Code Origin:- SFDC Stop (https://www.sfdcstop.com)
*/
public with sharing class AccountTriggerHandler {

    /*
    *   Description: This method is used to update ultimate parent account on the current account
    *   as well as the child accounts present in the hierarchy
    */
    public static void updateUltimateParentOnChildAccounts(List<Account> accounts) {
        NAryTree tree = new NAryTree();
        IdCompare idCompare = new IdCompare();
        Account masterParentAccount = new Account(Name = 'MasterParentAccount');
        tree.insertData(masterParentAccount, NULL, NULL);
        for(Account account : accounts) {
            /*
            *   If an account's new parent is also updated, this account's ultimated parent account
            *   field should get updated automatically in the tree for the parent account.
            *   So, we don't need to keep it as a part of tree as it'll lead to duplicate nodes
            */
            if(
                account.ParentId==NULL ||
                tree.getRoot().getChildNodesMap()==NULL ||
                !tree.getRoot().getChildNodesMap().containsKey(account.ParentId)
            ) {
                tree.insertData(account, masterParentAccount, idCompare);
            }
        }
        Set<Id> accountIdSet = new Set<Id>(tree.getRoot().getChildNodesMap().keySet());
        accountIdSet.remove(null);
        while(!accountIdSet.isEmpty()) {
            List<Account> childAccounts = [SELECT Id, ParentId, Name FROM Account WHERE ParentId IN: accountIdSet];
            // * If there are no more accounts to process, break the loop
            if(childAccounts.isEmpty()) {
                accountIdSet.clear();
            }
            for(Account account : childAccounts) {
                // * If current child account is already present in the tree
                if(tree.getRoot().getChildNodesMap().containsKey(account.Id)) {
                    // * Getting current node
                    NAryTree.Node tempNode = tree.getRoot().getChildNodesMap().get(account.Id);
                    // * De-link current node from root node
                    tree.getRoot().getChildNodesMap().remove(account.Id);
                    // * Remove current node and it's subtree from the tree
                    tree.deleteTree(tempNode);
                }
                // * Inserting the child account in the tree
                tree.insertData(account, new Account(Id = account.ParentId), idCompare);
                // * Removing the parent account's id from the accountIdSet
                accountIdSet.remove(account.ParentId);
                // * Adding the child account's id to the set
                accountIdSet.add(account.Id);
            }
        }
        // * For debugging purposes - Printing the final tree
        tree.levelOrderPrint();
        // * Getting the root node from the tree and initial child nodes
        NAryTree.Node rootNode = tree.getRoot();
        Map<Id, NAryTree.Node> childNodesMap = rootNode.getChildNodesMap();
        // * Creating a list of accounts to update
        List<Account> accountsToUpdate = new List<Account>();
        // * Getting initial accounts with their parent information
        List<Account> accountsWithParentInformation = [SELECT Id, Parent.UltimateParentAccount__c FROM Account WHERE Id IN: childNodesMap.keySet()];
        // * Looping accounts
        for(Account account : accountsWithParentInformation) {
            // * Setting up ultimate parent account in the current account and it's descendants
            if(account.ParentId!=NULL) {
                account.UltimateParentAccount__c = account.Parent.UltimateParentAccount__c !=NULL ? account.Parent.UltimateParentAccount__c : account.ParentId;
                accountsToUpdate.addAll(updateUltimateParentByDFSOnCurrentSubtree(childNodesMap.get(account.Id), account.UltimateParentAccount__c));
            } else {
                account.UltimateParentAccount__c = NULL;
                accountsToUpdate.addAll(updateUltimateParentByDFSOnCurrentSubtree(childNodesMap.get(account.Id), account.Id));
            }
        }
        // * Adding current account to accounts to update list
        accountsToUpdate.addAll(accountsWithParentInformation);
        // * Updating accounts
        update accountsToUpdate;
    }

    /*
    *   Description: This method is used to update ultimate parent account id
    *   in all the nodes of a tree starting from the root node which is passed as a parameter
    */
    static List<Account> updateUltimateParentByDFSOnCurrentSubtree(NAryTree.Node root, Id ultimateParentAccountId) {
        // * Creating a queue to perform DFS and forming a list of accounts to update
        List<NAryTree.Node> accountNodesQueue = root.getChildNodesMap()?.values();
        List<Account> accountsToUpdate = new List<Account>();
        // * If account's queue is not NULL
        if(accountNodesQueue!=NULL) {
            // * Looping while queue is not empty
            while(!accountNodesQueue.isEmpty()) {
                // * Getting nodes at current level
                Integer nodesAtCurrentLevel = accountNodesQueue.size();
                // * Looping while nodes at current level is more than 0
                while(nodesAtCurrentLevel>0) {
                    // * Getting the current node and removing it from the queue
                    NAryTree.Node current = accountNodesQueue.get(0);
                    accountNodesQueue.remove(0);
                    // * Updating the ultimate parent account for the current account
                    // * and adding it to accounts to update list
                    Account currentAccount = (Account) current.getData();
                    currentAccount.UltimateParentAccount__c = ultimateParentAccountId;
                    accountsToUpdate.add(currentAccount);
                    // * Getting the child nodes for the current node and adding them to the queue
                    Map<Id, NAryTree.Node> childNodesMap = current.getChildNodesMap();
                    if(childNodesMap!=NULL) {
                        for(NAryTree.Node childNode : childNodesMap.values()) {
                            accountNodesQueue.add(childNode);
                        }
                    }
                    // * Decrementing nodes at current level by 1
                    nodesAtCurrentLevel--;
                }
            }
        }
        // * Returning the accounts to update list
        return accountsToUpdate;
    }
}


Happy Trailblazing..!!

Sunday, 24 October 2021

System.DmlException: Insert/Upsert failed. First exception on row 1; first error: INVALID_FIELD, Cannot specify both an external ID reference Parent and a salesforce id, ParentId: []

Hello Trailblazers,


In this post we're going to see the solution of the error "System.DmlException: Insert failed. First exception on row 1; first error: INVALID_FIELD, Cannot specify both an external ID reference Parent and a salesforce id, ParentId: []" OR "System.DmlException: Upsert failed. First exception on row 1; first error: INVALID_FIELD, Cannot specify both an external ID reference Parent and a salesforce id, ParentId: []"


The most common scenario for this error is when we're trying to insert both the parent and child records at the same time and we're providing both - the Salesforce ID of the parent object and the parent object record reference with External ID, mentioned in the child record. Ideally we should only provide one out of Salesforce ID of parent object or parent object reference with External ID but not both. Let's have a look at an example.


Inserting Account and Contact record together

You must be aware of the fact that we can use external ids to create a parent-child relationship automatically while inserting new records in salesforce. For ex: In case of account and contact, you don't need to insert account and then get it's record id to insert contact, in order to link it with earlier inserted account record. You can leverage external id on account to insert both records in a single transaction. Let's see how:

As you can see in the above snippet, I've created a list of sObjects in which I have added one account record and one contact record. I have created an external id field on account with API name as: Reference_ID__c. In case of contact record, I have referred to the parent account record by populating the relationship field "Account" (will be Field_Name__r in case of custom object) with an instance of account object consisting of external id only. Using this we're telling salesforce that this contact is linked to the account with reference id as 123. The above code snippet worked perfectly fine upon execution and as a result, account and contact records are created in salesforce. Give it a try!!

Let's do a very small change now:

As you can see above, now we've also specified AccountId in contact record and that too as null which probably should have no impact. Let's execute the code and see what we get:


As you can see we're getting the same error System.DmlException: Upsert failed. First exception on row 1; first error: INVALID_FIELD, Cannot specify both an external ID reference Account and a salesforce id, AccountId: []

As per the post heading error message: "System.DmlException: Insert/Upsert failed. First exception on row 1; first error: INVALID_FIELD, Cannot specify both an external ID reference Parent and a salesforce id, ParentId: []" 

Here our Parent is Account and ParentId is AccountId these can be different depending upon your field name and what objects you're dealing with. This simply means:

"Whenever we're performing a DML on child records, we cannot specify both the parent reference record, as well as the parent record id together, even if the parent record id is null"

So, make sure you're only specifying one of these. That's all for this tutorial everyone, I hope you liked it. Let me know your feedback in the comments down below.

Happy Trailblazing!!

Saturday, 2 October 2021

First error: Exceeded max size limit of 6000000 OR Exceeded max size limit of 12000000 - Solution

Hello Trailblazers, in this post we're going to talk about the resolution of a very common error as given below:

First error: Exceeded max size limit of 12000000 OR First error: Exceeded max size limit of 6000000 

This error appears when you've exceeded the heap size limit in a single transaction. The heap size limit for a synchronous transaction is 6MB and for an asynchronous transaction is 12MB. Asynchronous transactions basically consists of batch classes, future methods and queueable apex.

How to resolve this error?

You need to reduce the heap size in order to resolve this error. Heap size is basically the memory that your code is taking up while executing. It is the sum of all the memory your variables, maps, sets, lists or any other data structure is consuming. In order to resolve the error you need to reduce the heap size. Below are some tips to help you out:

1. Use Limits.getHeapSize() to understand how much heap size (approx) is used in the current transaction upto a particular point.

2. Use Limits.getLimitHeapSize() to get the total amout of heap size available in the current transaction.

3. Use SOQL For Loops, if you use SOQL for loops the result of the query is given in the batch of 200 records while using a list variable or a single record is returned while using a single instance variable. In both the cases, lesser memory is consumed as we're only storing a maximum 200 records at a time (compared to 50,000 records queried in a single SOQL statement).
List<Account> accounts = [SELECT Id FROM Account];
// Stores 50,000 records at max - greater heap size
for(List<Account> accounts : [SELECT Id FROM Account]) {
}
// Stores 200 records at max - lesser heap size

4. Manage your code effectively using helper functions - Avoid class level variables.
void callMe()
{
    List<Account> accounts = [SELECT Id FROM Account];
    System.debug(Limits.getHeapSize()); // Coming as: 321085
}
callme();
System.debug(Limits.getHeapSize()); // Coming as: 1044

As you can see above, the heap size is reduced after the function call is complete because the call stack is now empty and the local variables are destroyed.

5. Clear the lists/map/set/variables which are not in use or make them null



These are some of the tips that can be used to prevent the heap size limit error. Make sure to write effective code when working with large data. If you have any other suggestion, do comment it down below and I'll include that in the blog post. 

That's all for this tutorial everyone I hope you liked it, the full code used in this post can be found here. Do let me know your feedback/thoughts in the comments down below.

Sharing a Salesforce Help Article related to this error: https://help.salesforce.com/s/articleView?id=000321537&type=1

Happy Trailblazing!!