Environment Variables in Custom Metadata Types

Developing custom solutions for Salesforce has some challenges. One of them is having Environment Variables for your application, that can be deployed from org to org without having to change them. We’ve been using Custom Metadata Types (CMT), but once it get to another environment such as production, we end up having to change it. This is basically hardcoding it – which is bad.

What I want is something like an .ini file, that has the environment (org) and key values like below:

[orgname1]

key1=val1
key2=val2
...

[orgname2]

key1=val3
key2=val4
...

This basically can get deployed to any org, and it will just dynamically map to the right set of key values.

Why not use Custom Settings?

So custom settings are independent from org to org. This sounds like the perfect solution for our scenario. But I want something in 1 file – similar to .env files in Node. One that I can include in the SFDX project and get deployed. I believe deploying custom settings – will deploy the same records (unless you do something like below).

Why not YAML or JSON?

I found that INI format is easier for admins to edit. All they have to worry about is grouping by orgs (enclosed in brackets) and immediately following are key value pairs separated by a new line. Note that the format is key=val.

In addition, YAML and JSON requires additional rules such as spaces, tabs, curly braces and commas, and the CMT editor is hard to work with when it comes to formatting. Again, INI format is easier for admins.

I also chose CMT – because its a bit more secure and flexible to work with. You can also use a static file (static resource) or an object. But I don’t want an important file just sitting in a pool of static resources, as well as waste a SOQL call for the later.

Setup the Custom Metadata Type

First step is to create the CMT. Click create a new custom metadata type, fill in the fields and hit “Save“. I chose the name “LS Setting“, so we can store other records in this CMT.

Now add a custom field. This will store the actual contents of our file. Click on “New Custom Field” and fill in the fields. Use a Text Area Long type for this field. I named ours “Value“.

Now let’s enter a record. Name it whatever makes sense to you – in our case, its “Environment Variables” and for the “Value” – add our INI content and click “Save”.

Simply replace the highlighted parts with your org names, enclosed in brackets.

Parsing the Variables with Apex

So this is the fun part. We have a file that we can deploy – we simply have to write the logic that looks in this file and get’s the correct value that we need.

First, I want two static methods that I can use in combination with each other, to produce the desired results:

//first method:
// can return a list of all key value pairs for the org
getEnvVars(null); 
// returns a list with just the string value for spec key passed
getEnvVars('key1'); 

// second method: 
// can be used to get just 1 value 
getEnvVar('key1', envVars);

The purpose of the second method Class.getEnvVar(‘key1’, envVars); is so that we can use it in a class multiple times, without having to parse again and again. We can get the 2nd parameter by running the first method in the class constructor or something like that.

The getEnvVars() method

Let’s build the core of the parser here. So we need to go through each line of the file and put them in the necessary maps and lists. Let’s built the method shell and add the data structures:

On a side note, I know developers are always looking for freelance work. I discovered Braintrust: a user-owned talent platform created by and for the world's top talent. Head over to Braintrust and signup today!
public static List<String> getEnvVars(String key){
    List<String> result = new List<String>(); //final result
    Map<String,List<String>> rawMap = new Map<String,List<String>>(); //temp store
    List<Integer> envIndexes = new List<Integer>(); //indexes of our orgs
    List<Integer> nodeIndexes = new List<Integer>(); //indexes of key=values

    //logic here...



    return result;
}

I guess it would also help to grab the CMT. Add this in our function:

List<OTP_Setting__mdt> settings = [SELECT Value__c FROM LS_Setting__mdt WHERE DeveloperName = 'Environment_Variables'];
String metaData = settings[0].Value__c; 

Then let’s do a split on metadata. Let’s also remove the duplicates and populate the envIndexes and nodeIndexes in one go.

List<String> rawList = new List<String>();
for(String str : metaData.split('\n')){ //REMOVES EMPTY LINES...
   if(String.isNotBlank(str)){
      rawList.add(str);
   }
}
Integer index = 0; 
for(String str : rawList){
  if(str.startsWith('[')){ //AN ENVIRONMENT
     envIndexes.add(index);
  }else if(str.contains('=')){ //A NODE KEY/VAL has to have an '='
     nodeIndexes.add(index);
  }
  index++;
}

Now let’s go through our lists of environment and node indexes, and build the rawMap map. One thing to note here, we have 2 arrays of indexes, and a rawList of values. See below:

// rawList [node1,kv1,kv2,kv3,node2,kv4,kv5,kv6]
// envIndexes [0,4]
//             0 1
// nodeIndexes [1,2,3,5,6,7]
//              0 1 2 3 4 5 

So rawList is flat, and we have the arrays of indexes, so we can get the values of each item in rawList according to the indexes. Also, we determine which org they belong to by doing a < and > comparison to the next environment index.

for(Integer y=0; y<rawList.size();y++){
    for(Integer i=0; i<envIndexes.size();i++){
        if(y == envIndexes[i]){
           List<String> nodes = new List<String>();
               for(Integer x=0; x<nodeIndexes.size();x++){
                   if(i == envIndexes.size()-1){ //at the last envindex
                       if(nodeIndexes[x] > envIndexes[i]){
                          nodes.add(rawList[nodeIndexes[x]]);
                       }
                   }else{ //not last index
                       if(nodeIndexes[x] > envIndexes[i] && nodeIndexes[x] < envIndexes[i+1] ){
                          nodes.add(rawList[nodeIndexes[x]]);
                       }
                   }                            
                }       
     String env = rawList[envIndexes[i]];     
     env = env.replace('[','').replace(']','').toLowerCase().trim();
     rawMap.put(env,nodes);
     }
  }
}

Finally, let’s build the result – depending on the environment, and if there is a key parameter or not

String curOrg = URL.getSalesforceBaseUrl().getHost(); 
List<String> envVars = rawMap.get(curOrg);

if(key == null){
   result = envVars;
}else{
   result.add(Class.getEnvVar(key,envVars)); //we haven't built this yet...
}      

The getEnvVar() method

This one is a small method that we use above – but we can also call in our other classes.

 public static String getEnvVar(String key, List<String> envVars){
    String result = ''; 
    for(String ev : envVars){                 
       if(ev.startsWithIgnoreCase(key)){
          List<String> temp = ev.split('=');
          if(temp[1] != null){
             result = temp[1];
          }
        }                
      }
    return result;
 }

Now we can use the two methods anywhere in our project where we need environment variables.

Don’t forget to include this in your SFDX project using VSCode, I recommend using SFDX PackageXML generator – so you can select the custom object (custom metadata type), custom field and the actual record that you’ve created. This makes it so that you can commit to your repo using GIT and deploy to any org of your choice.

Improvements

Of course this code can be improved drastically. One thing I can think of is having a “default” org fallback. This is so that you don’t have to replicate entire blocks of key=values for every org – especially if they share the same settings. Also maybe an “inherited” type of org, where you can do multiple orgs in separate lines that share the same settings. I’ll leave that up to you.

Feel free to check out the code in Github.

affiliate link arrowdigitalocean banner

Leave a Comment.