Skip to content

Commit 2ff8bb3

Browse files
committed
Adding Composite Request support to SF CRM client.
1 parent 0a80223 commit 2ff8bb3

11 files changed

+401
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package net.glenmazza.sfclient.model;
2+
3+
public class AccountInsertCompositeRecord extends CompositeEntityRecord {
4+
5+
private final Body body = new Body();
6+
public AccountInsertCompositeRecord(String referenceId) {
7+
super("Account", Method.POST, referenceId);
8+
}
9+
10+
public Body getBody() {
11+
return body;
12+
}
13+
14+
public static class Body {
15+
16+
public String name;
17+
public int numberOfEmployees;
18+
public String site;
19+
20+
public String getName() {
21+
return name;
22+
}
23+
24+
public void setName(String name) {
25+
this.name = name;
26+
}
27+
28+
public int getNumberOfEmployees() {
29+
return numberOfEmployees;
30+
}
31+
32+
public void setNumberOfEmployees(int numberOfEmployees) {
33+
this.numberOfEmployees = numberOfEmployees;
34+
}
35+
36+
public String getSite() {
37+
return site;
38+
}
39+
40+
public void setSite(String site) {
41+
this.site = site;
42+
}
43+
}
44+
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package net.glenmazza.sfclient.model;
2+
3+
public class AccountUpdateCompositeRecord extends CompositeEntityRecord {
4+
5+
private final Body body = new Body();
6+
public AccountUpdateCompositeRecord(String referenceId) {
7+
super("Account", Method.PATCH, referenceId);
8+
}
9+
10+
public Body getBody() {
11+
return body;
12+
}
13+
14+
public static class Body {
15+
16+
public int numberOfEmployees;
17+
public String site;
18+
19+
public int getNumberOfEmployees() {
20+
return numberOfEmployees;
21+
}
22+
23+
public void setNumberOfEmployees(int numberOfEmployees) {
24+
this.numberOfEmployees = numberOfEmployees;
25+
}
26+
27+
public String getSite() {
28+
return site;
29+
}
30+
31+
public void setSite(String site) {
32+
this.site = site;
33+
}
34+
}
35+
}

sf_client/src/itest/java/net/glenmazza/sfclient/service/RESTServicesTest.java

+98-12
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
import com.fasterxml.jackson.core.JsonProcessingException;
44
import com.fasterxml.jackson.core.type.TypeReference;
55
import com.fasterxml.jackson.databind.ObjectMapper;
6+
67
import net.glenmazza.sfclient.TestApplication;
78
import net.glenmazza.sfclient.model.AccountCreateRecord;
9+
import net.glenmazza.sfclient.model.AccountInsertCompositeRecord;
810
import net.glenmazza.sfclient.model.AccountMultipleEntityRecord;
911
import net.glenmazza.sfclient.model.AccountQueryRecord;
12+
import net.glenmazza.sfclient.model.AccountUpdateCompositeRecord;
1013
import net.glenmazza.sfclient.model.AccountUpdateRecord;
1114
import net.glenmazza.sfclient.model.ApexAccountRecord;
15+
import net.glenmazza.sfclient.model.CompositeEntityRecord;
16+
import net.glenmazza.sfclient.model.CompositeEntityRecordRequest;
17+
import net.glenmazza.sfclient.model.CompositeEntityRecordResponse;
1218
import net.glenmazza.sfclient.model.MultipleEntityRecord;
1319
import net.glenmazza.sfclient.model.MultipleEntityRecord201Response;
1420
import net.glenmazza.sfclient.model.MultipleEntityRecord400ResponseException;
@@ -30,18 +36,17 @@
3036

3137
import java.time.LocalDate;
3238
import java.time.temporal.ChronoUnit;
39+
import java.util.ArrayList;
3340
import java.util.HashMap;
3441
import java.util.List;
3542
import java.util.Map;
3643

3744
import static org.junit.jupiter.api.Assertions.assertEquals;
3845
import static org.junit.jupiter.api.Assertions.assertTrue;
3946

40-
4147
/**
4248
* Test cases cover the three AbstractRESTService subclasses supporting Salesforce entity calls, Apex endpoints,
4349
* and SOQLQueries.
44-
*
4550
* Will need to have your test Salesforce instance configured in application-test.properties~template. Also,
4651
* the Apex REST test requires a certain Apex endpoint to be installed see the header for that
4752
* test for more info.
@@ -61,6 +66,9 @@ public class RESTServicesTest {
6166
@Autowired
6267
private SalesforceMultipleRecordInserter smri;
6368

69+
@Autowired
70+
private SalesforceCompositeRequestService scrs;
71+
6472
@Autowired
6573
private ApexRESTCaller arc;
6674

@@ -78,7 +86,7 @@ public static void initialize() {
7886
void testSalesforceRecordManagerInsertsGetsAndSOQLQueries() throws JsonProcessingException {
7987
// insert first Account via Class & SOQL query
8088
AccountCreateRecord acr = new AccountCreateRecord();
81-
String account1Name = "Test Account " + LocalDate.now();
89+
String account1Name = "ACR Test Account " + LocalDate.now();
8290
acr.setName(account1Name);
8391
acr.setSite("Philadelphia");
8492
acr.setNumberOfEmployees(25);
@@ -98,7 +106,7 @@ void testSalesforceRecordManagerInsertsGetsAndSOQLQueries() throws JsonProcessin
98106

99107
// create via Map
100108
Map<String, Object> accountViaMap = new HashMap<>();
101-
String account2Name = "Test Account 2 " + LocalDate.now();
109+
String account2Name = "ACR Test Account 2 " + LocalDate.now();
102110
accountViaMap.put("Name", account2Name);
103111
accountViaMap.put("Site", "Baltimore");
104112
accountViaMap.put("NumberOfEmployees", 30);
@@ -188,8 +196,11 @@ void testSalesforceRecordManagerUpdates() throws JsonProcessingException {
188196
srm.deleteObject("Account", rcr1.getId());
189197
}
190198

199+
200+
// Note if this test fails, may be necessary to delete Test MER Account1 and 2 that it creates
201+
// from Salesforce CRM. This test requires that neither exist in SF prior to running.
191202
@Test
192-
void testMultipleEntityRecordInsertions() throws JsonProcessingException {
203+
void testMultipleEntityRecordInsertionsAndCompositeCalls() throws JsonProcessingException {
193204
AccountMultipleEntityRecord amer = new AccountMultipleEntityRecord("111");
194205
amer.setName("Test MER Account1");
195206
amer.setSite("Raleigh");
@@ -212,11 +223,77 @@ void testMultipleEntityRecordInsertions() throws JsonProcessingException {
212223
MultipleEntityRecord201Response response = smri.bulkInsert("Account", merr);
213224
assertEquals(2, response.getResults().size());
214225

215-
// method also deletes objects once done comparing
216-
queryAndConfirmValues(response, amer);
217-
queryAndConfirmValues(response, amer2);
218-
219-
// now test exception handling: will activate by having both accounts have the same reference ID.
226+
String acct1SfId = queryAndConfirmMultipleRecordValues(response, amer);
227+
String acct2SfId = queryAndConfirmMultipleRecordValues(response, amer2);
228+
229+
// use the Composite objects to update the values
230+
AccountUpdateCompositeRecord aucr1 = new AccountUpdateCompositeRecord(acct1SfId);
231+
aucr1.getBody().setNumberOfEmployees(30);
232+
aucr1.getBody().setSite("Asheville");
233+
234+
AccountUpdateCompositeRecord aucr2 = new AccountUpdateCompositeRecord(acct2SfId);
235+
aucr2.getBody().setNumberOfEmployees(40);
236+
aucr2.getBody().setSite("Greensboro");
237+
238+
// ...and add a third company, note SF ID not known yet, so just choosing a unique ref ID
239+
AccountInsertCompositeRecord aicr = new AccountInsertCompositeRecord("mythirdcompany");
240+
aicr.getBody().setName("Test MER Account3");
241+
aicr.getBody().setSite("Winston-Salem");
242+
aicr.getBody().setNumberOfEmployees(25);
243+
244+
CompositeEntityRecordRequest cerReq = new CompositeEntityRecordRequest(false);
245+
List<? extends CompositeEntityRecord> compList = new ArrayList<>(List.of(aucr1, aucr2, aicr));
246+
cerReq.setCompositeRequest(compList);
247+
248+
CompositeEntityRecordResponse cerr = scrs.bulkProcess(cerReq);
249+
250+
queryAndConfirmCompositeValues(acct1SfId, aucr1);
251+
queryAndConfirmCompositeValues(acct2SfId, aucr2);
252+
253+
// test that create worked fine
254+
String acct3SfId = (String) cerr.getCompositeResponse().get(2).getSuccessResultsMap().get("id");
255+
256+
AccountCreateRecord acr = srm.getObject("Account",
257+
acct3SfId, AccountCreateRecord.class);
258+
259+
List<CompositeEntityRecordResponse.Result> results = cerr.getCompositeResponse();
260+
assertEquals(201, results.get(2).getHttpStatusCode());
261+
assertEquals("mythirdcompany", results.get(2).getReferenceId());
262+
Map<String, Object> body = results.get(2).getSuccessResultsMap();
263+
assertEquals(true, body.get("success"));
264+
assertEquals(aicr.getBody().getName(), acr.getName());
265+
assertEquals(aicr.getBody().getSite(), acr.getSite());
266+
assertEquals(aicr.getBody().getNumberOfEmployees(), acr.getNumberOfEmployees());
267+
srm.deleteObject("Account", acct3SfId);
268+
269+
// now test composite errors, will attempt to update Acct 1 even though it doesn't exist anymore
270+
srm.deleteObject("Account", acct1SfId);
271+
aucr2.getBody().setNumberOfEmployees(50);
272+
aucr2.getBody().setSite("Durham");
273+
274+
cerr = scrs.bulkProcess(cerReq);
275+
276+
results = cerr.getCompositeResponse();
277+
assertEquals(400, results.get(0).getHttpStatusCode());
278+
assertEquals(acct1SfId, results.get(0).getReferenceId());
279+
List<Map<String, Object>> errors = results.get(0).getErrorResultsList();
280+
assertEquals("ENTITY_IS_DELETED", errors.get(0).get("errorCode"));
281+
assertEquals("entity is deleted", errors.get(0).get("message"));
282+
assertEquals(0, ((List<?>) errors.get(0).get("fields")).size());
283+
284+
assertEquals(200, results.get(1).getHttpStatusCode());
285+
assertEquals(acct2SfId, results.get(1).getReferenceId());
286+
body = results.get(1).getSuccessResultsMap();
287+
assertEquals(acct2SfId, body.get("id"));
288+
assertEquals(true, body.get("success"));
289+
assertEquals(false, body.get("created"));
290+
291+
// check update occurred for success case
292+
queryAndConfirmCompositeValues(acct2SfId, aucr2);
293+
294+
srm.deleteObject("Account", acct2SfId);
295+
296+
// now test Multiple Entry exception handling: will activate by having both accounts have the same reference ID.
220297
amer2.setAttributes(new MultipleEntityRecord.Attributes("Account", "111"));
221298

222299
/* Error response should be:
@@ -261,7 +338,7 @@ void testMultipleEntityRecordInsertions() throws JsonProcessingException {
261338
assertEquals("Duplicate ReferenceId provided in the request.", message);
262339
}
263340

264-
private void queryAndConfirmValues(MultipleEntityRecord201Response response, AccountMultipleEntityRecord amer)
341+
private String queryAndConfirmMultipleRecordValues(MultipleEntityRecord201Response response, AccountMultipleEntityRecord amer)
265342
throws JsonProcessingException {
266343
String accountSfId = response.getResults().stream()
267344
.filter(r -> amer.getAttributes().getReferenceId().equals(r.getReferenceId()))
@@ -275,7 +352,16 @@ private void queryAndConfirmValues(MultipleEntityRecord201Response response, Acc
275352
assertEquals(acr.getRating().name(), amer.getRating().name());
276353
assertEquals(acr.getLeadDate(), amer.getLeadDate());
277354

278-
srm.deleteObject("Account", accountSfId);
355+
return accountSfId;
356+
}
357+
358+
private void queryAndConfirmCompositeValues(String accountSfId, AccountUpdateCompositeRecord aucr)
359+
throws JsonProcessingException {
360+
361+
// query and check values
362+
AccountCreateRecord acr = srm.getObject("Account", accountSfId, AccountCreateRecord.class);
363+
assertEquals(aucr.getBody().getSite(), acr.getSite());
364+
assertEquals(aucr.getBody().getNumberOfEmployees(), acr.getNumberOfEmployees());
279365
}
280366

281367
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package net.glenmazza.sfclient.model;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnore;
4+
5+
/**
6+
* Support for
7+
* <a href="https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/resources_composite_composite_post.htm">Composite calls</a>,
8+
* max of 25 subrequests per call.
9+
* For bulk inserts where each insert has no relation to the others MultipleEntityRecord is preferred, as it
10+
* has a 200 max per call.
11+
*/
12+
public abstract class CompositeEntityRecord {
13+
14+
@JsonIgnore
15+
private final String entity;
16+
private final String referenceId;
17+
private final Method method;
18+
19+
private String url;
20+
21+
public CompositeEntityRecord(String entity, Method method, String referenceId) {
22+
this.entity = entity;
23+
this.method = method;
24+
this.referenceId = referenceId;
25+
}
26+
27+
// entity represents the type of object the CRUD action applies to (Contact, Account, etc.)
28+
29+
public String getEntity() {
30+
return entity;
31+
}
32+
33+
public String getUrl() {
34+
return url;
35+
}
36+
37+
public void setUrl(String url) {
38+
this.url = url;
39+
}
40+
41+
public String getReferenceId() {
42+
return referenceId;
43+
}
44+
45+
public Method getMethod() {
46+
return method;
47+
}
48+
49+
public enum Method {
50+
PATCH, // updates
51+
POST; // inserts
52+
}
53+
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package net.glenmazza.sfclient.model;
2+
3+
import java.util.List;
4+
5+
public class CompositeEntityRecordRequest {
6+
7+
// allOrNone: if true, error in one causes rollback of all.
8+
// https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/requests_composite.htm
9+
// requiring in constructor as it is an important property
10+
public CompositeEntityRecordRequest(boolean allOrNone) {
11+
this.allOrNone = allOrNone;
12+
}
13+
14+
List<? extends CompositeEntityRecord> compositeRequest;
15+
16+
//
17+
boolean allOrNone;
18+
19+
public List<? extends CompositeEntityRecord> getCompositeRequest() {
20+
return compositeRequest;
21+
}
22+
23+
public void setCompositeRequest(List<? extends CompositeEntityRecord> compositeRequest) {
24+
this.compositeRequest = compositeRequest;
25+
}
26+
27+
public boolean isAllOrNone() {
28+
return allOrNone;
29+
}
30+
31+
}

0 commit comments

Comments
 (0)