Apex Integrations SF Standard 

Remote Control Your Picklists and Global Value Sets

What’s more annoying than a picklist? A Global Value Set. What’s more annoying than a Global Value Set? A Global Value Set that requires regular maintenance.

But you can update picklists and Global Value Sets in Visual Studio Code, adding, removing (actually, deactivating), repositioning and sorting values as you wish, you might intervene now. All correct, but what if picklist values are determined from a, say, third party system and you simply don’t have a working student to keep the values updated on a, say, hourly base?

That’s where the beloved Metadata API comes in handy. Below you’ll find a collection of a total of 7 APEXREST enabled classes, which enable you to remote control available values in your local and global picklist. All you need as a prerequisite are

  • Salesforce’s MetadataService class (don’t forget to add the MetadataServiceTest class when productionizing – you won’t need all classes though), and
  • a custom PicklistHelper class, a collection of user-friendly methods abstracted from MetadataService.

Here comes the overview of all available classes – one for each operation:

  • GlobalValueSetActivateService
  • GlobalValueSetDeactivateService
  • GlobalValueSetUpdateService
  • PicklistActivateService
  • PicklistAddToRecordTypeService
  • PicklistDeactivateService
  • PicklistUpdateService

As you see, it’s still not possible to delete picklist or global values – while Salesforce does not state this officially anywhere, there is simply no such operation available in the Metadata API. Deactivating a picklist value remains the highest of highs for the time being.

Class collection

PicklistHelper

public without sharing class PicklistHelper {
    /**
     * @description     Initializes a MetadataService.MetadataPort and populates it with the user session ID, default session headers and a custom configured timeout
     * @return          A configured MetadataService.MetadataPort
     */
    public static MetadataService.MetadataPort createService(){
        MetadataService.MetadataPort service = new MetadataService.MetadataPort();
        service.SessionHeader = new MetadataService.SessionHeader_element();
        service.SessionHeader.sessionId = UserInfo.getSessionId();
        service.timeout_x=120000;
        return service;
    }

    /**
     * @description         Retrieves Picklist Values for a Record Type
     * @param sObjectName   String; API Name of sObject
     * @param fieldName     String; API Name of Field
     * @return              List<Schema.PicklistEntry> Picklist Values of the specified field
     */
    public static List<Schema.PicklistEntry> retrievePicklistData(String sObjectName, String fieldName){
        // Prepare the values of the picklist field
        List<Schema.PicklistEntry> ple = Schema.getGlobalDescribe()?.get(sObjectName)?.getDescribe()?.fields.getMap()?.get(fieldName)?.getDescribe().getPicklistValues(); 
        return ple;
    }
    
    /**
     * @description        Constructs an empty Custom Field of the Picklist Type to be populated in the Service classes
     * @param sObjectName  String; API Name of sObject
     * @param fieldName    String; API Name of Field
     * @return             MetadataService.CustomField Custom Field Definition
     */
    public static MetadataService.CustomField createCustomField(String sObjectName, String fieldName){
        MetadataService.CustomField customField = new MetadataService.CustomField(); 
        customField.fullName = sObjectName + '.' + fieldName;        
        customField.label = Schema.getGlobalDescribe()?.get(sObjectName)?.getDescribe()?.fields.getMap()?.get(fieldName)?.getDescribe().getLabel();
        customField.type_x = 'Picklist';
        return customField;
    }
    
    /**
     * @description                 Saves specified Metadata, with options to notify admins on Success
     * @param service               MetadataService.MetadataPort; An open Metadata Port
     * @param obj                   MetadataService.Metadata; prepared Metadata to save
     * @param notify                Boolean; whether to notify Administrators
     * @param notificationMessage   String; message to notify Administrators
     * @return                      Boolean: true for success; false for error (save errors)
     */
    public static Boolean saveMetadata(MetadataService.MetadataPort service, MetadataService.Metadata obj, Boolean notify, String notificationMessage){
        List<MetadataService.SaveResult> lstResults = service.updateMetadata( new List<MetadataService.Metadata>{ obj });
        if(lstResults != null && lstResults.size() > 0){
            for(MetadataService.SaveResult result : lstResults) {
                if(result.errors != null) {
                    // Error logging
                    ErrorLog__c errorLog = new ErrorLog__c(
                        ErrorBody__c = 'Metadata Save Error. First Error: ' + result.errors.toString(),
                        RequestBody__c = 'Request: ' + obj.toString(),
                        Process__c = 'Metadata Insert'
                    );
                    insert errorLog;
                    return false;
                }
            }   
            if(notify){
                NotifyAdminController.sendNotification('Metadata updated!', notificationMessage, null);
            }
            return true;
        }   
        // Error logging
        ErrorLog__c errorLog = new ErrorLog__c(
            ErrorBody__c = 'Metadata Save Error. No results',
            RequestBody__c = 'Request: ' + obj.toString(),
            Process__c = 'Metadata Insert'
        );
        insert errorLog;
        return false;
    }
    
    /**
     * @description        Apply a Value Set to a Custom Field (unsorted)
     * @param customField  MetadataService.CustomField; Custom Field definition
     * @param newValues    MetadataService.CustomValue; List of Values to apply to Custom Field
     * @param sortMe       Boolean; with alpha sorting = true; without alpha sorting = false
     * @return             MetadataService.CustomField Manipulated (but not yet saved) Custom Field
     */
    public static MetadataService.CustomField applyNewValueSet(MetadataService.CustomField customField, List<MetadataService.CustomValue> newValues, Boolean sorted){
        customField.valueSet = new MetadataService.ValueSet();
        MetadataService.ValueSetValuesDefinition vd = new MetadataService.ValueSetValuesDefinition();
        vd.sorted = sorted;
        vd.value = newValues;
        customField.valueSet.valueSetDefinition = vd;
        return customField;
    }
}

GlobalValueSetActivateService

@RestResource(urlMapping='/GlobalValueSetActivateService/*')
global without sharing class GlobalValueSetActivateService{
    /**
     * @routing            /GlobalValueSetActivateService
     * @description        Activates a value on a Global Value Set. newValue does not necessarily have to exist on the Global Value Set,
     * so this can also be used to create and immediately activate a value on a Global Value Set.
     * @caution            If any field on the Global Value Set is on an object which uses Record Types, the Picklist Value needs to be added
     * to applicable Record Types via PicklistValueRecordTypeService 
     * @param valueSetName String; API Name of the Global Value Set
     * @param newValue     String; API Name of the old value
     * @param newLabel     String; it's possible to give the new value a different label; usually just set the same value as for newValue
     * @param position     Integer; at which position the new value should be inserted
     * @param isDefault    Boolean; whether the activated value should be set as the Global Value Set's Default Value
     * @return             Boolean; success = true; failure = false
     *                     Potential failure reasons: Global Value Set not retrieved; Global Value Set empty; Save errors
     */
    @HttpPost
    global static Boolean activateGlobalValue(String valueSetName, String newValue, String newLabel, Integer position, Boolean isDefault, Boolean sortMe) {
        // Creates Service and retrieves Global Value Set values
        MetadataService.MetadataPort service = PicklistHelper.createService();
        MetadataService.IReadResult readResult = service.readMetadata('GlobalValueSet', new String[] { valueSetName });
        if(readResult != null){
            MetadataService.GlobalValueSet globalValueSet = (MetadataService.GlobalValueSet)readResult.getRecords()[0];
            // Create a new list of values
            List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
            // Loop through values retrieved from Global Value Set and let a counter run
            // Fill the new list with retrieved values unless the retrieved value is the one to activate
            // If the counter is at the desired position, put the new value here and set it to active
            if(globalValueSet.customValue != null){
                Integer i = 0;
                for (MetadataService.CustomValue customValue : globalValueSet.customValue) {
                    i++;
                    if(i == position){
                        MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
                        newCustomValue.fullName = newValue;
                        newCustomValue.label = newLabel;
                        newCustomValue.default_x = isDefault;
                        newCustomValue.isActive = true;
                        newValues.add(newCustomValue);
                    }
                    if(customValue.fullName != newValue){
                        MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
                        newCustomValue.fullName = customValue.fullName;
                        newCustomValue.label = customValue.label;
                        newCustomValue.default_x = !isDefault ? customValue.default_x : false;
                        newCustomValue.isActive = customValue.fullName == newValue ? true : customValue.isActive;
                        newValues.add(newCustomValue);
                    }
                }
                // Apply new Values to Global Value Set
                globalValueSet.customValue = newValues;
                globalValueSet.sorted = sortMe;
                // Save changed Metadata
                Boolean success = PicklistHelper.saveMetadata(service, globalValueSet, true, 'Picklist value activated on Global Value Set: ' + valueSetName + ' and value: ' + newValue + '.');
                return success;
            }   
            return false;
        }   
        return false;
    }
}

GlobalValueSetDeactivateService

