Skip to content

Instantly share code, notes, and snippets.

@dschach
Last active February 15, 2024 17:02
Show Gist options
  • Save dschach/65fe2e3d2423eb2dfd458944c38475bc to your computer and use it in GitHub Desktop.
Save dschach/65fe2e3d2423eb2dfd458944c38475bc to your computer and use it in GitHub Desktop.
Apex class and test class for CustomMetadataClient synchronous update/deletes
/**
* @author https://www.tostring.co.uk/custom-metadata-webservice-client
* @group Configuration
*/
@SuppressWarnings('PMD.ApexDoc,PMD.FieldNamingConventions,PMD.LocalVariableNamingConventions,PMD.ClassNamingConventions,PMD.ExcessiveParameterList')
public inherited sharing class CustomMetadataClient {
static public Database.UpsertResult upsertMetadata(SObjectType objectType, Map<SObjectField, Object> record) {
return upsertMetadata(objectType, new List<Map<SObjectField, Object>>{ record })[0];
}
static public List<Database.UpsertResult> upsertMetadata(SObjectType type, List<Map<SObjectField, Object>> metadatas) {
if (Test.isRunningTest()) {
Test.setMock(WebServiceMock.class, new UpsertMetadataMock());
}
//turn maps of fields and values into web service DTOs
List<CustomMetadata> customMetadatas = new List<CustomMetadata>();
for (Map<SObjectField, Object> metadata : metadatas) {
customMetadatas.add(new CustomMetadata(type, metadata));
}
//invoke metadata api
MetadataClient client = new MetadataClient(sid);
//MetadataClient client = new MetadataClient(UserInfo.getSessionId());
List<UpsertResult> metadataResults = client.upsertMetadata(customMetadatas, true);
//coerce to familiar database class
List<Database.UpsertResult> databaseResults = new List<Database.UpsertResult>();
for (UpsertResult metadataResult : metadataResults) {
databaseResults.add(metadataResult.toDatabaseUpsertResult());
}
return databaseResults;
}
class UpsertMetadataMock implements WebServiceMock {
public void doInvoke(
Object stub,
Object request,
Map<String, Object> response,
String endpoint,
String soapAction,
String requestName,
String responseNS,
String responseName,
String responseType
) {
upsertMetadataResponse_element element = new upsertMetadataResponse_element();
element.result = new List<UpsertResult>();
for (Metadata metadata : ((upsertMetadata_element) request).metadata) {
UpsertResult result = new UpsertResult();
result.success = true;
element.result.add(result);
}
response.put('response_x', element);
}
}
static public Database.DeleteResult deleteMetadata(SObjectType type, String developerName) {
return deleteMetadata(type, new List<String>{ developerName })[0];
}
static public List<Database.DeleteResult> deleteMetadata(SObjectType type, List<String> developerNames) {
if (Test.isRunningTest()) {
Test.setMock(WebServiceMock.class, new DeleteMetadataMock());
}
//turn names into qualified full names
List<String> fullNames = new List<String>();
for (String developerName : developerNames) {
fullNames.add(String.valueOf(type) + '.' + developerName);
}
//invoke metadata api
MetadataClient client = new MetadataClient(sid);
//MetadataClient client = new MetadataClient(UserInfo.getSessionId());
List<DeleteResult> metadataResults = client.deleteMetadata('CustomMetadata', fullNames, true);
//coerce to familiar database class
List<Database.DeleteResult> databaseResults = new List<Database.DeleteResult>();
for (DeleteResult metadataResult : metadataResults) {
databaseResults.add(metadataResult.toDatabaseDeleteResult());
}
return databaseResults;
}
class DeleteMetadataMock implements WebServiceMock {
public void doInvoke(
Object stub,
Object request,
Map<String, Object> response,
String endpoint,
String soapAction,
String requestName,
String responseNS,
String responseName,
String responseType
) {
deleteMetadataResponse_element element = new deleteMetadataResponse_element();
element.result = new List<DeleteResult>();
for (String fullName : ((deleteMetadata_element) request).fullNames) {
DeleteResult result = new DeleteResult();
result.success = true;
result.fullName = fullName;
element.result.add(result);
}
response.put('response_x', element);
}
}
/**
* This webservice class creates Custom Metadata
* records synchronously using the Metadata API.
*/
public class MetadataClient {
String endpoint;
Integer timeout_x; //special variable on the stub
SessionHeader_element SessionHeader = new SessionHeader_element();
AllOrNoneHeader_element AllOrNoneHeader = new AllOrNoneHeader_element();
String SessionHeader_hns = 'SessionHeader=http://soap.sforce.com/2006/04/metadata';
String AllOrNoneHeader_hns = 'AllOrNoneHeader=http://soap.sforce.com/2006/04/metadata';
String[] ns_map_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'CustomMetadataClient.MetadataClient' };
/**
* @example
* String sessionId = UserInfo.getSessionId();
* MetadataClient client = new MetadataClient(sessionId);
*/
public MetadataClient(String sessionId) {
//set timeout
Integer timeout = 60 * 1000;
this.timeout_x = timeout;
//set session id
this.SessionHeader.sessionId = sessionId;
//set metadata endpoint
this.endpoint = protocolAndHost + '/services/Soap/m/59.0';
}
/**
* @example
* CustomMetadata customMetadata = new CustomMetadata();
* customMetadata.label = 'My Meta Record1';
* customMetadata.fullName = 'MyMeta__mdt.Record1';
* client.upsertMetadata(new List<CustomMetadata>{customMetadata}, true);
*/
public List<UpsertResult> upsertMetadata(List<Metadata> metadatas, Boolean allOrNone) {
if (metadatas.isEmpty()) {
return new List<UpsertResult>();
}
this.AllOrNoneHeader.allOrNone = allOrNone;
upsertMetadata_element request = new upsertMetadata_element();
request.metadata = metadatas;
upsertMetadataResponse_element response;
Map<String, upsertMetadataResponse_element> response_map = new Map<String, upsertMetadataResponse_element>();
response_map.put('response_x', response);
//System.debug('request: ' + request);
WebServiceCallout.invoke(
this,
request,
response_map,
new List<String>{
endpoint,
'',
'http://soap.sforce.com/2006/04/metadata',
'upsertMetadata',
'http://soap.sforce.com/2006/04/metadata',
'upsertMetadataResponse',
'CustomMetadataClient.upsertMetadataResponse_element'
}
);
response = response_map.get('response_x');
//System.debug('response: ' + response);
return response.result;
}
/**
* @example
* String type = 'CustomMetadata';
* String fullName = 'MyMeta__mdt.Record1';
* client.deleteMetadata(type, new List<String>{fullName}, true);
*/
public List<DeleteResult> deleteMetadata(String type, List<String> fullNames, Boolean allOrNone) {
if (fullNames.isEmpty()) {
return new List<DeleteResult>();
}
this.AllOrNoneHeader.allOrNone = allOrNone;
deleteMetadata_element request = new deleteMetadata_element();
request.type = type;
request.fullNames = fullNames;
deleteMetadataResponse_element response;
Map<String, deleteMetadataResponse_element> response_map = new Map<String, deleteMetadataResponse_element>();
response_map.put('response_x', response);
WebServiceCallout.invoke(
this,
request,
response_map,
new List<String>{
endpoint,
'',
'http://soap.sforce.com/2006/04/metadata',
'deleteMetadata',
'http://soap.sforce.com/2006/04/metadata',
'deleteMetadataResponse',
'CustomMetadataClient.deleteMetadataResponse_element'
}
);
response = response_map.get('response_x');
return response.result;
}
}
class CustomMetadataValue {
String field;
String value;
/**
* @example
* CustomMetadataValue value = new CustomMetadataValue(
* Stage__mdt.Position__c,
* 3
* );
*/
CustomMetadataValue(SObjectField field, Object value) {
this.field = String.valueOf(field);
this.value = String.valueOf(value);
}
String[] field_type_info = new List<String>{ 'field', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
String[] value_type_info = new List<String>{ 'value', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'true' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'field', 'value' };
}
class SessionHeader_element {
String sessionId;
String[] sessionId_type_info = new List<String>{ 'sessionId', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'sessionId' };
}
class upsertMetadata_element {
Metadata[] metadata;
String[] metadata_type_info = new List<String>{ 'metadata', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'metadata' };
}
class upsertMetadataResponse_element {
UpsertResult[] result;
String[] result_type_info = new List<String>{ 'result', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'result' };
}
class deleteMetadata_element {
String type;
String[] fullNames;
String[] type_type_info = new List<String>{ 'type', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
String[] fullNames_type_info = new List<String>{ 'fullNames', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'type', 'fullNames' };
}
class deleteMetadataResponse_element {
DeleteResult[] result;
String[] result_type_info = new List<String>{ 'result', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'result' };
}
class AllOrNoneHeader_element {
Boolean allOrNone;
String[] allOrNone_type_info = new List<String>{ 'allOrNone', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'allOrNone' };
}
class Error {
String message;
String[] fields;
String statusCode;
transient String[] fields_type_info = new List<String>{ 'fields', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
transient String[] message_type_info = new List<String>{ 'message', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] statusCode_type_info = new List<String>{ 'statusCode', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
transient String[] field_order_type_info = new List<String>{ 'fields', 'message', 'statusCode' };
}
abstract class Metadata {
//inheritance doesn't work
}
class CustomMetadata extends Metadata {
String label;
String fullName;
String description;
Boolean isProtected;
CustomMetadataValue[] values;
/**
* @example
* SObjectType type = SObjectType.MyMeta__mdt;
* Map<SObjectField,Object> metadata = new Map<SObjectField,Object>();
* metadata.put(MyMeta__mdt.DeveloperName, 'Record_1')
* metadata.put(MyMeta__mdt.MasterLabel, 'Record One')
* CustomMetadata customMetadata = new CustomMetadata(type, metadata);
*/
CustomMetadata(SObjectType type, Map<SObjectField, Object> metadata) {
this.values = new List<CustomMetadataValue>();
for (SObjectField field : metadata.keySet()) {
//populate special properties for developer name and master label
if (String.valueOf(field) == 'MasterLabel') {
this.label = String.valueOf(metadata.get(field));
}
if (String.valueOf(field) == 'DeveloperName') {
this.fullName = String.valueOf(type) + '.' + metadata.get(field);
}
if (!String.valueOf(field).endsWith('__c')) {
continue; //ignore Id, Label, Language, NamespacePrefix, QualifiedApiName etc
}
//coerce all other keys and values to value DTOs
this.values.add(new CustomMetadataValue(field, metadata.get(field)));
}
}
String type = 'CustomMetadata';
String[] type_att_info = new List<String>{ 'xsi:type' };
String[] fullName_type_info = new List<String>{ 'fullName', 'http://soap.sforce.com/2006/04/metadata', null, '0', '1', 'false' };
String[] description_type_info = new List<String>{ 'description', 'http://soap.sforce.com/2006/04/metadata', null, '0', '1', 'false' };
String[] label_type_info = new List<String>{ 'label', 'http://soap.sforce.com/2006/04/metadata', null, '0', '1', 'false' };
String[] isProtected_type_info = new List<String>{ 'protected', 'http://soap.sforce.com/2006/04/metadata', null, '0', '1', 'false' };
String[] values_type_info = new List<String>{ 'values', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
String[] field_order_type_info = new List<String>{ 'fullName', 'description', 'label', 'isProtected', 'values' };
}
class DeleteResult {
String id;
Error[] errors;
Boolean success;
transient String fullName;
transient String[] errors_type_info = new List<String>{ 'errors', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
transient String[] fullName_type_info = new List<String>{ 'fullName', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] success_type_info = new List<String>{ 'success', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
transient String[] field_order_type_info = new List<String>{ 'errors', 'fullName', 'success' };
Database.DeleteResult toDatabaseDeleteResult() {
this.id = this.fullName;
return (Database.DeleteResult) Json.deserialize(Json.serialize(this), Database.DeleteResult.class);
}
}
@TestVisible
class UpsertResult {
String id;
Error[] errors;
Boolean success;
Boolean created;
transient String fullName;
transient String[] created_type_info = new List<String>{ 'created', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] errors_type_info = new List<String>{ 'errors', 'http://soap.sforce.com/2006/04/metadata', null, '0', '-1', 'false' };
transient String[] fullName_type_info = new List<String>{ 'fullName', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] success_type_info = new List<String>{ 'success', 'http://soap.sforce.com/2006/04/metadata', null, '1', '1', 'false' };
transient String[] apex_schema_type_info = new List<String>{ 'http://soap.sforce.com/2006/04/metadata', 'true', 'false' };
transient String[] field_order_type_info = new List<String>{ 'created', 'errors', 'fullName', 'success' };
Database.UpsertResult toDatabaseUpsertResult() {
this.id = this.fullName;
return (Database.UpsertResult) Json.deserialize(Json.serialize(this), Database.UpsertResult.class);
}
}
private static String sid {
get {
if (sid == null) {
sid = UserInfo.getSessionId();
}
return sid;
}
private set;
}
/**
* Determines the true API hostname for a Salesforce org using the Identity URL
* <br>
* <br>Why not just use Url.getSalesforceBaseUrl?
* <br>The return value can be any of the following:
* <br>- http://pod.salesforce.com (from a batch apex class)
* <br>- https://c.na1.visual.force.com (a local Visualforce Page)
* <br>- https://mysite.secure.force.com (from a Force.com Site)
* <br>- https://ns.pod.visual.force.com (some page in a managed package)
*/
public static String protocolAndHost {
get {
if (protocolAndHost == null) {
/* //memoize
String uid = UserInfo.getUserId();
//String sid = UserInfo.getSessionId();
String oid = UserInfo.getOrganizationId();
String base = Url.getOrgDomainUrl().toExternalForm();
//System.debug('base: ' + base);
//use getOrgDomainUrl within batches and schedules (not Visualforce), and fix inconsistent protocol
if (sid == null) {
return base.replaceFirst('http:', 'https:');
}
//within test context use url class, else derive from identity response
PageReference api = new PageReference('/id/' + oid + '/' + uid + '?access_token=' + sid);
System.debug(api);
String content = Test.isRunningTest() ? '{"urls":{"profile":"' + base + '"}}' : api.getContent().toString();
System.debug(content);
Url profile = new Url(content.substringBetween('"profile":"', '"'));
System.debug(profile);
protocolAndHost = profile.getProtocol() + '://' + profile.getHost();
System.debug(protocolAndHost); */
protocolAndHost = Url.getOrgDomainUrl().toExternalForm();
}
return protocolAndHost;
}
private set;
}
}
/***********************************************************************
TEST CLASS
***********************************************************************?
/**
* @author https://www.tostring.co.uk/custom-metadata-webservice-client
* @group Configuration
*/
@IsTest
class CustomMetadataClientTest {
@IsTest
static void testUpsertMetadataResultIsSuccess() {
//arrange
Map<SObjectField, Object> metadata = new Map<SObjectField, Object>{ Document.Name => 'Label', Document.DeveloperName => 'Name' };
//act
Database.UpsertResult result = CustomMetadataClient.upsertMetadata(Document.SObjectType, metadata);
//assert
Assert.isTrue(result.success, 'wrong result');
}
@IsTest
static void testDeleteMetadataResultIsSuccess() {
//arrange
String metadata = 'DeveloperName';
//act
Database.DeleteResult result = CustomMetadataClient.deleteMetadata(Document.SObjectType, metadata);
//assert
Assert.isTrue(result.success, 'wrong result');
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment