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
Showing posts with label API. Show all posts
Showing posts with label API. Show all posts

Tuesday, 18 October 2022

Getting started with Composite API in Salesforce

Hello Trailblazers,


In this post we're going to learn about Composite Resources in salesforce. Composite resources can improve your application's performance by minimizing round trips between client and salesforce (server). Let's try to learn by an example.


Get account details with related contacts and opportunities from Salesforce

Let's consider this small requirement: You need to sync account records along with the related contacts and opportunities from Salesforce to an external system. Let's say you're syncing one account at a time using the account's id. You may think of a solution which consist of 3 steps:

  1. Get the account record using the account id by making a callout to standard Rest API
  2. Get the contact records related to the account record by making another callout to the standard Rest API
  3. Get the opportunity records related to the account record by making another callout to the standard Rest API

Therefore, you got your account record along with the related contacts and opportunities in 3 Rest API calls to salesforce. There can be other alternative approaches as well depending upon the use case and requirement. The goal here is to know about what's provided to us out of the box by salesforce and how can we leverage it.

Composite API in Action - Get Records

What if I tell you: You can fetch account, related contacts and related opportunities as well, all in a single API callout without writing any custom code? Yes that's possible using the Composite API. Let's see how:

I am not covering the client Authentication part (how to get access token from salesforce) in this blog as we've a full blog dedicated to it: How to connect to Salesforce with Postman?

Composite API can execute a series of REST API requests (like: the 3 requests for our use case, which we're planning to execute one by one) in a single POST request. It can also retrieve a list of other composite resources with a GET request.

You can send multiple REST API requests together using composite api and the thing to note here is: The output of one request can be used as the input to a subsequent request. All requests in a composite call are called subrequests and all subrequests are executed in the context of the same user who's calling the API. The good thing here is: All the requests mentioned in the Composite API are sent together and they count as a single call towards your API limits.

Let's solve our use case now. We need to get Account record along with it's related Contacts and Opportunities and we only have the account id that we can pass to the request.

This is my account record in salesforce along with the related records:


As you can see above, the account's name is Sample Account and under that we have 2 Contact records: Sample Contact 1 and Sample Contact 2. Similarly, we have 2 Opportunity records as well: Sample Opportunity 1 and Sample Opportunity 2. We're going to fetch this whole information using composite API. Let's have a look at the request below:
{
    "compositeRequest": [
        {
            "method": "GET",
            "url": "/services/data/v55.0/sobjects/Account/0016D00000fHjSLQA0",
            "referenceId": "refAccount"
        },
        {
            "method": "GET",
            "url": "/services/data/v55.0/sobjects/Account/@{refAccount.Id}/Contacts",
            "referenceId": "refContacts"
        },
        {
            "method": "GET",
            "url": "/services/data/v55.0/sobjects/Account/@{refAccount.Id}/Opportunities",
            "referenceId": "refOpportunities"
        }
    ]
}
As you can see above, under the compositeRequest array, I've specified various requests that I want to make to salesforce. Below are the 3 requests I've specified:

  1. Get Account record using record id: Here, I've specified record id of the account record in the request.

  2. Get Contacts related to the current account: Here, you can also specify the account id itself in the request body but I've referenced the response of previous request and used it as an input here. Our previous request will return the account record in the request body which I'm referring as refAccount. That record will have an Id key in the response body with the value as record id of the account. So, I've specified @{refAccount.Id} in the URL of the second request where we're fetching the Contacts related to the account. We've specified refContacts as the reference id for this API response.

  3. Get Opportunities related to the current account: This is exact similar to the 2nd request, the only difference is - instead of getting contacts, here we're getting the Opportunities related to the current account record. The reference id for this request's response is refOpportunities.

This is how my request looks like in postman:

You can make a POST request and the URL for your API will be in this format: https://<my-domain-name>.my.salesforce.com/services/data/vXX.X/composite

Remember to add the Authorization header to the request with Bearer<space><access token> as the value, that we got while doing authorization. Let's have a look at the Authorization header below:

As you click on Send you'll get the response similar to what's shown below:

I am also sharing the full response below for your reference:
{
    "compositeResponse": [
        {
            "body": {
                "attributes": {
                    "type": "Account",
                    "url": "/services/data/v55.0/sobjects/Account/0016D00000fHjSLQA0"
                },
                "Id": "0016D00000fHjSLQA0",
                "IsDeleted": false,
                "MasterRecordId": null,
                "Name": "Sample Account",
                "Type": null,
                "ParentId": null,
                "BillingStreet": null,
                "BillingCity": null,
                "BillingState": null,
                "BillingPostalCode": null,
                "BillingCountry": null,
                "BillingLatitude": null,
                "BillingLongitude": null,
                "BillingGeocodeAccuracy": null,
                "BillingAddress": null,
                "ShippingStreet": null,
                "ShippingCity": null,
                "ShippingState": null,
                "ShippingPostalCode": null,
                "ShippingCountry": null,
                "ShippingLatitude": null,
                "ShippingLongitude": null,
                "ShippingGeocodeAccuracy": null,
                "ShippingAddress": null,
                "Phone": null,
                "Fax": null,
                "AccountNumber": null,
                "Website": null,
                "PhotoUrl": "/services/images/photo/0016D00000fHjSLQA0",
                "Sic": null,
                "Industry": null,
                "AnnualRevenue": null,
                "NumberOfEmployees": null,
                "Ownership": null,
                "TickerSymbol": null,
                "Description": null,
                "Rating": null,
                "Site": null,
                "OwnerId": "0056D000005v9kXQAQ",
                "CreatedDate": "2022-10-15T08:29:16.000+0000",
                "CreatedById": "0056D000005v9kXQAQ",
                "LastModifiedDate": "2022-10-15T08:29:16.000+0000",
                "LastModifiedById": "0056D000005v9kXQAQ",
                "SystemModstamp": "2022-10-15T08:29:16.000+0000",
                "LastActivityDate": null,
                "LastViewedDate": "2022-10-16T07:55:17.000+0000",
                "LastReferencedDate": "2022-10-16T07:55:17.000+0000",
                "Jigsaw": null,
                "JigsawCompanyId": null,
                "CleanStatus": "Pending",
                "AccountSource": null,
                "DunsNumber": null,
                "Tradestyle": null,
                "NaicsCode": null,
                "NaicsDesc": null,
                "YearStarted": null,
                "SicDesc": null,
                "DandbCompanyId": null,
                "OperatingHoursId": null
            },
            "httpHeaders": {
                "ETag": "\"gTI7lF2oYMlDxM+gTW1a62nzqxfxxihRqAygUNh9DPs=\"",
                "Last-Modified": "Sat, 15 Oct 2022 08:29:16 GMT"
            },
            "httpStatusCode": 200,
            "referenceId": "refAccount"
        },
        {
            "body": {
                "totalSize": 2,
                "done": true,
                "records": [
                    {
                        "attributes": {
                            "type": "Contact",
                            "url": "/services/data/v55.0/sobjects/Contact/0036D00000UAXTNQA5"
                        },
                        "Id": "0036D00000UAXTNQA5",
                        "IsDeleted": false,
                        "MasterRecordId": null,
                        "AccountId": "0016D00000fHjSLQA0",
                        "LastName": "Contact 1",
                        "FirstName": "Sample",
                        "Salutation": "Mr.",
                        "Name": "Sample Contact 1",
                        "OtherStreet": null,
                        "OtherCity": null,
                        "OtherState": null,
                        "OtherPostalCode": null,
                        "OtherCountry": null,
                        "OtherLatitude": null,
                        "OtherLongitude": null,
                        "OtherGeocodeAccuracy": null,
                        "OtherAddress": null,
                        "MailingStreet": null,
                        "MailingCity": null,
                        "MailingState": null,
                        "MailingPostalCode": null,
                        "MailingCountry": null,
                        "MailingLatitude": null,
                        "MailingLongitude": null,
                        "MailingGeocodeAccuracy": null,
                        "MailingAddress": null,
                        "Phone": null,
                        "Fax": null,
                        "MobilePhone": null,
                        "HomePhone": null,
                        "OtherPhone": null,
                        "AssistantPhone": null,
                        "ReportsToId": null,
                        "Email": null,
                        "Title": null,
                        "Department": null,
                        "AssistantName": null,
                        "LeadSource": null,
                        "Birthdate": null,
                        "Description": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-15T08:29:16.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:46:44.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:46:44.000+0000",
                        "LastActivityDate": null,
                        "LastCURequestDate": null,
                        "LastCUUpdateDate": null,
                        "LastViewedDate": "2022-10-16T07:46:44.000+0000",
                        "LastReferencedDate": "2022-10-16T07:46:44.000+0000",
                        "EmailBouncedReason": null,
                        "EmailBouncedDate": null,
                        "IsEmailBounced": false,
                        "PhotoUrl": "/services/images/photo/0036D00000UAXTNQA5",
                        "Jigsaw": null,
                        "JigsawContactId": null,
                        "CleanStatus": "Pending",
                        "IndividualId": null
                    },
                    {
                        "attributes": {
                            "type": "Contact",
                            "url": "/services/data/v55.0/sobjects/Contact/0036D00000ULNcUQAX"
                        },
                        "Id": "0036D00000ULNcUQAX",
                        "IsDeleted": false,
                        "MasterRecordId": null,
                        "AccountId": "0016D00000fHjSLQA0",
                        "LastName": "Contact 2",
                        "FirstName": "Sample",
                        "Salutation": "Ms.",
                        "Name": "Sample Contact 2",
                        "OtherStreet": null,
                        "OtherCity": null,
                        "OtherState": null,
                        "OtherPostalCode": null,
                        "OtherCountry": null,
                        "OtherLatitude": null,
                        "OtherLongitude": null,
                        "OtherGeocodeAccuracy": null,
                        "OtherAddress": null,
                        "MailingStreet": null,
                        "MailingCity": null,
                        "MailingState": null,
                        "MailingPostalCode": null,
                        "MailingCountry": null,
                        "MailingLatitude": null,
                        "MailingLongitude": null,
                        "MailingGeocodeAccuracy": null,
                        "MailingAddress": null,
                        "Phone": null,
                        "Fax": null,
                        "MobilePhone": null,
                        "HomePhone": null,
                        "OtherPhone": null,
                        "AssistantPhone": null,
                        "ReportsToId": null,
                        "Email": null,
                        "Title": null,
                        "Department": null,
                        "AssistantName": null,
                        "LeadSource": null,
                        "Birthdate": null,
                        "Description": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-16T07:46:25.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:46:25.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:46:25.000+0000",
                        "LastActivityDate": null,
                        "LastCURequestDate": null,
                        "LastCUUpdateDate": null,
                        "LastViewedDate": "2022-10-16T07:46:26.000+0000",
                        "LastReferencedDate": "2022-10-16T07:46:26.000+0000",
                        "EmailBouncedReason": null,
                        "EmailBouncedDate": null,
                        "IsEmailBounced": false,
                        "PhotoUrl": "/services/images/photo/0036D00000ULNcUQAX",
                        "Jigsaw": null,
                        "JigsawContactId": null,
                        "CleanStatus": "Pending",
                        "IndividualId": null
                    }
                ]
            },
            "httpHeaders": {},
            "httpStatusCode": 200,
            "referenceId": "refContacts"
        },
        {
            "body": {
                "totalSize": 2,
                "done": true,
                "records": [
                    {
                        "attributes": {
                            "type": "Opportunity",
                            "url": "/services/data/v55.0/sobjects/Opportunity/0066D000005z3tpQAA"
                        },
                        "Id": "0066D000005z3tpQAA",
                        "IsDeleted": false,
                        "AccountId": "0016D00000fHjSLQA0",
                        "IsPrivate": false,
                        "Name": "Sample Opportunity 1",
                        "Description": null,
                        "StageName": "Prospecting",
                        "Amount": null,
                        "Probability": 10.0,
                        "ExpectedRevenue": null,
                        "TotalOpportunityQuantity": null,
                        "CloseDate": "2022-10-17",
                        "Type": null,
                        "NextStep": null,
                        "LeadSource": null,
                        "IsClosed": false,
                        "IsWon": false,
                        "ForecastCategory": "Pipeline",
                        "ForecastCategoryName": "Pipeline",
                        "CampaignId": null,
                        "HasOpportunityLineItem": false,
                        "Pricebook2Id": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-16T07:29:30.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:47:09.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:47:09.000+0000",
                        "LastActivityDate": null,
                        "PushCount": 0,
                        "LastStageChangeDate": null,
                        "FiscalQuarter": 4,
                        "FiscalYear": 2022,
                        "Fiscal": "2022 4",
                        "ContactId": null,
                        "LastViewedDate": "2022-10-16T07:47:09.000+0000",
                        "LastReferencedDate": "2022-10-16T07:47:09.000+0000",
                        "HasOpenActivity": false,
                        "HasOverdueTask": false,
                        "LastAmountChangedHistoryId": null,
                        "LastCloseDateChangedHistoryId": null
                    },
                    {
                        "attributes": {
                            "type": "Opportunity",
                            "url": "/services/data/v55.0/sobjects/Opportunity/0066D000005z3wPQAQ"
                        },
                        "Id": "0066D000005z3wPQAQ",
                        "IsDeleted": false,
                        "AccountId": "0016D00000fHjSLQA0",
                        "IsPrivate": false,
                        "Name": "Sample Opportunity 2",
                        "Description": null,
                        "StageName": "Prospecting",
                        "Amount": null,
                        "Probability": 10.0,
                        "ExpectedRevenue": null,
                        "TotalOpportunityQuantity": null,
                        "CloseDate": "2022-10-17",
                        "Type": null,
                        "NextStep": null,
                        "LeadSource": null,
                        "IsClosed": false,
                        "IsWon": false,
                        "ForecastCategory": "Pipeline",
                        "ForecastCategoryName": "Pipeline",
                        "CampaignId": null,
                        "HasOpportunityLineItem": false,
                        "Pricebook2Id": null,
                        "OwnerId": "0056D000005v9kXQAQ",
                        "CreatedDate": "2022-10-16T07:47:01.000+0000",
                        "CreatedById": "0056D000005v9kXQAQ",
                        "LastModifiedDate": "2022-10-16T07:47:01.000+0000",
                        "LastModifiedById": "0056D000005v9kXQAQ",
                        "SystemModstamp": "2022-10-16T07:47:01.000+0000",
                        "LastActivityDate": null,
                        "PushCount": 0,
                        "LastStageChangeDate": null,
                        "FiscalQuarter": 4,
                        "FiscalYear": 2022,
                        "Fiscal": "2022 4",
                        "ContactId": null,
                        "LastViewedDate": "2022-10-16T07:47:01.000+0000",
                        "LastReferencedDate": "2022-10-16T07:47:01.000+0000",
                        "HasOpenActivity": false,
                        "HasOverdueTask": false,
                        "LastAmountChangedHistoryId": null,
                        "LastCloseDateChangedHistoryId": null
                    }
                ]
            },
            "httpHeaders": {},
            "httpStatusCode": 200,
            "referenceId": "refOpportunities"
        }
    ]
}
Notice that in the compositeResponse array, we have 3 objects:

The first object is the response from callout to the URL mentioned in the first object of compositeRequest array, in our request body, which is returning the account record. This API response has a body attribute which corresponds to our account record referred by refAccount as shown below:
The second object is the response from URL mentioned in the second object of our compositeRequest array, in our request body, which is returning the contact records related to this account. As you can see below, there are 2 contact records linked to the current account:

We're referring to the body of this response using refContacts. Similarly, the third object is the response from the URL mentioned in the third object of our compositeRequest array, in our request body, which is returning the opportunities related to this account. You can see a similar response as above for opportunities below:
We're referring to the body of this response as refOpportunities.

So, this is how you can make a simple callout to Composite API to get your account, it's related contacts as well as it's related opportunities in one go!

Till now, we talked about how we can get the parent record and it's child records together using composite resources. You may ask: What if I have a lookup field and I want to get that related record details as well? - You can easily do that using composite API!

For example: Le'ts say I want to get the account owner's information as well i.e. the user record related to the account using composite API while I am fetching the account record. To do that, I can just add the below object to my compositeRequest array in the request body:
{
    "method": "GET",
    "url": "/services/data/v55.0/sobjects/User/@{refAccount.OwnerId}",
    "referenceId": "refUser"
}
As you can see above, I am referring to the OwnerId of my account record using @{refAccount.OwnerId} and I am getting the details from the User object using this OwnerId. The overall request is shown below:
I am referring to the body of user API output i.e. the user record as refUser. In the response also, we'll get the user details at the end as shown below:
So, all you need to understand here is: How to refer the previous response variables in the subsequent requests to fetch related records? Once you understood this, you can easily use composite API for your use case. You can easily reference the previous API response body using the reference id you have specified in the request.

Here we had 4 subrequests in a single composite API callout. We can have a maximum of 25 subrequests in a single call. Out of these 25, 5 requests can be related to query operations or sObject collections.

Creating Records using Composite API

Before marking this blog post as complete. I would like to show you one example of creating related records using composite API. This time we're going to create one account record, two opportunities and two contacts related to it using composite api. Let's have a look at the request body below:
{
    "compositeRequest": [
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Account",
            "referenceId": "refAccount",
            "body": {
                "Name": "My Sample Account"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact1",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 1",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact2",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 2",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity1",
            "body": {
                "Name": "My Sample Opportunity 1",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact1.id}",
                "StageName": "Prospecting",
                "CloseDate": "2022-10-20"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity2",
            "body": {
                "Name": "My Sample Opportunity 2",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact2.id}",
                "StageName": "Qualification",
                "CloseDate": "2022-10-20"
            }
        }
    ]
}
As you can see above, this time, the method for each request mentioned in the compositeRequest array is POST because we're creating records here. We also have a body in each request with the data we want to store in the record we're creating. First of all we created an account record with the name My Sample Account referred by refAccount, then we created two contact records: My Sample Contact 1 referred by refContact1 and My Sample Contact 2 referred by refContact2 under that account by mentioning the AccountId as @{refAccount.id}

