From 114355b5bc7bbf9699872ea9c5ab2357b28fd95a Mon Sep 17 00:00:00 2001 From: Srinivas Chippagiri Date: Thu, 5 Nov 2026 01:13:02 -0800 Subject: [PATCH] feat: allow overriding the store ID per request Add a storeId option to the options of every OpenFgaClient method that works on a store. When it is set, the request goes to that store instead of the one in the client configuration, so one client and its connection pool can serve several stores. clientBatchCheck and listRelations pass it on to their checks. Fixes #150 Signed-off-by: Srinivas Chippagiri --- README.md | 5 +- .../openfga/sdk/api/client/OpenFgaClient.java | 52 +++--- .../ClientStreamedListObjectsOptions.java | 13 ++ .../ClientBatchCheckClientOptions.java | 14 ++ .../ClientBatchCheckOptions.java | 13 ++ .../api/configuration/ClientCheckOptions.java | 13 ++ .../ClientDeleteStoreOptions.java | 13 ++ .../ClientDeleteTuplesOptions.java | 13 ++ .../configuration/ClientExpandOptions.java | 13 ++ .../configuration/ClientGetStoreOptions.java | 13 ++ .../ClientListObjectsOptions.java | 13 ++ .../ClientListRelationsOptions.java | 14 ++ .../configuration/ClientListUsersOptions.java | 13 ++ .../ClientReadAssertionsOptions.java | 13 ++ .../ClientReadAuthorizationModelOptions.java | 13 ++ .../ClientReadAuthorizationModelsOptions.java | 13 ++ .../ClientReadChangesOptions.java | 13 ++ ...ntReadLatestAuthorizationModelOptions.java | 13 ++ .../api/configuration/ClientReadOptions.java | 13 ++ .../ClientWriteAssertionsOptions.java | 13 ++ .../ClientWriteAuthorizationModelOptions.java | 13 ++ .../api/configuration/ClientWriteOptions.java | 13 ++ .../ClientWriteTuplesOptions.java | 13 ++ .../OpenFgaClientStoreIdOverrideTest.java | 163 ++++++++++++++++++ 24 files changed, 473 insertions(+), 22 deletions(-) create mode 100644 src/test/java/dev/openfga/sdk/api/client/OpenFgaClientStoreIdOverrideTest.java diff --git a/README.md b/README.md index 6644c24a..d9710940 100644 --- a/README.md +++ b/README.md @@ -738,7 +738,10 @@ var request = new ClientCheckRequest() var options = new ClientCheckOptions() .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"); + .authorizationModelId("01GXSA8YR785C4FYS3C0RTG7B1") + // You can rely on the store id set in the configuration or override it for this specific request, + // e.g. to use one client for several stores. Every method that works on a store has this option. + .storeId("01YCP46JKYM8FJCQ37NMBYHE5X"); var response = fgaClient.check(request, options).get(); // response.getAllowed() = true diff --git a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java index a01a0386..aeb58dbe 100644 --- a/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java +++ b/src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java @@ -175,7 +175,7 @@ public CompletableFuture getStore() throws FgaInvalidPar public CompletableFuture getStore(ClientGetStoreOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.getStore(storeId, overrides)).thenApply(ClientGetStoreResponse::new); } @@ -197,7 +197,7 @@ public CompletableFuture deleteStore() throws FgaInva public CompletableFuture deleteStore(ClientDeleteStoreOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.deleteStore(storeId, overrides)).thenApply(ClientDeleteStoreResponse::new); } @@ -224,7 +224,7 @@ public CompletableFuture readAuthorizatio public CompletableFuture readAuthorizationModels( ClientReadAuthorizationModelsOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); Integer pageSize; String continuationToken; @@ -263,7 +263,7 @@ public CompletableFuture writeAuthorizati WriteAuthorizationModelRequest request, ClientWriteAuthorizationModelOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.writeAuthorizationModel(storeId, request, overrides)) .thenApply(ClientWriteAuthorizationModelResponse::new); @@ -291,7 +291,7 @@ public CompletableFuture readAuthorization public CompletableFuture readAuthorizationModel( ClientReadAuthorizationModelOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); // Set authorizationModelId from options if available; otherwise, require a valid configuration value String authorizationModelId; if (options != null && !isNullOrWhitespace(options.getAuthorizationModelId())) { @@ -322,7 +322,7 @@ public CompletableFuture readLatestAuthori public CompletableFuture readLatestAuthorizationModel( ClientReadLatestAuthorizationModelOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.readAuthorizationModels(storeId, 1, null, overrides)) .thenApply(ClientReadAuthorizationModelResponse::latestOf); @@ -351,7 +351,7 @@ public CompletableFuture readChanges( ClientReadChangesRequest request, ClientReadChangesOptions readChangesOptions) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(readChangesOptions != null ? readChangesOptions.getStoreId() : null); var options = readChangesOptions != null ? readChangesOptions : new ClientReadChangesOptions(); var overrides = new ConfigurationOverride().addHeaders(options); return call(() -> api.readChanges( @@ -381,7 +381,7 @@ public CompletableFuture read(ClientReadRequest request) thr public CompletableFuture read(ClientReadRequest request, ClientReadOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); ReadRequest body = new ReadRequest(); @@ -512,7 +512,7 @@ public CompletableFuture write(ClientWriteRequest request) public CompletableFuture write(ClientWriteRequest request, ClientWriteOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); if (options != null && !options.isTransactionsEnabled()) { return writeNonTransaction(storeId, request, options); @@ -780,7 +780,7 @@ public CompletableFuture writeTuples(List t public CompletableFuture writeTuples( List tupleKeys, ClientWriteTuplesOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); var body = new WriteRequest(); @@ -821,7 +821,7 @@ public CompletableFuture deleteTuples( List tupleKeys, ClientDeleteTuplesOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); var body = new WriteRequest(); @@ -870,7 +870,7 @@ public CompletableFuture check(ClientCheckRequest request) public CompletableFuture check(ClientCheckRequest request, ClientCheckOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); CheckRequest body = request.asCheckRequest(); if (options != null) { @@ -911,7 +911,7 @@ public CompletableFuture> clientBatchCheck( List requests, ClientBatchCheckClientOptions batchCheckOptions) throws FgaInvalidParameterException { configuration.assertValid(); - configuration.assertValidStoreId(); + getStoreIdChecked(batchCheckOptions != null ? batchCheckOptions.getStoreId() : null); var options = batchCheckOptions != null ? batchCheckOptions @@ -964,7 +964,7 @@ public CompletableFuture batchCheck( ClientBatchCheckRequest requests, ClientBatchCheckOptions batchCheckOptions) throws FgaInvalidParameterException, FgaValidationError { configuration.assertValid(); - configuration.assertValidStoreId(); + String storeId = getStoreIdChecked(batchCheckOptions != null ? batchCheckOptions.getStoreId() : null); var options = batchCheckOptions != null ? batchCheckOptions @@ -1048,7 +1048,7 @@ public CompletableFuture batchCheck( body.authorizationModelId(authorizationModelId); } - return api.batchCheck(configuration.getStoreId(), body, override); + return api.batchCheck(storeId, body, override); }) .handle((batchCheckResponseApiResponse, throwable) -> { if (throwable != null) { @@ -1103,7 +1103,7 @@ public CompletableFuture expand(ClientExpandRequest reques public CompletableFuture expand(ClientExpandRequest request, ClientExpandOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); ExpandRequest body = new ExpandRequest(); @@ -1149,7 +1149,7 @@ public CompletableFuture listObjects(ClientListObject public CompletableFuture listObjects( ClientListObjectsRequest request, ClientListObjectsOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); ListObjectsRequest body = new ListObjectsRequest(); @@ -1301,7 +1301,7 @@ public CompletableFuture streamedListObjects( throw new FgaInvalidParameterException("consumer", "streamedListObjects"); } configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); ListObjectsRequest body = new ListObjectsRequest(); @@ -1404,7 +1404,7 @@ public CompletableFuture listUsers(ClientListUsersReque public CompletableFuture listUsers( ClientListUsersRequest request, ClientListUsersOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); ListUsersRequest body = new ListUsersRequest(); @@ -1460,7 +1460,7 @@ public CompletableFuture readAssertions() throws F public CompletableFuture readAssertions(ClientReadAssertionsOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); String authorizationModelId; if (options != null && options.hasValidAuthorizationModelId()) { @@ -1494,7 +1494,7 @@ public CompletableFuture writeAssertions( List assertions, ClientWriteAssertionsOptions options) throws FgaInvalidParameterException { configuration.assertValid(); - String storeId = configuration.getStoreIdChecked(); + String storeId = getStoreIdChecked(options != null ? options.getStoreId() : null); String authorizationModelId; if (options != null && options.hasValidAuthorizationModelId()) { @@ -1511,6 +1511,16 @@ public CompletableFuture writeAssertions( .thenApply(ClientWriteAssertionsResponse::new); } + /** + * Returns the store ID to send a request to: the per-request override when it is set, otherwise the store ID + * from the client configuration. + * + * @throws FgaInvalidParameterException When neither is set + */ + private String getStoreIdChecked(String storeIdOverride) throws FgaInvalidParameterException { + return !isNullOrWhitespace(storeIdOverride) ? storeIdOverride : configuration.getStoreIdChecked(); + } + /** * A {@link FunctionalInterface} for calling a low-level API from {@link OpenFgaApi}. It wraps exceptions * encountered with {@link CompletableFuture#failedFuture(Throwable)} diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientStreamedListObjectsOptions.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientStreamedListObjectsOptions.java index bf4c8c61..73c18444 100644 --- a/src/main/java/dev/openfga/sdk/api/client/model/ClientStreamedListObjectsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientStreamedListObjectsOptions.java @@ -25,6 +25,7 @@ public class ClientStreamedListObjectsOptions implements AdditionalHeadersSuppli private String authorizationModelId; private ConsistencyPreference consistency; private Map additionalHeaders; + private String storeId; public ClientStreamedListObjectsOptions authorizationModelId(String authorizationModelId) { this.authorizationModelId = authorizationModelId; @@ -53,4 +54,16 @@ public ClientStreamedListObjectsOptions additionalHeaders(Map ad public Map getAdditionalHeaders() { return additionalHeaders; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientStreamedListObjectsOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java index 42b9cc9a..609f2f58 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckClientOptions.java @@ -6,6 +6,7 @@ public class ClientBatchCheckClientOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private Integer maxParallelRequests; private String authorizationModelId; private ConsistencyPreference consistency; @@ -51,6 +52,19 @@ public ClientCheckOptions asClientCheckOptions() { return new ClientCheckOptions() .additionalHeaders(additionalHeaders != null ? new HashMap<>(additionalHeaders) : null) .authorizationModelId(authorizationModelId) + .storeId(storeId) .consistency(consistency); } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientBatchCheckClientOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java index 240b01a1..8b683a4b 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientBatchCheckOptions.java @@ -5,6 +5,7 @@ public class ClientBatchCheckOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private Integer maxParallelRequests; private Integer maxBatchSize; private String authorizationModelId; @@ -55,4 +56,16 @@ public ClientBatchCheckOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientBatchCheckOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java index 24c6daa0..b0bf4cc9 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientCheckOptions.java @@ -5,6 +5,7 @@ public class ClientCheckOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; private ConsistencyPreference consistency; @@ -35,4 +36,16 @@ public ClientCheckOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientCheckOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteStoreOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteStoreOptions.java index f55bbb4e..0c940351 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteStoreOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteStoreOptions.java @@ -4,6 +4,7 @@ public class ClientDeleteStoreOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; public ClientDeleteStoreOptions additionalHeaders(Map additionalHeaders) { this.additionalHeaders = additionalHeaders; @@ -14,4 +15,16 @@ public ClientDeleteStoreOptions additionalHeaders(Map additional public Map getAdditionalHeaders() { return this.additionalHeaders; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientDeleteStoreOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteTuplesOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteTuplesOptions.java index 64a917a1..a9bdbb55 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteTuplesOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientDeleteTuplesOptions.java @@ -5,6 +5,7 @@ public class ClientDeleteTuplesOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private WriteRequestDeletes.OnMissingEnum onMissing; public ClientDeleteTuplesOptions additionalHeaders(Map additionalHeaders) { @@ -25,4 +26,16 @@ public ClientDeleteTuplesOptions onMissing(WriteRequestDeletes.OnMissingEnum onM public WriteRequestDeletes.OnMissingEnum getOnMissing() { return onMissing; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientDeleteTuplesOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java index 9c0afe21..cc520c79 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientExpandOptions.java @@ -5,6 +5,7 @@ public class ClientExpandOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; private ConsistencyPreference consistency; @@ -35,4 +36,16 @@ public ClientExpandOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientExpandOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientGetStoreOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientGetStoreOptions.java index 6650ac69..c717852b 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientGetStoreOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientGetStoreOptions.java @@ -4,6 +4,7 @@ public class ClientGetStoreOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; public ClientGetStoreOptions additionalHeaders(Map additionalHeaders) { this.additionalHeaders = additionalHeaders; @@ -14,4 +15,16 @@ public ClientGetStoreOptions additionalHeaders(Map additionalHea public Map getAdditionalHeaders() { return this.additionalHeaders; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientGetStoreOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java index 54ce0e53..b35b9b71 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListObjectsOptions.java @@ -5,6 +5,7 @@ public class ClientListObjectsOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; private ConsistencyPreference consistency; @@ -35,4 +36,16 @@ public ClientListObjectsOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientListObjectsOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java index 1e0441dc..7cd5e124 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListRelationsOptions.java @@ -6,6 +6,7 @@ public class ClientListRelationsOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private Integer maxParallelRequests; private String authorizationModelId; private ConsistencyPreference consistency; @@ -52,6 +53,19 @@ public ClientBatchCheckClientOptions asClientBatchCheckClientOptions() { .authorizationModelId(authorizationModelId) .additionalHeaders(additionalHeaders != null ? new HashMap<>(additionalHeaders) : null) .maxParallelRequests(maxParallelRequests) + .storeId(storeId) .consistency(consistency); } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientListRelationsOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientListUsersOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientListUsersOptions.java index cf222508..ca52adef 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientListUsersOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientListUsersOptions.java @@ -5,6 +5,7 @@ public class ClientListUsersOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; private ConsistencyPreference consistency; @@ -35,4 +36,16 @@ public ClientListUsersOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientListUsersOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAssertionsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAssertionsOptions.java index 2f54974d..be59528f 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAssertionsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAssertionsOptions.java @@ -7,6 +7,7 @@ public class ClientReadAssertionsOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; public ClientReadAssertionsOptions additionalHeaders(Map additionalHeaders) { @@ -42,4 +43,16 @@ public String getAuthorizationModelIdChecked() throws FgaInvalidParameterExcepti assertValidAuthorizationModelId(); return authorizationModelId; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientReadAssertionsOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelOptions.java index 6918dd3e..aa6123ee 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelOptions.java @@ -7,6 +7,7 @@ public class ClientReadAuthorizationModelOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; public ClientReadAuthorizationModelOptions additionalHeaders(Map additionalHeaders) { @@ -38,4 +39,16 @@ public String getAuthorizationModelIdChecked() throws FgaInvalidParameterExcepti assertValidAuthorizationModelId(); return authorizationModelId; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientReadAuthorizationModelOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelsOptions.java index 92103b32..e8ebbcb0 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadAuthorizationModelsOptions.java @@ -4,6 +4,7 @@ public class ClientReadAuthorizationModelsOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private Integer pageSize; private String continuationToken; @@ -34,4 +35,16 @@ public ClientReadAuthorizationModelsOptions continuationToken(String continuatio public String getContinuationToken() { return continuationToken; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientReadAuthorizationModelsOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadChangesOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadChangesOptions.java index 9a0abf95..a462dc61 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadChangesOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadChangesOptions.java @@ -4,6 +4,7 @@ public class ClientReadChangesOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private Integer pageSize; private String continuationToken; @@ -34,4 +35,16 @@ public ClientReadChangesOptions continuationToken(String continuationToken) { public String getContinuationToken() { return continuationToken; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientReadChangesOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadLatestAuthorizationModelOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadLatestAuthorizationModelOptions.java index 4135f5a9..4cb516d7 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadLatestAuthorizationModelOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadLatestAuthorizationModelOptions.java @@ -4,6 +4,7 @@ public class ClientReadLatestAuthorizationModelOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; public ClientReadLatestAuthorizationModelOptions additionalHeaders(Map additionalHeaders) { this.additionalHeaders = additionalHeaders; @@ -14,4 +15,16 @@ public ClientReadLatestAuthorizationModelOptions additionalHeaders(Map getAdditionalHeaders() { return this.additionalHeaders; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientReadLatestAuthorizationModelOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java index e949bb87..b7055ca3 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientReadOptions.java @@ -5,6 +5,7 @@ public class ClientReadOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private Integer pageSize; private String continuationToken; private ConsistencyPreference consistency; @@ -45,4 +46,16 @@ public ClientReadOptions consistency(ConsistencyPreference consistency) { public ConsistencyPreference getConsistency() { return consistency; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientReadOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAssertionsOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAssertionsOptions.java index 4d7f0fbd..cd0fb5fe 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAssertionsOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAssertionsOptions.java @@ -7,6 +7,7 @@ public class ClientWriteAssertionsOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; public ClientWriteAssertionsOptions additionalHeaders(Map additionalHeaders) { @@ -42,4 +43,16 @@ public String getAuthorizationModelIdChecked() throws FgaInvalidParameterExcepti assertValidAuthorizationModelId(); return authorizationModelId; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientWriteAssertionsOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAuthorizationModelOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAuthorizationModelOptions.java index bef5bf63..d9411b1e 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAuthorizationModelOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteAuthorizationModelOptions.java @@ -4,6 +4,7 @@ public class ClientWriteAuthorizationModelOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; public ClientWriteAuthorizationModelOptions additionalHeaders(Map additionalHeaders) { this.additionalHeaders = additionalHeaders; @@ -14,4 +15,16 @@ public ClientWriteAuthorizationModelOptions additionalHeaders(Map getAdditionalHeaders() { return this.additionalHeaders; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientWriteAuthorizationModelOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java index 04686fd9..cb4a22dd 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteOptions.java @@ -12,6 +12,7 @@ */ public class ClientWriteOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private String authorizationModelId; private boolean transactionsEnabled = true; private int transactionChunkSize; @@ -179,4 +180,16 @@ public ClientWriteOptions onMissing(WriteRequestDeletes.OnMissingEnum onMissing) public WriteRequestDeletes.OnMissingEnum getOnMissing() { return onMissing; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientWriteOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteTuplesOptions.java b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteTuplesOptions.java index 932bd749..10d8de5c 100644 --- a/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteTuplesOptions.java +++ b/src/main/java/dev/openfga/sdk/api/configuration/ClientWriteTuplesOptions.java @@ -5,6 +5,7 @@ public class ClientWriteTuplesOptions implements AdditionalHeadersSupplier { private Map additionalHeaders; + private String storeId; private WriteRequestWrites.OnDuplicateEnum onDuplicate; public ClientWriteTuplesOptions additionalHeaders(Map additionalHeaders) { @@ -25,4 +26,16 @@ public ClientWriteTuplesOptions onDuplicate(WriteRequestWrites.OnDuplicateEnum o public WriteRequestWrites.OnDuplicateEnum getOnDuplicate() { return onDuplicate; } + + /** + * Sets the store to send this request to, instead of the store in the client configuration. + */ + public ClientWriteTuplesOptions storeId(String storeId) { + this.storeId = storeId; + return this; + } + + public String getStoreId() { + return storeId; + } } diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientStoreIdOverrideTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientStoreIdOverrideTest.java new file mode 100644 index 00000000..7c4a4d72 --- /dev/null +++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientStoreIdOverrideTest.java @@ -0,0 +1,163 @@ +package dev.openfga.sdk.api.client; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import dev.openfga.sdk.api.client.model.ClientBatchCheckItem; +import dev.openfga.sdk.api.client.model.ClientBatchCheckRequest; +import dev.openfga.sdk.api.client.model.ClientCheckRequest; +import dev.openfga.sdk.api.client.model.ClientListObjectsRequest; +import dev.openfga.sdk.api.client.model.ClientListRelationsRequest; +import dev.openfga.sdk.api.client.model.ClientReadRequest; +import dev.openfga.sdk.api.client.model.ClientStreamedListObjectsOptions; +import dev.openfga.sdk.api.client.model.ClientTupleKey; +import dev.openfga.sdk.api.client.model.ClientWriteRequest; +import dev.openfga.sdk.api.configuration.ClientBatchCheckOptions; +import dev.openfga.sdk.api.configuration.ClientCheckOptions; +import dev.openfga.sdk.api.configuration.ClientConfiguration; +import dev.openfga.sdk.api.configuration.ClientListObjectsOptions; +import dev.openfga.sdk.api.configuration.ClientListRelationsOptions; +import dev.openfga.sdk.api.configuration.ClientReadOptions; +import dev.openfga.sdk.api.configuration.ClientWriteOptions; +import dev.openfga.sdk.errors.FgaInvalidParameterException; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Requests are sent to the store set in the per-request options, and to the configured store otherwise. + */ +@WireMockTest +class OpenFgaClientStoreIdOverrideTest { + private static final String CONFIGURED_STORE_ID = "01YCP46JKYM8FJCQ37NMBYHE5X"; + private static final String OVERRIDE_STORE_ID = "01HVMMBCMGZNT3SED4Z17ECXCA"; + + private String apiUrl; + + @BeforeEach + void setUp(WireMockRuntimeInfo wm) { + apiUrl = wm.getHttpBaseUrl(); + stubFor(post(urlPathMatching("/stores/[^/]+/check")) + .willReturn(okJson("{\"allowed\":true,\"resolution\":\"\"}"))); + stubFor(post(urlPathMatching("/stores/[^/]+/batch-check")).willReturn(okJson("{\"result\":{}}"))); + stubFor(post(urlPathMatching("/stores/[^/]+/write")).willReturn(okJson("{}"))); + stubFor(post(urlPathMatching("/stores/[^/]+/read")) + .willReturn(okJson("{\"tuples\":[],\"continuation_token\":\"\"}"))); + stubFor(post(urlPathMatching("/stores/[^/]+/list-objects")).willReturn(okJson("{\"objects\":[]}"))); + stubFor(post(urlPathMatching("/stores/[^/]+/streamed-list-objects")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/x-ndjson") + .withBody("{\"result\":{\"object\":\"document:1\"}}\n"))); + } + + private OpenFgaClient client(String storeId) throws FgaInvalidParameterException { + return new OpenFgaClient(new ClientConfiguration().apiUrl(apiUrl).storeId(storeId)); + } + + private static ClientCheckRequest checkRequest(String relation) { + return new ClientCheckRequest().user("user:anne").relation(relation)._object("document:1"); + } + + @Test + void usesTheConfiguredStoreWithoutAnOverride() throws Exception { + client(CONFIGURED_STORE_ID).check(checkRequest("viewer")).get(); + + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + CONFIGURED_STORE_ID + "/check"))); + } + + @Test + void checkUsesTheStoreFromTheOptions() throws Exception { + client(CONFIGURED_STORE_ID) + .check(checkRequest("viewer"), new ClientCheckOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/check"))); + verify(0, postRequestedFor(urlPathEqualTo("/stores/" + CONFIGURED_STORE_ID + "/check"))); + } + + @Test + void theOverrideWorksWithoutAConfiguredStore() throws Exception { + client(null) + .check(checkRequest("viewer"), new ClientCheckOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/check"))); + } + + @Test + void failsWithoutAConfiguredStoreOrAnOverride() throws Exception { + assertThrows( + FgaInvalidParameterException.class, () -> client(null).check(checkRequest("viewer"), null)); + } + + @Test + void writeReadAndListObjectsUseTheStoreFromTheOptions() throws Exception { + var fga = client(CONFIGURED_STORE_ID); + var tuple = new ClientTupleKey().user("user:anne").relation("viewer")._object("document:1"); + + fga.write(new ClientWriteRequest().writes(List.of(tuple)), new ClientWriteOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + fga.read(new ClientReadRequest(), new ClientReadOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + fga.listObjects( + new ClientListObjectsRequest() + .user("user:anne") + .relation("viewer") + .type("document"), + new ClientListObjectsOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/write"))); + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/read"))); + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/list-objects"))); + } + + @Test + void listRelationsPassesTheStoreToItsChecks() throws Exception { + client(CONFIGURED_STORE_ID) + .listRelations( + new ClientListRelationsRequest() + .user("user:anne") + .relations(List.of("viewer", "editor")) + ._object("document:1"), + new ClientListRelationsOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + + verify(2, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/check"))); + verify(0, postRequestedFor(urlPathEqualTo("/stores/" + CONFIGURED_STORE_ID + "/check"))); + } + + @Test + void batchCheckUsesTheStoreFromTheOptions() throws Exception { + client(CONFIGURED_STORE_ID) + .batchCheck( + new ClientBatchCheckRequest() + .checks(List.of(new ClientBatchCheckItem() + .user("user:anne") + .relation("viewer") + ._object("document:1") + .correlationId("1"))), + new ClientBatchCheckOptions().storeId(OVERRIDE_STORE_ID)) + .get(); + + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/batch-check"))); + } + + @Test + void streamedListObjectsUsesTheStoreFromTheOptions() throws Exception { + client(CONFIGURED_STORE_ID) + .streamedListObjects( + new ClientListObjectsRequest() + .user("user:anne") + .relation("viewer") + .type("document"), + new ClientStreamedListObjectsOptions().storeId(OVERRIDE_STORE_ID), + response -> {}) + .get(); + + verify(1, postRequestedFor(urlPathEqualTo("/stores/" + OVERRIDE_STORE_ID + "/streamed-list-objects"))); + } +}