Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: support server-side batch check endpoint #141

Merged
merged 1 commit into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,11 @@ src/main/java/dev/openfga/sdk/api/client/ApiResponse.java
src/main/java/dev/openfga/sdk/api/client/ClientAssertion.java
src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckItem.java
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckRequest.java
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckResponse.java
src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckSingleResponse.java
src/main/java/dev/openfga/sdk/api/client/model/ClientCheckRequest.java
src/main/java/dev/openfga/sdk/api/client/model/ClientCheckResponse.java
src/main/java/dev/openfga/sdk/api/client/model/ClientCreateStoreResponse.java
Expand Down Expand Up @@ -163,6 +167,7 @@ src/main/java/dev/openfga/sdk/api/client/model/ClientWriteResponse.java
src/main/java/dev/openfga/sdk/api/configuration/AdditionalHeadersSupplier.java
src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java
src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java
src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java
src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java
src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java
src/main/java/dev/openfga/sdk/api/configuration/ClientConfiguration.java
Expand Down Expand Up @@ -286,6 +291,7 @@ src/main/java/dev/openfga/sdk/errors/FgaApiRateLimitExceededError.java
src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java
src/main/java/dev/openfga/sdk/errors/FgaError.java
src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java
src/main/java/dev/openfga/sdk/errors/FgaValidationError.java
src/main/java/dev/openfga/sdk/errors/HttpStatusCode.java
src/main/java/dev/openfga/sdk/telemetry/Attribute.java
src/main/java/dev/openfga/sdk/telemetry/Attributes.java
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

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

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

BREAKING CHANGES:

- Usage of the existing `batchCheck` method should now use the `clientBatchCheck` method.

## v0.7.2

### [0.7.2](https://github.com/openfga/java-sdk/compare/v0.7.1...v0.7.2) (2024-12-18)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@ var response = fgaClient.check(request, options).get();
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.
If 429s or 5xxs are encountered, the underlying check will retry up to 3 times before giving up.

> Passing `ClientBatchCheckOptions` is optional. All fields of `ClientBatchCheckOptions` are optional.
> Passing `ClientBatchCheckClientOptions` is optional. All fields of `ClientBatchCheckClientOptions` are optional.

```java
var request = List.of(
Expand Down Expand Up @@ -637,7 +637,7 @@ var request = List.of(
.relation("deleter")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
);
var options = new ClientBatchCheckOptions()
var options = new ClientBatchCheckClientOptions()
.additionalHeaders(Map.of("Some-Http-Header", "Some value"))
// You can rely on the model id set in the configuration or override it for this specific request
.authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1")
Expand Down Expand Up @@ -1093,7 +1093,7 @@ If you have found a bug or if you have a feature request, please report them on

### Pull Requests

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.
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.

## Author

Expand Down
8 changes: 4 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
// Quality
id 'jacoco'
id 'jvm-test-suite'
id 'com.diffplug.spotless' version '7.0.2'
id 'com.diffplug.spotless' version '6.25.0'

// IDE
id 'idea'
Expand Down Expand Up @@ -66,7 +66,7 @@ dependencies {
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
implementation "org.openapitools:jackson-databind-nullable:0.2.6"
implementation platform("io.opentelemetry:opentelemetry-bom:1.46.0")
implementation platform("io.opentelemetry:opentelemetry-bom:1.45.0")
implementation "io.opentelemetry:opentelemetry-api"
}

Expand All @@ -78,9 +78,9 @@ testing {
dependencies {
implementation project()
implementation "org.junit.jupiter:junit-jupiter:$junit_version"
implementation "org.mockito:mockito-core:5.15.2"
implementation "org.mockito:mockito-core:5.14.2"
runtimeOnly "org.junit.platform:junit-platform-launcher"
implementation "org.wiremock:wiremock:3.11.0"
implementation "org.wiremock:wiremock:3.10.0"

// This test-only dependency is convenient but not widely used.
// Review project activity before updating the version here.
Expand Down
4 changes: 2 additions & 2 deletions example/example1/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id 'application'
id 'com.diffplug.spotless' version '7.0.2'
id 'org.jetbrains.kotlin.jvm' version '2.1.10'
id 'com.diffplug.spotless' version '6.25.0'
id 'org.jetbrains.kotlin.jvm' version '2.1.0'
}

application {
Expand Down
132 changes: 123 additions & 9 deletions src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

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

public OpenFgaClient(ClientConfiguration configuration) throws FgaInvalidParameterException {
this(configuration, new ApiClient());
Expand Down Expand Up @@ -574,29 +577,29 @@ public CompletableFuture<ClientCheckResponse> check(ClientCheckRequest request,
*
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
*/
public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(List<ClientCheckRequest> requests)
public CompletableFuture<List<ClientBatchCheckClientResponse>> clientBatchCheck(List<ClientCheckRequest> requests)
throws FgaInvalidParameterException {
return batchCheck(requests, null);
return clientBatchCheck(requests, null);
}

/**
* BatchCheck - Run a set of checks (evaluates)
*
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
*/
public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(
List<ClientCheckRequest> requests, ClientBatchCheckOptions batchCheckOptions)
public CompletableFuture<List<ClientBatchCheckClientResponse>> clientBatchCheck(
List<ClientCheckRequest> requests, ClientBatchCheckClientOptions batchCheckOptions)
throws FgaInvalidParameterException {
configuration.assertValid();
configuration.assertValidStoreId();

var options = batchCheckOptions != null
? batchCheckOptions
: new ClientBatchCheckOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS);
: new ClientBatchCheckClientOptions().maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS);
if (options.getAdditionalHeaders() == null) {
options.additionalHeaders(new HashMap<>());
}
options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck");
options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "ClientBatchCheck");
options.getAdditionalHeaders()
.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString());

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

var responses = new ConcurrentLinkedQueue<ClientBatchCheckResponse>();
var responses = new ConcurrentLinkedQueue<ClientBatchCheckClientResponse>();

final var clientCheckOptions = options.asClientCheckOptions();

Consumer<ClientCheckRequest> singleClientCheckRequest =
request -> call(() -> this.check(request, clientCheckOptions))
.handleAsync(ClientBatchCheckResponse.asyncHandler(request))
.handleAsync(ClientBatchCheckClientResponse.asyncHandler(request))
.thenAccept(responses::add)
.thenRun(latch::countDown);

Expand All @@ -627,6 +630,117 @@ public CompletableFuture<List<ClientBatchCheckResponse>> batchCheck(
}
}

/**
* BatchCheck - Run a set of checks (evaluates)
*
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
*/
public CompletableFuture<ClientBatchCheckResponse> batchCheck(ClientBatchCheckRequest request)
throws FgaInvalidParameterException, FgaValidationError {
return batchCheck(request, null);
}

/**
* BatchCheck - Run a set of checks (evaluates)
*
* @throws FgaInvalidParameterException When the Store ID is null, empty, or whitespace
*/
public CompletableFuture<ClientBatchCheckResponse> batchCheck(
ClientBatchCheckRequest requests, ClientBatchCheckOptions batchCheckOptions)
throws FgaInvalidParameterException, FgaValidationError {
configuration.assertValid();
configuration.assertValidStoreId();

var options = batchCheckOptions != null
? batchCheckOptions
: new ClientBatchCheckOptions()
.maxParallelRequests(DEFAULT_MAX_METHOD_PARALLEL_REQS)
.maxBatchSize(DEFAULT_MAX_BATCH_SIZE);
if (options.getAdditionalHeaders() == null) {
options.additionalHeaders(new HashMap<>());
}
options.getAdditionalHeaders().putIfAbsent(CLIENT_METHOD_HEADER, "BatchCheck");
options.getAdditionalHeaders()
.putIfAbsent(CLIENT_BULK_REQUEST_ID_HEADER, randomUUID().toString());

Map<String, ClientBatchCheckItem> correlationIdToCheck = new HashMap<>();

List<BatchCheckItem> collect = new ArrayList<>();
for (ClientBatchCheckItem check : requests.getChecks()) {
String correlationId = check.getCorrelationId();
correlationId = correlationId == null || correlationId.isBlank()
? randomUUID().toString()
: correlationId;

BatchCheckItem batchCheckItem = new BatchCheckItem()
.tupleKey(new CheckRequestTupleKey()
.user(check.getUser())
.relation(check.getRelation())
._object(check.getObject()))
.context(check.getContext())
.correlationId(correlationId);

List<ClientTupleKey> contextualTuples = check.getContextualTuples();
if (contextualTuples != null && !contextualTuples.isEmpty()) {
batchCheckItem.contextualTuples(ClientTupleKey.asContextualTupleKeys(contextualTuples));
}

collect.add(batchCheckItem);

if (correlationIdToCheck.containsKey(correlationId)) {
throw new FgaValidationError(
"correlationId", "When calling batchCheck, correlation IDs must be unique");
}

correlationIdToCheck.put(correlationId, check);
}

int maxBatchSize = options.getMaxBatchSize() != null ? options.getMaxBatchSize() : DEFAULT_MAX_BATCH_SIZE;
List<List<BatchCheckItem>> batchedChecks = IntStream.range(
0, (collect.size() + maxBatchSize - 1) / maxBatchSize)
.mapToObj(i -> collect.subList(i * maxBatchSize, Math.min((i + 1) * maxBatchSize, collect.size())))
.collect(Collectors.toList());

int maxParallelRequests = options.getMaxParallelRequests() != null
? options.getMaxParallelRequests()
: DEFAULT_MAX_METHOD_PARALLEL_REQS;
var executor = Executors.newScheduledThreadPool(maxParallelRequests);
var latch = new CountDownLatch(batchedChecks.size());

var responses = new ConcurrentLinkedQueue<ClientBatchCheckSingleResponse>();

var override = new ConfigurationOverride().addHeaders(options);

Consumer<List<BatchCheckItem>> singleBatchCheckRequest = request -> call(() ->
api.batchCheck(configuration.getStoreId(), new BatchCheckRequest().checks(request), override))
.handleAsync((batchCheckResponseApiResponse, throwable) -> {
Map<String, BatchCheckSingleResult> response =
batchCheckResponseApiResponse.getData().getResult();

List<ClientBatchCheckSingleResponse> batchResults = new ArrayList<>();
response.forEach((key, result) -> {
boolean allowed = Boolean.TRUE.equals(result.getAllowed());
ClientBatchCheckItem checkItem = correlationIdToCheck.get(key);
var singleResponse =
new ClientBatchCheckSingleResponse(allowed, checkItem, key, result.getError());
batchResults.add(singleResponse);
});
return batchResults;
})
.thenAccept(responses::addAll)
.thenRun(latch::countDown);

try {
batchedChecks.forEach(batch -> executor.execute(() -> singleBatchCheckRequest.accept(batch)));
latch.await();
return CompletableFuture.completedFuture(new ClientBatchCheckResponse(new ArrayList<>(responses)));
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
} finally {
executor.shutdown();
}
}

/**
* Expand - Expands the relationships in userset tree format (evaluates)
*
Expand Down Expand Up @@ -764,7 +878,7 @@ public CompletableFuture<ClientListRelationsResponse> listRelations(
.context(request.getContext()))
.collect(Collectors.toList());

return this.batchCheck(batchCheckRequests, options.asClientBatchCheckOptions())
return this.clientBatchCheck(batchCheckRequests, options.asClientBatchCheckClientOptions())
.thenCompose(responses -> call(() -> ClientListRelationsResponse.fromBatchCheckResponses(responses)));
}

Expand Down
Loading