Finally, we created two opportunity records as well named: My Sample Opportunity 1 referred by refOpportunity1 and My Sample Opportunity 2 referred by refOpportunity2. Notice that both the opportunity records are linked to the same account using: @{refAccount.id} as the AccountId but different contacts as the first one is referring to @{refContact1.id} as the ContactId, however the second one is using @{refContact2.id} as the ContactId. Value for StageName is also different for both the opportunity records.

Below is the response for this API callout:
{
    "compositeResponse": [
        {
            "body": {
                "id": "0016D00000fSq4HQAS",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Account/0016D00000fSq4HQAS"
            },
            "httpStatusCode": 201,
            "referenceId": "refAccount"
        },
        {
            "body": {
                "id": "0036D00000ULNmoQAH",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Contact/0036D00000ULNmoQAH"
            },
            "httpStatusCode": 201,
            "referenceId": "refContact1"
        },
        {
            "body": {
                "id": "0036D00000ULNmtQAH",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Contact/0036D00000ULNmtQAH"
            },
            "httpStatusCode": 201,
            "referenceId": "refContact2"
        },
        {
            "body": {
                "id": "0066D000005z4LPQAY",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Opportunity/0066D000005z4LPQAY"
            },
            "httpStatusCode": 201,
            "referenceId": "refOpportunity1"
        },
        {
            "body": {
                "id": "0066D000005z4LQQAY",
                "success": true,
                "errors": []
            },
            "httpHeaders": {
                "Location": "/services/data/v55.0/sobjects/Opportunity/0066D000005z4LQQAY"
            },
            "httpStatusCode": 201,
            "referenceId": "refOpportunity2"
        }
    ]
}
Let's verify our results in our salesforce org as well!

