Skip to content

Commit 162d7ca

Browse files
feat(java-sdk): implement individual tuple error handling for non-tra… (#573)
2 parents c06203b + 319252f commit 162d7ca

File tree

10 files changed

+436
-39
lines changed

10 files changed

+436
-39
lines changed

config/clients/java/config.overrides.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@
181181
"destinationFilename": "src/main/java/dev/openfga/sdk/api/client/model/ClientWriteResponse.java",
182182
"templateType": "SupportingFiles"
183183
},
184+
"src/main/api/client/model/ClientWriteSingleResponse.java.mustache": {
185+
"destinationFilename": "src/main/java/dev/openfga/sdk/api/client/model/ClientWriteSingleResponse.java",
186+
"templateType": "SupportingFiles"
187+
},
188+
"src/main/api/client/model/ClientWriteStatus.java.mustache": {
189+
"destinationFilename": "src/main/java/dev/openfga/sdk/api/client/model/ClientWriteStatus.java",
190+
"templateType": "SupportingFiles"
191+
},
184192
"src/main/api/client/ClientAssertion.java.mustache": {
185193
"destinationFilename": "src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java",
186194
"templateType": "SupportingFiles"
@@ -429,6 +437,14 @@
429437
"destinationFilename": "src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java",
430438
"templateType": "SupportingFiles"
431439
},
440+
"src/test/api/client/model/ClientWriteStatusTest.java.mustache": {
441+
"destinationFilename": "src/test/java/dev/openfga/sdk/api/client/model/ClientWriteStatusTest.java",
442+
"templateType": "SupportingFiles"
443+
},
444+
"src/test/api/client/model/ClientWriteSingleResponseTest.java.mustache": {
445+
"destinationFilename": "src/test/java/dev/openfga/sdk/api/client/model/ClientWriteSingleResponseTest.java",
446+
"templateType": "SupportingFiles"
447+
},
432448
"src/test/api/configuration/ClientCredentialsTest.java.mustache": {
433449
"destinationFilename": "src/test/java/dev/openfga/sdk/api/configuration/ClientCredentialsTest.java",
434450
"templateType": "SupportingFiles"

config/clients/java/template/example/example1/src/main/java/dev/openfga/sdk/example/Example1.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public void run(String apiUrl) throws Exception {
1818
if (System.getenv("FGA_CLIENT_ID") != null) {
1919
credentials = new Credentials(new ClientCredentials()
2020
.apiAudience(System.getenv("FGA_API_AUDIENCE"))
21-
.apiTokenIssuer(System.getenv("FGA_TOKEN_ISSUER"))
22-
.clientId("FGA_CLIENT_ID")
23-
.clientSecret("FGA_CLIENT_SECRET"));
21+
.apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER"))
22+
.clientId(System.getenv("FGA_CLIENT_ID"))
23+
.clientSecret(System.getenv("FGA_CLIENT_SECRET")));
2424
} else {
2525
System.out.println("Proceeding with no credentials (expecting localhost)");
2626
}
@@ -102,10 +102,20 @@ public void run(String apiUrl) throws Exception {
102102
fgaClient
103103
.write(
104104
new ClientWriteRequest()
105-
.writes(List.of(new ClientTupleKey()
106-
.user("user:anne")
107-
.relation("writer")
108-
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"))),
105+
.writes(List.of(
106+
new ClientTupleKey()
107+
.user("user:anne")
108+
.relation("writer")
109+
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
110+
new ClientTupleKey()
111+
.user("user:anne")
112+
.relation("writer")
113+
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"), // duplicate
114+
new ClientTupleKey()
115+
.user("user:anne")
116+
.relation("owner")
117+
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a") // different relation
118+
)),
109119
new ClientWriteOptions()
110120
.disableTransactions(true)
111121
.authorizationModelId(authorizationModel.getAuthorizationModelId()))
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
pluginManagement {
2+
repositories {
3+
gradlePluginPortal()
4+
mavenCentral()
5+
}
6+
}
7+
18
rootProject.name = '{{artifactId}}'

config/clients/java/template/src/main/api/client/OpenFgaClient.java.mustache

Lines changed: 231 additions & 22 deletions
Large diffs are not rendered by default.

config/clients/java/template/src/main/api/client/model/ClientWriteResponse.java.mustache

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,29 @@ package {{clientPackage}}.model;
44
import {{clientPackage}}.ApiResponse;
55
import java.util.List;
66
import java.util.Map;
7+
import java.util.Collections;
78

89
public class ClientWriteResponse {
910
private final int statusCode;
1011
private final Map<String, List<String>> headers;
1112
private final String rawResponse;
13+
private final List<ClientWriteSingleResponse> writes;
14+
private final List<ClientWriteSingleResponse> deletes;
1215
1316
public ClientWriteResponse(ApiResponse<Object> apiResponse) {
1417
this.statusCode = apiResponse.getStatusCode();
1518
this.headers = apiResponse.getHeaders();
1619
this.rawResponse = apiResponse.getRawResponse();
20+
this.writes = Collections.emptyList();
21+
this.deletes = Collections.emptyList();
22+
}
23+
24+
public ClientWriteResponse(List<ClientWriteSingleResponse> writes, List<ClientWriteSingleResponse> deletes) {
25+
this.statusCode = 200;
26+
this.headers = Collections.emptyMap();
27+
this.rawResponse = "";
28+
this.writes = writes != null ? writes : Collections.emptyList();
29+
this.deletes = deletes != null ? deletes : Collections.emptyList();
1730
}
1831

1932
public int getStatusCode() {
@@ -27,4 +40,12 @@ public class ClientWriteResponse {
2740
public String getRawResponse() {
2841
return rawResponse;
2942
}
43+
44+
public List<ClientWriteSingleResponse> getWrites() {
45+
return writes;
46+
}
47+
48+
public List<ClientWriteSingleResponse> getDeletes() {
49+
return deletes;
50+
}
3051
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{{>licenseInfo}}
2+
package {{clientPackage}}.model;
3+
4+
import {{modelPackage}}.TupleKey;
5+
6+
public class ClientWriteSingleResponse {
7+
private final TupleKey tupleKey;
8+
private final ClientWriteStatus status;
9+
private final Exception error;
10+
11+
public ClientWriteSingleResponse(TupleKey tupleKey, ClientWriteStatus status) {
12+
this(tupleKey, status, null);
13+
}
14+
15+
public ClientWriteSingleResponse(TupleKey tupleKey, ClientWriteStatus status, Exception error) {
16+
this.tupleKey = tupleKey;
17+
this.status = status;
18+
this.error = error;
19+
}
20+
21+
public TupleKey getTupleKey() {
22+
return tupleKey;
23+
}
24+
25+
public ClientWriteStatus getStatus() {
26+
return status;
27+
}
28+
29+
public Exception getError() {
30+
return error;
31+
}
32+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{{>licenseInfo}}
2+
package {{clientPackage}}.model;
3+
4+
public enum ClientWriteStatus {
5+
SUCCESS("success"),
6+
FAILURE("failure");
7+
8+
private final String value;
9+
10+
ClientWriteStatus(String value) {
11+
this.value = value;
12+
}
13+
14+
public String getValue() {
15+
return value;
16+
}
17+
18+
@Override
19+
public String toString() {
20+
return value;
21+
}
22+
}

config/clients/java/template/src/test/api/client/OpenFgaClientTest.java.mustache

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,13 @@ public class OpenFgaClientTest {
11441144
// Then
11451145
mockHttpClient.verify().post(postPath).withBody(is(expectedBody)).called(1);
11461146
assertEquals(200, response.getStatusCode());
1147+
1148+
// Verify new response structure for transaction-based writes
1149+
assertEquals(1, response.getWrites().size());
1150+
assertEquals(0, response.getDeletes().size());
1151+
assertEquals(ClientWriteStatus.SUCCESS, response.getWrites().get(0).getStatus());
1152+
assertEquals(DEFAULT_USER, response.getWrites().get(0).getTupleKey().getUser());
1153+
assertNull(response.getWrites().get(0).getError());
11471154
}
11481155

11491156
/**
@@ -1245,18 +1252,18 @@ public class OpenFgaClientTest {
12451252
}
12461253

12471254
@Test
1248-
public void writeTest_nonTransactionsWithFailure() {
1255+
public void writeTest_nonTransactionsWithFailure() throws Exception {
12491256
// Given
12501257
String postPath = "https://api.fga.example/stores/01YCP46JKYM8FJCQ37NMBYHE5X/write";
12511258
String firstUser = "user:first";
12521259
String failedUser = "user:SECOND";
1253-
String skippedUser = "user:third";
1260+
String thirdUser = "user:third";
12541261
Function<String, String> writeBody = user -> String.format(
12551262
"{\"writes\":{\"tuple_keys\":[{\"user\":\"%s\",\"relation\":\"%s\",\"object\":\"%s\",\"condition\":{\"name\":\"condition\",\"context\":{\"some\":\"context\"}}}]},\"deletes\":null,\"authorization_model_id\":\"%s\"}",
12561263
user, DEFAULT_RELATION, DEFAULT_OBJECT, DEFAULT_AUTH_MODEL_ID);
12571264
mockHttpClient
12581265
.onPost(postPath)
1259-
.withBody(isOneOf(writeBody.apply(firstUser), writeBody.apply(skippedUser)))
1266+
.withBody(isOneOf(writeBody.apply(firstUser), writeBody.apply(thirdUser)))
12601267
.withHeader(CLIENT_METHOD_HEADER, "Write")
12611268
.withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID())
12621269
.doReturn(200, EMPTY_RESPONSE_BODY);
@@ -1267,7 +1274,7 @@ public class OpenFgaClientTest {
12671274
.withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID())
12681275
.doReturn(400, "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}");
12691276
ClientWriteRequest request = new ClientWriteRequest()
1270-
.writes(Stream.of(firstUser, failedUser, skippedUser)
1277+
.writes(Stream.of(firstUser, failedUser, thirdUser)
12711278
.map(user -> new ClientTupleKey()
12721279
._object(DEFAULT_OBJECT)
12731280
.relation(DEFAULT_RELATION)
@@ -1278,8 +1285,7 @@ public class OpenFgaClientTest {
12781285
new ClientWriteOptions().disableTransactions(true).transactionChunkSize(1);
12791286
12801287
// When
1281-
var execException = assertThrows(
1282-
ExecutionException.class, () -> fga.write(request, options).get());
1288+
ClientWriteResponse response = fga.write(request, options).get();
12831289
12841290
// Then
12851291
mockHttpClient
@@ -1299,12 +1305,28 @@ public class OpenFgaClientTest {
12991305
mockHttpClient
13001306
.verify()
13011307
.post(postPath)
1302-
.withBody(is(writeBody.apply(skippedUser)))
1308+
.withBody(is(writeBody.apply(thirdUser)))
13031309
.withHeader(CLIENT_METHOD_HEADER, "Write")
13041310
.withHeader(CLIENT_BULK_REQUEST_ID_HEADER, anyValidUUID())
1305-
.called(0);
1306-
var exception = assertInstanceOf(FgaApiValidationError.class, execException.getCause());
1307-
assertEquals(400, exception.getStatusCode());
1311+
.called(1);
1312+
1313+
// Verify response structure
1314+
assertEquals(3, response.getWrites().size());
1315+
assertEquals(0, response.getDeletes().size());
1316+
1317+
// Check individual tuple statuses
1318+
var writes = response.getWrites();
1319+
assertEquals(ClientWriteStatus.SUCCESS, writes.get(0).getStatus());
1320+
assertEquals(firstUser, writes.get(0).getTupleKey().getUser());
1321+
assertNull(writes.get(0).getError());
1322+
1323+
assertEquals(ClientWriteStatus.FAILURE, writes.get(1).getStatus());
1324+
assertEquals(failedUser, writes.get(1).getTupleKey().getUser());
1325+
assertNotNull(writes.get(1).getError());
1326+
1327+
assertEquals(ClientWriteStatus.SUCCESS, writes.get(2).getStatus());
1328+
assertEquals(thirdUser, writes.get(2).getTupleKey().getUser());
1329+
assertNull(writes.get(2).getError());
13081330
}
13091331

13101332
@Test
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{{>licenseInfo}}
2+
package {{clientPackage}}.model;
3+
4+
import static org.junit.jupiter.api.Assertions.*;
5+
import org.junit.jupiter.api.Test;
6+
import {{modelPackage}}.TupleKey;
7+
8+
public class ClientWriteSingleResponseTest {
9+
10+
@Test
11+
public void testSuccessfulResponse() {
12+
TupleKey tupleKey = new TupleKey()
13+
.user("user:alice")
14+
.relation("viewer")
15+
._object("document:test");
16+
17+
ClientWriteSingleResponse response = new ClientWriteSingleResponse(tupleKey, ClientWriteStatus.SUCCESS);
18+
19+
assertEquals(tupleKey, response.getTupleKey());
20+
assertEquals(ClientWriteStatus.SUCCESS, response.getStatus());
21+
assertNull(response.getError());
22+
}
23+
24+
@Test
25+
public void testFailedResponse() {
26+
TupleKey tupleKey = new TupleKey()
27+
.user("user:bob")
28+
.relation("editor")
29+
._object("document:test");
30+
Exception error = new RuntimeException("Test error");
31+
32+
ClientWriteSingleResponse response = new ClientWriteSingleResponse(tupleKey, ClientWriteStatus.FAILURE, error);
33+
34+
assertEquals(tupleKey, response.getTupleKey());
35+
assertEquals(ClientWriteStatus.FAILURE, response.getStatus());
36+
assertEquals(error, response.getError());
37+
}
38+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{{>licenseInfo}}
2+
package {{clientPackage}}.model;
3+
4+
import static org.junit.jupiter.api.Assertions.*;
5+
import org.junit.jupiter.api.Test;
6+
7+
public class ClientWriteStatusTest {
8+
9+
@Test
10+
public void testSuccessValue() {
11+
assertEquals("success", ClientWriteStatus.SUCCESS.getValue());
12+
assertEquals("success", ClientWriteStatus.SUCCESS.toString());
13+
}
14+
15+
@Test
16+
public void testFailureValue() {
17+
assertEquals("failure", ClientWriteStatus.FAILURE.getValue());
18+
assertEquals("failure", ClientWriteStatus.FAILURE.toString());
19+
}
20+
}

0 commit comments

Comments
 (0)