
In Salesforce, users have the option of creating custom objects and their fields via two main routes. The primary and more straightforward method is by utilizing the Object Manager's user interface, where you simply click on the "New" button and define the object as needed. Alternatively, for a more dynamic approach, users can leverage the power of code to achieve this task. In this context, the MetadataService class becomes our tool of choice.
Salesforce's Metadata API provides developers with a versatile means of interfacing with the platform's metadata. By leveraging this API, one can manage and customize various aspects of their Salesforce instance. In today's guide, we'll specifically focus on how you can utilize the `MetadataService` class to create custom objects.
1. Preparing the MetadataService:
Before delving into the code:
Ensure the MetadataService class is present in your Salesforce org.
This class is available on the FinancialForce GitHub repository.
Once acquired, deploy the class to your Salesforce org using tools such as the Salesforce CLI or Workbench.
2. Grasping the MetadataService:
MetadataService acts as a wrapper for Salesforce's Metadata API. This allows for operations like creation, retrieval, modification, and deletion of metadata items including but not limited to Custom Objects, Layouts, and Custom Fields, directly from Apex.
3. Introducing the MetadataServiceHandler Class:
To facilitate the creation of custom objects, we'll make use of a helper class, MetadataServiceHandler, which will in turn utilize the standard methods from MetadataService class.
MetaDataServiceHandler.cls
public with sharing class MetaDataServiceHandler {
public static void createObject(ObjectWrapper data) {
MetadataService.MetadataPort metadataservice = new MetadataService.MetadataPort();
metadataservice = createService();
List<MetadataService.CustomObject> objectList = new List<MetadataService.CustomObject>();
MetadataService.CustomObject customobject = new MetadataService.CustomObject();
customobject.fullName = data.objectAPI;
customobject.label = data.objectName;
customobject.pluralLabel = data.objectPlural;
customObject.nameField = new MetadataService.CustomField();
customobject.nameField.type_x = 'Text';
customobject.nameField.label = 'Name';
customobject.deploymentStatus = 'Deployed';
customObject.sharingModel = 'ReadWrite';
objectList.add(customobject);
metadataservice.createMetadata(objectList);
}
public static List<MetadataService.SaveResult> createDifferentTypeField(List<fieldWrapper> fieldWrappers, String objectName) {
MetadataService.MetadataPort service = new MetadataService.MetadataPort();
service = createService();
List<MetadataService.Metadata> dataToInsert = new List<MetadataService.Metadata>();
for (fieldWrapper currentField : fieldWrappers) {
dataToInsert.add(getFieldByType(currentField, objectName));
}
List<MetadataService.SaveResult> results = service.createMetadata(dataToInsert);
return results;
}
public static MetadataService.MetadataPort createService(){
MetadataService.MetadataPort service = new MetadataService.MetadataPort();
service.SessionHeader = new MetadataService.SessionHeader_element();
service.SessionHeader.sessionId = GetSessionIdController.getSessionIdFromVFPage(Page.SessionId);
return service;
}
public static MetadataService.CustomField getFieldByType(fieldWrapper currentField, string objectName) {
MetadataService.CustomField field = new MetadataService.CustomField();
field.fullName = objectName + '.' + currentField.apiName;
field.label = currentField.label ;
if (currentField.type == 'text') {
field.length = Integer.valueOf(currentField.textLength);
field.type_x = 'Text';
}
else if (currentField.type == 'number') {
field.precision = Integer.valueOf(currentField.textLength);
field.scale = Integer.valueOf(currentField.scale);
field.type_x = 'Number';
field.unique = false;
}
else if (currentField.type == 'checkbox') {
field.defaultvalue = currentField.defaultvalue;
field.type_x = 'Checkbox';
}
else if (currentField.type == 'date') {
field.type_x = 'Date';
}
else if (currentField.type == 'lookup') {
field.type_x = 'Lookup';
field.relationshipLabel = currentField.relationshipLabel;
field.relationshipName = currentField.relationshipName;
field.referenceTo = currentField.referenceTo;
}
else if (currentField.type == 'longTextArea') {
field.type_x = 'LongTextArea';
field.length = Integer.valueOf(currentField.textLength);
field.visibleLines = Integer.valueOf(currentField.visibleLines);
}
else if (currentField.type == 'masterdetail') {
field.type_x = 'MasterDetail';
field.relationshipLabel = currentField.relationshipLabel;
field.relationshipName = currentField.relationshipName;
field.referenceTo = currentField.referenceTo;
}
return field;
}
}
Here's a concise explanation of the key components:
fullName: Represents the API name of your object. Custom objects should always end with ‘__c’.
label & pluralLabel: These dictate how the object will be displayed within Salesforce's UI.
deploymentStatus: When set to 'Deployed', the object becomes readily available. If not, it stays in a developmental phase.
sharingModel: This is an indicator of the data access settings. A 'ReadWrite' status allows users to both view and modify the data by default.
referenceTo:- It will hold the related object API name.
Dive Deep into Methods:
createObject: Feed it an ObjectWrapper, and watch it birth a new object in your org.
createDifferentTypeField: Gift it a list of Field Wrappers and an Object name, and it'll craft fields based on type, right in your org.
createService: A hero in disguise! Since LWC doesn't spill the session id beans (security first, folks!), we'll fetch it through a visualforce page.
The different Classes that are involved in the successful execution of the service handler method are:
GetSessionIdController.cls
global with sharing class GetSessionIdController {
global static String getSessionIdFromVFPage(PageReference visualforcePage){
String content = visualforcePage.getContent().toString();
Integer s = content.indexOf('Start_Of_Session_Id') + 'Start_Of_Session_Id'.length(),
e = content.indexOf('End_Of_Session_Id');
return content.substring(s, e);
}
}
SessionId.page
SObjectDomain.cls
ObjectWrapper.cls
SObjectProcessor.cls
SObjectWrapper.cls
createCustomFieldController.cls
createCustomObjectController.cls
fieldWrapper.cls
After Creating all these classes We need to create three LWC.
1. CreateCustomFields
CreateCustomFields.js
import { LightningElement, track, wire } from'lwc';
import getSObjects from'@salesforce/apex/createCustomFieldController.getSObjects';
import createFields from'@salesforce/apex/createCustomFieldController.createFields';
import { ShowToastEvent } from'lightning/platformShowToastEvent';
import FIELD_REMOVE_ERRORMESSAGE from'@salesforce/label/c.FIELD_REMOVE_ERRORMESSAGE';
import ERROR_MESSAGE from'@salesforce/label/c.ERROR_MESSAGE';
import REMOVE_THIS_FIELD from'@salesforce/label/c.REMOVE_THIS_FIELD';
import FIELD_LABEL from'@salesforce/label/c.FIELD_LABEL';
import FIELD_API_NAME from'@salesforce/label/c.FIELD_API_NAME';
import FIELD_TYPE from'@salesforce/label/c.FIELD_TYPE';
import TEXT_LENGTH from'@salesforce/label/c.TEXT_LENGTH';
import TEXT_LENGTH_LONGTEXT from'@salesforce/label/c.TEXT_LENGTH_LONGTEXT';
import VISIBLE_LINES from'@salesforce/label/c.VISIBLE_LINES';
import NUMBER_LENGTH from'@salesforce/label/c.NUMBER_LENGTH';
import DECIMAL_PLACES from'@salesforce/label/c.DECIMAL_PLACES';
import DEFAUL_VALUE from'@salesforce/label/c.DEFAUL_VALUE';
import RELATIONSHIP_LABEL from'@salesforce/label/c.RELATIONSHIP_LABEL';
import RELATIONSHIP_NAME from'@salesforce/label/c.RELATIONSHIP_NAME';
import RELETED_TO from'@salesforce/label/c.RELETED_TO';
import ADD_FIELD from'@salesforce/label/c.ADD_FIELD';
import CREATE_FIELDS from'@salesforce/label/c.CREATE_FIELDS';
import ERROR from'@salesforce/label/c.ERROR';
import SUCCESS from'@salesforce/label/c.SUCCESS';
import FIELD_CREATION_SUCCESS_MESSAGE from'@salesforce/label/c.FIELD_CREATION_SUCCESS_MESSAGE';
import FIELD_CREATION_ERROR_MESSAGE from'@salesforce/label/c.FIELD_CREATION_ERROR_MESSAGE';
import SUCCESS_TITLE from'@salesforce/label/c.SUCCESS_TITLE';
import ERROR_TITLE from'@salesforce/label/c.ERROR_TITLE';
import { getToRenderFieldType, getFieldValue, getFieldTypeOption, getCheckBoxDefalutOptions, getFormatedAPIName, hasSpecialCharacters } from'./customFieldHelper.js'
export default class CreateCustomFields extends LightningElement {
isObjectSelected = false;
SObjectList;
selectObjects = '';
errorFound = false;
errorMessage = '';
isRequired = true;
@track referenceObjectList;
@track allData = [
{
field: getFieldValue(),
componentType: getToRenderFieldType(),
isErrorFound : false,
fieldNumber : 1
}
]
label = {
REMOVE_THIS_FIELD :REMOVE_THIS_FIELD,
FIELD_LABEL : FIELD_LABEL,
FIELD_API_NAME : FIELD_API_NAME,
FIELD_TYPE : FIELD_TYPE,
TEXT_LENGTH : TEXT_LENGTH,
TEXT_LENGTH_LONGTEXT : TEXT_LENGTH_LONGTEXT,
VISIBLE_LINES : VISIBLE_LINES,
NUMBER_LENGTH : NUMBER_LENGTH,
DECIMAL_PLACES : DECIMAL_PLACES,
DEFAUL_VALUE : DEFAUL_VALUE,
RELATIONSHIP_LABEL : RELATIONSHIP_LABEL,
RELATIONSHIP_NAME : RELATIONSHIP_NAME,
RELETED_TO : RELETED_TO,
ADD_FIELD : ADD_FIELD,
CREATE_FIELDS : CREATE_FIELDS
}
@track fieldTypeOptions = getFieldTypeOption();
checkBoxDefaulOption = getCheckBoxDefalutOptions();
allFields =[];
scaleMaxValue;
isLoaded = true;
@wire(getSObjects) getSObjectsData({ data, error }) {
if (data) {
this.SObjectList = data;
}
else if (error) {
}
}
addField() {
let fieldtemp = getFieldValue();
fieldtemp.id = this.allData[this.allData.length-1].field.id + 1;
this.allData = [...this.allData, {
field: fieldtemp,
componentType: getToRenderFieldType(),
fieldNumber : fieldtemp.id + 1
}];
}
createFields() {
this.isLoaded = false;
for(let i=0; i<this.allData.length; i++){
this.allFields.push(this.allData[i].field);
}
createFields({inputData: JSON.stringify(this.allFields), objectName : this.selectObjects})
.then (result => {
this.allData = [
{
field: getFieldValue(),
componentType: getToRenderFieldType(),
isErrorFound : false,
}
]
this.showToast(SUCCESS_TITLE, FIELD_CREATION_SUCCESS_MESSAGE, SUCCESS);
this.isLoaded = true;
})
.catch (error => {
this.showToast(ERROR_TITLE, FIELD_CREATION_ERROR_MESSAGE, ERROR);
})
}
handleTypeChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.type = event.target.value;
this.handleFieldTypeChange(event.target.value, index);
}
handleTextLengthChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.textLength = event.target.value;
this.scaleMaxValue = 18 - event.target.value;
}
handlevisibleLinesChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.visibleLines = event.target.value;
}
handleScaleChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.scale = event.target.value;
}
handleTextAreaTypeChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.textAreaType = event.target.value;
}
handleCheckBoxDefaultChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.defaultvalue = event.target.value;
}
handleRelationshipLabelChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.relationshipLabel = event.target.value;
}
handleRelationshipNameChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.relationshipName = event.target.value;
}
handleReferenceToChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.referenceTo = event.target.value;
}
handleTypeChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.type = event.target.value;
this.handleFieldTypeChange(event.target.value, index);
}
handleTextLengthChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.textLength = event.target.value;
this.scaleMaxValue = 18 - event.target.value;
}
handlevisibleLinesChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.visibleLines = event.target.value;
}
handleScaleChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.scale = event.target.value;
}
handleTextAreaTypeChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.textAreaType = event.target.value;
}
handleCheckBoxDefaultChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.defaultvalue = event.target.value;
}
handleRelationshipLabelChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.relationshipLabel = event.target.value;
}
handleRelationshipNameChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.relationshipName = event.target.value;
}
handleReferenceToChange(event) {
const index = event.target.dataset.index;
this.allData[index].field.referenceTo = event.target.value;
}
handleSelectObjectChange(evert) {
this.selectObjects = evert.target.value;
if (this.selectObjects !== '') {
this.isObjectSelected = true;
}
this.referenceObjectList = this.SObjectList.filter(obj => obj.value !== this.selectObjects);
}
handleFieldTypeChange(value, index) {
this.allData[index].componentType = getToRenderFieldType();
if (value === "text") {
this.allData[index].componentType.isTextComponent = true;
}
else if (value === "number") {
this.allData[index].componentType.isNumberComponent = true;
}
else if (value === "checkbox") {
this.allData[index].componentType.isCheckboxComponent = true;
}
else if (value === "date") {
this.allData[index].componentType.isDateComponent = true;
}
else if (value === "lookup") {
this.allData[index].componentType.isLookupComponent = true;
}
else if (value === "longTextArea") {
this.allData[index].componentType.isLongTextAreaComponent = true;
}
else if (value === "masterdetail") {
this.allData[index].componentType.isMasterDetailComponent = true;
}
else if (value === "pickList") {
this.allData[index].componentType.isPickListComponent = true;
}
}
showToast(title, message, variant) {
this.dispatchEvent(
new ShowToastEvent({
title : title,
message : message,
variant : variant,
mode :'dismissable'
})
);
}
}
CreateCustomFields.html
<template>
<lightning-card title="Field Creator">
<div class="slds-m-around_large">
<div if:true={isLoaded}>
<div class="slds-p-horizontal_medium slds-p-vertical_medium">
<div class="slds-grid">
<div class="slds-col">
<lightning-combobox label="Select Object" value={selectObjects} options={SObjectList}
onchange= {handleSelectObjectChange} required={isRequired} ></lightning-combobox>
</div>
</div>
<template for:each={allData} for:item="data" for:index="index">
<div key={data.field.id}>
<template if:true={isObjectSelected}>
<hr style="margin-bottom: 2px"/>
<div class="slds-grid slds-gutters_small">
<div class="slds-col slds-side_6-of-12">
<h1>Field Number - {data.fieldNumber}</h1>
</div>
<div class="slds-col slds-side_6-of-12" style="text-align: right;">
<lightning-button label={label.REMOVE_THIS_FIELD} variant="distractive" data-index={index} onclick={handleDelete}></lightning-button>
</div>
</div>
<span if:true={data.fieldRemoveError} style="font-size: 10px;padding-left: 10px;color: red;">{data.fieldRemoveErrorMessage}</span>
<hr style="margin-top: 2px"/>
<div class="slds-grid slds-gutters_small">
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.FIELD_LABEL} value={data.field.label} onchange={handleLabelChange}
data-index={index} required={isRequired}></lightning-input>
<span if:true={data.isErrorFound} style="font-size: 10px;padding-left: 10px;color: red;">{errorMessage}</span>
</div>
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.FIELD_API_NAME} value={data.field.apiName}
onchange={handleAPINameChange} data-index={index} required={isRequired} readonly={isRequired}></lightning-input>
</div>
</div>
<div class="slds-grid">
<div class="slds-col slds-side_12-of-12">
<lightning-combobox label={label.FIELD_TYPE} value={data.field.type} options={fieldTypeOptions}
onchange={handleTypeChange} data-index={index} required={isRequired}></lightning-combobox>
</div>
</div>
</template>
<template if:true={data.componentType.isTextComponent}>
<lightning-input label={label.TEXT_LENGTH} type="number" value={data.field.textLength}
onchange={handleTextLengthChange} data-index={index} min = "1" max = "255" required={isRequired}></lightning-input>
</template>
<template if:true={data.componentType.isLongTextAreaComponent}>
<div class="slds-grid slds-gutters_small">
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.TEXT_LENGTH_LONGTEXT} type="number" value={data.field.textLength}
onchange={handleTextLengthChange} data-index={index} min = "255" max = "131072" required={isRequired}></lightning-input>
</div>
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.VISIBLE_LINES} type="number" value={data.field.visibleLines}
onchange={handlevisibleLinesChange} data-index={index} min = "2" max = "99" required={isRequired}></lightning-input>
<span if:true={data.isErrorFound} style="font-size: 10px;padding-left: 10px;color: red;">{errorMessage}</span>
</div>
</div>
</template>
<template if:true={data.componentType.isNumberComponent}>
<div class="slds-grid slds-gutters_small">
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.NUMBER_LENGTH} type="number" value={data.field.textLength}
onchange={handleTextLengthChange} data-index={index} min = "1" max = "18" required={isRequired}></lightning-input>
</div>
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.DECIMAL_PLACES} type="number" value={data.field.scale}
onchange={handleScaleChange} data-index={index} min = "0" max = {scaleMaxValue} required={isRequired}></lightning-input>
</div>
</div>
</template>
<template if:true={data.componentType.isCheckboxComponent}>
<lightning-combobox label={label.DEFAUL_VALUE} value={data.field.defaultvalue} options={checkBoxDefaulOption}
onchange={handleCheckBoxDefaultChange} data-index={index}></lightning-combobox>
</template>
<template if:true={data.componentType.isLookupComponent}>
<div class="slds-grid slds-gutters_small">
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.RELATIONSHIP_LABEL} value={data.field.relationshipLabel} onchange={handleRelationshipLabelChange}
data-index={index} required={isRequired}></lightning-input>
</div>
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.RELATIONSHIP_NAME} value={data.field.relationshipName}
onchange={handleRelationshipNameChange} data-index={index} required={isRequired}></lightning-input>
</div>
</div>
<div class="slds-grid">
<div class="slds-col slds-side_12-of-12">
<lightning-combobox label={label.RELETED_TO} value={data.field.referenceTo} options={referenceObjectList}
onchange={handleReferenceToChange} data-index={index} required={isRequired}></lightning-combobox>
</div>
</div>
</template>
<template if:true={data.componentType.isMasterDetailComponent}>
<div class="slds-grid slds-gutters_small">
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.RELATIONSHIP_LABEL} value={data.field.relationshipLabel} onchange={handleRelationshipLabelChange}
data-index={index} required={isRequired}></lightning-input>
</div>
<div class="slds-col slds-side_6-of-12">
<lightning-input label={label.RELATIONSHIP_NAME} value={data.field.relationshipName}
onchange={handleRelationshipNameChange} data-index={index} required={isRequired}></lightning-input>
</div>
</div>
<div class="slds-grid">
<div class="slds-col slds-side_12-of-12">
<lightning-combobox label={label.RELETED_TO} value={data.field.referenceTo} options={referenceObjectList}
onchange={handleReferenceToChange} data-index={index} required={isRequired}></lightning-combobox>
</div>
</div>
</template>
</div>
</template>
<div class="slds-p-top_small">
<lightning-button class= "slds-p-around_small" label={label.ADD_FIELD} variant="brand" onclick={addField}></lightning-button>
<lightning-button class= "slds-p-around_small" label={label.CREATE_FIELDS} variant="brand" onclick={createFields}></lightning-button>
</div>
</div>
</div>
<div if:false={isLoaded} class="slds-is-relative slds-align_absolute-center" style="height:100px;">
<lightning-spinner
alternative-text="Loading..." variant="brand">
</lightning-spinner>
</div>
</div>
</lightning-card>
</template>
CreateCustomFields.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>56.0</apiVersion>
<isExposed>true</isExposed>
<targets>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
</targets>
</LightningComponentBundle>
customFieldHelper.js
const getToRenderFieldType = () => {
return {
"isTextComponent" : false,
"isNumberComponent" : false,
"isCheckboxComponent" : false,
"isDateComponent" : false,
"isLookupComponent" : false,
"isLongTextAreaComponent" : false,
"isMasterDetailComponent" : false,
"isPickListComponent" : false,
}
}
const getFieldValue = () => {
return{
"id":0,
"label":"",
"apiName":"",
"type":"",
"textLength":"",
"defaultvalue":"",
"relationshipLabel" : "",
"relationshipName" : "",
"referenceTo" : "",
"visibleLines" : "",
"scale" : 0,
}
}
const getFieldTypeOption = () => {
return [
{ label: 'None', value: '' },
{ label: 'Text', value: 'text' },
{ label: 'Number', value: 'number' },
{ label: 'Checkbox', value: 'checkbox' },
{ label: 'Date', value: 'date' },
{ label: 'Lookup', value: 'lookup' },
{ label: 'Long Text Area', value: 'longTextArea' },
{ label: 'Master Detail', value: 'masterdetail' },
];
}
const getCheckBoxDefalutOptions = () => {
return [
{ label: 'None', value: '' },
{ label: 'True', value: 'true' },
{ label: 'False', value: 'false' },
]
}
const getFormatedAPIName = (inputString) => {
if(inputString !== ''){
inputString = inputString.trim();
let modifiedString = inputString.replace(/\s+/g, "_");
if(!modifiedString.endsWith("__c")){
modifiedString = modifiedString + "__c";
}
return modifiedString;
}
else {
return '';
}
}
const hasSpecialCharacters = (inputString) => {
// Regular expression to match special characters
var specialCharsRegex = /[^A-Za-z0-9\s]/;
// Check if the input string contains any special characters
return specialCharsRegex.test(inputString);
}
export {
getToRenderFieldType,
getFieldValue,
getFieldTypeOption,
getCheckBoxDefalutOptions,
getFormatedAPIName,
hasSpecialCharacters
};
2. CreateCustomObject
3. Object Manager
In this way, you can create multiple fields at a time with different field types.
Happy Coding! You can leave a comment to help me understand how the blog helped you. If you need further assistance, please get in touch with us at Reach us. You can click "Reach Us" on the website and share the issue with me.
Blog Credit:
S. Chaudhary
Salesforce Developer
Avenoir Technologies Pvt. Ltd.
Reach us: team@avenoir.ai
Comments