We have an account record named My Sample Account with two contacts and two opportunities linked to it as shown below:
Notice the contact and opportunity names, the stage and close date of the opportunity records, they're the exact same as we mentioned in the request. You can open the opportunities and verify that both My Sample Opportunity 1 and My Sample Opportunity 2 are linked with My Sample Contact 1 and My Sample Contact 2 respectively as shown below:


That's how we can Create Related Records using Composite API. Till now, we're making a POST request to the composite API. There are some other composite resources as well, that we can use and we can get a list of those by making a GET request to the composite API as shown below:
I think the information we learned today should be enough for you to get started with composite API. If you want to learn about other resources that are mentioned in the response above, let me know and I'll create new blog post(s) for the same.

Bonus Content: All or None

Thank you for reading and reaching to the end of the tutorial (almost). I hope you learned something new! Before we close this post, I want to talk about one most important flag in the composite request - the allOrNone flag. If we mark this flag as true, the whole composite request will rollback if any of the subsequent API request fail. So you can now make sure that you create all the records with correct relationships or none of them.

Considering our above example of Account, Contacts and Opportunities creation, let's mark the allOrNone flag as true in the request body as shown below:
Notice that I removed the StageName value from the second opportunity record and it's present in the first opportunity record (highlighted above). As StageName is a required field, the opportunity record creation will fail and it should rollback the whole request as we've specified the allOrNone parameter as true. Just to make it more clear, allOrNone parameter is not a part of the compositeRequest array, it's a separate key in the request body as shown below:
I am also sharing the whole request body below for your reference:
{
    "compositeRequest": [
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Account",
            "referenceId": "refAccount",
            "body": {
                "Name": "My Sample Account"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact1",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 1",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Contact",
            "referenceId": "refContact2",
            "body": {
                "FirstName": "My Sample",
                "LastName": "Contact 2",
                "AccountId": "@{refAccount.id}"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity1",
            "body": {
                "Name": "My Sample Opportunity 1",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact1.id}",
                "StageName": "Prospecting",
                "CloseDate": "2022-10-20"
            }
        },
        {
            "method": "POST",
            "url": "/services/data/v55.0/sobjects/Opportunity",
            "referenceId": "refOpportunity2",
            "body": {
                "Name": "My Sample Opportunity 2",
                "AccountId": "@{refAccount.id}",
                "ContactId": "@{refContact2.id}",
                "CloseDate": "2022-10-20"
            }
        }
    ],
    "allOrNone": true
}
The response of this request is also provided below:
{
    "compositeResponse": [
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refAccount"
        },
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refContact1"
        },
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refContact2"
        },
        {
            "body": [
                {
                    "errorCode": "PROCESSING_HALTED",
                    "message": "The transaction was rolled back since another operation in the same transaction failed."
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refOpportunity1"
        },
        {
            "body": [
                {
                    "message": "Required fields are missing: [StageName]",
                    "errorCode": "REQUIRED_FIELD_MISSING",
                    "fields": [
                        "StageName"
                    ]
                }
            ],
            "httpHeaders": {},
            "httpStatusCode": 400,
            "referenceId": "refOpportunity2"
        }
    ]
}
As you can see above, the account request, subsequent contact requests, as well as the request to create first opportunity record all were rolled back with errorCode as PROCESSING_HALTED because the 2nd opportunity record creation failed because of a required field that we removed: StageName. I am sharing the postman screenshot below as well for reference:
Notice that the status code of the response for main request is 200 whereas for subsequent requests it's 400. This is because the composite request was still successful even when the subsequent calls failed because there was no error encountered in the composite api callout (as a whole). The correct error code and messages are provided for each subsequent requests in the response body.

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