@RestResource(urlMapping='/GlobalValueSetDeactivateService/*')
global without sharing class GlobalValueSetDeactivateService{

    /**
     * @routing            /GlobalValueSetDeactivateService
     * @description        Deactivates a value on a Global Value Set. If oldValue does not exist on the Global Value Set, no change is made.
     * @param valueSetName String; API Name of the Global Value Set
     * @param oldValue     String; API Name of the value to deactivate
     * @return             Boolean; success = true; failure = false
     *                     Potential failure reasons: Global Value Set not retrieved; Global Value Set empty; Save errors
     */  
    @HttpPost
    global static Boolean deactivateGlobalValue(String valueSetName, String oldValue, Boolean sortMe) {
        // Creates Service and retrieves Global Value Set values
        MetadataService.MetadataPort service = PicklistHelper.createService();
        MetadataService.IReadResult readResult = service.readMetadata('GlobalValueSet', new String[] { valueSetName });
        if(readResult != null){
            MetadataService.GlobalValueSet globalValueSet = (MetadataService.GlobalValueSet)readResult.getRecords()[0];
            // Create a new list of values
            List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();

            // Loop through values retrieved from Global Value Set and let a counter run
            // If value is not the value to deactivate, put the value into the new list
            if(globalValueSet.customValue != null){
                for (MetadataService.CustomValue customValue : globalValueSet.customValue) {
                    MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
                    newCustomValue.fullName = customValue.fullName;
                    newCustomValue.label = customValue.label;
                    newCustomValue.default_x = customValue.default_x;
                    newCustomValue.isActive = customValue.fullName == oldValue ? false : customValue.isActive;
                    newValues.add(newCustomValue);
                }
                // Apply new Values to Global Value Set
                globalValueSet.customValue = newValues;
                globalValueSet.sorted = sortMe;
                // Save changed Metadata
                return PicklistHelper.saveMetadata(service, globalValueSet, true, 'Picklist value deactivated on Global Value Set: ' + valueSetName + ' and value: ' + oldValue + '.');
            }   
            return false;
        }   
        return false;
    }
}

GlobalValueSetUpdateService

@RestResource(urlMapping='/GlobalValueSetUpdateService/*')
global without sharing class GlobalValueSetUpdateService{
    
    /**
     * @routing            /GlobalValueSetUpdateService
     * @description        Update a picklist value. Technically it consists of deactivating the old value, and entering another value at the same position.
     * Records carrying the old value will not be changed.
     * @param valueSetName String; API Name of the Global Value Set
     * @param newValue     String; API Name of the new value
     * @param newLabel     String; Label of the new value; usually same as newValue
     * @param oldValue     String; API Name of the old value
     * @param isDefault    Boolean; whether the new value should be set as the Global Value Set's Default Value
     * @return             Boolean; success = true; failure = false
     *                     Potential failure reasons: Global Value Set not retrieved; Global Value Set empty; Save errors
     */
    @HttpPost
    global static Boolean updateGlobalValue(String valueSetName, String newValue, String newLabel, String oldValue, Boolean isDefault, Boolean sortMe) {
        // Creates Service and retrieves Global Value Set values
        MetadataService.MetadataPort service = PicklistHelper.createService();
        MetadataService.IReadResult readResult = service.readMetadata('GlobalValueSet', new String[] { valueSetName });
        if(readResult != null){
            MetadataService.GlobalValueSet globalValueSet = (MetadataService.GlobalValueSet)readResult.getRecords()[0];
            // Create a new list of values
            List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();
            // Loop through values retrieved from Global Value Set
            // Stop execution if the value to rename to is already part of the Value Set
            // Otherwise put the looped value into the new list - unmanipulated if it's not the value to update, manipulated otherwise
            if(globalValueSet.customValue != null){
                for (MetadataService.CustomValue customValue : globalValueSet.customValue) {
                    MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
                    newCustomValue.isActive = customValue.isActive;
                    if(customValue.fullName == newValue){
                        return false;
                    } else if(customValue.fullName == oldValue){
                        newCustomValue.fullName = newValue;
                        newCustomValue.label = newLabel;
                        newCustomValue.default_x = isDefault;
                    } else {
                        newCustomValue.fullName = customValue.fullName;
                        newCustomValue.label = customValue.label;
                        newCustomValue.default_x = !isDefault ? customValue.default_x : false;
                    }
                    newValues.add(newCustomValue);
                }
                // Apply new Values to Global Value Set
                globalValueSet.customValue = newValues;
                globalValueSet.sorted = sortMe;
                // Save changed Metadata
                return PicklistHelper.saveMetadata(service, globalValueSet, true, 'Picklist value updated on Global Value Set: ' + valueSetName + ' from value: ' + oldValue + ' to value: ' + newValue + '.');
            }   
            return false;
        }   
        return false;
    }
}

