Skip to content

Commit 508bd49

Browse files
authored
Added default deploy callback + new Flow input controls (#9)
* Add default callback + email controls in Flow * Updated README with details about using custom callbacks * Removed default picklist value on example picklist field * Fixed a test that was calling the wrong deploy method overload
1 parent 131de83 commit 508bd49

File tree

5 files changed

+234
-12
lines changed

5 files changed

+234
-12
lines changed

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,53 @@ Since Apex can already update custom metadata records (it just can't save the ch
5151
// Bonus, get the deployment job IDs if you want to monitor them
5252
List<Id> deploymentJobIds = CustomMetadataSaver.getDeploymentJobIds();
5353
```
54+
55+
## Custom Deployment Callback
56+
57+
When deploying metadata through Apex, Salesforce provides the ability to create a callback class using [Metadata.DeployCallback](https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_interface_Metadata_DeployCallback.htm).
58+
59+
### Using the Default Callback
60+
61+
Out-of-the-box, this `CustomMetadataSaver` uses a `private` inner class, `CustomMetadataSaver.DefaultDeployCallback`, when deploying CMDT records. It provides 2 options:
62+
63+
- `sendEmailOnError` - sends an email to the current user when there are 1 or more deployment errors
64+
- `sendEmailOnSuccess` - sends an email to the current user when there are no deployment errors
65+
66+
### Using Your Own Custom Callback
67+
68+
To use your own custom Apex class for the callback, first create your class similar to this example
69+
70+
```java
71+
public class ExampleCallback implements Metadata.DeployCallback {
72+
73+
public void handleResult(Metadata.DeployResult result, Metadata.DeployCallbackContext context) {
74+
System.debug('ExampleCallback is running!');
75+
}
76+
}
77+
78+
```
79+
80+
Within Apex, you can then pass an instance of your class as a parameter to the method `CustomMetadataSaver.deploy()`
81+
82+
```java
83+
// Create your CMDT records
84+
List<CustomMetadataDeployTest__mdt> myCMDTRecords = new List<CustomMetadataDeployTest__mdt>();
85+
86+
CustomMetadataDeployTest__mdt myExampleCMDT = new CustomMetadataDeployTest__mdt();
87+
myExampleCMDT.MasterLabel = 'My CMDT Record';
88+
myExampleCMDT.DeveloperName = 'My_CMDT_Record';
89+
myExampleCMDT.ExampleTextField__c = 'Some value';
90+
91+
myCMDTRecords.add(myExampleCMDT);
92+
93+
// Create an instance of your custom callback class
94+
ExampleCallback myCustomCallback = new ExampleCallback();
95+
96+
// Pass the CMDT records and your custom callback to CustomMetadataSaver
97+
CustomMetadataSaver.deploy(myCMDTRecords, myCustomCallback);
98+
99+
```
100+
101+
Within Flow, you can also specify a custom callback class by specifying the Apex class name within the deploy action
102+
103+
![Flow: Deploy with Custom Callback](./content/flow-deploy-with-custom-callback.png)
61.7 KB
Loading

force-app/main/custom-metadata-saver/classes/CustomMetadataSaver.cls

+134-10
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,35 @@
44
//----------------------------------------------------------------------------------------------------//
55

66
public inherited sharing class CustomMetadataSaver {
7+
private static final Boolean DEFAULT_SEND_EMAIL_ON_ERROR = false;
8+
private static final Boolean DEFAULT_SEND_EMAIL_ON_SUCCESS = false;
79
private static final List<String> DEPLOYMENT_JOB_IDS = new List<String>();
810
private static final Set<String> IGNORED_FIELD_NAMES = getIgnoredFieldNames();
911

1012
public class FlowInput {
11-
@InvocableVariable(required=true label='The collection of custom metadata type records to deploy')
13+
@InvocableVariable(required=true label='Custom Metadata Type Records to Deploy')
1214
public List<SObject> customMetadataRecords;
15+
16+
@InvocableVariable(
17+
required=true
18+
label='Send Email Alert if the Deployment Fails'
19+
description='This option is only used by the default callback Apex class.'
20+
)
21+
public Boolean sendEmailOnError;
22+
23+
@InvocableVariable(
24+
required=true
25+
label='Send Email Alert if the Deployment Succeeds'
26+
description='This option is only used by the default callback Apex class.'
27+
)
28+
public Boolean sendEmailOnSuccess;
29+
30+
@InvocableVariable(
31+
required=false
32+
label='(Optional) Custom Callback Apex Class'
33+
description='The name of your Apex class to execute after the deployment completes. When provided, this is used instead of the default callback.'
34+
)
35+
public String customCallbackName;
1336
}
1437

1538
private CustomMetadataSaver() {
@@ -18,27 +41,49 @@ public inherited sharing class CustomMetadataSaver {
1841

1942
@InvocableMethod(
2043
category='Custom Metadata'
21-
label='Deploy Changes to Custom Metadata Records'
44+
label='Deploy Custom Metadata Type Records'
2245
description='Deploys changes to the list of custom metadata records'
2346
)
24-
public static void deploy(List<FlowInput> inputs) {
47+
public static List<String> deploy(List<FlowInput> inputs) {
2548
System.debug('inputs==' + inputs);
2649

27-
List<SObject> consolidatedList = new List<SObject>();
50+
Boolean sendEmailOnError = DEFAULT_SEND_EMAIL_ON_ERROR;
51+
Boolean sendEmailOnSuccess = DEFAULT_SEND_EMAIL_ON_SUCCESS;
52+
String customCallbackName;
53+
54+
// Combine all CMDT records into 1 list so that there is only 1 deployment
55+
// TODO allow this to be controlled via FlowInput - there are uses cases for also having multiple deployments
56+
List<SObject> consolidatedCustomMetadataRecords = new List<SObject>();
2857
for (FlowInput input : inputs) {
29-
consolidatedList.addAll(input.customMetadataRecords);
58+
consolidatedCustomMetadataRecords.addAll(input.customMetadataRecords);
59+
// If any of the inputs confirm that an email should be sent, then send it - otherwise, no email will be sent
60+
if (input.sendEmailOnError == true) {
61+
sendEmailOnError = input.sendEmailOnError;
62+
}
63+
// If any of the inputs confirm that an email should be sent, then send it - otherwise, no email will be sent
64+
if (input.sendEmailOnSuccess == true) {
65+
sendEmailOnSuccess = input.sendEmailOnSuccess;
66+
}
67+
// Assumption: only a single custom callback will be specified
68+
// If we find a custom callback class name, update the variable
69+
if (String.isNotBlank(input.customCallbackName)) {
70+
customCallbackName = input.customCallbackName;
71+
}
3072
}
3173

32-
System.debug('consolidatedList==' + consolidatedList);
74+
System.debug('consolidatedCustomMetadataRecords==' + consolidatedCustomMetadataRecords);
75+
76+
Metadata.DeployCallback callback = getFlowDeployCallback(customCallbackName, sendEmailOnError, sendEmailOnSuccess);
77+
System.debug('callback==' + callback);
3378

34-
deploy(consolidatedList, null);
79+
return new List<String>{ deploy(consolidatedCustomMetadataRecords, callback) };
3580
}
3681

37-
public static void deploy(List<SObject> customMetadataRecords) {
38-
deploy(customMetadataRecords, null);
82+
public static String deploy(List<SObject> customMetadataRecords) {
83+
return deploy(customMetadataRecords, new DefaultDeployCallback());
3984
}
4085

41-
public static void deploy(List<SObject> customMetadataRecords, Metadata.DeployCallback callback) {
86+
public static String deploy(List<SObject> customMetadataRecords, Metadata.DeployCallback callback) {
4287
Metadata.DeployContainer deployment = new Metadata.DeployContainer();
4388

4489
for (SObject customMetadataRecord : customMetadataRecords) {
@@ -49,6 +94,8 @@ public inherited sharing class CustomMetadataSaver {
4994
String jobId = Test.isRunningTest() ? 'Fake Job ID' : Metadata.Operations.enqueueDeployment(deployment, callback);
5095
DEPLOYMENT_JOB_IDS.add(jobId);
5196
System.debug(LoggingLevel.INFO, 'Deployment Job ID: ' + jobId);
97+
98+
return jobId;
5299
}
53100

54101
public static List<String> getDeploymentJobIds() {
@@ -93,4 +140,81 @@ public inherited sharing class CustomMetadataSaver {
93140

94141
return customMetadata;
95142
}
143+
144+
private static Metadata.DeployCallback getFlowDeployCallback(String customCallbackName, Boolean sendEmailOnError, Boolean sendEmailOnSuccess) {
145+
Metadata.DeployCallback callback;
146+
if (String.isNotBlank(customCallbackName) && Type.forName(customCallbackName) != null) {
147+
// Dynamically create a new instance of the specified class
148+
// Assumption: specified class uses a parameterless constructor
149+
System.debug('customCallbackName==' + customCallbackName);
150+
callback = (Metadata.DeployCallback) Type.forName(customCallbackName).newInstance();
151+
} else {
152+
// If no custom callback class is specified, use the default inner class
153+
callback = new DefaultDeployCallback(sendEmailOnError, sendEmailOnSuccess);
154+
}
155+
return callback;
156+
}
157+
158+
private static void sendEmail(String subject, String textBody) {
159+
Messaging.SingleEmailMessage singleEmail = new Messaging.SingleEmailMessage();
160+
singleEmail.setToAddresses(new List<String>{ UserInfo.getUserEmail() });
161+
singleEmail.setSubject(subject);
162+
singleEmail.setPlainTextBody(textBody);
163+
164+
Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ singleEmail });
165+
}
166+
167+
// Inner class used as the default callback if no other callback is specified
168+
@testVisible
169+
private class DefaultDeployCallback implements Metadata.DeployCallback {
170+
@testVisible
171+
private Boolean success;
172+
173+
private Boolean sendEmailOnError;
174+
private Boolean sendEmailOnSuccess;
175+
176+
@testVisible
177+
private DefaultDeployCallback() {
178+
this(DEFAULT_SEND_EMAIL_ON_ERROR, DEFAULT_SEND_EMAIL_ON_SUCCESS);
179+
}
180+
@testVisible
181+
private DefaultDeployCallback(Boolean sendEmailOnError, Boolean sendEmailOnSuccess) {
182+
this.sendEmailOnError = sendEmailOnError;
183+
this.sendEmailOnSuccess = sendEmailOnSuccess;
184+
}
185+
186+
public void handleResult(Metadata.DeployResult result, Metadata.DeployCallbackContext context) {
187+
System.debug('deploy result.success==' + result.success);
188+
System.debug('deploy result==' + result);
189+
System.debug('deploy callback context==' + context);
190+
191+
System.debug('this.sendEmailOnError==' + this.sendEmailOnError);
192+
System.debug('this.sendEmailOnSuccess==' + this.sendEmailOnSuccess);
193+
194+
this.success = result.success;
195+
196+
// Build the email pieces
197+
String subject = 'Custom metadata type deployment completed';
198+
String textBody = 'Deployment ID {0} completed\nStatus: {1}\n{2} total items deployed\n{3} items failed\nDetails: {4}';
199+
List<Object> textReplacements = new List<Object>{
200+
result.id,
201+
result.status,
202+
result.numberComponentsTotal,
203+
result.numberComponentErrors,
204+
Json.serializePretty(result.details)
205+
};
206+
textBody = String.format(textBody, textReplacements);
207+
208+
// Send the email
209+
if (result.success == true && this.sendEmailOnSuccess == true) {
210+
System.debug('Deployment Succeeded!');
211+
212+
sendEmail(subject, textBody);
213+
} else if (result.success == false && this.sendEmailOnError == true) {
214+
System.debug('Deployment Failed!');
215+
216+
sendEmail(subject, textBody);
217+
}
218+
}
219+
}
96220
}

force-app/tests/custom-metadata-saver/classes/CustomMetadataSaver_Tests.cls

+49-1
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,64 @@ private class CustomMetadataSaver_Tests {
4141
List<CustomMetadataDeployTest__mdt> cdmtRecords = new List<CustomMetadataDeployTest__mdt>{ cmdtRecord };
4242
CustomMetadataSaver.FlowInput input = new CustomMetadataSaver.FlowInput();
4343
input.customMetadataRecords = cdmtRecords;
44+
input.sendEmailOnError = true;
45+
input.sendEmailOnSuccess = true;
4446

4547
List<CustomMetadataSaver.FlowInput> inputs = new List<CustomMetadataSaver.FlowInput>();
4648
inputs.add(input);
4749

4850
System.assertEquals(1, cdmtRecords.size());
4951

5052
Test.startTest();
51-
CustomMetadataSaver.deploy(cdmtRecords);
53+
CustomMetadataSaver.deploy(inputs);
5254
Test.stopTest();
5355

5456
System.assertEquals(1, CustomMetadataSaver.getDeploymentJobIds().size());
5557
}
58+
59+
@isTest
60+
static void it_should_handle_deploy_success() {
61+
// Instantiate the callback.
62+
Boolean sendEmailOnError = true;
63+
Boolean sendEmailOnSuccess = true;
64+
CustomMetadataSaver.DefaultDeployCallback callback = new CustomMetadataSaver.DefaultDeployCallback(sendEmailOnError, sendEmailOnSuccess);
65+
66+
// Create test result and context objects.
67+
Metadata.DeployResult result = new Metadata.DeployResult();
68+
result.success = true;
69+
result.numberComponentsTotal = 5;
70+
result.numberComponentErrors = 0;
71+
Metadata.DeployCallbackContext context = new Metadata.DeployCallbackContext();
72+
73+
System.assertEquals(null, callback.success);
74+
75+
Test.startTest();
76+
callback.handleResult(result, context);
77+
Test.stopTest();
78+
79+
System.assertEquals(true, callback.success);
80+
}
81+
82+
@isTest
83+
static void it_should_handle_deploy_error() {
84+
// Instantiate the callback
85+
Boolean sendEmailOnError = true;
86+
Boolean sendEmailOnSuccess = true;
87+
CustomMetadataSaver.DefaultDeployCallback callback = new CustomMetadataSaver.DefaultDeployCallback(sendEmailOnError, sendEmailOnSuccess);
88+
89+
// Setup the deploy result and context objects
90+
Metadata.DeployResult result = new Metadata.DeployResult();
91+
result.success = false;
92+
result.numberComponentsTotal = 5;
93+
result.numberComponentErrors = 2;
94+
Metadata.DeployCallbackContext context = new Metadata.DeployCallbackContext();
95+
96+
System.assertEquals(null, callback.success);
97+
98+
Test.startTest();
99+
callback.handleResult(result, context);
100+
Test.stopTest();
101+
102+
System.assertEquals(false, callback.success);
103+
}
56104
}

force-app/tests/custom-metadata-saver/objects/CustomMetadataDeployTest__mdt/fields/ExamplePicklistField__c.field-meta.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
<sorted>false</sorted>
1313
<value>
1414
<fullName>Option 1</fullName>
15-
<default>true</default>
15+
<default>false</default>
1616
<label>Option 1</label>
1717
</value>
1818
<value>

0 commit comments

Comments
 (0)