Skip to content

Commit fdab6d2

Browse files
authored
Merge pull request #141 from piotrooo/feat/batch-check
feat!: support server-side batch check endpoint
2 parents c249fb6 + 6e9777a commit fdab6d2

17 files changed

+706
-143
lines changed

.openapi-generator/FILES

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,11 @@ src/main/java/dev/openfga/sdk/api/client/ApiResponse.java
131131
src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java
132132
src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
133133
src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
134+
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java
135+
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckItem.java
136+
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckRequest.java
134137
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java
138+
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckSingleResponse.java
135139
src/main/java/dev/openfga/sdk/api/client/model/ClientCheckRequest.java
136140
src/main/java/dev/openfga/sdk/api/client/model/ClientCheckResponse.java
137141
src/main/java/dev/openfga/sdk/api/client/model/ClientCreateStoreResponse.java
@@ -163,6 +167,7 @@ src/main/java/dev/openfga/sdk/api/client/model/ClientWriteResponse.java
163167
src/main/java/dev/openfga/sdk/api/configuration/AdditionalHeadersSupplier.java
164168
src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java
165169
src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java
170+
src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java
166171
src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java
167172
src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java
168173
src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java
@@ -286,6 +291,7 @@ src/main/java/dev/openfga/sdk/errors/FgaApiRateLimitExceededError.java
286291
src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java
287292
src/main/java/dev/openfga/sdk/errors/FgaError.java
288293
src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java
294+
src/main/java/dev/openfga/sdk/errors/FgaValidationError.java
289295
src/main/java/dev/openfga/sdk/errors/HttpStatusCode.java
290296
src/main/java/dev/openfga/sdk/telemetry/Attribute.java
291297
src/main/java/dev/openfga/sdk/telemetry/Attributes.java

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.7.2...HEAD)
44

5+
- feat!: add support for server-side `BatchCheck` method
56
- feat: add support for `start_time` parameter in `ReadChanges` endpoint
67

8+
BREAKING CHANGES:
9+
10+
- Usage of the existing `batchCheck` method should now use the `clientBatchCheck` method.
11+
712
## v0.7.2
813

