CDC · Uncategorized

Change Data Capture Events in Salesforce – Keep your UI Updated with latest data

Change Data Capture(CDC) events :-

This is still a new term in Salesforce world . Do you know how powerful it is ? If not read this post and learn CDC events

Have you ever faced the issue when you are viewing the record and at the same time other user has modified the same record and the data you are seeing is not up to date !! How to tackle the issue ? Here is the solution for that … Let’s start!!

What is CDC ?

  • CDC events are basically used to track the changes in a Salesforce record. This means, it tracks create, update, delete and undelete changes of a Salesforce Record, and fires an event when the change occurred.

  • You can subscribe to CDC channel to listen for these events. You can even subscribe to these events from you your own platform using javascript libraries like cometD.CDC events are basically used to track the changes in a Salesforce record. This means, it tracks create, update, delete and undelete changes of a Salesforce Record, and fires an event when the change occurred.

  • Once you subscribe to these events, you will get a notification whenever a new event is generated, the notification will include a payload in JSON format which will have all the details about the event like:

    1. SObject name – where the changed happened ex: Account ,opportunity
    2. RecordId’s – List of record id’s which are changed
    3.Change Type – Create/Update/Delete/Undelete
    4.Changed Fields – Fields which are modified
    5.User – The User who performed the change
    6.Timestamp – When the change happened


  • You can use this payload to perform your operation. For example, if you want to keep your in-house application in sync with Salesforce Data, you can use payload information to update your application with data changes happened in Salesforce.
Sample Payload
{
"data":{  
   "event":{  
      "createdDate":"2018-10-18T01:17:26.855Z",
      "replayId":61,
      "type":"updated"
   },
   "sobject":{  
      "Type":"Customer - Channel",
      "Phone":"(785) 241-6201",
      "Website":"dickenson-consulting.com",
      "Id":"001B000000mAT1ZIAW",
      "Name":"Dickenson .inc"
   }
},
"channel":"/topic/AccountSpy"
}

CDC Events tracks all changes and all fields in an object and if any of the fields are modified the event will be triggered. This makes it very easy to listen for all type of changes in a record. You can use Streaming API for a similar purpose, but there you specifically need to specify which fields and change type you want to track while creating PushTopics.


Implementation Example

Let’s discuss an implementation scenario of CDC events where you want to keep your UI updated with latest changes

  • User “Ron Weasley” is viewing an Opportunity record which is being viewed by the user “Harry Potter” as well at the same time.
  • Harry Potter has changed the record data and has modified Opportunity amount.
  • Ron Weasley who is unaware of this change and still viewing the old record, has to make a very important decision based on Opportunity amount and send the same to higher management.
  • Now in this situation, Ron Weasley may take an incorrect decision and send incorrect data to higher management.

How to avoid the above situation? Well, lets leverage Change Data Capture events and lightning:empApi (Winter’19 release)component to track changes in Opportunity Record, and update the data on Ron Weasley’s screen as soon as the data is modified by Harry Potter. Our coding solution will include below components:

RecordChangeEventHandler Component:

The main component to subscribe, unsubscribe and receive messages from streaming channel of Change Data Capture events. Once this component receives a message, this will fire a component event, which would be handled by the parent component. The channel name is like below:

  • Standard Object: “ChangeEvent” appended in the end. Example, AccountChangeEvent, OpportunityChangeEvent etc.
  • Custom Object: “__ChangeEvent” appended in the end. Like for custom object “Car__c”, the change event channel name would be “Car__ChangeEvent”
<aura:component access="global">
    
<!-- ChannelName, which needs to subscribed -->
    
<aura:attribute name="channelName" type="String" required="true"/>
    
<!-- Save the reference of current subscription, which can be unsubscribe later on -->
    
<aura:attribute name="subscription" type="Object"/>
     
    
    
<!-- This event is fired when a component is destroyed. 
		
Handle this event if you need to do custom cleanup when a component is destroyed.-->
    
<aura:handler name="destroy" value="{!this}" action="{!c.unsubscribe}"/>
	
<!-- init event -->
   
 <aura:handler name="init" value="{!this}" action="{!c.subscribe}"/>
    
   
 <!-- empApi component which will be used to subscribe/unsubscribe to a channel -->
    
<lightning:empApi aura:id="empApi"/>
    
    
<!-- Component event, which will be fired once the message is received
       
  This event will be handled by parent component to perform needful action on stream event -->
    
<aura:registerEvent name="onRecordChange" type="c:RecordChangeEvent"/>

</aura:component>




RecordChangeEventHandlerController.js :


