Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
f36e79e
feat: raw request builder
SoulPancake Jan 13, 2026
a205822
fix: fmt
SoulPancake Jan 13, 2026
ef8f654
feat: raw requests integration tests
SoulPancake Jan 13, 2026
9d64d06
fix: rawapi doc
SoulPancake Jan 13, 2026
f8360be
Merge branch 'main' into feat/raw-requests
SoulPancake Jan 14, 2026
b38e829
feat: calling other endpoints section
SoulPancake Jan 14, 2026
03cd5e2
fix: typo in readme
SoulPancake Jan 14, 2026
e8e1119
feat: refactor examples
SoulPancake Jan 14, 2026
382f798
fix: refactor example
SoulPancake Jan 14, 2026
94cb6fd
fix: comment
SoulPancake Jan 14, 2026
32868d9
feat: use list stores via raw req
SoulPancake Jan 14, 2026
947eddf
feat: refactor add typed resp in example
SoulPancake Jan 14, 2026
60b69a4
fix: spotless fmt
SoulPancake Jan 14, 2026
e1e3f68
feat: address copilot comments
SoulPancake Jan 14, 2026
d7325a2
feat: address coderabbit comments
SoulPancake Jan 14, 2026
541be99
fix: use gradle 8.2.1 for example
SoulPancake Jan 14, 2026
1f08814
Merge branch 'main' into feat/raw-requests
SoulPancake Jan 14, 2026
f169187
feat: use build buulder chain for consistency
SoulPancake Jan 14, 2026
1bfd8fb
feat: rename and refactor to APIExecutor for consistency
SoulPancake Jan 27, 2026
cf34ecb
Merge branch 'main' into feat/raw-requests
SoulPancake Jan 27, 2026
040446d
fix: rename consistent naming in docs
SoulPancake Jan 27, 2026
69f19ee
fix: changelog
SoulPancake Jan 27, 2026
0fa1cea
Merge branch 'main' into feat/raw-requests
SoulPancake Jan 27, 2026
d40ec71
fix: naming
SoulPancake Jan 27, 2026
b0fe538
fix: naming convention in example
SoulPancake Jan 27, 2026
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
### Added
- feat: support for streamed list objects (#252, #272)

### Added
- Introduced `ApiExecutor` for executing custom HTTP requests to OpenFGA API endpoints

## v0.9.4

### [0.9.4](https://github.com/openfga/java-sdk/compare/v0.9.3...v0.9.4) (2025-12-05)
Expand Down
88 changes: 86 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ This is an autogenerated Java SDK for OpenFGA. It provides a wrapper around the
- [Assertions](#assertions)
- [Read Assertions](#read-assertions)
- [Write Assertions](#write-assertions)
- [Calling Other Endpoints](#calling-other-endpoints)
- [Retries](#retries)
- [API Endpoints](#api-endpoints)
- [Models](#models)
Expand Down Expand Up @@ -746,7 +747,7 @@ Similar to [check](#check), but instead of checking a single user-object relatio
> Passing `ClientBatchCheckOptions` is optional. All fields of `ClientBatchCheckOptions` are optional.

```java
var reequst = new ClientBatchCheckRequest().checks(
var request = new ClientBatchCheckRequest().checks(
List.of(
new ClientBatchCheckItem()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
Expand Down Expand Up @@ -774,7 +775,7 @@ var reequst = new ClientBatchCheckRequest().checks(
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("creator")
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a")
.correlationId("cor-3), // optional, one will be generated for you if not provided
.correlationId("cor-3"), // optional, one will be generated for you if not provided
new ClientCheckRequest()
.user("user:81684243-9356-4421-8fbf-a4f8d36aa31b")
.relation("deleter")
Expand Down Expand Up @@ -1167,6 +1168,89 @@ try {
}
```

### Calling Other Endpoints

The API Executor provides direct HTTP access to OpenFGA endpoints not yet wrapped by the SDK. It maintains the SDK's client configuration including authentication, telemetry, retries, and error handling.

Use cases:
- Calling endpoints not yet supported by the SDK
- Using an SDK version that lacks support for a particular endpoint
- Accessing custom endpoints that extend the OpenFGA API

Initialize the SDK normally and access the API Executor via the `fgaClient` instance:

```java
// Initialize the client, same as above
ClientConfiguration config = new ClientConfiguration()
.apiUrl("http://localhost:8080")
.storeId("01YCP46JKYM8FJCQ37NMBYHE5X");
OpenFgaClient fgaClient = new OpenFgaClient(config);

// Custom new endpoint that doesn't exist in the SDK yet
Map<String, Object> requestBody = Map.of(
"user", "user:bob",
"action", "custom_action",
"resource", "resource:123"
);

// Build the request
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/custom-endpoint")
.pathParam("store_id", storeId)
.queryParam("page_size", "20")
.queryParam("continuation_token", "eyJwayI6...")
.body(requestBody)
.header("X-Experimental-Feature", "enabled")
.build();
```

#### Example: Calling a new "Custom Endpoint" endpoint and handling raw response

```java
// Get raw response without automatic decoding
ApiResponse<String> rawResponse = fgaClient.apiExecutor().send(request).get();

String rawJson = rawResponse.getData();
System.out.println("Response: " + rawJson);

// You can access fields like headers, status code, etc. from rawResponse:
System.out.println("Status Code: " + rawResponse.getStatusCode());
System.out.println("Headers: " + rawResponse.getHeaders());
```

#### Example: Calling a new "Custom Endpoint" endpoint and decoding response into a struct

```java
// Define a class to hold the response
class CustomEndpointResponse {
private boolean allowed;
private String reason;

public boolean isAllowed() { return allowed; }
public void setAllowed(boolean allowed) { this.allowed = allowed; }
public String getReason() { return reason; }
public void setReason(String reason) { this.reason = reason; }
}

// Get response decoded into CustomEndpointResponse class
ApiResponse<CustomEndpointResponse> response = fgaClient.apiExecutor()
.send(request, CustomEndpointResponse.class)
.get();

CustomEndpointResponse customEndpointResponse = response.getData();
System.out.println("Allowed: " + customEndpointResponse.isAllowed());
System.out.println("Reason: " + customEndpointResponse.getReason());

// You can access fields like headers, status code, etc. from response:
System.out.println("Status Code: " + response.getStatusCode());
System.out.println("Headers: " + response.getHeaders());
```

For a complete working example, see [examples/api-executor](examples/api-executor).

#### Documentation

See [docs/ApiExecutor.md](docs/ApiExecutor.md) for complete API reference and examples.

### API Endpoints

| Method | HTTP request | Description |
Expand Down
167 changes: 167 additions & 0 deletions docs/ApiExecutor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# API Executor

Direct HTTP access to OpenFGA endpoints.

## Quick Start

```java
OpenFgaClient client = new OpenFgaClient(config);

// Build request
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/check")
.pathParam("store_id", storeId)
.body(Map.of("tuple_key", Map.of("user", "user:jon", "relation", "reader", "object", "doc:1")))
.build();

// Execute - typed response
ApiResponse<CheckResponse> response = client.apiExecutor().send(request, CheckResponse.class).get();

// Execute - raw JSON
ApiResponse<String> rawResponse = client.apiExecutor().send(request).get();
```

## API Reference

### ApiExecutorRequestBuilder

**Factory:**
```java
ApiExecutorRequestBuilder.builder(String method, String path)
```

**Methods:**
```java
.pathParam(String key, String value) // Replace {key} in path, URL-encoded
.queryParam(String key, String value) // Add query parameter, URL-encoded
.header(String key, String value) // Add HTTP header
.body(Object body) // Set request body (auto-serialized to JSON)
.build() // Complete the builder (required)
```

**Example:**
```java
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/write")
.pathParam("store_id", "01ABC")
.queryParam("dry_run", "true")
.header("X-Request-ID", "uuid")
.body(requestObject)
.build();
```

### ApiExecutor

**Access:**
```java
ApiExecutor apiExecutor = client.apiExecutor();
```

**Methods:**
```java
CompletableFuture<ApiResponse<String>> send(ApiExecutorRequestBuilder request)
CompletableFuture<ApiResponse<T>> send(ApiExecutorRequestBuilder request, Class<T> responseType)
```

### ApiResponse<T>

```java
int getStatusCode() // HTTP status
Map<String, List<String>> getHeaders() // Response headers
String getRawResponse() // Raw JSON body
T getData() // Deserialized data
```

## Examples

### GET Request
```java
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("GET", "/stores/{store_id}/feature")
.pathParam("store_id", storeId)
.build();

client.apiExecutor().send(request, FeatureResponse.class)
.thenAccept(r -> System.out.println("Status: " + r.getStatusCode()));
```

### POST with Body
```java
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/bulk-delete")
.pathParam("store_id", storeId)
.queryParam("force", "true")
.body(new BulkDeleteRequest("2023-01-01", "user", 1000))
.build();

client.apiExecutor().send(request, BulkDeleteResponse.class).get();
```

### Raw JSON Response
```java
ApiResponse<String> response = client.apiExecutor().send(request).get();
String json = response.getRawResponse(); // Raw JSON
```

### Query Parameters
```java
ApiExecutorRequestBuilder.builder("GET", "/stores/{store_id}/items")
.pathParam("store_id", storeId)
.queryParam("page", "1")
.queryParam("limit", "50")
.queryParam("sort", "created_at")
.build();
```

### Custom Headers
```java
ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/action")
.header("X-Request-ID", UUID.randomUUID().toString())
.header("X-Idempotency-Key", "key-123")
.body(data)
.build();
```

### Error Handling
```java
client.apiExecutor().send(request, ResponseType.class)
.exceptionally(e -> {
if (e.getCause() instanceof FgaError) {
FgaError error = (FgaError) e.getCause();
System.err.println("API Error: " + error.getStatusCode());
}
return null;
});
```

### Map as Request Body
```java
ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/settings")
.pathParam("store_id", storeId)
.body(Map.of(
"setting", "value",
"enabled", true,
"threshold", 100,
"options", List.of("opt1", "opt2")
))
.build();
```

## Notes

- Path/query parameters are URL-encoded automatically
- Authentication tokens injected from client config
- `{store_id}` auto-replaced if not provided via `.pathParam()`

## Migration to Typed Methods

When SDK adds typed methods for an endpoint, you can migrate from API Executor:

```java
// API Executor
ApiExecutorRequestBuilder request = ApiExecutorRequestBuilder.builder("POST", "/stores/{store_id}/check")
.body(req)
.build();

client.apiExecutor().send(request, CheckResponse.class).get();

// Typed SDK (when available)
client.check(req).get();
```

7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,10 @@ A simple example that creates a store, runs a set of calls against it including

#### OpenTelemetry Examples
- `opentelemetry/` - Demonstrates OpenTelemetry integration both via manual code configuration, as well as no-code instrumentation using the OpenTelemetry java agent

#### Streaming Examples
- `streamed-list-objects/` - Demonstrates using the StreamedListObjects API to retrieve large result sets without pagination limits

#### API Executor Examples
- `api-executor/` - Demonstrates direct HTTP access to OpenFGA endpoints not yet wrapped by the SDK, maintaining SDK configuration (authentication, retries, error handling)

17 changes: 17 additions & 0 deletions examples/api-executor/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.PHONY: build run run-openfga
all: build

project_name=.
openfga_version=latest
language=java

build:
../../gradlew -P language=$(language) build

run:
../../gradlew -P language=$(language) run

run-openfga:
docker pull docker.io/openfga/openfga:${openfga_version} && \
docker run -p 8080:8080 docker.io/openfga/openfga:${openfga_version} run

Loading
Loading