914
### [0.7.2](https://github.com/openfga/java-sdk/compare/v0.7.1...v0.7.2) (2024-12-18)

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,7 @@ var response = fgaClient.check(request, options).get();
604604
Run a set of [checks](#check). Batch Check will return `allowed: false` if it encounters an error, and will return the error in the body.
605605
If 429s or 5xxs are encountered, the underlying check will retry up to 3 times before giving up.
606606

607-
> Passing `ClientBatchCheckOptions` is optional. All fields of `ClientBatchCheckOptions` are optional.
607+
> Passing `ClientBatchCheckClientOptions` is optional. All fields of `ClientBatchCheckClientOptions` are optional.
608608
609609
```java
610610
var request = List.of(
@@ -637,7 +637,7 @@ var request = List.of(
637637
.relation("deleter")
638638
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
639639
);
640-
var options = new ClientBatchCheckOptions()
640+
var options = new ClientBatchCheckClientOptions()
641641
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
642642
// You can rely on the model id set in the configuration or override it for this specific request
643643
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1")
@@ -1093,7 +1093,7 @@ If you have found a bug or if you have a feature request, please report them on
10931093

10941094
### Pull Requests
10951095

1096-
While we accept Pull Requests on this repository, the SDKs are autogenerated so please consider additionally submitting your Pull Requests to the [sdk-generator](https://github.com/openfga/sdk-generator) and linking the two PRs together and to the corresponding issue. This will greatly assist the OpenFGA team in being able to give timely reviews as well as deploying fixes and updates to our other SDKs as well.
1096+
While we accept Pull Requests on this repository, the SDKs are autogenerated so please consider additionally submitting your Pull Requests to the [sdk-generator](https://github.com/openfga/sdk-generator) and linking the two PRs together and to the corresponding issue. This will greatly assist the OpenFGA team in being able to give timely reviews as well as deploying fixes and updates to our other SDKs as well.
10971097

10981098
## Author
10991099

build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
// Quality
55
id 'jacoco'
66
id 'jvm-test-suite'
7-
id 'com.diffplug.spotless' version '7.0.2'
7+
id 'com.diffplug.spotless' version '6.25.0'
88

99
// IDE
1010
id 'idea'
@@ -66,7 +66,7 @@ dependencies {
6666
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
6767
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
6868
implementation "org.openapitools:jackson-databind-nullable:0.2.6"
69-
implementation platform("io.opentelemetry:opentelemetry-bom:1.46.0")
69+
implementation platform("io.opentelemetry:opentelemetry-bom:1.45.0")
7070
implementation "io.opentelemetry:opentelemetry-api"
7171
}
7272

@@ -78,9 +78,9 @@ testing {
7878
dependencies {
7979
implementation project()
8080
implementation "org.junit.jupiter:junit-jupiter:$junit_version"
81-
implementation "org.mockito:mockito-core:5.15.2"
81+
implementation "org.mockito:mockito-core:5.14.2"
8282
runtimeOnly "org.junit.platform:junit-platform-launcher"
83-
implementation "org.wiremock:wiremock:3.11.0"
83+
implementation "org.wiremock:wiremock:3.10.0"
8484

8585
// This test-only dependency is convenient but not widely used.
8686
// Review project activity before updating the version here.

example/example1/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
plugins {
22
id 'application'
3-
id 'com.diffplug.spotless' version '7.0.2'
4-
id 'org.jetbrains.kotlin.jvm' version '2.1.10'
3+
id 'com.diffplug.spotless' version '6.25.0'
4+
id 'org.jetbrains.kotlin.jvm' version '2.1.0'
55
}
66

77
application {

src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java

Lines changed: 123 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@
2323
import java.util.ArrayList;
2424
import java.util.HashMap;
2525
import java.util.List;
26+
import java.util.Map;
2627
import java.util.concurrent.*;
2728
import java.util.function.Consumer;
2829
import java.util.stream.Collectors;
30+
import java.util.stream.IntStream;
2931
import java.util.stream.Stream;
3032

3133
public class OpenFgaClient {
@@ -36,6 +38,7 @@ public class OpenFgaClient {
3638
private static final String CLIENT_BULK_REQUEST_ID_HEADER = "X-OpenFGA-Client-Bulk-Request-Id";
3739
private static final String CLIENT_METHOD_HEADER = "X-OpenFGA-Client-Method";
3840
private static final int DEFAULT_MAX_METHOD_PARALLEL_REQS = 10;
41+
private static final int DEFAULT_MAX_BATCH_SIZE = 50;
3942

4043
public OpenFgaClient(ClientConfiguration configuration) throws FgaInvalidParameterException {
4144
this(configuration, new ApiClient());
@@ -574,29 +577,29 @@ public CompletableFuture<ClientCheckResponse> check(ClientCheckRequest request,
574577
*
575578
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
576579
*/
577-
public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(List<ClientCheckRequest> requests)
580+
public CompletableFuture<List<ClientBatchCheckClientResponse>> clientBatchCheck(List<ClientCheckRequest> requests)
578581
throws FgaInvalidParameterException {
579-
return batchCheck(requests, null);
582+
return clientBatchCheck(requests, null);
580583
}
581584

582585
/**
583586
* BatchCheck - Run a set of checks (evaluates)
584587
*
585588
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
586589
*/
587-
public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(
588-
List<ClientCheckRequest> requests, ClientBatchCheckOptions batchCheckOptions)
590+
public CompletableFuture<List<ClientBatchCheckClientResponse>> clientBatchCheck(
591+
List<ClientCheckRequest> requests, ClientBatchCheckClientOptions batchCheckOptions)
589592
throws FgaInvalidParameterException {
590593
configuration.assertValid();
591594
configuration.assertValidStoreId();
592595

593596
var options = batchCheckOptions != null
594597
? batchCheckOptions
595-
: new ClientBatchCheckOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS);
598+
: new ClientBatchCheckClientOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS);
596599
if (options.getAdditionalHeaders() == null) {
597600
options.additionalHeaders(new HashMap<>());
598601
}
599-
options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck");
602+
options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "ClientBatchCheck");
600603
options.getAdditionalHeaders()
601604
.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString());
602605

@@ -606,13 +609,13 @@ public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(
606609
var executor = Executors.newScheduledThreadPool(maxParallelRequests);
607610
var latch = new CountDownLatch(requests.size());
608611

609-
var responses = new ConcurrentLinkedQueue<ClientBatchCheckResponse>();
612+
var responses = new ConcurrentLinkedQueue<ClientBatchCheckClientResponse>();
610613

611614
final var clientCheckOptions = options.asClientCheckOptions();
612615

613616
Consumer<ClientCheckRequest> singleClientCheckRequest =
614617
request -> call(() -> this.check(request, clientCheckOptions))
615-
.handleAsync(ClientBatchCheckResponse.asyncHandler(request))
618+
.handleAsync(ClientBatchCheckClientResponse.asyncHandler(request))
616619
.thenAccept(responses::add)
617620
.thenRun(latch::countDown);
618621

@@ -627,6 +630,117 @@ public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(
627630
}
628631
}
629632

633+
/**
634+
* BatchCheck - Run a set of checks (evaluates)
635+
*
636+
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
637+
*/
638+
public CompletableFuture<ClientBatchCheckResponse> batchCheck(ClientBatchCheckRequest request)
639+
throws FgaInvalidParameterException, FgaValidationError {
640+
return batchCheck(request, null);
641+
}
642+
643+
/**
644+
* BatchCheck - Run a set of checks (evaluates)
645+
*
646+
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
647+
*/
648+
public CompletableFuture<ClientBatchCheckResponse> batchCheck(
649+
ClientBatchCheckRequest requests, ClientBatchCheckOptions batchCheckOptions)
650+
throws FgaInvalidParameterException, FgaValidationError {
651+
configuration.assertValid();
652+
configuration.assertValidStoreId();
653+
654+
var options = batchCheckOptions != null
655+
? batchCheckOptions
656+
: new ClientBatchCheckOptions()
657+
.maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS)
658+
.maxBatchSize(DEFAULT_MAX_BATCH_SIZE);
659+
if (options.getAdditionalHeaders() == null) {
660+
options.additionalHeaders(new HashMap<>());
661+
}
662+
options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck");
663+
options.getAdditionalHeaders()
664+
.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString());
665+
666+
Map<String, ClientBatchCheckItem> correlationIdToCheck = new HashMap<>();
667+
668+
List<BatchCheckItem> collect = new ArrayList<>();
669+
for (ClientBatchCheckItem check : requests.getChecks()) {
670+
String correlationId = check.getCorrelationId();
671+
correlationId = correlationId == null || correlationId.isBlank()
672+
? randomUUID().toString()
673+
: correlationId;
674+
675+
BatchCheckItem batchCheckItem = new BatchCheckItem()
676+
.tupleKey(new CheckRequestTupleKey()
677+
.user(check.getUser())
678+
.relation(check.getRelation())
679+
._object(check.getObject()))
680+
.context(check.getContext())
681+
.correlationId(correlationId);
682+
683+
List<ClientTupleKey> contextualTuples = check.getContextualTuples();
684+
if (contextualTuples != null && !contextualTuples.isEmpty()) {
685+
batchCheckItem.contextualTuples(ClientTupleKey.asContextualTupleKeys(contextualTuples));
686+
}
687+
688+
collect.add(batchCheckItem);
689+
690+
if (correlationIdToCheck.containsKey(correlationId)) {
691+
throw new FgaValidationError(
692+
"correlationId", "When calling batchCheck, correlation IDs must be unique");
693+
}
694+
695+
correlationIdToCheck.put(correlationId, check);
696+
}
697+
698+
int maxBatchSize = options.getMaxBatchSize() != null ? options.getMaxBatchSize() : DEFAULT_MAX_BATCH_SIZE;
699+
List<List<BatchCheckItem>> batchedChecks = IntStream.range(
700+
0, (collect.size() + maxBatchSize - 1) / maxBatchSize)
701+
.mapToObj(i -> collect.subList(i * maxBatchSize, Math.min((i + 1) * maxBatchSize, collect.size())))
702+
.collect(Collectors.toList());
703+
704+
int maxParallelRequests = options.getMaxParallelRequests() != null
705+
? options.getMaxParallelRequests()
706+
: DEFAULT_MAX_METHOD_PARALLEL_REQS;
707+
var executor = Executors.newScheduledThreadPool(maxParallelRequests);
708+
var latch = new CountDownLatch(batchedChecks.size());
709+
710+
var responses = new ConcurrentLinkedQueue<ClientBatchCheckSingleResponse>();
711+
712+
var override = new ConfigurationOverride().addHeaders(options);
713+
714+
Consumer<List<BatchCheckItem>> singleBatchCheckRequest = request -> call(() ->
715+
api.batchCheck(configuration.getStoreId(), new BatchCheckRequest().checks(request), override))
716+
.handleAsync((batchCheckResponseApiResponse, throwable) -> {
717+
Map<String, BatchCheckSingleResult> response =
718+
batchCheckResponseApiResponse.getData().getResult();
719+
720+
List<ClientBatchCheckSingleResponse> batchResults = new ArrayList<>();
721+
response.forEach((key, result) -> {
722+
boolean allowed = Boolean.TRUE.equals(result.getAllowed());
723+
ClientBatchCheckItem checkItem = correlationIdToCheck.get(key);
724+
var singleResponse =
725+
new ClientBatchCheckSingleResponse(allowed, checkItem, key, result.getError());
726+
batchResults.add(singleResponse);
727+
});
728+
return batchResults;
729+
})
730+
.thenAccept(responses::addAll)
731+
.thenRun(latch::countDown);
732+
733+
try {
734+
batchedChecks.forEach(batch -> executor.execute(() -> singleBatchCheckRequest.accept(batch)));
735+
latch.await();
736+
return CompletableFuture.completedFuture(new ClientBatchCheckResponse(new ArrayList<>(responses)));
737+
} catch (Exception e) {
738+
return CompletableFuture.failedFuture(e);
739+
} finally {
740+
executor.shutdown();
741+
}
742+
}
743+
630744
/**
631745
* Expand - Expands the relationships in userset tree format (evaluates)
632746
*
@@ -764,7 +878,7 @@ public CompletableFuture<ClientListRelationsResponse> listRelations(
764878
.context(request.getContext()))
765879
.collect(Collectors.toList());
766880

767-
return this.batchCheck(batchCheckRequests, options.asClientBatchCheckOptions())
881+
return this.clientBatchCheck(batchCheckRequests, options.asClientBatchCheckClientOptions())
768882
.thenCompose(responses -> call(() -> ClientListRelationsResponse.fromBatchCheckResponses(responses)));
769883
}
770884

0 commit comments

Comments
 (0)