({
    
    subscribe: function(component, event, helper) {
        
       
 // Get the empApi component.
       
 var empApi = component.find("empApi");
       
 // Get the channel name from attribute
      
  var channel = component.get("v.channelName");
      
  //fetch latest events
      
  var replayId = -1;
        
      
  // Callback function to be passed in the subscribe call.
       
 // After an event is received, this callback fire a custom
       
 // event to notify parent component and pass payload object
      
  var subscribeCallback = function (message) {
        
    
//Fire the component event to notify parent component
          
  var messageEvent = component.getEvent("onRecordChange");
           
 if(messageEvent!=null) {
            
    messageEvent.setParam("recordData", message.data);
                messageEvent.fire();                            
          
  }
           
 //Display event data in browser console
           
 console.log("Received [" + message.channel +
                        " : " + message.data.event.replayId + "] payload=" +
                    
    JSON.stringify(message.data.payload));
       
 }.bind(this);
    
        
        
       
 // Register error listener and pass in the error handler function.
     
   empApi.onError(function(error){
         
   console.log("Received error ", error);
       
 }.bind(this));
        
      
  // Subscribe to the channel and save the returned subscription object.
       
 empApi.subscribe(channel, replayId, subscribeCallback).then(function(value) {
          
  console.log("Subscribed to channel " + channel);
            component.set("v.subscription", value);
        });
    },
    
   
   
 unsubscribe : function (component, event, helper) {
      
  try{
          
  // Get the empApi component.
         
   var empApi = component.find("empApi");
         
   // Get the channel name from attribute
           
 var channel = component.get("v.channelName");
            
          
  // Callback function to be passed in the unsubscribe call.
           
 var unsubscribeCallback = function (message) {
             
   console.log("Unsubscribed from channel " + channel);
           
 }.bind(this);
            
            // Error handler function that prints the error to the console.
            
var errorHandler = function (message) {
           
     console.log("Received error ", message);
           
 }.bind(this);
            
          
  // Object that contains subscription attributes used to
           
 // unsubscribe.
           
 var subscription = {"id": component.get("v.subscription")["id"],
                                "channel": component.get("v.subscription")["channel"]};
            
           
 // Register error listener and pass in the error handler function.
           
 empApi.onError(function (error) {
            
    console.log("Received error ", error);
           
 }.bind(this));
            
           
 // Unsubscribe from the channel using the sub object.
           
 empApi.unsubscribe(subscription, unsubscribeCallback);
       
 }catch(e){}
   
 },

})




RecordChangeEvent Component Event:-

This event will be fired by RecordChangeEventHandler component whenever it receives a message from Change Event. This component event will be handled by the parent component.

<!-- This event will be fired whenever new record change has been captured by RecordChangeEventHandler component -->
<aura:event type="COMPONENT" description="Event template">
    <aura:attribute name="recordData" type="Object" />
</aura:event>




Record Change Capture:-

This component is parent component which will include actual markup and handle the above-mentioned component event fired my RecordChangeEventHandler component.

<aura:component implements="force:appHostable,flexipage:availableForRecordHome,force:hasRecordId,force:hasSObjectName" 
                access="global" 
                controller="RecordChangeCaptureLightningController">
    
    
<aura:attribute name="channelName" type="String" default="" />
   
 <aura:attribute name="autoRefresh" type="String" default="Yes" />
   
 <aura:attribute name="isSupported" type="Boolean" default="false" />
    
   
 <!--Loading list of supported object for change events
   
  - This can be configured in a custom setting or custom metadata type also
     
- however, in that case you need to make a server side call to get the data
     
- which i am trying to avoid here -->
   
 <ltng:require scripts="{!$Resource.SupportedObjectsForChangeEvents}" afterScriptsLoaded="{!c.checkCompatibility}" />
    
    
<!--Include EmpApiDemo child component and pass channel name to subscribe
     
 ex: "/topic/AccountSpy" is my pushtopic channel name
    
once the event is fired, it will handled in handleMessage controller method -->
    
<aura:if isTrue="{!v.isSupported}">
       
 <c:RecordChangeEventHandler channelName="{!v.channelName}" onRecordChange="{!c.handleMessage}" />
      
  <aura:set attribute="else">
           
 <div style="color:red;font-weight: bold;">Record Change Capture does not support this object/record page.</div>
       
 </aura:set>
   
 </aura:if>

</aura:component>






RecordChangeCaptureController.js:-

({
    checkCompatibility : function(component, event, helper){
        //get current object
        var objectName = component.get("v.sObjectName");
        
        //Check is object name is not undefined/null
        if(objectName){
            //Get channel name for change event
            var channelName = helper.getChannelName(objectName);
            
            //Check if channel name is not null or undefined
            if(channelName){
                component.set("v.channelName", channelName);
                //setting supported varibale true
                component.set("v.isSupported", true);
            } else{
                //Object does not support change event
                component.set("v.isSupported", false);
            }
            
        } else{
            //Object name is undefined or null, hence component will not support this page
            component.set("v.isSupported", false);
        }
    },
    
    /**
     * Handling the message when change event is fired.
     * */
    handleMessage : function(component, event, helper) {
        const message = event.getParam('recordData');
        const eventType = message.payload.ChangeEventHeader.changeType;
        const entityName = message.payload.ChangeEventHeader.entityName;
        const userId = message.payload.ChangeEventHeader.commitUser.substring(0,15); //15 digit id of transaction commit user
        const signedInUser= $A.get("$SObjectType.CurrentUser.Id").substring(0,15); //15 digit id of logged in user
        /**
         * Conditions:
         * - Change Type should not be create
         * - Record Id must present in modified recordIds
         * - User who modified the record should not be logged in user
         * */
        
        if(!(eventType === "CREATE")){
            //Condition 1 - Change type is not "created"
            Array.from(message.payload.ChangeEventHeader.recordIds).forEach( recordId => {
                if(recordId === component.get("v.recordId") && !(signedInUser === userId)){
                    //Condition 2 - Record Id match found &&
                    //Condition 3 - commit user is not logged in user
                
                    //Display console log with changed values
                    console.log(`${eventType} event captured on ${entityName} by user id ${userId}`);
                    /*console.log("Values changed are:");
                    for(k in message.payload){
                        if(k){
                            console.log(`Field Name: ${k} | New Value:${message.payload[k]}`);
                        }
                    }*/
            
                    //Now call helper function to get user name and display toast
                    helper.getUser(component, userId, eventType, entityName);
            	}
             });
        }
    }
})




RecordChangeCaptureHelper.js:-

({
    
    /*
     * This method will call the server side action to get user name
     * Once user name retrieved, it will show a warning toast to the user
     * */
    getUser : function(component, userId, eventType, entityName) {
        var action = component.get("c.getUserName");
        action.setParams({
            "userId" : userId
        });
        
        
        action.setCallback(this,function(response) {
            var state = response.getState();
            if (state === "SUCCESS") { 
                // pass returned value to callback function
                var userName = response.getReturnValue(); 
                this.showToast({
                    "title":`Record ${eventType}ED`,
                    "type": "warning",
                    "message": `This record has been ${eventType}D by ${userName}`
                });
                
                //Auto refresh the page to get latest details if auto refresh is selected
                if(component.get("v.autoRefresh") === "Yes"){
                    this.autoRefresh();
                }
                 
            } else if (state === "ERROR") {
                // generic error handler
                var errors = response.getError();
                if (errors) {
                    console.error("Error is getting username: ", errors);
                }
                return null;
            }
        });
        
        $A.enqueueAction(action);
    },
    
    /**
     * Get change event object name from 
     * current page's object
     * */
    getChannelName : function(objectName) {
        var isSupported = false;
        //If it is custom object, then it is supported
        if(objectName.includes("__c")){//Custom Object
            objectName = objectName.slice(0, -3); //removing __c from the end
            objectName += "__ChangeEvent"; //appending __ChangeEvent in the end of custom object
            isSupported = true;
        } 
        //check if it is one of the supported standard object from static resource
        else {//Standard Object
            //iterate over supported object list
            window.supportedObjectForChangeEvents.forEach(obj => {
                if(obj.toLowerCase().indexOf(objectName.toLowerCase()) != -1){ 
                //Match found, Object is supported
                objectName += "ChangeEvent" //appending ChangeEvent in the end of standard object
                isSupported = true;
            } 
            });
            
        }
        if(isSupported === true){//is object supported, return channel name
            return `/data/${objectName}`;
        } else{//if object not supported, return null
            return null;
        }
        
    },
    
    /*
     * This function displays toast based on the parameter values passed to it
     * */
    showToast : function(params) {
        var toastEvent = $A.get("e.force:showToast");
        if(toastEvent){
            toastEvent.setParams(params);
            toastEvent.fire();
        } else{
            alert(params.message);
        }
    },
    
    /**
     * Auto refresh the page to get latets details
     * */
    autoRefresh : function(){
        var refreshEvent = $A.get('e.force:refreshView');
        if(refreshEvent){
            refreshEvent.fire();
        } else{
            console.error("Auto refresh is not supported in current context");
        }
    }
})




RecordChangeCapture.design

<design:component>
   
 <design:attribute name="autoRefresh" label="Auto refresh the page?" 

datasource="Yes,No" default="Yes" />

</design:component>





SupportedObjectsForChangeEvents.js:-

//Add standard object list here if any new standard object supports change data events

//At the time of winter'19 below object support change data events

window.supportedObjectForChangeEvents = 
['Account','Asset','Campaign','Case','Contact','ContractLineItem','Entitlement','Lead',
                                        'LiveChatTranscript','Opportunity','Order','OrderItem','Product2','Quote','QuoteLineItem','ServiceContract'];






RecordChangeCaptureLightningController.apxc:-

public class RecordChangeCaptureLightningController {
    
    @AuraEnabled
    public static String getUserName(Id userId){
        User u = [SELECT Name FROM User WHERE Id=:userId];
        return u.Name;
    }

}



Happy Coding!!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s