When working on enhancements or user stories for the Salesforce application, we often introduce numerous new features. Before making modifications to the existing system, it is crucial to analyze the objects that are being referenced. Identifying these referenced objects is not always straightforward.
Salesforce has addressed this challenge by introducing a new object called MetadataComponentDependency in the Tooling API. This new object allows us to retrieve references to various custom objects, such as fields, classes, or lightning components.
Additionally, this object can be utilized to identify metadata that is no longer in use, enabling us to remove it from the organization and thereby increase the code limit.
Now, let’s explore how we can obtain dependencies for a metadata object. Within this blog post, I’ve developed a Lightning Web Component that dynamically displays dependencies based on the selected type of metadata, such as fields, Apex classes, or lightning components.
Procedures:
- Retrieve Custom Field, Class, and Lightning Bundle Details
- Obtain Dependencies using the Dependency API
- Display Dependencies on a Lightning Web Component
- Export Dependency Data in CSV Format
1.Obtain Details for Custom Fields, Classes, and Lightning Bundles
Let’s gather information about custom objects within the Salesforce org. We can leverage standard objects like ApexClass, AuraDefinitionBundle, and Tooling API objects to retrieve this data.
a. Customized Fields:
We can obtain detailed information about custom fields by utilizing the CustomField tooling object. The Field TableEnumOrId in this object is employed to determine which object this field is associated with. For standard objects, we receive the object name, such as ‘Account’ or ‘Contract,’ while for custom objects, we obtain the object ID representing the table name.
SELECT Id,DeveloperName,TableEnumOrId from CustomField
Since we aim to display the object in the first dropdown menu followed by the field dropdown, we need to acquire the custom object label instead of its ID from the TableEnumOrId mentioned earlier. To achieve this, we can utilize another Tooling API object called CustomObject.
SELECT Id,DeveloperName from CustomObject
We can establish a connection between CustomObject.Id and CustomField.TableEnumOrId in order to retrieve the label of custom objects.
This comparison can be performed within the Lightning Web Component (LWC) to obtain the custom object label.
// To Get Label for Standard Object var obj=objs.find(x => x.Id === this.objectFields[i].TableEnumOrId); if(obj!=undefined) { label=obj.DeveloperName; }
b. Apex Class
We can obtain a list of Apex classes using the ApexClass object.
Select Id,Name from ApexClass
c. Lightning Component
We can utilize the AuraDefinitionBundle object to retrieve details about Lightning components.
Select Id,DeveloperName from AuraDefinitionBundle
2.Acquire Dependencies Using the Dependency API
We can obtain dependencies for any custom object by utilizing the MetadataComponentDependency object in the Tooling API. Although this object is still in beta version, it has been made available to Developers and Administrators following the Summer ’20 Release.
To retrieve dependencies for any metadata, you can employ the following query in the Tooling API.
Select MetadataComponentId, MetadataComponentName, RefMetadataComponentName, RefMetadataComponentId,MetadataComponentType from MetadataComponentDependency where RefMetadataComponentId=\'id\''
Substitute the ‘id’ with the identifier of any metadata entity.
3.Display Dependencies on a Lightning Web Component
Having acquired custom metadata in the first step and dependencies in the second step, let’s now present this information on a Lightning Web Component.
We can include a dropdown menu to display the type of entity, such as field, Apex class, or Lightning component.
@api get types() { return [ { label: 'Please Select', value: '' }, { label: 'Apex Class', value: 'apex' }, { label: 'Lightning Component', value: 'lightning' }, { label: 'Field', value: 'field' }, ]; }
<lightning-combobox name="metdataType" label="Metdata Type" placeholder="Select Type" options={types} onchange={handleType}> </lightning-combobox>
Depending on the chosen metadata type, we can display the corresponding dropdown options, such as Apex Class, Lightning Component, Object, and Field.
We can invoke an Apex method using the wire API and then customize the data according to our needs.
import getDepdency from '@salesforce/apex/DependencyController.getDepdency'; getDepdency({ id: objectId}) .then(data => { if (data) { this.fields=data; this.error = undefined; } else if (error) { this.error = error; } })
4.Export Dependency Data to CSV Format
We can download the dependency result data in various formats, and for this purpose, I have opted for CSV as the format.
let csvContent = "data:text/csv;charset=utf-8,"; this.fields.forEach(function(rowArray) { let row = rowArray.MetadataComponentName+","+rowArray.MetadataComponentType+","; csvContent += row + "\r\n"; }); var encodedUri = encodeURI(csvContent); var link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", "Dependent.csv"); document.body.appendChild(link); link.click();
CRUCIAL STEP:
It is essential to invoke the Tooling API from the Lightning Web Component, as explained in the post titled “Calling the Tooling API from a Lightning Web Component.” You can utilize the “Default” scope with the “refresh_token” set to “full” in the Auth Provider and Named Credential configuration if you wish to avoid changing the user context.
Full Code:
Lightning Web Component:
<template> <lightning-card title="Metadata Dependency Explorer" icon-name="custom:custom10"> <lightning-combobox name="metdataType" label="Metadata Type" placeholder="Select Type" options={types} onchange={handleType}> </lightning-combobox> <template if:true={isApexSelected}> <lightning-combobox name="objects" label="Select Object" placeholder="Select Object" value={selectedApex} options={apexList} onchange={handleApexChange}> </lightning-combobox> </template> <template if:true={isLightningSelected}> <lightning-combobox name="Comp" label="Select Lightning Component" placeholder="Select Lightning Component" value={selectedComp} options={compList} onchange={handleCompChange}> </lightning-combobox> </template> <template if:true={isFieldSelected}> <lightning-combobox name="Object" label="Object" placeholder="Select Object" options={objectList} value='object' onchange={handleObjectList}> </lightning-combobox> <lightning-combobox name="field" label="Select Field" placeholder="Select Field" options={fieldList} value={selectedField} onchange={handleFieldList}> </lightning-combobox> </template> <div class="slds-grid slds-wrap"> <div class="slds-size--1-of-1"> <div> <lightning-button label="Export in CSV" onclick={handleFileDownload}></lightning-button> </div> </div> <div class="slds-size--1-of-1"> <div class="slds-float_left slds-m-around_medium"> <lightning-datatable key-field="id" data={fields} columns={columns} show-row-number-column hide-checkbox-column> </lightning-datatable> </div> </div> </div> </lightning-card> </template> v
import { LightningElement,track,wire,api } from 'lwc'; import getObjects from '@salesforce/apex/DependencyController.getObjects'; import getApex from '@salesforce/apex/DependencyController.getApex'; import getLightningComponent from '@salesforce/apex/DependencyController.getLightningComponent'; import getDepdency from '@salesforce/apex/DependencyController.getDepdency'; const columnList = [ { label: 'Metadata Component Name', fieldName: 'MetadataComponentName' }, { label: 'Metadata Component Type', fieldName: 'MetadataComponentType' } ]; export default class Dependencynfo extends LightningElement { @track objectList= []; @track fieldList=[]; @track fields=[]; @track objectFields=[]; @track rows=[]; @track columns = columnList; @track apexList=[]; @track compList=[]; error; @track type = ''; isApexSelected = false; isLightningSelected = false; isFieldSelected = false; selectedApex=''; selectedComp=''; object=''; selectedField=''; @api get types() { return [ { label: 'Please Select', value: '' }, { label: 'Apex Class', value: 'apex' }, { label: 'Lightning Component', value: 'lightning' }, { label: 'Field', value: 'field' }, ]; } @wire(getObjects) wiredObject({ error, data }) { if (data) { var objs=data.Objects; this.objectFields=data.Fields; for(var i=0; i<this.objectFields.length; i++) { var label=this.objectFields[i].TableEnumOrId; var id=this.objectFields[i].TableEnumOrId; // To Get Label for Standard Object var obj=objs.find(x => x.Id === this.objectFields[i].TableEnumOrId); if(obj!=undefined){ label=obj.DeveloperName; } //Check Duplicate Records var objFound=this.objectList.filter(x => x.label === label); if(objFound.length==0) { this.objectList = [...this.objectList ,{value: id , label: label} ]; } } this.error = undefined; } else if (error) { this.error = error; } } handleType(event) { this.type=event.detail.value; this.isFieldSelected=(this.type === 'field'); this.isLightningSelected=(this.type === 'lightning'); this.isApexSelected=(this.type === 'apex'); if(this.isApexSelected) { this.getApexList(); } if(this.isLightningSelected) { this.getComponentList(); } } getApexList() { if(this.apexList.length==0) { getApex() .then(data => { if (data) { for(var i=0; i<data.length; i++) { this.apexList = [...this.apexList ,{value: data[i].Id, label: data[i].Name} ]; } } else if (error) { this.error = error; } }) .catch(error => { this.error = error; console.log(error); }); } } getComponentList() { if(this.compList.length==0) { getLightningComponent() .then(data => { if (data) { for(var i=0; i<data.length; i++) { this.compList = [...this.compList ,{value: data[i].Id, label: data[i].DeveloperName} ]; } } else if (error) { this.error = error; } console.log(JSON.stringify(this.compList)); }) .catch(error => { this.error = error; console.log(error); }); } } handleObjectList(event) { this.fieldList=[]; const selectedOption = event.detail.value; this.object=selectedOption; //Filter selected Object's Fields var fields=this.objectFields.filter(x => x.TableEnumOrId === selectedOption); for(var i=0; i<fields.length; i++) { this.fieldList = [...this.fieldList ,{value: fields[i].Id, label: fields[i].DeveloperName} ]; } } handleFieldList(event) { this.selectedField= event.detail.value; this.handleDependency(event.detail.value); } handleDependency(objectId) { getDepdency({ id: objectId}) .then(data => { if (data) { this.fields=data; this.error = undefined; } else if (error) { this.error = error; } }) .catch(error => { this.error = error; }); } handleFileDownload(event) { if(this.fields!==undefined) { let csvContent = "data:text/csv;charset=utf-8,"; this.fields.forEach(function(rowArray) { let row = rowArray.MetadataComponentName+","+rowArray.MetadataComponentType+","; csvContent += row + "\r\n"; }); var encodedUri = encodeURI(csvContent); var link = document.createElement("a"); link.setAttribute("href", encodedUri); link.setAttribute("download", "Dependent.csv"); document.body.appendChild(link); link.click(); } } handleCompChange(event) { this.selectedComp=event.detail.value; this.handleDependency(event.detail.value); } handleApexChange(event) { this.selectedApex=event.detail.value; this.handleDependency(event.detail.value); } }
<?xml version="1.0" encoding="UTF-8"?> <LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata"> <apiVersion>48.0</apiVersion> <isExposed>true</isExposed> <targets> <target>lightningCommunity__Page</target> <target>lightningCommunity__Default</target> <target>lightning__RecordPage</target> <target>lightning__AppPage</target> <target>lightning__HomePage</target> </targets> </LightningComponentBundle>
Apex Code:
public class DependencyController { //Retrieve All Objects in org @Auraenabled(cacheable=true) public static ObjectDetail getObjects() { List<FieldInfo.Record> objects=getCustomObject(); List<FieldInfo.Record> fields=getObjectFields(); ObjectDetail detail=new ObjectDetail(); detail.Objects=objects; detail.Fields=fields; return detail; } @Auraenabled(cacheable=true) public static List<ApexClass> getApex() { return [Select Id,Name from ApexClass]; } @Auraenabled(cacheable=true) public static List<AuraDefinitionBundle> getLightningComponent() { return [Select Id,DeveloperName from AuraDefinitionBundle]; } public static List<FieldInfo.Record> getCustomObject() { string sql='SELECT Id,DeveloperName from CustomObject'; String result = toolingAPISOQL(sql); if(string.isNotBlank(result)) { FieldInfo data=FieldInfo.parse(result); return data.records; } return null; } @Auraenabled(cacheable=true) public static List<FieldInfo.Record> getObjectFields() { string sql='SELECT Id,DeveloperName,TableEnumOrId from CustomField'; String result = toolingAPISOQL(sql); if(string.isNotBlank(result)) { FieldInfo data=FieldInfo.parse(result); return data.records; } return null; } static String toolingAPISOQL(string query) { String baseURL='callout:ToolingRest?'; return HttpCallout.restGet( baseURL +'q='+ (query.replace(' ', '+')),'GET', UserInfo.getSessionId()); } @AuraEnabled(cacheable=true) public static List<DependentInfo.Record> getDepdency(string id) { string sql='Select MetadataComponentId, MetadataComponentName, RefMetadataComponentName, RefMetadataComponentId,MetadataComponentType from MetadataComponentDependency where RefMetadataComponentId=\'id\''; String result = toolingAPISOQL(sql.replace('id', id)); if(string.isNotBlank(result)) { DependentInfo data=DependentInfo.parse(result); return data.records; } return null; } public class ObjectDetail { @AuraEnabled public List<FieldInfo.Record> Objects {get;set;} @AuraEnabled public List<FieldInfo.Record> Fields {get;set;} } }
public class DependentInfo { public Integer size {get;set;} public Integer totalSize {get;set;} public Boolean done {get;set;} public Object queryLocator {get;set;} public String entityTypeName {get;set;} public List<Record> records {get;set;} public class Attributes { public String type_Z {get;set;} // in json: type public String url {get;set;} } public class Record { public String MetadataComponentId {get;set;} @AuraEnabled public String MetadataComponentName {get;set;} public String RefMetadataComponentName {get;set;} public String RefMetadataComponentId {get;set;} @AuraEnabled public String MetadataComponentType {get;set;} } public static DependentInfo parse(String json){ return (DependentInfo) System.JSON.deserialize(json, DependentInfo.class); } }
public class FieldInfo{ public Integer size {get;set;} public Integer totalSize {get;set;} public Boolean done {get;set;} public Object queryLocator {get;set;} public String entityTypeName {get;set;} public List<Record> records {get;set;} public class Attributes { public String type_Z {get;set;} // in json: type public String url {get;set;} } public class Record { @AuraEnabled public String TableEnumOrId {get;set;} @AuraEnabled public String DeveloperName {get;set;} @AuraEnabled public String Id{get;set;} } public static FieldInfo parse(String json){ return (FieldInfo) System.JSON.deserialize(json, FieldInfo.class); } }
public class public class HttpCallout { public static String restGet(String endPoint, String method, String sid) { try { Http h = new Http(); HttpRequest hr = new HttpRequest(); hr.setHeader('Authorization', 'Bearer ' + sid); hr.setTimeout(60000); hr.setEndpoint(endPoint); hr.setMethod(method); HttpResponse r = h.send(hr); return r.getBody(); } catch(Exception ex) { system.debug('Exception in tooling API Call:'+ ex.getMessage()); return ex.getMessage(); } } } { public static String restGet(String endPoint, String method, String sid) { try { Http h = new Http(); HttpRequest hr = new HttpRequest(); hr.setHeader('Authorization', 'Bearer ' + sid); hr.setTimeout(60000); hr.setEndpoint(endPoint); hr.setMethod(method); HttpResponse r = h.send(hr); return r.getBody(); } catch(Exception ex) { system.debug('Exception in tooling API Call:'+ ex.getMessage()); return ex.getMessage(); } } }