Happy Trailblazing!!

Monday, 16 August 2021

Call External API from Lightning Web Component | Fetch API in JavaScript

Hello Trailblazers,


In this post, we're going to learn how we can call an External System API from a Lightning Web Component. We're going to use Fetch API which provides an interface for fetching resources. You can consider it as an advanced version of XMLHttpRequest. This API is more powerful and easy to use. The Fetch Web API provides a global fetch() method which can be used in JavaScript to perform any kind of callout to an external system across the network and get the data.


The Promise returned from fetch() method won’t reject on HTTP error status even if the response is an HTTP 404 or 500. Instead, the Promise will resolve normally. The ok property of the response is set to true if the callout is successful and it's set to false if the response isn’t in the range 200–299 and it will only reject on network failure or if anything prevented the request from completing.

Tutorial Video



To learn about fetch(), we're going to create a lwc component to get the details of a user from GitHub as shown below:



Let's have a look at the below code snippets of this component along with the explanation:

githubInfo.html

<template>
    <lightning-card title="Show Github Stats">
        <div class="slds-var-m-around_large">
            <!-- * Input Username -->
            <lightning-layout vertical-align="end">
                <lightning-layout-item flexibility="grow">
                    <lightning-input type="search" value={username} onchange={updateUsername} label="Enter Username"></lightning-input>
                </lightning-layout-item>
                <lightning-layout-item class="slds-var-p-left_small">
                    <lightning-button label="Search" variant="brand" onclick={getGithubStats}></lightning-button>
                </lightning-layout-item>
            </lightning-layout>
            <br />
            <!-- * Display User Details -->
            <div if:true={userPopulated}>
                <img src={user.image} height="200" width="200" />
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_large">{user.name}</div>
                <br />
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_small"><b>Github Profile:</b><a href={githubURL} target="_blank"> {githubURL}</a></div>
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_small"><b>Website:</b><a href={user.blog} target="_blank"> {user.blog}</a></div>
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_small"><b>About:</b> {user.about}</div>
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_small"><b>Repos:</b> {user.repos}</div>
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_small"><b>Gists:</b> {user.gists}</div>
                <div class="slds-var-p-vertical_xxx-small slds-text-heading_small"><b>Followers:</b> {user.followers}</div>
            </div>
        </div>
    </lightning-card>    
</template>
The above HTML code is fairly simple as it's only the design and layout. We have an input field of type search where we're accepting the github username. This input field is binded with username variable which we're going to define in js and it's going to call updateUsername() method whenever we're typing something in this input field so that we can update the username accordingly.

We also have a button here which is going to call getGithubStats() method from js whenever this button is clicked. The getGithubStats() will be used to fetch data from github using the Fetch API and the details will be displayed in the user details section.

To display the details of user, we've created a user object in js and we're checking if the userPopulated boolean variable is true or not. We're going to define it as a getter in js, which will return true or false depending upon whether the user object has details or not. We're going to display the details about the user by using the user object such as: user.about, user.blog, user.repos etc.

Now, let's have a look at the js code quickly:

githubInfo.js

import { LightningElement } from 'lwc';

// * GitHub API Base URL
const GITHUB_URL = 'https://api.github.com/users/';

export default class GithubInfo extends LightningElement {

    username;
    user = {};

    // * This method will return if the user object is populated or not
    get userPopulated() {
        return this.user && this.user.id;
    }

    // * This method will return the github url for the searched user
    get githubURL() {
        return 'https://www.github.com/' + this.username;
    }

    // * This method will set the username as the user is typing the text in the input field
    updateUsername(event) {
        this.username = event.target.value;
    }