PicklistActivateService

@RestResource(urlMapping='/PicklistActivateService/*')
global without sharing class PicklistActivateService{
  
    /**
     * @routing            /PicklistActivateService
     * @description        Activates a value on a Picklist. oldValue does not necessarily have to exist on the Picklist
     * so this can also be used to create and immediately activate a value on a Picklist.
     * @caution            If the field's object uses Record Types, the Picklist Value needs to be added to applicable Record Types via PicklistValueRecordTypeService 
     * @param sObjectName  String; API Name of the Object
     * @param fieldName    String; API Name of the field
     * @param oldValue     String; API Name of the old value
     * @param newLabel     String; it's possible to give the new value a different label; usually just set the same value as for oldValue
     * @param position     Integer; at which position the new value should be inserted
     * @param isDefault    Boolean; whether the activated value should be set as the Picklist's Default Value
     * @param sortMe       Boolean; with alpha sorting = true; without alpha sorting = false
     * @return             Boolean; success = true; failure = false
     *                     Potential failure reasons: Object or field invalid; field not a picklist; values empty; Save errors
     */
    @HttpPost
    global static Boolean activatePicklistValue(String sObjectName, String fieldName, String oldValue, String newLabel, Integer position, Boolean isDefault, Boolean sortMe) {
        // Creates Service and retrieves Picklist values
        MetadataService.MetadataPort service = PicklistHelper.createService();
        List<Schema.PicklistEntry> ple = PicklistHelper.retrievePicklistData(sObjectName, fieldName);
        MetadataService.CustomField customField = PicklistHelper.createCustomField(sObjectName, fieldName);

        // Create a new list of values
        List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();

        // Loop through values retrieved from Global Value Set and let a counter run
        // Fill the new list with retrieved values unless the retrieved value is the one to activate
        // If the counter is at the desired position, put the new value here and set it to active
        Integer i = 0;
        for(Schema.PicklistEntry pickListVal : ple) {
            i++;
            if(i == position){
                MetadataService.CustomValue newCustomValue = new MetadataService.CustomValue();
                newCustomValue.fullName = oldValue;
                newCustomValue.label = newLabel;
                newCustomValue.default_x = isDefault;
                newCustomValue.isActive = true;
                newValues.add(newCustomValue);
            }
            if(pickListVal.getValue() != oldValue){
                MetadataService.CustomValue customValue = new MetadataService.CustomValue();
                customValue.fullName = String.valueOf(pickListVal.getValue()); //API name of picklist value
                customValue.label = String.valueOf(pickListVal.getLabel());
                customValue.default_x = !isDefault ? pickListVal.isDefaultValue() : false;
                customValue.isActive = pickListVal.getValue() == oldValue ? true : pickListVal.isActive();
                newValues.add(customValue);
            }
        }
        
        // Apply new list to Custom Field
        customField = PicklistHelper.applyNewValueSet(customField, newValues, sortMe);
        // Save changed Metadata
        return PicklistHelper.saveMetadata(service, customField, true, 'Picklist value activated on Object: ' + sObjectName + ', field: ' + fieldName + ', value: ' + oldValue + '.');
    }
}

PicklistAddToRecordTypeService

