Files
Queue-Creation/force-app/main/default/classes/MetadataApiClient.cls
T
2026-08-21 16:38:06 +00:00

306 lines
9.9 KiB
OpenEdge ABL

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 =
'<met:readMetadata>' +
'<met:type>' +
MetadataXml.escape(metadataType) +
'</met:type>' +
'<met:fullNames>' +
MetadataXml.escape(fullName) +
'</met:fullNames>' +
'</met:readMetadata>';
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(
'<met:retrieve><met:retrieveRequest>' +
'<met:apiVersion>' +
API_VERSION +
'</met:apiVersion><met:singlePackage>true</met:singlePackage>' +
'<met:unpackaged><met:members>' +
MetadataXml.escape(member) +
'</met:members><met:name>' +
MetadataXml.escape(metadataType) +
'</met:name></met:unpackaged>' +
'</met:retrieveRequest></met:retrieve>'
);
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(
'<met:checkRetrieveStatus><met:asyncProcessId>' +
MetadataXml.escape(retrieveId) +
'</met:asyncProcessId><met:includeZip>true</met:includeZip>' +
'</met:checkRetrieveStatus>'
);
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(
'<met:deploy><met:ZipFile>' +
EncodingUtil.base64Encode(zipFile) +
'</met:ZipFile><met:DeployOptions>' +
'<met:allowMissingFiles>false</met:allowMissingFiles>' +
'<met:autoUpdatePackage>false</met:autoUpdatePackage>' +
'<met:checkOnly>false</met:checkOnly>' +
'<met:ignoreWarnings>false</met:ignoreWarnings>' +
'<met:performRetrieve>false</met:performRetrieve>' +
'<met:purgeOnDelete>false</met:purgeOnDelete>' +
'<met:rollbackOnError>true</met:rollbackOnError>' +
'<met:singlePackage>true</met:singlePackage>' +
'<met:testLevel>NoTestRun</met:testLevel>' +
'</met:DeployOptions></met:deploy>'
);
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(
'<met:checkDeployStatus><met:asyncProcessId>' +
MetadataXml.escape(deployId) +
'</met:asyncProcessId><met:includeDetails>true</met:includeDetails>' +
'</met:checkDeployStatus>'
);
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<String, Object> payload = (Map<String, Object>) JSON.deserializeUntyped(
response.getBody()
);
List<Object> records = (List<Object>) payload.get('records');
if (records == null || records.isEmpty()) {
throw new MetadataApiException('Auto Response Rule Id was not found.');
}
return (String) ((Map<String, Object>) records[0]).get('Name');
}
private static SaveResult save(
String operationName,
String metadataType,
String metadataChildrenXml
) {
String operation =
'<met:' +
operationName +
'><met:metadata xmlns="' +
METADATA_NS +
'" xsi:type="met:' +
MetadataXml.escape(metadataType) +
'">' +
metadataChildrenXml +
'</met:metadata></met:' +
operationName +
'>';
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(
'<?xml version="1.0" encoding="UTF-8"?>' +
'<soapenv:Envelope xmlns:soapenv="' +
SOAP_NS +
'" xmlns:met="' +
METADATA_NS +
'" xmlns:xsi="' +
XSI_NS +
'"><soapenv:Header/><soapenv:Body>' +
operationXml +
'</soapenv:Body></soapenv:Envelope>'
);
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;
}
}