    // * This method is used to call GitHub API using fetch method and get the user details
    getGithubStats() {
        if(this.username) {
            this.user = {};
            fetch(GITHUB_URL + this.username)
            .then(response => {
                console.log(response);
                if(response.ok) {
                    return response.json();
                } else {
                    throw Error(response);
                }
            })
            .then(githubUser => {
                this.user = {
                    id: githubUser.id,
                    name: githubUser.name,
                    image: githubUser.avatar_url,
                    blog: githubUser.blog,
                    about: githubUser.bio,
                    repos: githubUser.public_repos,
                    gists: githubUser.public_gists,
                    followers: githubUser.followers
                };
            })
            .catch(error => console.log(error))
        } else {
            alert('Please specify a username');
        }
    }

}
The above code consist of a GITHUB_URL constant which is basically storing our base URL for GitHub API. Inside the class, we have two data members: username and user as discussed before. We also have a userPopulated() method defined which is a getter and will return true if user record is present with an id, otherwise, it'll return false. Based on this value we'll display/hide the user details section in HTML.

After that, we also have a githubURL() getter which is going to form the profile URL of user, based on the username entered. Then we have an updateUsername() method, which is called automatically while updating text in the username input field, it's updating the username data member with the latest value. Finally, we have a getGithubStats() method which is performing the callout in order to get the user details from github on click of a button.


getGithubStats() - In this method, first of all we're checking if the username field is populated, then only we'll proceed ahead, otherwise, we're going to throw an error specifying: Please specify a username in the alert. You can also use a toast here, I have just added an alert to keep it simple.

If we have the username populated, we're first of all resetting the user object to a blank object in order to clear the previous user response (if any). Then, we're using the fetch() method to hit the GitHub API. The fetch method accept the URL as the first parameter which is constructed by appending username to the base url as: GITHUB_URL + this.username. After that, we've two then() followed by a catch(). The first then() is going to receive a Response object from the Fetch API. We are using it's ok property to check if the response is successful or not. If it's successful, we're returning the response body by converting it into a JSON object using response.json() which will be received by subsequent then(). If we receive an error, we're throwing an instance of Error by passing the response in the constructor. The subsequent then() will store the JSON result in githubUser object which is used to populate the user data member of the class in order to display the user's data.

In case of an error, we're simply displaying it using console.log().

To give you a reference of the Github API response, I am displaying it below:
{
  "login": "rahulmalhotra",
  "id": 16497903,
  "node_id": "MDQ6VXNlcjE2NDk3OTAz",
  "avatar_url": "https://avatars.githubusercontent.com/u/16497903?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/rahulmalhotra",
  "html_url": "https://github.com/rahulmalhotra",
  "followers_url": "https://api.github.com/users/rahulmalhotra/followers",
  "following_url": "https://api.github.com/users/rahulmalhotra/following{/other_user}",
  "gists_url": "https://api.github.com/users/rahulmalhotra/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/rahulmalhotra/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/rahulmalhotra/subscriptions",
  "organizations_url": "https://api.github.com/users/rahulmalhotra/orgs",
  "repos_url": "https://api.github.com/users/rahulmalhotra/repos",
  "events_url": "https://api.github.com/users/rahulmalhotra/events{/privacy}",
  "received_events_url": "https://api.github.com/users/rahulmalhotra/received_events",
  "type": "User",
  "site_admin": false,
  "name": "Rahul Malhotra",
  "company": null,
  "blog": "https://rahulmalhotra.github.io/",
  "location": null,
  "email": null,
  "hireable": true,
  "bio": "I am a developer and I love to Code. I am an independent Salesforce Consultant. Blogger and YouTuber at SFDC Stop (https://www.sfdcstop.com/)",
  "twitter_username": "rahulcoder",
  "public_repos": 58,
  "public_gists": 101,
  "followers": 71,
  "following": 2,
  "created_at": "2015-12-31T07:03:03Z",
  "updated_at": "2021-07-23T11:30:20Z"
}
As you can see above, we've properties like: name, avatar_url, public_repos, public_gists etc. That's why we've have used the same properties to map it to the properties of user object:
.then(githubUser => {
    this.user = {
        id: githubUser.id,
        name: githubUser.name,
        image: githubUser.avatar_url,
        blog: githubUser.blog,
        about: githubUser.bio,
        repos: githubUser.public_repos,
        gists: githubUser.public_gists,
        followers: githubUser.followers
    };
})

It's time to look at the meta file now:

githubInfo.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>52.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>
We've exposed this component by setting up isExposed as true and added a single target named as: lightning__HomePage as we want to embed this component in the home page.

Setting up CSP Trusted Sites

So, we embedded our component on the homepage and tried to execute this code to get the details from github by entering the username and clicking on Search button.


However, we received the below error in console:


This error is coming because we haven't notified salesforce that we're going to call this external api and by default salesforce will not allow us to call the external url from lwc. In order to resolve this, we need to tell salesforce that we're going to hit GitHub API from our lightning components. We can do this by creating a record of CSP Trusted Sites. Follow the below steps to add a record of the same:

1. Go to setup and search for CSP Trusted Sites


2. Click on New Trusted Site button and fill up the information as shown below:


Trusted Site Name: GithubAPI
Trusted Site URL: https://api.github.com
Description: GitHub API
Active: true
Context: LEX
Allow site for connect-src: true
Allow site for img-src: true

3. Click on Save button.

A new record will be created as shown below:


Now, refresh the page and try to get the information from GitHub API again by entering a username. This time, you should get a correct response as shown below:


