public inherited sharing class MetadataApiClient { public static final String NAMED_CREDENTIAL = 'Salesforce_Metadata'; public static final String API_VERSION = '65.0'; private static final String METADATA_NS = 'http://soap.sforce.com/2006/04/metadata'; private static final String SOAP_NS = 'http://schemas.xmlsoap.org/soap/envelope/'; private static final String XSI_NS = 'http://www.w3.org/2001/XMLSchema-instance'; public class MetadataApiException extends Exception { } public class SaveResult { public Boolean created; public String fullName; } public class RetrieveResult { public Boolean done; public String id; public String status; public String errorMessage; public Blob zipFile; } public class DeployResult { public Boolean done; public Boolean success; public String id; public String status; public String errorMessage; } public static Dom.XmlNode readMetadata(String metadataType, String fullName) { String operation = '' + '' + MetadataXml.escape(metadataType) + '' + '' + MetadataXml.escape(fullName) + '' + ''; Dom.XmlNode body = send(operation); Dom.XmlNode records = MetadataXml.findDescendant(body, 'records'); if ( records == null || 'true'.equalsIgnoreCase(records.getAttributeValue('nil', XSI_NS)) ) { return null; } return records; } public static SaveResult updateMetadata( String metadataType, String metadataChildrenXml ) { return save('updateMetadata', metadataType, metadataChildrenXml); } public static SaveResult upsertMetadata( String metadataType, String metadataChildrenXml ) { return save('upsertMetadata', metadataType, metadataChildrenXml); } public static String startRetrieve(String member, String metadataType) { Dom.XmlNode body = send( '' + '' + API_VERSION + 'true' + '' + MetadataXml.escape(member) + '' + MetadataXml.escape(metadataType) + '' + '' ); String id = MetadataXml.text( MetadataXml.findDescendant(body, 'result'), 'id' ); if (String.isBlank(id)) { throw new MetadataApiException( 'The Metadata API did not return a retrieve job ID.' ); } return id; } public static RetrieveResult checkRetrieve(String retrieveId) { Dom.XmlNode body = send( '' + MetadataXml.escape(retrieveId) + 'true' + '' ); Dom.XmlNode resultNode = MetadataXml.findDescendant(body, 'result'); RetrieveResult result = new RetrieveResult(); result.done = MetadataXml.booleanValue(resultNode, 'done'); result.id = MetadataXml.text(resultNode, 'id'); result.status = MetadataXml.text(resultNode, 'status'); result.errorMessage = MetadataXml.text(resultNode, 'errorMessage'); String zipFile = MetadataXml.text(resultNode, 'zipFile'); result.zipFile = String.isBlank(zipFile) ? null : EncodingUtil.base64Decode(zipFile); return result; } public static String startDeploy(Blob zipFile) { Dom.XmlNode body = send( '' + EncodingUtil.base64Encode(zipFile) + '' + 'false' + 'false' + 'false' + 'false' + 'false' + 'false' + 'true' + 'true' + 'NoTestRun' + '' ); String id = MetadataXml.text( MetadataXml.findDescendant(body, 'result'), 'id' ); if (String.isBlank(id)) { throw new MetadataApiException( 'The Metadata API did not return a deployment job ID.' ); } return id; } public static DeployResult checkDeploy(String deployId) { Dom.XmlNode body = send( '' + MetadataXml.escape(deployId) + 'true' + '' ); Dom.XmlNode resultNode = MetadataXml.findDescendant(body, 'result'); DeployResult result = new DeployResult(); result.done = MetadataXml.booleanValue(resultNode, 'done'); result.success = MetadataXml.booleanValue(resultNode, 'success'); result.id = MetadataXml.text(resultNode, 'id'); result.status = MetadataXml.text(resultNode, 'status'); result.errorMessage = MetadataXml.text(resultNode, 'errorMessage'); if (String.isBlank(result.errorMessage)) { Dom.XmlNode problem = MetadataXml.findDescendant(resultNode, 'problem'); result.errorMessage = problem == null ? null : problem.getText(); } return result; } public static String toolingAutoResponseRuleName(Id ruleId) { HttpRequest request = new HttpRequest(); request.setEndpoint( 'callout:' + NAMED_CREDENTIAL + '/services/data/v' + API_VERSION + '/tooling/query/?q=' + EncodingUtil.urlEncode( 'SELECT Id, Name, EntityDefinitionId FROM AutoResponseRule WHERE Id = \'' + String.escapeSingleQuotes(String.valueOf(ruleId)) + '\' LIMIT 1', 'UTF-8' ) ); request.setMethod('GET'); request.setTimeout(120000); HttpResponse response = new Http().send(request); if (response.getStatusCode() < 200 || response.getStatusCode() >= 300) { throw new MetadataApiException( 'Tooling API returned HTTP ' + response.getStatusCode() + ': ' + response.getBody() ); } Map payload = (Map) JSON.deserializeUntyped( response.getBody() ); List records = (List) payload.get('records'); if (records == null || records.isEmpty()) { throw new MetadataApiException('Auto Response Rule Id was not found.'); } return (String) ((Map) records[0]).get('Name'); } private static SaveResult save( String operationName, String metadataType, String metadataChildrenXml ) { String operation = '' + metadataChildrenXml + ''; Dom.XmlNode body = send(operation); Dom.XmlNode resultNode = MetadataXml.findDescendant(body, 'result'); if (resultNode == null) { throw new MetadataApiException( 'The Metadata API returned no save result.' ); } Boolean success = MetadataXml.booleanValue(resultNode, 'success'); if (!success) { Dom.XmlNode errors = MetadataXml.child(resultNode, 'errors'); String statusCode = errors == null ? null : MetadataXml.text(errors, 'statusCode'); String message = errors == null ? null : MetadataXml.text(errors, 'message'); throw new MetadataApiException( 'Metadata API save failed' + (String.isBlank(statusCode) ? '' : ' [' + statusCode + ']') + (String.isBlank(message) ? '.' : ': ' + message) ); } SaveResult result = new SaveResult(); result.created = MetadataXml.booleanValue(resultNode, 'created'); result.fullName = MetadataXml.text(resultNode, 'fullName'); return result; } private static Dom.XmlNode send(String operationXml) { HttpRequest request = new HttpRequest(); request.setEndpoint( 'callout:' + NAMED_CREDENTIAL + '/services/Soap/m/' + API_VERSION ); request.setMethod('POST'); request.setHeader('Content-Type', 'text/xml; charset=UTF-8'); request.setHeader('SOAPAction', '""'); request.setTimeout(120000); request.setBody( '' + '' + operationXml + '' ); HttpResponse response = new Http().send(request); Dom.Document document = new Dom.Document(); try { document.load(response.getBody()); } catch (Exception exceptionValue) { throw new MetadataApiException( 'Metadata API returned HTTP ' + response.getStatusCode() + ' with an unreadable response: ' + response.getBody() ); } Dom.XmlNode body = MetadataXml.findDescendant( document.getRootElement(), 'Body' ); Dom.XmlNode fault = MetadataXml.findDescendant(body, 'Fault'); if (fault != null) { String faultMessage = MetadataXml.text(fault, 'faultstring'); throw new MetadataApiException( String.isBlank(faultMessage) ? 'Unknown Metadata API SOAP fault.' : faultMessage ); } if (response.getStatusCode() < 200 || response.getStatusCode() >= 300) { throw new MetadataApiException( 'Metadata API returned HTTP ' + response.getStatusCode() + ': ' + response.getBody() ); } return body; } }