Last active
August 26, 2017 06:22
-
-
Save afawcett/678c4e4a02e7ab7fc84a00ebc0144586 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* | |
CustomMetadata.Operations | |
.callback( | |
// Platform event for deploy status | |
MetadataDeployment__e.getSObjectType(), | |
MetadataDeployment__e.DeploymentId__c, | |
MetadataDeployment__e.Result__c) | |
.enqueueUpsertRecords( | |
// Metadata record type | |
LookupRollupSummary2__mdt.getSObjectType(), | |
new List<Map<SObjectField, Object>> { | |
// Metadata record | |
new Map<SObjectField, Object> { | |
LookupRollupSummary2__mdt.DeveloperName => 'MyRecord2', | |
LookupRollupSummary2__mdt.Label => 'My Record', | |
LookupRollupSummary2__mdt.ParentObject__c => 'Account', | |
LookupRollupSummary2__mdt.AggregateResultField__c => 'X', | |
LookupRollupSummary2__mdt.ChildObject__c => 'Opportunity', | |
LookupRollupSummary2__mdt.FieldToAggregate__c => 'Amount', | |
LookupRollupSummary2__mdt.RelationshipField__c => 'AccountId' | |
} } ); | |
LookupRollupSummary2__mdt readRecord = | |
[select DeveloperName, Label, ParentObject__c, ChildObject__c from LookupRollupSummary2__mdt limit 1]; | |
CustomMetadata.Operations | |
.callback( | |
// Platform event for deploy status | |
MetadataDeployment__e.getSObjectType(), | |
MetadataDeployment__e.DeploymentId__c, | |
MetadataDeployment__e.Result__c) | |
.enqueueUpsertRecords( | |
new List<SObject> { readRecord } ); | |
**/ | |
public with sharing class CustomMetadata { | |
public static final Operations Operations = new Operations(); | |
public class Operations { | |
private SaveResultCallback deployCallback; | |
public String deployId {get;private set;} | |
private Operations() { } | |
/** | |
* Takes a raw custom metadata record list and deploys it | |
**/ | |
public Operations enqueueUpsertRecords(List<Metadata.CustomMetadata> records) { | |
// TODO: Throw an exception if duplicates found | |
// TODO: Throw exception if the callback has not been set | |
Metadata.DeployContainer mdContainer = new Metadata.DeployContainer(); | |
for(Metadata.CustomMetadata record : records) { | |
mdContainer.addMetadata(record); | |
} | |
// Caller will use this generated Id to associate the callback/event with this deployment | |
deployId = deployCallback.deployId; | |
// Current return Id from platform is "DeployRequest" record, callback Id passed by platform is "AsyncApexJob"? | |
Platform.metadatEnqueueDeployment(mdContainer, deployCallback); | |
return this; | |
} | |
/** | |
* Leverages the fact that MDT SObject's can be bound and thus edited on a VF page! | |
* And the hope that one day we can edit in memory native MDT SObject type fields ;-) | |
**/ | |
public Operations enqueueUpsertRecords(List<SObject> records) { | |
// TODO: Throw an exception for empty lists | |
SObjectType sObjectType = records[0].getSObjectType(); | |
DescribeSObjectResult dsr = sObjectType.getDescribe(); | |
Map<String, SObjectField> fieldTokenByName = dsr.fields.getMap(); | |
List<Map<SObjectField, Object>> sObjectRecords = new List<Map<SObjectField, Object>>(); | |
for(SObject record: records) { | |
Map<SObjectField, Object> valuesByField = new Map<SObjectField, Object>(); | |
Map<String, Object> populatedByField = record.getPopulatedFieldsAsMap(); | |
for(String fieldName : populatedByField.keySet()) { | |
if(fieldName == 'Id') { | |
continue; | |
} | |
SObjectField fieldToken = fieldTokenByName.get(fieldName); | |
Object fieldValue = populatedByField.get(fieldName); | |
valuesByField.put(fieldToken, fieldValue); | |
} | |
sObjectRecords.add(valuesByField); | |
} | |
return enqueueUpsertRecords(sObjectType, sObjectRecords); | |
} | |
/** | |
* Deploy custom metadata records described as a field value pair (good for keeping referential integrity) | |
* @param sobjectType the SObjectType of the MDT object via MyCustomMetadata__c.getSObjectType() | |
* @param sobjectRecords the list of field value maps representing the records | |
**/ | |
public Operations enqueueUpsertRecords(SObjectType sobjectType, List<Map<SObjectField, Object>> sObjectRecords) { | |
List<Metadata.CustomMetadata> records = new List<Metadata.CustomMetadata>(); | |
for (Map<SObjectField, Object> sObjectRecord : sObjectRecords) { | |
Metadata.CustomMetadata customMetadataRecord = new Metadata.CustomMetadata(); | |
customMetadataRecord.values = new List<Metadata.CustomMetadataValue>(); | |
for(SObjectField field : sObjectRecord.keySet()) { | |
DescribeFieldResult dsr = field.getDescribe(); | |
Object fieldValue = sObjectRecord.get(field); | |
if(dsr.getName() == 'DeveloperName') { | |
customMetadataRecord.fullName = | |
sobjectType.getDescribe().getName().replace('__mdt', '') + '.' + fieldValue; | |
} else if(dsr.getName() == 'Label') { | |
customMetadataRecord.label = (String) fieldValue; | |
} else { | |
Metadata.CustomMetadataValue cmv = new Metadata.CustomMetadataValue(); | |
cmv.field = dsr.getName(); | |
cmv.value = fieldValue; | |
customMetadataRecord.values.add(cmv); | |
} | |
} | |
records.add(customMetadataRecord); | |
} | |
return enqueueUpsertRecords(records) ; | |
} | |
/** | |
* Registers a custom callback implementation | |
**/ | |
public Operations callback(SaveResultCallback saveResultCallback) { | |
this.deployCallback = saveResultCallback; | |
return this; | |
} | |
/** | |
* Fires the given Platform Event populating the given message field with a | |
* JSON serialised representation of SaveRecordResult | |
**/ | |
public Operations callback(SObjectType eventType, SObjectField deploymentIdField, SObjectField messageField) { | |
return callback(new PublishEventCallback(eventType, deploymentIdField, messageField)); | |
} | |
} | |
/** | |
* Sends a Platform Event in response to a Metadata Deploy callback | |
**/ | |
private class PublishEventCallback extends SaveResultCallback { | |
private SObjectType eventType; | |
private SObjectField messageField; | |
private SObjectFIeld deploymentIdField; | |
public PublishEventCallback(SObjectType eventType, SObjectField deploymentIdField, SObjectField messageField) { | |
this.eventType = eventType; | |
this.messageField = messageField; | |
this.deploymentIdField = deploymentIdField; | |
} | |
public override void handleResult(String deployId, List<SaveRecordResult> result) { | |
// Create event to publish | |
SObject event = eventType.newSObject(); | |
event.put(deploymentIdField, deployId); | |
event.put(messageField, JSON.serialize(result, true)); | |
// Call method to publish events | |
List<Database.SaveResult> results = Platform.eventBusPublish(new List<SObject> { event }); | |
// Inspect publishing result for each event | |
// TODO: Consider another way to report this, since this is the phantom users options are limited | |
for (Database.SaveResult sr : results) { | |
if (sr.isSuccess()) { | |
System.debug('Successfully published event.'); | |
} else { | |
for(Database.Error err : sr.getErrors()) { | |
System.debug('Error returned: ' + | |
err.getStatusCode() + | |
err.getMessage()); | |
} | |
} | |
} | |
} | |
} | |
/** | |
* Base class handler for internal and external handlers, | |
* marshalls internal deploy results into something easier to handle | |
**/ | |
public abstract class SaveResultCallback implements Metadata.DeployCallback | |
{ | |
public String deployId {get; private set;} | |
public SaveResultCallback() { | |
// Credit: https://success.salesforce.com/ideaView?id=08730000000KgTYAA0 | |
deployId = EncodingUtil.convertToHex(Crypto.generateAesKey(128)); | |
} | |
public void handleResult(Metadata.DeployResult result, Metadata.DeployCallbackContext context) { | |
List<SaveRecordResult> saveRecordResults = new List<SaveRecordResult>(); | |
for(Metadata.DeployMessage deployMessage : result.details.componentFailures) { | |
if(deployMessage.fileName == 'package.xml') { | |
continue; | |
} | |
saveRecordResults.add(convertToSaveResult(deployMessage)); | |
} | |
for(Metadata.DeployMessage deployMessage : result.details.componentSuccesses) { | |
if(deployMessage.fileName == 'package.xml') { | |
continue; | |
} | |
saveRecordResults.add(convertToSaveResult(deployMessage)); | |
} | |
handleResult(deployId, saveRecordResults); | |
} | |
private SaveRecordResult convertToSaveResult(Metadata.DeployMessage deployMessage) { | |
SaveRecordResult srr = new SaveRecordResult(); | |
srr.fullName = deployMessage.fullName; | |
srr.status = deployMessage.success; | |
srr.message = deployMessage.problem; | |
return srr; | |
} | |
/** | |
* Handler for simplified metadata deployment result based around custom metadata records | |
**/ | |
public abstract void handleResult(String deployId, List<SaveRecordResult> results); | |
} | |
public class SaveRecordResult { | |
@AuraEnabled | |
// TODO: Maybe resolve to the SObjectType and avoid namespace issues, does this marshal via Aura though? | |
public String fullName; | |
@AuraEnabled | |
public Boolean status; | |
@AuraEnabled | |
public String message; | |
} | |
public class CustomMetadataException extends Exception {} | |
/** | |
* Basic dependency injection impl for platform APIs leveraged by this class | |
**/ | |
@TestVisible | |
private static Platform Platform = new RuntimePlatform(); | |
@TestVisible | |
private abstract class Platform { | |
public abstract List<Database.SaveResult> eventBusPublish(List<SObject> events); | |
public abstract Id metadatEnqueueDeployment(Metadata.DeployContainer mdContainer, Metadata.DeployCallback deployCallback); | |
} | |
private class RuntimePlatform extends Platform { | |
public override List<Database.SaveResult> eventBusPublish(List<SObject> events) { | |
return System.EventBus.publish(events); | |
} | |
public override Id metadatEnqueueDeployment(Metadata.DeployContainer mdContainer, Metadata.DeployCallback deployCallback) { | |
return Metadata.Operations.enqueueDeployment(mdContainer, deployCallback); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@IsTest | |
private class CustomMetadataTest { | |
@IsTest | |
private static void whenUpsertingByFieldMapSendsEvent() { | |
// Given | |
MockPlatform mockPlatform = new MockPlatform(); | |
mockPlatform.metadataEnqueueDeploymentResult = new Metadata.DeployResult(); | |
mockPlatform.metadataEnqueueDeploymentResult.details = new Metadata.DeployDetails(); | |
mockPlatform.metadataEnqueueDeploymentResult.details.componentFailures = new List<Metadata.DeployMessage>(); | |
mockPlatform.metadataEnqueueDeploymentResult.details.componentSuccesses = new List<Metadata.DeployMessage>(); | |
Metadata.DeployMessage deploySuccess = | |
(Metadata.DeployMessage) JSON.deserialize( | |
'{"fullName":"Account","success":true}', Metadata.DeployMessage.class); | |
mockPlatform.metadataEnqueueDeploymentResult.details.componentSuccesses.add(deploySuccess); | |
mockPlatform.eventBusPublishResponse = | |
(List<Database.SaveResult>) JSON.deserialize( | |
'[{"id":"e00xx000000001TAAQ","success":true,"errors":[],"warnings":[]}]', List<Database.SaveResult>.class); | |
CustomMetadata.Platform = mockPlatform; | |
List<Map<SObjectField, Object>> records = | |
new List<Map<SObjectField, Object>> { | |
new Map<SObjectField, Object> { | |
Account.Description => 'My Record' | |
} }; | |
// When | |
String deployId = | |
CustomMetadata.Operations | |
// Opportunity SObject emulating Event object | |
.callback(Opportunity.getSObjectType(), Opportunity.Name, Opportunity.Description) | |
// Account SObject emulating Custom Metadata object | |
.enqueueUpsertRecords(Account.getSObjectType(), records) | |
.deployId; | |
// Then | |
System.assertEquals( | |
'Description', ((Metadata.CustomMetadata) mockPlatform.metadataEnqueueDeploymentContainer.getMetadata()[0]).values[0].field); | |
System.assertEquals( | |
'My Record', ((Metadata.CustomMetadata) mockPlatform.metadataEnqueueDeploymentContainer.getMetadata()[0]).values[0].value); | |
System.assertEquals( | |
'[{"status":true,"fullName":"Account"}]', | |
mockPlatform.eventBusPublishEvents[0].get('Description')); | |
} | |
@IsTest | |
private static void whenUpsertingBySObjectSendsEvent() { | |
// Given | |
MockPlatform mockPlatform = new MockPlatform(); | |
mockPlatform.metadataEnqueueDeploymentResult = new Metadata.DeployResult(); | |
mockPlatform.metadataEnqueueDeploymentResult.details = new Metadata.DeployDetails(); | |
mockPlatform.metadataEnqueueDeploymentResult.details.componentFailures = new List<Metadata.DeployMessage>(); | |
mockPlatform.metadataEnqueueDeploymentResult.details.componentSuccesses = new List<Metadata.DeployMessage>(); | |
Metadata.DeployMessage deploySuccess = | |
(Metadata.DeployMessage) JSON.deserialize( | |
'{"fullName":"Account","success":true}', Metadata.DeployMessage.class); | |
mockPlatform.metadataEnqueueDeploymentResult.details.componentSuccesses.add(deploySuccess); | |
mockPlatform.eventBusPublishResponse = | |
(List<Database.SaveResult>) JSON.deserialize( | |
'[{"id":"e00xx000000001TAAQ","success":true,"errors":[],"warnings":[]}]', List<Database.SaveResult>.class); | |
CustomMetadata.Platform = mockPlatform; | |
List<SObject> records = new List<SObject> { new Account(Description = 'My Record') }; | |
// When | |
String deployId = | |
CustomMetadata.Operations | |
// Opportunity SObject emulating Event object | |
.callback(Opportunity.getSObjectType(), Opportunity.Name, Opportunity.Description) | |
// Account SObject emulating Custom Metadata object | |
.enqueueUpsertRecords(records) | |
.deployId; | |
// Then | |
System.assertEquals( | |
'Description', ((Metadata.CustomMetadata) mockPlatform.metadataEnqueueDeploymentContainer.getMetadata()[0]).values[0].field); | |
System.assertEquals( | |
'My Record', ((Metadata.CustomMetadata) mockPlatform.metadataEnqueueDeploymentContainer.getMetadata()[0]).values[0].value); | |
System.assertEquals( | |
'[{"status":true,"fullName":"Account"}]', | |
mockPlatform.eventBusPublishEvents[0].get('Description')); | |
} | |
/** | |
* Mock platform API behaviours and responses | |
**/ | |
public class MockPlatform extends CustomMetadata.Platform { | |
// Mock test inputs and captured responses | |
public List<Database.SaveResult> eventBusPublishResponse; | |
public List<SObject> eventBusPublishEvents; | |
public Metadata.DeployContainer metadataEnqueueDeploymentContainer; | |
public Metadata.DeployResult metadataEnqueueDeploymentResult; | |
// Mock System.EventBus.publish | |
public override List<Database.SaveResult> eventBusPublish(List<SObject> events) { | |
eventBusPublishEvents = events; | |
return eventBusPublishResponse; | |
} | |
// Mock Metadata.Operations.enqueueDeployment | |
public override Id metadatEnqueueDeployment(Metadata.DeployContainer mdContainer, Metadata.DeployCallback deployCallback) { | |
metadataEnqueueDeploymentContainer = mdContainer; | |
deployCallback.handleResult(metadataEnqueueDeploymentResult, new Metadata.DeployCallbackContext()); | |
return null; // Not used | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment