我想通过 Java 将 ApexTrigger 上传到 Salesforce,因此据我所知,我的可用选项是:
对于身份验证,我可以选择使用登录到 REST API 时从 Salesforce 返回的 OAuth 令牌 或 用户名、密码、安全令牌、客户端 ID、客户端密钥,而第一种方法是重用REST API 令牌将是我最喜欢的一个。
我的实际尝试还没有成功,我的问题显然是如何让它发挥作用或者我的错误在哪里。
我尝试过以下方法:
createMetadata()
POST /sobjects/SObjectName/
我从 Metadata API 方法开始:
@Test
void testTriggerUploadMetadataApi() {
String username = "REDACTED";
String password = "REDACTED";
String securityToken = "REDACTED";
String url = "https://REDACTEDDOMAIN.my.salesforce.com/services/Soap";
String authPrefix = "/u";
String servicePrefix = "/m";
String version = "/60.0";
LoginResponse loginResponse = this.authController
.authLoginPost(new Login("REDACTED", "REDACTED").thirdPartyConfig(thirdPartyConfig))
.block();
assertNotNull(loginResponse);
// Session is here a wrapper for the actual REST API OAuth Token with additional metadata
Session session =
assertDoesNotThrow(() -> new ObjectMapper().readValue(loginResponse.getToken(), Session.class));
ConnectorConfig connectorConfig = new ConnectorConfig();
connectorConfig.setUsername(username);
connectorConfig.setPassword(password + securityToken);
connectorConfig.setAuthEndpoint("https://login.salesforce.com/service/Soap" + authPrefix + version);
connectorConfig.setServiceEndpoint(url + servicePrefix + version);
connectorConfig.setProxy("REDACTED", 8080);
Transport transport = new JdkHttpTransport(connectorConfig);
connectorConfig.setTransport(transport.getClass());
MetadataConnection metadataConnection = assertDoesNotThrow(() -> new MetadataConnection(connectorConfig));
metadataConnection.setSessionHeader(session.getBearerTokenString());
ApexTrigger apexTrigger = new ApexTrigger();
apexTrigger.setFullName("SomeTrigger");
apexTrigger.setStatus(ApexCodeUnitStatus.Active);
apexTrigger.setApiVersion(56.0);
apexTrigger.setContent(
"trigger SomeTrigger on Account(after insert, after update {}".getBytes(StandardCharsets.UTF_8));
assertDoesNotThrow(() -> metadataConnection.upsertMetadata(new Metadata[] {apexTrigger}));
}
结果是:
org.opentest4j.AssertionFailedError: Unexpected exception thrown: com.sforce.ws.SoapFaultException: INVALID_TYPE: This type of object is not available for this organization
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:152)
at org.junit.jupiter.api.AssertDoesNotThrow.createAssertionFailedError(AssertDoesNotThrow.java:84)
at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:75)
at org.junit.jupiter.api.AssertDoesNotThrow.assertDoesNotThrow(AssertDoesNotThrow.java:58)
at org.junit.jupiter.api.Assertions.assertDoesNotThrow(Assertions.java:3228)
at redacted.package.name.SFLoginTest.testTriggerUploadMetadataApi(SFLoginTest.java:139)
然后我开始进行一些研究,发现以下开拓者线程在评论中来自Salesforce的Daniel Ballinger指出这可能是不可能的,因为“create()、delete()不支持此元数据类型,和 update() 调用。 虽然不明确,但这也适用于 createMetadata() 方法,create() 已被简化为该方法。”
他还指出“相反,您可以使用元数据API部署方法或工具API来创建Apex类。”以便我进一步研究在哪里找到这个trailblazer线程,其中Sunad Rasane指出他使用了Tooling API而是指向此stackexchange线程的链接,其中已经存在通过Tooling API触发上传的示例,因此我选择使用Tooling API,因为它看起来使用起来很方便:
@Test
void testTriggerUploadToolingApi() {
Map<String, Object> thirdPartyConfig = getThirdPartyConfig();
LoginResponse loginResponse = this.authController
.authLoginPost(new Login("REDACTED", "REDACTED").thirdPartyConfig(thirdPartyConfig))
.block();
assertNotNull(loginResponse);
// Session is here a wrapper for the actual REST API OAuth Token with additional metadata
Session session =
assertDoesNotThrow(() -> new ObjectMapper().readValue(loginResponse.getToken(), Session.class));
String url = session.getBaseUrl() + "/services/data/" + session.getApiVersion() + "/sobjects/ApexTrigger";
System.out.println("Request URL: [" + url + "]");
Map<String, String> apexTrigger = new LinkedHashMap<>();
apexTrigger.put("Name", "UploadTest_AccountTrigger");
apexTrigger.put("TableEnumOrId", "Account");
apexTrigger.put(
"Body",
assertDoesNotThrow(() -> Files.readString(
Path.of("src/test/resources/salesforce/apex/SimpleAccountTriggerExpected.txt"))));
String requestBody = assertDoesNotThrow(() -> new ObjectMapper().writeValueAsString(apexTrigger));
System.out.println("Request body: [" + requestBody + "]");
HttpClient httpClient = HttpClient.newBuilder()
.proxy(ProxySelector.of(new InetSocketAddress("REDACTED", 8080)))
.connectTimeout(Duration.of(60, ChronoUnit.SECONDS))
.build();
HttpRequest uploadRequest = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.of(60, ChronoUnit.SECONDS))
.POST(HttpRequest.BodyPublishers.ofString(requestBody, StandardCharsets.UTF_8))
.header("Authorization", "OAuth " + session.getBearerTokenString())
.header("Content-Type", "application/json")
.build();
HttpResponse<String> response =
assertDoesNotThrow(() -> httpClient.send(uploadRequest, HttpResponse.BodyHandlers.ofString()));
System.out.println("Status Code: [" + response.statusCode()+ "]");
System.out.println("Body: [" + response.body() + "]");
assertTrue(String.valueOf(response.statusCode()).startsWith("2"));
}
结果是:
Expected :true
Actual :false
<Click to see difference>
org.opentest4j.AssertionFailedError: expected: <true> but was: <false>
at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
at org.junit.jupiter.api.AssertTrue.failNotTrue(AssertTrue.java:63)
at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:36)
at org.junit.jupiter.api.AssertTrue.assertTrue(AssertTrue.java:31)
at org.junit.jupiter.api.Assertions.assertTrue(Assertions.java:183)
at redacted.package.nameSFLoginTest.testTriggerUploadToolingApi(SFLoginTest.java:83)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at io.micronaut.test.extensions.junit5.MicronautJunit5Extension$2.proceed(MicronautJunit5Extension.java:142)
at io.micronaut.test.extensions.AbstractMicronautExtension.interceptEach(AbstractMicronautExtension.java:157)
at io.micronaut.test.extensions.AbstractMicronautExtension.interceptTest(AbstractMicronautExtension.java:114)
at io.micronaut.test.extensions.junit5.MicronautJunit5Extension.interceptTestMethod(MicronautJunit5Extension.java:129)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
STDOUT 上有以下输出:
Request URL: [https://REDACTEDDOMAIN.my.salesforce.com/services/data/56.0/sobjects/ApexTrigger]
Request body: [{"Name":"UploadTest_AccountTrigger","TableEnumOrId":"Account","Body":"trigger Account_AccountWebhook on Account (after insert, after update) {}"}]
Status Code: [404]
Body: [[{"errorCode":"NOT_FOUND","message":"The requested resource does not exist"}]]
因此,在令人沮丧地发现该版本缺少“v”前缀之后,Tooling API 接近基本 URL,现在它按照描述的方式工作。
您可以在下面找到构建请求 URL 的正确方法(将其替换为 Tooling API Approach:
String url = session.getBaseUrl() + "/services/data/v" + session.getApiVersion() + "/sobjects/ApexTrigger";