and the information will be displayed in the component as follows:

Conclusion

You can use the fetch() method to hit any external API from lwc component. We can also add more data in the fetch request, for example, in case of a POST request, you may need to send a request body as well along with some headers. You can also send the request data as an object which can be passed as the 2nd parameter of the fetch() method. The basic syntax for that is shown below:
fetch('<request-url>', {
    method: '<method-name>', // * Like: GET, POST
    headers: {
        'Content-Type': '<content-type-passed-in-body>' // * Like: application/json, application/x-www-form-urlencoded
    },
    body: JSON.stringify(data) // * Please note that body data type must match "Content-Type" header
});
The then() and catch() methods followed by this fetch() method will remain the same.

That's all for this tutorial everyone, I hope you understood how you can call an external api from lwc using Fetch API. If you want to learn more about Fetch API in detail you can learn about it here. Let me know your feedback in the comments down below. You can find the full code for this tutorial in the fetch-api branch of salesforce-lwc-concepts github repository here.

Happy Trailblazing..!!

Tuesday, 30 June 2020

Extract Update and Deploy Metadata to Salesforce Org | SFDX Deploy Tool | Deploy Extracted Metadata

Hello Trailblazers,

In this tutorial, we're going to learn how we can very easily extract, update and then deploy metadata to a Salesforce Org by executing a single command from SFDX Deploy Tool for Windows.

Use Case:- In real life projects, when we're actually working in Salesforce, it usually happens that we need to deploy some metadata from one Salesforce Environment to another, let's say you've completed a feature in developer environment and you need to push all the changes to a QA sandbox, while pushing the changes it usually happens that we face some deployment issues, and we need to update some metadata and then try to deploy again and again. This can be very hectic specifically if you're using change sets which take time to upload and then you need to wait for it to be available in the destination org where you want to deploy.

We'll learn how we can make those deployments very easy using SFDX Deploy Tool for Windows

SFDX Deploy Tool can be downloaded from the github repository here:- https://github.com/rahulmalhotra/SFDX-Deploy-Tool

For this tutorial, we need SFDX Deploy Tool to be already setup with you. If you want to know more about how to setup SFDX Deploy Tool, have a look at the README file of the GitHub repository or have a look at this blog post or the tutorial below:-


Once you've setup SFDX Deploy Tool, i.e. the source and destination orgs, where you have to pull and push the metadata respectively. You can open the tool and you'll see the below screen:-


As you can see in the above image, I have updated this tool and added 3 more options at number:- 2, 5 and 6. These options are:-
  • Extract fetched metadata at option 2
  • Validate extracted metadata in destination org at opton 5
  • Deploy extracted metadata in destination org at option 6

Once you've setup the tool, the first step you need to do is to fetch the metadata from the source org. That you can do by setting up the package.xml present under the metadata folder. After you have the package.xml ready, open the deploy tool, choose option 1 and press the Enter key. A sample package.xml is shown below:-


Once you've fetched the metadata from the source org, you'll see an unpackaged.zip file automatically created in the base folder as shown in the above image. This zip file mainly consists of all the metadata that you've fetched using the package.xml. Our next step is to extract and update this metadata before deploying it to the destination org.

To extract the metadata, open SFDX Deploy Tool and choose option 2 with the name:- Extract fetched metadata this option will extract the fetched metadata using windows powershell, once the metadata is successfully extracted, you'll see an output as shown below:-


In the base folder, you can see that the unpackaged.zip file is extracted automatically and a new folder named unpackaged is created which consist of all the metadata that we fetched from the source org as shown below:-



In case you don't have powershell in your windows you can use any other tool to extract that zip file. Now, we can simply open any metadata and update it. Once, we've updated the metadata, it's time to validate and deploy the extracted metadata.

To Validate Extracted Metadata:- Open SFDX Deploy Tool and choose option 5 with the name:- Validate extracted metadata in destination org

To Deploy Extracted Metadata:- Open SFDX Deploy Tool and choose option 6 with the name:- Deploy extracted metadata in destination org. This option will automatically pick the updated files in the extracted folder and deploy them to your destination org.

If you face errors while deployment, you can simply update the extracted files in VSCode and use SFDX Deploy Tool to deploy the metadata again and again in a single command.

Tired of reading or just scrolled down ? Don't worry, you can watch the video too:-



That's all for this tutorial everyone. I hope you liked it, give a try to SFDX Deploy Tool and let me know your feedback in the comments down below.

Happy Trailblazing..!!

Wednesday, 15 April 2020

100% Test code coverage for multiple dependent apex HTTP callouts without creating a mock class

Hello Trailblazers,

Welcome to the 5th tutorial in Simplifying the Callouts in Salesforce Tutorial Series. In this tutorial, we're going to Create a Test Class for Multiple Dependent Apex HTTP Callouts in a single transaction Without Creating a Mock Class. This is a Video-Only Tutorial Series. So, I am giving you a summary of the tutorial below and you can learn in detail by watching the video at the end of the blog.

Note:- This tutorial is using resources from previous tutorial. So, in case, you want to implement the same example on your own, make sure to have a look at the previous tutorial once.

Remember our OrgConnectService class from the previous tutorial ? In this tutorial, we're going to create a test class for that. Just to give a brief, our OrgConnectService class has a single method named:- createAccountAndContact() which is performing 3 dependent callouts using HTTPCalloutFramework. The first callout is responsible to create an Account in the connected org using standard API. The second callout will create a contact and link that contact with the account that was created in the previous callout and the third callout is going to query this contact and account from linked org. You can have a look at that class here.

Now, let's have a look at the test class below:-

As you can see above, we have 4 methods in this test class but we'll concentrate in detail on 1st method only as it's covering positive scenarios and is responsible for 76% code coverage. The rest of 3 methods are covering the negative scenarios and are similar. In createAccountAndContactTest() method, we're first of all Creating Individual Mocks for each callout that we're going to perform from the service class. Then, we created an instance of HTTPCalloutServiceMultiMock named as:- multiMock and we added all 3 individual mocks to this multi-mock using addCalloutMock() method.

This method takes the endpoint URL as the first parameter and the individual mock in the second parameter. To set the endpoint URL properly, we created an instance of HTTPCalloutService class named as destinationOrgService by passing the custom metadata name in constructor as we've done in previous tutorials. Finally, we set the multi mock using our Test.setMock() method in which we passed HTTPCalloutMock.class in the first parameter which is the type and in the second parameter we passed our multiMock instance.

Then we simply called our createAccountAndContact() method and passed the required parameters and it automatically used our multi mock setup to get the fake responses that we've set in individual mocks according to the URL it is hitting. Finally, we checked the returnValueMap we're getting from the method to make sure that the callout is successful.

Let's discuss one method with a negative scenario too. In createAccountAndContactTestWrongResponseAccount() method, you can see that I intentionally passed the QUERY_SUCCESS_CODE in the mock just because I want to cover the scenario when my response code from account callout is not 201. As I am testing negative scenario for first request now, I don't need to create a multi mock because I'll be getting a response code as 200 (set using QUERY_SUCCESS_CODE) during test run (first callout for account creation where expected response code is 201) and it'll return an error without executing further code.

In another method createAccountAndContactTestWrongResponseContact(), I am checking negative scenario for second callout. Because of this, I created a multi-mock where in accountMock I passed in CREATE_SUCCESS_CODE as I want this request to be successful. Whereas, in contactMock I passed in QUERY_SUCCESS_CODE as I want this request to be failed and finally asserted the ERROR_CODE and CONTACT_ERROR_MESSAGE for this request.

Want to learn in depth ? Have a look at the below video:-



In this tutorial, we learned how we can get 100% code coverage for multiple dependent apex HTTP callouts without creating a mock class as shown below:-


If you liked this tutorial, make sure to share it in your network and let me know your feedback in the comments down below.

Happy Trailblazing..!!

Friday, 10 April 2020

Multiple Dependent Apex HTTP Callouts in a Single Transaction from Salesforce | HTTPCalloutFramework

Hello Trailblazers,

Welcome to the 4th tutorial in Simplifying the Callouts in Salesforce Tutorial Series. In this tutorial, we're going to perform Multiple Dependent Apex HTTP Callouts in a single transaction. This is a Video-Only Tutorial Series. So, I am giving you a summary of the tutorial below and you can learn in detail by watching the video at the end of the blog.

Note:- This tutorial is using resources from previous tutorial. So, in case, you want to implement the same example on your own, make sure to have a look at the previous tutorial once.

We've made some updates to our CustomerRubyOrg metadata record as shown below:-


As you can see above, I have added a header with key:- Content-Type and value:- application/json. I have updated the method as POST and also the endpoint as:- callout:CustomerRubyAPI/services/data/v48.0 where I am referring CustomerRubyAPI which is the name of my Named Credential record that we created in our previous tutorial.

Let's have a look at the code below:-


As you can see above, we have a OrgConnectService class. In which we have a single method named as createAccountAndContact(). This method is receiving a string for account name and a contact object as a parameter. Inside this method, we're performing 3 callouts to another salesforce org (we call it as source org) one by one. So, in total 3 operations are performed in source org as shown below:-
  1. Creating a new Account Record using the account name received in parameter.
  2. Updating the contact record received in parameter by linking it with Account Record and creating a new Contact Record.
  3. Querying the contact and related account record.

First of all we're creating an instance of HTTPCalloutService named as destinationOrgService and we passed the custom metadata name:- CustomerRubyOrg in the constructor. Then we're setting the endpoint URL and request body for the callout using getter and setter methods present in HTTPCalloutService which is a part of HTTPCalloutFramework.

Finally, we're sending the request and checking if the response code is correct or not. For record creation in salesforce, the response code should be 201 and for querying it should be 200. For the first two callouts, I am parsing the response body using JSON.deserializeUntyped() method which is returning an Object in the response that I am typecasting into Map<String, Object>. I am checking the value of success and getting the id of the record which is created. However, in the third callout, I am simply displaying the response using System.debug().

Wether the request is successful or not, we're forming a Map<String, String> which we're returning by this method. We'll be using this map in the next tutorial where we'll be creating a test class for this class in order to add asserts for all 3 callouts depending upon the return value map.

I have added necessary comments to help you understand the code. The basic flow is :- We're creating an account record by calling out to salesforce standard api. Then we're creating a contact record and linking it with the account record whose id we got in the response from first API callout. Finally, we're querying the contact and related account record using the contact id that we got in the response from 2nd callout. So, in this way each callout depends on the previous callout response which is a very common requirement that we get usually face in real life projects while working on integration.

Want to learn in depth ? Have a look at the below video:-



In the next tutorial we'll see how we can create a test class for such a scenario where we have 3 dependent callouts in a single transaction and that too without creating a mock class using HTTPCalloutFramework. If you liked this tutorial, make sure to share it in your network and let me know your feedback in the comments down below.

Happy Trailblazing..!!