@RestResource(urlMapping='/PicklistAddToRecordTypeService/*')
global without sharing class PicklistAddToRecordTypeService{
  
    /**
     * @routing            /PicklistAddToRecordTypeService
     * @description        Activates a Picklist Value for a Record Type.
     * 
     * @param sObjectName               String; API Name of the Object
     * @param fieldName                 String; API Name of the field
     * @param recordTypeDeveloperName   String; API Name of the Record Type
     * @param picklistValueToAdd        String; name of the value to add.
     * @param isDefault                 Boolean; whether the activated value should be set as the Picklist's Default Value
     * @return                          Boolean; success = true; failure = false
     *                                  Potential failure reasons: Object or field invalid; field not a picklist; picklistValueToAdd not on the Picklist; values empty; Save errors
     */
    @HttpPost
    global static Boolean addPicklistValue(String sObjectName, String fieldName, String recordTypeDeveloperName, String picklistValueToAdd, Boolean isDefault) {
        String auxSObjectName = (sObjectName == 'PersonAccount') ? 'Account' : sObjectName;
        // Creates Service and retrieves Picklist values
        MetadataService.MetadataPort service = PicklistHelper.createService();
        // Generate field
        MetadataService.CustomField customField = PicklistHelper.createCustomField(auxSObjectName, fieldName);
        // Validate
        List<Schema.PicklistEntry> picklistEntries = PicklistHelper.retrievePicklistData(auxSObjectName, fieldName);
        // Check if picklistValueToAdd exists on the Picklist
        Boolean hasValue = false;
        for(Schema.PicklistEntry pickListVal : picklistEntries) {
            if(String.valueOf(pickListVal.getValue()) == picklistValueToAdd){
                hasValue = true;
                break;
            }
        }
        if(hasValue == false){
            return hasValue;
        }
        // Retrieve Record Type and its Picklist Values
        MetadataService.IReadResult readResult = service.readMetadata('RecordType', new String[] { sObjectName + '.' + recordTypeDeveloperName });
        if(readResult != null){
            MetadataService.RecordType recordType = (MetadataService.RecordType) readResult.getRecords()[0];
            List<MetadataService.RecordTypePicklistValue> rtPickValues = recordType.picklistValues;
            // Create new List of Record Type Picklist Values
            List<MetadataService.RecordTypePicklistValue> newRTPicklistValues = new List<MetadataService.RecordTypePicklistValue>();
            // Loop through all Picklists under the Record Type Picklist Values. When we hit the field, add the value.
            for(MetadataService.RecordTypePicklistValue rtPickValue : rtPickValues){
                MetadataService.RecordTypePicklistValue auxRTPickValue = new MetadataService.RecordTypePicklistValue();
                auxRTPickValue.picklist = rtPickValue.picklist;
                if(rtPickValue.picklist == fieldName){
                    List<MetadataService.PicklistValue> newValues = rtPickValue.values;
                    MetadataService.PickListValue newPickVal = new MetadataService.PickListValue();
                    newPickVal.fullName = picklistValueToAdd;
                    newPickVal.isActive = true;
                    newPickVal.default_x = isDefault;
                    newValues.add(newPickVal);
                    auxRTPickValue.values = newValues;
                } else {
                    auxRTPickValue.values = rtPickValue.values;
                }
                newRTPicklistValues.add(auxRTPickValue);
            }
            // Apply new Record Type Picklist Values to Record Type
            recordType.PicklistValues = newRTPicklistValues;
            // Save changed Metadata
            return PicklistHelper.saveMetadata(service, recordType, false, null);
        }
        return false;
    }
}

PicklistDeactivateService

@RestResource(urlMapping='/PicklistDeactivateService/*')
global without sharing class PicklistDeactivateService{
  
    /**
     * @routing            /PicklistDeactivateService
     * @description        Deactivates a value on a Picklist Field. If oldValue does not exist on the Picklist, no change is made.
     * @param sObjectName  String; API Name of the Object
     * @param fieldName    String; API Name of the field
     * @param oldValue     String; API Name of the old value
     * @param sortMe       Boolean; with alpha sorting = true; without alpha sorting = false
     * @return             Boolean; success = true; failure = false
     *                     Potential failure reasons: Object or field invalid; field not a picklist; values empty; Save errors
     */
    @HttpPost
    global static Boolean deactivatePicklistValue(String sObjectName, String fieldName, String oldValue, Boolean sortMe) {
        // Creates Service and retrieves Picklist values
        MetadataService.MetadataPort service = PicklistHelper.createService();
        List<Schema.PicklistEntry> ple = PicklistHelper.retrievePicklistData(sObjectName, fieldName);
        MetadataService.CustomField customField = PicklistHelper.createCustomField(sObjectName, fieldName);
        // Create a new list of values
        List<MetadataService.CustomValue> newValues = new List<MetadataService.CustomValue>();

        // Loop through values retrieved from Global Value Set and let a counter run
        // If value is not the value to deactivate, put the value into the new list
        for(Schema.PicklistEntry pickListVal : ple) {
            MetadataService.CustomValue customValue = new MetadataService.CustomValue();
            customValue.fullName = String.valueOf(pickListVal.getValue());
            customValue.label = String.valueOf(pickListVal.getLabel());
            customValue.default_x = pickListVal.isDefaultValue();
            customValue.isActive = pickListVal.getValue() == oldValue ? false : pickListVal.isActive();
            newValues.add(customValue);
        }
        
        // Apply new list to Custom Field
        customField = PicklistHelper.applyNewValueSet(customField, newValues, sortMe);
        // Save changed Metadata
        return PicklistHelper.saveMetadata(service, customField, true, 'Picklist value deactivated on Object: ' + sObjectName + ', field: ' + fieldName + ', value: ' + oldValue + '.');
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *