From 032f081001679b5cbb9efc29ab24631db9d27e73 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Tue, 14 Apr 2026 19:08:43 +0530 Subject: [PATCH 1/6] feat: apiexecutor docs --- .../setup-openfga/configuration.mdx | 55 +- docs/content/interacting/overview.mdx | 5 + docs/content/interacting/raw-api-requests.mdx | 758 ++++++++++++++++++ docs/sidebars.js | 5 + static/llms.txt | 10 + 5 files changed, 819 insertions(+), 14 deletions(-) create mode 100644 docs/content/interacting/raw-api-requests.mdx diff --git a/docs/content/getting-started/setup-openfga/configuration.mdx b/docs/content/getting-started/setup-openfga/configuration.mdx index 42e8798ec3..e6dcd0e29f 100644 --- a/docs/content/getting-started/setup-openfga/configuration.mdx +++ b/docs/content/getting-started/setup-openfga/configuration.mdx @@ -101,7 +101,7 @@ docker run docker.io/openfga/openfga:latest run \ ## List of options -The following table lists the configuration options for the OpenFGA server [v1.8.9](https://github.com/openfga/openfga/releases/tag/v1.8.9), based on the [config-schema.json](https://raw.githubusercontent.com/openfga/openfga/refs/tags/v1.8.9/.config-schema.json). +The following table lists the configuration options for the OpenFGA server [v1.14.1](https://github.com/openfga/openfga/releases/tag/v1.14.1), based on the [config-schema.json](https://raw.githubusercontent.com/openfga/openfga/refs/tags/v1.14.1/.config-schema.json). | Config File | Env Var | Flag Name | Type | Description | Default Value | |-------------|---------|-----------|------|-------------|---------------| @@ -116,29 +116,39 @@ The following table lists the configuration options for the OpenFGA server [v1.8 | `maxConditionEvaluationCost` |
OPENFGA_MAX_CONDITION_EVALUATION_COST
| `max-condition-evaluation-cost` | integer | The maximum cost for CEL condition evaluation before a request returns an error (default is 100). | `100` | | `changelogHorizonOffset` |
OPENFGA_CHANGELOG_HORIZON_OFFSET
| `changelog-horizon-offset` | integer | The offset (in minutes) from the current time. Changes that occur after this offset will not be included in the response of ReadChanges. | | | `resolveNodeLimit` |
OPENFGA_RESOLVE_NODE_LIMIT
| `resolve-node-limit` | integer | Maximum resolution depth to attempt before throwing an error (defines how deeply nested an authorization model can be before a query errors out). | `25` | -| `resolveNodeBreadthLimit` |
OPENFGA_RESOLVE_NODE_BREADTH_LIMIT
| `resolve-node-breadth-limit` | integer | Defines how many nodes on a given level can be evaluated concurrently in a Check resolution tree. | `100` | +| `resolveNodeBreadthLimit` |
OPENFGA_RESOLVE_NODE_BREADTH_LIMIT
| `resolve-node-breadth-limit` | integer | Defines how many nodes on a given level can be evaluated concurrently in a Check resolution tree. | `10` | | `listObjectsDeadline` |
OPENFGA_LIST_OBJECTS_DEADLINE
| `list-objects-deadline` | string (duration) | The timeout deadline for serving ListObjects requests | `3s` | | `listObjectsMaxResults` |
OPENFGA_LIST_OBJECTS_MAX_RESULTS
| `list-objects-max-results` | integer | The maximum results to return in the non-streaming ListObjects API response. If 0, all results can be returned | `1000` | +| `listObjectsPipelineEnabled` |
OPENFGA_LIST_OBJECTS_PIPELINE_ENABLED
| `list-objects-pipeline-enabled` | boolean | Enables the ListObjects pipeline optimization algorithm, which can significantly improve the latency of ListObjects requests. When enabled, the server will attempt to resolve intermediate nodes in the ListObjects resolution tree concurrently. This optimization is most effective for workloads with large and complex authorization models, but may not suit all cases. Can be disabled if it causes increased resource usage. | `true` | | `listUsersDeadline` |
OPENFGA_LIST_USERS_DEADLINE
| `list-users-deadline` | string (duration) | The timeout deadline for serving ListUsers requests. If 0s, there is no deadline | `3s` | | `listUsersMaxResults` |
OPENFGA_LIST_USERS_MAX_RESULTS
| `list-users-max-results` | integer | The maximum results to return in ListUsers API response. If 0, all results can be returned | `1000` | +| `readChangesMaxPageSize` |
OPENFGA_READ_CHANGES_MAX_PAGE_SIZE
| `read-changes-max-page-size` | integer | The maximum page size allowed for ReadChanges API requests | `100` | | `requestDurationDatastoreQueryCountBuckets` |
OPENFGA_REQUEST_DURATION_DATASTORE_QUERY_COUNT_BUCKETS
| `request-duration-datastore-query-count-buckets` | []integer | Datastore query count buckets used to label the histogram metric for measuring request duration. | `50,200` | | `requestDurationDispatchCountBuckets` |
OPENFGA_REQUEST_DURATION_DISPATCH_COUNT_BUCKETS
| `request-duration-dispatch-count-buckets` | []integer | Dispatch count buckets used to label the histogram metric for measuring request duration. | `50,200` | | `contextPropagationToDatastore` |
OPENFGA_CONTEXT_PROPAGATION_TO_DATASTORE
| `context-propagation-to-datastore` | boolean | Propagate a requests context to the datastore implementation. Settings this parameter can result in connection pool draining on request aborts and timeouts. | `false` | -| `experimentals` |
OPENFGA_EXPERIMENTALS
| `experimentals` | []string (enum=[`enable-check-optimizations`, `enable-list-objects-optimizations`, `enable-access-control`]) | a list of experimental features to enable | `` | +| `experimentals` |
OPENFGA_EXPERIMENTALS
| `experimentals` | []string (enum=[`enable-check-optimizations`, `enable-list-objects-optimizations`, `enable-access-control`, `datastore_throttling`, `pipeline_list_objects`, `authzen`]) | a comma-separated list of experimental features to enable | `pipeline_list_objects` | +| `authzen.baseURL` |
OPENFGA_AUTHZEN_BASE_URL
| `authzen-base-url` | string | The canonical absolute base URL published in AuthZEN discovery metadata. It may include an optional path prefix. | | | `accessControl.enabled` |
OPENFGA_ACCESS_CONTROL_ENABLED
| `access-control-enabled` | boolean | Enable/disable the access control store. | `false` | | `accessControl.storeId` |
OPENFGA_ACCESS_CONTROL_STORE_ID
| `access-control-store-id` | string | The storeId to be used for the access control store. | | | `accessControl.modelId` |
OPENFGA_ACCESS_CONTROL_MODEL_ID
| `access-control-model-id` | string | The modelId to be used for the access control store. | | -| `playground.enabled` |
OPENFGA_PLAYGROUND_ENABLED
| `playground-enabled` | boolean | Enable/disable the OpenFGA Playground. | `true` | -| `playground.port` |
OPENFGA_PLAYGROUND_PORT
| `playground-port` | integer | The port to serve the local OpenFGA Playground on. | `3000` | +| `playground.enabled` |
OPENFGA_PLAYGROUND_ENABLED
| `playground-enabled` | boolean | Enable/disable the OpenFGA Playground. The Playground can only be run when the authentication method is set to 'none'. Note that the built-in Playground is intended for local development and testing purposes, and is not recommended for production use. It has been deprecated and will be removed in a subsequent release. | `false` | +| `playground.port` |
OPENFGA_PLAYGROUND_PORT
| `playground-port` | integer | Deprecated: The port to serve the local OpenFGA Playground on. Use 'addr' instead. | `3000` | +| `playground.addr` |
OPENFGA_PLAYGROUND_ADDR
| `playground-addr` | string | The host:port address to serve the local OpenFGA Playground on. | | | `profiler.enabled` |
OPENFGA_PROFILER_ENABLED
| `profiler-enabled` | boolean | Enabled/disable pprof profiling. | `false` | | `profiler.addr` |
OPENFGA_PROFILER_ADDR
| `profiler-addr` | string | The host:port address to serve the pprof profiler server on. | `:3001` | | `datastore.engine` |
OPENFGA_DATASTORE_ENGINE
| `datastore-engine` | string (enum=[`memory`, `postgres`, `mysql`, `sqlite`]) | The datastore engine that will be used for persistence. | `memory` | | `datastore.uri` |
OPENFGA_DATASTORE_URI
| `datastore-uri` | string | The connection uri to use to connect to the datastore (for any engine other than 'memory'). | | +| `datastore.secondaryUri` |
OPENFGA_DATASTORE_SECONDARY_URI
| `datastore-secondary-uri` | string | The connection uri to use to connect to the secondary datastore (for postgres only). | | | `datastore.username` |
OPENFGA_DATASTORE_USERNAME
| `datastore-username` | string | The connection username to connect to the datastore (overwrites any username provided in the connection uri). | | +| `datastore.secondaryUsername` |
OPENFGA_DATASTORE_SECONDARY_USERNAME
| `datastore-secondary-username` | string | The connection username to connect to the secondary datastore (overwrites any username provided in the connection uri). | | | `datastore.password` |
OPENFGA_DATASTORE_PASSWORD
| `datastore-password` | string | The connection password to connect to the datastore (overwrites any password provided in the connection uri). | | +| `datastore.secondaryPassword` |
OPENFGA_DATASTORE_SECONDARY_PASSWORD
| `datastore-secondary-password` | string | The connection password to connect to the secondary datastore (overwrites any password provided in the connection uri). | | | `datastore.maxCacheSize` |
OPENFGA_DATASTORE_MAX_CACHE_SIZE
| `datastore-max-cache-size` | integer | The maximum number of authorization models that will be cached in memory | `100000` | +| `datastore.maxTypesystemCacheSize` |
OPENFGA_DATASTORE_MAX_TYPESYSTEM_CACHE_SIZE
| `datastore-max-typesystem-cache-size` | integer | The maximum number of type system models that will be cached in memory | `100000` | | `datastore.maxOpenConns` |
OPENFGA_DATASTORE_MAX_OPEN_CONNS
| `datastore-max-open-conns` | integer | The maximum number of open connections to the datastore. | `30` | +| `datastore.minOpenConns` |
OPENFGA_DATASTORE_MIN_OPEN_CONNS
| `datastore-min-open-conns` | integer | The minimum number of open connections to the datastore. This is only available for PostgreSQL. | `0` | | `datastore.maxIdleConns` |
OPENFGA_DATASTORE_MAX_IDLE_CONNS
| `datastore-max-idle-conns` | integer | the maximum number of connections to the datastore in the idle connection pool. | `10` | +| `datastore.minIdleConns` |
OPENFGA_DATASTORE_MIN_IDLE_CONNS
| `datastore-min-idle-conns` | integer | the minimum number of connections to the datastore in the idle connection pool. This is only available for PostgreSQL. | `0` | | `datastore.connMaxIdleTime` |
OPENFGA_DATASTORE_CONN_MAX_IDLE_TIME
| `datastore-conn-max-idle-time` | string (duration) | the maximum amount of time a connection to the datastore may be idle | `0s` | | `datastore.connMaxLifetime` |
OPENFGA_DATASTORE_CONN_MAX_LIFETIME
| `datastore-conn-max-lifetime` | string (duration) | the maximum amount of time a connection to the datastore may be reused | `0s` | | `datastore.metrics.enabled` |
OPENFGA_DATASTORE_METRICS_ENABLED
| `datastore-metrics-enabled` | boolean | enable/disable sql metrics for the datastore | `false` | @@ -150,6 +160,7 @@ The following table lists the configuration options for the OpenFGA server [v1.8 | `authn.oidc.subjects` |
OPENFGA_AUTHN_OIDC_SUBJECTS
| `authn-oidc-subjects` | []string | the OIDC subject names that will be accepted as valid when verifying the `sub` field of the JWTs. If empty, every `sub` will be allowed | | | `authn.oidc.clientIdClaims` |
OPENFGA_AUTHN_OIDC_CLIENT_ID_CLAIMS
| `authn-oidc-client-id-claims` | []string | the OIDC client id claims that will be used to parse the clientID - configure in order of priority (first is highest). Defaults to [`azp`, `client_id`] | | | `grpc.addr` |
OPENFGA_GRPC_ADDR
| `grpc-addr` | string | The host:port address to serve the grpc server on. | `0.0.0.0:8081` | +| `grpc.maxRecvMsgBytes` |
OPENFGA_GRPC_MAX_RECV_MSG_BYTES
| `grpc-max-recv-msg-bytes` | integer | The maximum size, in bytes, of a received gRPC message. | `616448` | | `grpc.tls.enabled` |
OPENFGA_GRPC_TLS_ENABLED
| `grpc-tls-enabled` | boolean | Enables or disables transport layer security (TLS). | `false` | | `grpc.tls.cert` |
OPENFGA_GRPC_TLS_CERT
| `grpc-tls-cert` | string | The (absolute) file path of the certificate to use for the TLS connection. | | | `grpc.tls.key` |
OPENFGA_GRPC_TLS_KEY
| `grpc-tls-key` | string | The (absolute) file path of the TLS key that should be used for the TLS connection. | | @@ -165,10 +176,11 @@ The following table lists the configuration options for the OpenFGA server [v1.8 | `log.level` |
OPENFGA_LOG_LEVEL
| `log-level` | string (enum=[`none`, `debug`, `info`, `warn`, `error`, `panic`, `fatal`]) | The log level to set. For production we recommend 'info' format. | `info` | | `log.timestampFormat` |
OPENFGA_LOG_TIMESTAMP_FORMAT
| `log-timestamp-format` | string (enum=[`Unix`, `ISO8601`]) | The timestamp format to use for the log output. | `Unix` | | `trace.enabled` |
OPENFGA_TRACE_ENABLED
| `trace-enabled` | boolean | Enable tracing. | `false` | -| `trace.otlp.endpoint` |
OPENFGA_TRACE_OTLP_ENDPOINT
| `trace-otlp-endpoint` | string | The grpc endpoint of the trace collector | `0.0.0.0:4317` | +| `trace.otlp.endpoint` |
OPENFGA_TRACE_OTLP_ENDPOINT,OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,OTEL_EXPORTER_OTLP_ENDPOINT
| `trace-otlp-endpoint,otel-exporter-otlp-traces-endpoint,otel-exporter-otlp-endpoint` | string | The grpc endpoint of the trace collector | `0.0.0.0:4317` | | `trace.otlp.tls.enabled` |
OPENFGA_TRACE_OTLP_TLS_ENABLED
| `trace-otlp-tls-enabled` | boolean | Whether to use TLS connection for the trace collector | `false` | -| `trace.sampleRatio` |
OPENFGA_TRACE_SAMPLE_RATIO
| `trace-sample-ratio` | number | The fraction of traces to sample. 1 means all, 0 means none. | `0.2` | -| `trace.serviceName` |
OPENFGA_TRACE_SERVICE_NAME
| `trace-service-name` | string | The service name included in sampled traces. | `openfga` | +| `trace.sampleRatio` |
OPENFGA_TRACE_SAMPLE_RATIO,OTEL_TRACES_SAMPLER_ARG
| `trace-sample-ratio,otel-traces-sampler-arg` | number | The fraction of traces to sample. 1 means all, 0 means none. | `0.2` | +| `trace.serviceName` |
OPENFGA_TRACE_SERVICE_NAME,OTEL_SERVICE_NAME
| `trace-service-name,otel-service-name` | string | The service name included in sampled traces. | `openfga` | +| `trace.resourceAttributes` |
OTEL_RESOURCE_ATTRIBUTES
| `otel-resource-attributes` | string | Key-value pairs to be used as resource attributes | | | `metrics.enabled` |
OPENFGA_METRICS_ENABLED
| `metrics-enabled` | boolean | enable/disable prometheus metrics on the '/metrics' endpoint | `true` | | `metrics.addr` |
OPENFGA_METRICS_ADDR
| `metrics-addr` | string | the host:port address to serve the prometheus metrics server on | `0.0.0.0:2112` | | `metrics.enableRPCHistograms` |
OPENFGA_METRICS_ENABLE_RPC_HISTOGRAMS
| `metrics-enable-rpc-histograms` | boolean | enables prometheus histogram metrics for RPC latency distributions | `false` | @@ -179,21 +191,36 @@ The following table lists the configuration options for the OpenFGA server [v1.8 | `checkQueryCache.enabled` |
OPENFGA_CHECK_QUERY_CACHE_ENABLED
| `check-query-cache-enabled` | boolean | enable caching of Check requests. The key is a string representing a query, and the value is a boolean. For example, if you have a relation `define viewer: owner or editor`, and the query is Check(user:anne, viewer, doc:1), we'll evaluate the `owner` relation and the `editor` relation and cache both results: (user:anne, viewer, doc:1) -> allowed=true and (user:anne, owner, doc:1) -> allowed=true. The cache is stored in-memory; the cached values are overwritten on every change in the result, and cleared after the configured TTL. This flag improves latency, but turns Check and ListObjects into eventually consistent APIs. If the request's consistency is HIGHER_CONSISTENCY, this cache is not used. | `false` | | `checkQueryCache.limit` |
OPENFGA_CHECK_QUERY_CACHE_LIMIT
| `check-query-cache-limit` | integer | DEPRECATED use OPENFGA_CHECK_CACHE_LIMIT. If caching of Check and ListObjects calls is enabled, this is the size limit (in items) of the cache | `10000` | | `checkQueryCache.ttl` |
OPENFGA_CHECK_QUERY_CACHE_TTL
| `check-query-cache-ttl` | string (duration) | if caching of Check and ListObjects is enabled, this is the TTL of each value | `10s` | -| `cacheController.enabled` |
OPENFGA_CACHE_CONTROLLER_ENABLED
| `cache-controller-enabled` | boolean | enabling dynamic invalidation of check query cache and check iterator cache based on whether there are recent tuple writes. If enabled, cache will be invalidated when either 1) there are tuples written to the store OR 2) the check query cache or check iterator cache TTL has expired. | `false` | -| `cacheController.ttl` |
OPENFGA_CACHE_CONTROLLER_TTL
| `cache-controller-ttl` | string (duration) | if cache controller is enabled, control how frequent read changes are invoked internally to query for recent tuple writes to the store. | `10s` | +| `cacheController.enabled` |
OPENFGA_CACHE_CONTROLLER_ENABLED
| `cache-controller-enabled` | boolean | enable invalidation of check query cache and iterator cache based on recent tuple writes. Invalidation is triggered by Check and List Objects requests, which periodically check the datastore's changelog table for writes and invalidate cache entries earlier than recent writes. Invalidations from Check requests are rate-limited by cache-controller-ttl, whereas List Objects requests invalidate every time if list objects iterator cache is enabled. | `false` | +| `cacheController.ttl` |
OPENFGA_CACHE_CONTROLLER_TTL
| `cache-controller-ttl` | string (duration) | if cache controller is enabled, this is the minimum time interval for Check requests to trigger cache invalidation. List Objects requests may trigger invalidation even sooner if list objects iterator cache is enabled. | `10s` | | `checkDispatchThrottling.enabled` |
OPENFGA_CHECK_DISPATCH_THROTTLING_ENABLED
| `check-dispatch-throttling-enabled` | boolean | enable throttling when check request's number of dispatches is high | `false` | | `checkDispatchThrottling.frequency` |
OPENFGA_CHECK_DISPATCH_THROTTLING_FREQUENCY
| `check-dispatch-throttling-frequency` | string (duration) | the frequency period that the deprioritized throttling queue is evaluated for a check request. A higher value will result in more aggressive throttling | `10µs` | | `checkDispatchThrottling.threshold` |
OPENFGA_CHECK_DISPATCH_THROTTLING_THRESHOLD
| `check-dispatch-throttling-threshold` | integer | define the number of recursive operations to occur before getting throttled for a check request | `100` | | `checkDispatchThrottling.maxThreshold` |
OPENFGA_CHECK_DISPATCH_THROTTLING_MAX_THRESHOLD
| `check-dispatch-throttling-max-threshold` | integer | define the maximum dispatch threshold beyond above which requests will be throttled. 0 will use the 'dispatchThrottling.threshold' value as maximum | `0` | -| `listObjectsDispatchThrottling.enabled` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_ENABLED
| `list-objects-dispatch-throttling-enabled` | boolean | enable throttling when list objects request's number of dispatches is high | `false` | -| `listObjectsDispatchThrottling.frequency` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_FREQUENCY
| `list-objects-dispatch-throttling-frequency` | string (duration) | the frequency period that the deprioritized throttling queue is evaluated for a list objects request. A higher value will result in more aggressive throttling | `10µs` | -| `listObjectsDispatchThrottling.threshold` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_THRESHOLD
| `list-objects-dispatch-throttling-threshold` | integer | define the number of recursive operations to occur before getting throttled for a list objects request | `100` | -| `listObjectsDispatchThrottling.maxThreshold` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_MAX_THRESHOLD
| `list-objects-dispatch-throttling-max-threshold` | integer | define the maximum dispatch threshold beyond above which requests will be throttled for a list objects request. 0 will use the 'dispatchThrottling.threshold' value as maximum | `0` | +| `listObjectsIteratorCache.enabled` |
OPENFGA_LIST_OBJECTS_ITERATOR_CACHE_ENABLED
| `list-objects-iterator-cache-enabled` | boolean | enable caching of datastore iterators in ListObjects. The key is a string representing a database query, and the value is a list of tuples. Each iterator is the result of a database query, for example usersets related to a specific object, or objects related to a specific user, up to a certain number of tuples per iterator. If the request's consistency is HIGHER_CONSISTENCY, this cache is not used. | `false` | +| `listObjectsIteratorCache.maxResults` |
OPENFGA_LIST_OBJECTS_ITERATOR_CACHE_MAX_RESULTS
| `list-objects-iterator-cache-max-results` | integer | if caching of datastore iterators of ListObjects requests is enabled, this is the limit of tuples to cache per key | `10000` | +| `listObjectsIteratorCache.ttl` |
OPENFGA_LIST_OBJECTS_ITERATOR_CACHE_TTL
| `list-objects-iterator-cache-ttl` | string (duration) | if caching of datastore iterators of ListObjects requests is enabled, this is the TTL of each value | `10s` | +| `listObjectsDispatchThrottling.enabled` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_ENABLED
| `list-objects-dispatch-throttling-enabled` | boolean | enable throttling when ListObjects request's number of dispatches is high. Only applies when pipeline is disabled. | `false` | +| `listObjectsDispatchThrottling.frequency` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_FREQUENCY
| `list-objects-dispatch-throttling-frequency` | string (duration) | the frequency period that the deprioritized throttling queue is evaluated for a ListObjects request. A higher value will result in more aggressive throttling | `10µs` | +| `listObjectsDispatchThrottling.threshold` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_THRESHOLD
| `list-objects-dispatch-throttling-threshold` | integer | define the number of recursive operations to occur before getting throttled for a ListObjects request | `100` | +| `listObjectsDispatchThrottling.maxThreshold` |
OPENFGA_LIST_OBJECTS_DISPATCH_THROTTLING_MAX_THRESHOLD
| `list-objects-dispatch-throttling-max-threshold` | integer | define the maximum dispatch threshold beyond above which requests will be throttled for a ListObjects request. 0 will use the 'dispatchThrottling.threshold' value as maximum | `0` | | `listUsersDispatchThrottling.enabled` |
OPENFGA_LIST_USERS_DISPATCH_THROTTLING_ENABLED
| `list-users-dispatch-throttling-enabled` | boolean | enable throttling when list users request's number of dispatches is high | `false` | | `listUsersDispatchThrottling.frequency` |
OPENFGA_LIST_USERS_DISPATCH_THROTTLING_FREQUENCY
| `list-users-dispatch-throttling-frequency` | string (duration) | the frequency period that the deprioritized throttling queue is evaluated for a list users request. A higher value will result in more aggressive throttling | `10µs` | | `listUsersDispatchThrottling.threshold` |
OPENFGA_LIST_USERS_DISPATCH_THROTTLING_THRESHOLD
| `list-users-dispatch-throttling-threshold` | integer | define the number of recursive operations to occur before getting throttled for a list users request | `100` | | `listUsersDispatchThrottling.maxThreshold` |
OPENFGA_LIST_USERS_DISPATCH_THROTTLING_MAX_THRESHOLD
| `list-users-dispatch-throttling-max-threshold` | integer | define the maximum dispatch threshold beyond above which requests will be throttled for a list users request. 0 will use the 'dispatchThrottling.threshold' value as maximum | `0` | +| `checkDatastoreThrottle.threshold` |
OPENFGA_CHECK_DATASTORE_THROTTLE_THRESHOLD
| `check-datastore-throttle-threshold` | integer | define the number of datastore requests allowed before being throttled. A value of 0 means throttling is disabled. | | +| `checkDatastoreThrottle.duration` |
OPENFGA_CHECK_DATASTORE_THROTTLE_DURATION
| `check-datastore-throttle-duration` | string (duration) | defines the time for which the datastore request will be suspended for being throttled. | `0s` | +| `listObjectsDatastoreThrottle.threshold` |
OPENFGA_LIST_OBJECTS_DATASTORE_THROTTLE_THRESHOLD
| `list-objects-datastore-throttle-threshold` | integer | define the number of datastore requests allowed before being throttled. A value of 0 means throttling is disabled. | | +| `listObjectsDatastoreThrottle.duration` |
OPENFGA_LIST_OBJECTS_DATASTORE_THROTTLE_DURATION
| `list-objects-datastore-throttle-duration` | string (duration) | defines the time for which the datastore request will be suspended for being throttled. | `0s` | +| `listUsersDatastoreThrottle.threshold` |
OPENFGA_LIST_USERS_DATASTORE_THROTTLE_THRESHOLD
| `list-users-datastore-throttle-threshold` | integer | define the number of datastore requests allowed before being throttled. A value of 0 means throttling is disabled. | | +| `listUsersDatastoreThrottle.duration` |
OPENFGA_LIST_USERS_DATASTORE_THROTTLE_DURATION
| `list-users-datastore-throttle-duration` | string (duration) | defines the time for which the datastore request will be suspended for being throttled. | `0s` | +| `sharedIterator.enabled` |
OPENFGA_SHARED_ITERATOR_ENABLED
| `shared-iterator-enabled` | boolean | enabling sharing of datastore iterators with different consumers. Each iterator is the result of a database query, for example usersets related to a specific object, or objects related to a specific user, up to a certain number of tuples per iterator. | `false` | +| `sharedIterator.limit` |
OPENFGA_SHARED_ITERATOR_LIMIT
| `shared-iterator-limit` | integer | if shared-iterator-enabled is enabled, this is the limit of the number of iterators that can be shared. | `1000000` | | `requestTimeout` |
OPENFGA_REQUEST_TIMEOUT
| `request-timeout` | string (duration) | The timeout duration for a request. | `3s` | +| `shutdownTimeout` |
OPENFGA_SHUTDOWN_TIMEOUT
| `shutdown-timeout` | string (duration) | The timeout duration for a graceful shutdown. | `10s` | +| `planner.initialGuess` |
OPENFGA_PLANNER_INITIAL_GUESS
| `planner-initial-guess` | string (duration) | The initial guess for the planners estimation. | `10ms` | +| `planner.evictionThreshold` |
OPENFGA_PLANNER_EVICTION_THRESHOLD
| `planner-eviction-threshold` | | How long a planner key can be unused before being evicted. | `0` | +| `planner.cleanupInterval` |
OPENFGA_PLANNER_CLEANUP_INTERVAL
| `planner-cleanup-interval` | string (duration) | How often the planner checks for stale keys. | `0` | ## Related Sections diff --git a/docs/content/interacting/overview.mdx b/docs/content/interacting/overview.mdx index 9b45bf0c8f..4e9cb8fd6a 100644 --- a/docs/content/interacting/overview.mdx +++ b/docs/content/interacting/overview.mdx @@ -52,5 +52,10 @@ This section helps you integrate diff --git a/docs/content/interacting/raw-api-requests.mdx b/docs/content/interacting/raw-api-requests.mdx new file mode 100644 index 0000000000..4c8beb647e --- /dev/null +++ b/docs/content/interacting/raw-api-requests.mdx @@ -0,0 +1,758 @@ +--- +title: Calling Other Endpoints +sidebar_position: 9 +slug: /interacting/raw-api-requests +description: Making raw HTTP calls to OpenFGA endpoints not yet wrapped by the SDK +--- + +import { + DocumentationNotice, + languageLabelMap, + ProductName, + ProductNameFormat, + RelatedSection, + SdkSetupPrerequisite, + SupportedLanguage, +} from '@components/Docs'; +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Calling Other Endpoints + + + +In certain cases you may want to call API endpoints that are not yet available as dedicated methods in the SDK. Every SDK provides an **API Executor** that lets you make raw HTTP calls to any endpoint while still honoring the client configuration — including authentication, telemetry, retries, and error handling. + +## When to use + +This is useful when: + +- You want to call a **new endpoint** that is not yet supported by the SDK. +- You are using an **earlier version** of the SDK that doesn't yet have a particular method. +- You have a **custom endpoint** deployed that extends the API. + +## Before you start + + + +## Making a request + +Initialize the SDK client as usual (see [Setup SDK Client](../getting-started/setup-sdk-client.mdx)), then use the API Executor to build and send a request. + +The examples below call a hypothetical custom endpoint, but you can use the same pattern for **any** API path. + +### Calling a custom endpoint with POST + + + + +```javascript +const { OpenFgaClient } = require("@openfga/sdk"); + +const fgaClient = new OpenFgaClient({ + apiUrl: process.env.FGA_API_URL, + storeId: process.env.FGA_STORE_ID, +}); + +// Call a custom endpoint using path parameters +const response = await fgaClient.executeApiRequest({ + operationName: "CustomEndpoint", // For telemetry/logging + method: "POST", + path: "/stores/{store_id}/custom-endpoint", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { + user: "user:bob", + action: "custom_action", + resource: "resource:123", + }, + queryParams: { + page_size: 20, + }, +}); + +console.log("Response:", response); +``` + + + + +```go +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + + openfga "github.com/openfga/go-sdk" + . "github.com/openfga/go-sdk/client" +) + +func main() { + fgaClient, err := NewSdkClient(&ClientConfiguration{ + // ... configure as usual + }) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Get the generic API executor + executor := fgaClient.GetAPIExecutor() + + requestBody := map[string]interface{}{ + "user": "user:bob", + "action": "custom_action", + "resource": "resource:123", + } + + // Build and execute the request + request := openfga.NewAPIExecutorRequestBuilder( + "CustomEndpoint", http.MethodPost, "/stores/{store_id}/custom-endpoint", + ). + WithPathParameter("store_id", storeID). + WithQueryParameter("page_size", "20"). + WithBody(requestBody). + Build() + + rawResponse, err := executor.Execute(context.Background(), request) + if err != nil { + log.Fatalf("Request failed: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(rawResponse.Body, &result); err != nil { + log.Fatalf("Failed to decode: %v", err) + } + + fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) + fmt.Printf("Response: %+v\n", result) +} +``` + + + + +```csharp +using OpenFga.Sdk.Client; +using OpenFga.Sdk.ApiClient; + +var configuration = new ClientConfiguration() { + ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL") ?? "http://localhost:8080", + StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), +}; + +var fgaClient = new OpenFgaClient(configuration); +var executor = fgaClient.ApiExecutor; + +// Build the request using fluent API +var request = RequestBuilder + .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/custom-endpoint") + .WithPathParameter("store_id", storeId) + .WithQueryParameter("page_size", "20") + .WithBody(new { + user = "user:bob", + action = "custom_action", + resource = "resource:123" + }); + +var response = await executor.ExecuteAsync>( + request, "CustomEndpoint" +); + +if (response.IsSuccessful) { + Console.WriteLine($"Status: {response.StatusCode}"); + Console.WriteLine($"Raw JSON: {response.RawResponse}"); + Console.WriteLine($"Data: {response.Data}"); +} else { + Console.WriteLine($"Request failed: {response.StatusCode}"); +} +``` + + + + +```python +import asyncio +import os +from openfga_sdk import ClientConfiguration, OpenFgaClient + +FGA_API_URL = os.environ.get("FGA_API_URL") +FGA_STORE_ID = os.environ.get("FGA_STORE_ID") + +async def main(): + configuration = ClientConfiguration( + api_url=FGA_API_URL, + store_id=FGA_STORE_ID, + ) + + async with OpenFgaClient(configuration) as fga_client: + response = await fga_client.execute_api_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={ + "user": "user:bob", + "action": "custom_action", + "resource": "resource:123", + }, + query_params={ + "page_size": 20, + }, + ) + + if response.status == 200: + result = response.json() + print(f"Response: {result}") + +asyncio.run(main()) +``` + + + + +```java +import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.client.HttpMethod; +import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; +import dev.openfga.sdk.api.configuration.ClientConfiguration; + +import java.util.Map; + +public class Example { + public static void main(String[] args) throws Exception { + var config = new ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) + .storeId(System.getenv("FGA_STORE_ID")); + + var fgaClient = new OpenFgaClient(config); + String storeId = System.getenv("FGA_STORE_ID"); + + Map requestBody = Map.of( + "user", "user:bob", + "action", "custom_action", + "resource", "resource:123" + ); + + var request = ApiExecutorRequestBuilder.builder( + HttpMethod.POST, "/stores/{store_id}/custom-endpoint" + ) + .pathParam("store_id", storeId) + .queryParam("page_size", "20") + .body(requestBody) + .build(); + + // Get raw response + var rawResponse = fgaClient.apiExecutor().send(request).get(); + System.out.println("Status Code: " + rawResponse.getStatusCode()); + System.out.println("Response: " + rawResponse.getData()); + } +} +``` + + + + +:::note +The examples above use `POST`, but the API Executor supports any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.). For `GET` requests, simply omit the `body` parameter. +::: + +### Using path parameters + +Path parameters are specified in the path using `{param_name}` syntax and must be provided via the path parameters option. They are URL-encoded automatically. + + + + +```javascript +const response = await fgaClient.executeApiRequest({ + operationName: "GetAuthorizationModel", + method: "GET", + path: "/stores/{store_id}/authorization-models/{model_id}", + pathParams: { + store_id: "your-store-id", + model_id: "your-model-id", + }, +}); +``` + + + + +```go +request := openfga.NewAPIExecutorRequestBuilder( + "GetAuthorizationModel", http.MethodGet, + "/stores/{store_id}/authorization-models/{model_id}", +). + WithPathParameter("store_id", "your-store-id"). + WithPathParameter("model_id", "your-model-id"). + Build() +``` + + + + +```csharp +var request = RequestBuilder + .Create(HttpMethod.Get, configuration.ApiUrl, + "/stores/{store_id}/authorization-models/{model_id}") + .WithPathParameter("store_id", "your-store-id") + .WithPathParameter("model_id", "your-model-id"); +``` + + + + +```python +response = await fga_client.execute_api_request( + operation_name="GetAuthorizationModel", + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + path_params={ + "store_id": "your-store-id", + "model_id": "your-model-id", + }, +) +``` + + + + +```java +var request = ApiExecutorRequestBuilder.builder( + HttpMethod.GET, "/stores/{store_id}/authorization-models/{model_id}" +) + .pathParam("store_id", "your-store-id") + .pathParam("model_id", "your-model-id") + .build(); +``` + + + + +### Decoding the response into a typed object + +Some SDKs let you decode the raw response directly into a typed struct or class, avoiding manual JSON parsing. + + + + +The JavaScript SDK's `executeApiRequest` already returns a parsed JSON object by default — no additional decoding step is needed. + +```javascript +const response = await fgaClient.executeApiRequest({ + operationName: "CustomEndpoint", + method: "POST", + path: "/stores/{store_id}/custom-endpoint", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { user: "user:bob" }, +}); + +// response is already a parsed object +console.log("Allowed:", response.allowed); +console.log("Reason:", response.reason); +``` + + + + +Use `ExecuteWithDecode` to automatically unmarshal the response into a Go struct: + +```go +type CustomEndpointResponse struct { + Allowed bool `json:"allowed"` + Reason string `json:"reason"` +} + +var customResponse CustomEndpointResponse + +rawResponse, err := executor.ExecuteWithDecode(ctx, request, &customResponse) +if err != nil { + log.Fatalf("Request failed: %v", err) +} + +fmt.Printf("Allowed: %v, Reason: %s\n", customResponse.Allowed, customResponse.Reason) +fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) +``` + + + + +Specify the response type as a generic parameter to `ExecuteAsync`: + +```csharp +// The response is automatically deserialized into the specified type +var response = await executor.ExecuteAsync( + request, "GetStore" +); + +if (response.IsSuccessful) { + Console.WriteLine($"Store Name: {response.Data.Name}"); + Console.WriteLine($"Raw JSON: {response.RawResponse}"); +} +``` + + + + +The Python SDK returns a response object whose `.json()` method gives you a parsed dictionary: + +```python +response = await fga_client.execute_api_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={"user": "user:bob"}, +) + +result = response.json() +print(f"Allowed: {result['allowed']}, Reason: {result['reason']}") +``` + + + + +Pass a class to `send()` to have the response decoded automatically: + +```java +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; } +} + +var response = fgaClient.apiExecutor() + .send(request, CustomEndpointResponse.class) + .get(); + +CustomEndpointResponse data = response.getData(); +System.out.println("Allowed: " + data.isAllowed()); +System.out.println("Reason: " + data.getReason()); +System.out.println("Status Code: " + response.getStatusCode()); +``` + + + + +## Streaming requests + +For endpoints that stream results (such as `StreamedListObjects`), use the streaming variant of the API Executor. Streaming endpoints return results incrementally as they are computed, rather than waiting for all results before responding. + + + + +Use `executeStreamedApiRequest` instead of `executeApiRequest`: + +```javascript +const stream = fgaClient.executeStreamedApiRequest({ + operationName: "StreamedListObjects", + method: "POST", + path: "/stores/{store_id}/streamed-list-objects", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { + type: "document", + relation: "viewer", + user: "user:anne", + authorization_model_id: process.env.FGA_MODEL_ID, + }, +}); + +for await (const chunk of stream) { + if (chunk.result) { + console.log("Object:", chunk.result.object); // e.g. "document:roadmap" + } +} +``` + + + + +Use `ExecuteStreaming` which returns an `APIExecutorStreamingChannel` with `Results` and `Errors` channels: + +```go +import ( + "encoding/json" + + openfga "github.com/openfga/go-sdk" +) + +request := openfga.NewAPIExecutorRequestBuilder( + "StreamedListObjects", http.MethodPost, "/stores/{store_id}/streamed-list-objects", +). + WithPathParameter("store_id", storeID). + WithBody(openfga.ListObjectsRequest{ + AuthorizationModelId: openfga.PtrString(modelID), + Type: "document", + Relation: "viewer", + User: "user:alice", + }). + Build() + +channel, err := executor.ExecuteStreaming(ctx, request, openfga.DefaultStreamBufferSize) +if err != nil { + log.Fatalf("Streaming request failed: %v", err) +} +defer channel.Close() + +for { + select { + case result, ok := <-channel.Results: + if !ok { + // Stream completed — check for final errors + select { + case err := <-channel.Errors: + if err != nil { + log.Fatalf("Stream error: %v", err) + } + default: + } + fmt.Println("Stream completed successfully") + return + } + + var response openfga.StreamedListObjectsResponse + if err := json.Unmarshal(result, &response); err != nil { + log.Fatalf("Failed to decode: %v", err) + } + fmt.Printf("Object: %s\n", response.Object) + + case err := <-channel.Errors: + if err != nil { + log.Fatalf("Stream error: %v", err) + } + } +} +``` + + + + +Use `ExecuteStreamingAsync` which returns an `IAsyncEnumerable`: + +```csharp +using OpenFga.Sdk.Client; +using OpenFga.Sdk.ApiClient; + +var request = RequestBuilder + .Create(HttpMethod.Post, configuration.ApiUrl, + "/stores/{store_id}/streamed-list-objects") + .WithPathParameter("store_id", storeId) + .WithBody(new { + user = "user:anne", + relation = "can_read", + type = "document", + authorization_model_id = authorizationModelId + }); + +await foreach (var item in executor.ExecuteStreamingAsync( + request, "StreamedListObjects")) +{ + Console.WriteLine($"Object: {item.Object}"); +} +``` + + + + +Use `execute_streamed_api_request` which returns an `AsyncIterator` (or `Iterator` in the sync client) that yields one parsed JSON object per chunk: + +```python +async for chunk in fga_client.execute_streamed_api_request( + operation_name="StreamedListObjects", + method="POST", + path="/stores/{store_id}/streamed-list-objects", + path_params={"store_id": FGA_STORE_ID}, + body={ + "type": "document", + "relation": "viewer", + "user": "user:anne", + "authorization_model_id": FGA_MODEL_ID, + }, +): + # Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}} + if "result" in chunk: + print(chunk["result"]["object"]) # e.g. "document:roadmap" +``` + + + + +Use `streamingApiExecutor` which delivers each response object to a consumer callback as it arrives: + +```java +import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; +import dev.openfga.sdk.api.model.ListObjectsRequest; +import dev.openfga.sdk.api.model.StreamedListObjectsResponse; +import dev.openfga.sdk.api.client.HttpMethod; + +var request = ApiExecutorRequestBuilder.builder( + HttpMethod.POST, "/stores/{store_id}/streamed-list-objects" +) + .body(new ListObjectsRequest() + .user("user:anne") + .relation("viewer") + .type("document")) + .build(); + +fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class) + .stream( + request, + response -> System.out.println("Object: " + response.getObject()), + error -> System.err.println("Stream error: " + error.getMessage()) + ) + .thenRun(() -> System.out.println("Streaming complete")) + .exceptionally(err -> { + System.err.println("Fatal error: " + err.getMessage()); + return null; + }); +``` + + + + +## Error handling + +Always check the response status and handle errors appropriately. The API Executor preserves the SDK's built-in error handling, but you may want to inspect status codes for custom endpoints. + + + + +```javascript +try { + const response = await fgaClient.executeApiRequest({ + operationName: "CustomEndpoint", + method: "POST", + path: "/stores/{store_id}/custom-endpoint", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { user: "user:bob" }, + }); + console.log("Success:", response); +} catch (error) { + console.error("Request failed:", error.message); +} +``` + + + + +```go +rawResponse, err := executor.Execute(ctx, request) +if err != nil { + log.Fatalf("Request failed: %v", err) +} + +fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) +fmt.Printf("Headers: %+v\n", rawResponse.Headers) +``` + + + + +```csharp +var response = await executor.ExecuteAsync( + request, "GetStore" +); + +if (!response.IsSuccessful) { + switch ((int)response.StatusCode) { + case 404: + Console.WriteLine("Not found"); + break; + case 401: + Console.WriteLine("Unauthorized — check your credentials"); + break; + case 429: + Console.WriteLine("Rate limited — retry after delay"); + break; + case >= 500: + Console.WriteLine($"Server error: {response.RawResponse}"); + break; + default: + Console.WriteLine($"Request failed: {response.StatusCode}"); + break; + } + return; +} + +Console.WriteLine($"Store Name: {response.Data.Name}"); +``` + + + + +```python +response = await fga_client.execute_api_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={"user": "user:bob"}, +) + +if response.status == 200: + result = response.json() + print(f"Response: {result}") +else: + print(f"Request failed with status: {response.status}") +``` + + + + +```java +try { + var rawResponse = fgaClient.apiExecutor().send(request).get(); + System.out.println("Status Code: " + rawResponse.getStatusCode()); + System.out.println("Response: " + rawResponse.getData()); +} catch (Exception e) { + System.err.println("Request failed: " + e.getMessage()); + System.err.println("Cause: " + e.getCause().getClass().getSimpleName()); +} +``` + + + + +## SDK examples + +For complete working examples, refer to the API Executor examples in each SDK repository: + +- [Go](https://github.com/openfga/go-sdk/tree/main/example/api_executor) +- [.NET](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample) +- [Python](https://github.com/openfga/python-sdk/tree/main/example/execute-api-request) +- [Java](https://github.com/openfga/java-sdk/tree/main/examples/api-executor) + + + + + + + + diff --git a/docs/sidebars.js b/docs/sidebars.js index 03a5f542c7..f6cd14e2da 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -425,6 +425,11 @@ const sidebars = { label: 'AuthZEN API', id: 'content/interacting/authzen', }, + { + type: 'doc', + label: 'Calling Other Endpoints', + id: 'content/interacting/raw-api-requests', + }, ], }, diff --git a/static/llms.txt b/static/llms.txt index affe5ec453..d07011bacd 100644 --- a/static/llms.txt +++ b/static/llms.txt @@ -95,6 +95,13 @@ OpenFGA implements authorization through: - [Migrating Models](https://openfga.dev/docs/modeling/migrating/migrating-models) +#### Authorization for Agents +- [Overview](https://openfga.dev/docs/modeling/agents/overview) - Authorization for Agents overview + - [Modeling Agents as Principals](https://openfga.dev/docs/modeling/agents/agents-as-principals) + - [RAG Authorization](https://openfga.dev/docs/modeling/agents/rag-authorization) + - [Authorization for MCP Servers](https://openfga.dev/docs/modeling/agents/mcp-authorization) + - [Task-Based Authorization](https://openfga.dev/docs/modeling/agents/task-based-authorization) + #### Interacting with the API - [Overview](https://openfga.dev/docs/interacting/overview) - Interacting with the API overview - [Manage User Access](https://openfga.dev/docs/interacting/managing-user-access) @@ -106,11 +113,14 @@ OpenFGA implements authorization through: - [Relationship Queries](https://openfga.dev/docs/interacting/relationship-queries) - [Get Tuple Changes](https://openfga.dev/docs/interacting/read-tuple-changes) - [Search with Permissions](https://openfga.dev/docs/interacting/search-with-permissions) + - [AuthZEN API](https://openfga.dev/docs/interacting/authzen) + - [Calling Other Endpoints](https://openfga.dev/docs/interacting/raw-api-requests) #### Best Practices - [Overview](https://openfga.dev/docs/best-practices/overview) - Best Practices overview - [Adoption Patterns](https://openfga.dev/docs/best-practices/adoption-patterns) - [Authorization Model Design Principles](https://openfga.dev/docs/best-practices/modeling-design-principles) + - [Modeling ABAC](https://openfga.dev/docs/best-practices/modeling-abac) - [Modeling Roles](https://openfga.dev/docs/best-practices/modeling-roles) - [Source of Truth](https://openfga.dev/docs/best-practices/source-of-truth) - [Running OpenFGA in Production](https://openfga.dev/docs/best-practices/running-in-production) From 4712d5602f91c628d481b841888d69c90cab7b62 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Tue, 14 Apr 2026 20:02:29 +0530 Subject: [PATCH 2/6] fix: address comments --- docs/content/interacting/raw-api-requests.mdx | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/docs/content/interacting/raw-api-requests.mdx b/docs/content/interacting/raw-api-requests.mdx index 4c8beb647e..4b09fe2580 100644 --- a/docs/content/interacting/raw-api-requests.mdx +++ b/docs/content/interacting/raw-api-requests.mdx @@ -83,14 +83,19 @@ import ( "fmt" "log" "net/http" + "os" openfga "github.com/openfga/go-sdk" . "github.com/openfga/go-sdk/client" ) func main() { + ctx := context.Background() + storeID := os.Getenv("FGA_STORE_ID") + fgaClient, err := NewSdkClient(&ClientConfiguration{ - // ... configure as usual + ApiUrl: os.Getenv("FGA_API_URL"), + StoreId: storeID, }) if err != nil { log.Fatalf("Failed to create client: %v", err) @@ -114,7 +119,7 @@ func main() { WithBody(requestBody). Build() - rawResponse, err := executor.Execute(context.Background(), request) + rawResponse, err := executor.Execute(ctx, request) if err != nil { log.Fatalf("Request failed: %v", err) } @@ -147,7 +152,7 @@ var executor = fgaClient.ApiExecutor; // Build the request using fluent API var request = RequestBuilder .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/custom-endpoint") - .WithPathParameter("store_id", storeId) + .WithPathParameter("store_id", configuration.StoreId) .WithQueryParameter("page_size", "20") .WithBody(new { user = "user:bob", @@ -359,6 +364,7 @@ console.log("Reason:", response.reason); Use `ExecuteWithDecode` to automatically unmarshal the response into a Go struct: ```go +// Using ctx, executor, and request from the example above type CustomEndpointResponse struct { Allowed bool `json:"allowed"` Reason string `json:"reason"` @@ -477,10 +483,17 @@ Use `ExecuteStreaming` which returns an `APIExecutorStreamingChannel` with `Resu ```go import ( "encoding/json" + "fmt" + "log" + "net/http" + "os" openfga "github.com/openfga/go-sdk" ) +storeID := os.Getenv("FGA_STORE_ID") +modelID := os.Getenv("FGA_MODEL_ID") + request := openfga.NewAPIExecutorRequestBuilder( "StreamedListObjects", http.MethodPost, "/stores/{store_id}/streamed-list-objects", ). @@ -538,6 +551,9 @@ Use `ExecuteStreamingAsync` which returns an `IAsyncEnumerable`: using OpenFga.Sdk.Client; using OpenFga.Sdk.ApiClient; +var storeId = configuration.StoreId; +var authorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"); + var request = RequestBuilder .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/streamed-list-objects") @@ -756,3 +772,8 @@ For complete working examples, refer to the API Executor examples in each SDK re + + + + + From 2fdf385db662c1358448b213985204b4d0da127d Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Wed, 15 Apr 2026 12:34:53 +0530 Subject: [PATCH 3/6] feat: refactor address comments and add JS example dir --- docs/content/interacting/raw-api-requests.mdx | 691 +---------------- .../SnippetViewer/ExecuteApiRequestViewer.tsx | 705 ++++++++++++++++++ src/components/Docs/SnippetViewer/index.ts | 1 + 3 files changed, 717 insertions(+), 680 deletions(-) create mode 100644 src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx diff --git a/docs/content/interacting/raw-api-requests.mdx b/docs/content/interacting/raw-api-requests.mdx index 4b09fe2580..739a2e18c0 100644 --- a/docs/content/interacting/raw-api-requests.mdx +++ b/docs/content/interacting/raw-api-requests.mdx @@ -7,15 +7,16 @@ description: Making raw HTTP calls to OpenFGA endpoints not yet wrapped by the S import { DocumentationNotice, - languageLabelMap, + ExecuteApiRequestViewer, + ExecuteApiRequestPathParamsViewer, + ExecuteApiRequestDecodeViewer, + ExecuteApiRequestStreamingViewer, + ExecuteApiRequestErrorViewer, ProductName, ProductNameFormat, RelatedSection, SdkSetupPrerequisite, - SupportedLanguage, } from '@components/Docs'; -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; # Calling Other Endpoints @@ -43,220 +44,7 @@ The examples below call a hypothetical custom endpoint, but you can use the same ### Calling a custom endpoint with POST - - - -```javascript -const { OpenFgaClient } = require("@openfga/sdk"); - -const fgaClient = new OpenFgaClient({ - apiUrl: process.env.FGA_API_URL, - storeId: process.env.FGA_STORE_ID, -}); - -// Call a custom endpoint using path parameters -const response = await fgaClient.executeApiRequest({ - operationName: "CustomEndpoint", // For telemetry/logging - method: "POST", - path: "/stores/{store_id}/custom-endpoint", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { - user: "user:bob", - action: "custom_action", - resource: "resource:123", - }, - queryParams: { - page_size: 20, - }, -}); - -console.log("Response:", response); -``` - - - - -```go -import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "os" - - openfga "github.com/openfga/go-sdk" - . "github.com/openfga/go-sdk/client" -) - -func main() { - ctx := context.Background() - storeID := os.Getenv("FGA_STORE_ID") - - fgaClient, err := NewSdkClient(&ClientConfiguration{ - ApiUrl: os.Getenv("FGA_API_URL"), - StoreId: storeID, - }) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Get the generic API executor - executor := fgaClient.GetAPIExecutor() - - requestBody := map[string]interface{}{ - "user": "user:bob", - "action": "custom_action", - "resource": "resource:123", - } - - // Build and execute the request - request := openfga.NewAPIExecutorRequestBuilder( - "CustomEndpoint", http.MethodPost, "/stores/{store_id}/custom-endpoint", - ). - WithPathParameter("store_id", storeID). - WithQueryParameter("page_size", "20"). - WithBody(requestBody). - Build() - - rawResponse, err := executor.Execute(ctx, request) - if err != nil { - log.Fatalf("Request failed: %v", err) - } - - var result map[string]interface{} - if err := json.Unmarshal(rawResponse.Body, &result); err != nil { - log.Fatalf("Failed to decode: %v", err) - } - - fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) - fmt.Printf("Response: %+v\n", result) -} -``` - - - - -```csharp -using OpenFga.Sdk.Client; -using OpenFga.Sdk.ApiClient; - -var configuration = new ClientConfiguration() { - ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL") ?? "http://localhost:8080", - StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), -}; - -var fgaClient = new OpenFgaClient(configuration); -var executor = fgaClient.ApiExecutor; - -// Build the request using fluent API -var request = RequestBuilder - .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/custom-endpoint") - .WithPathParameter("store_id", configuration.StoreId) - .WithQueryParameter("page_size", "20") - .WithBody(new { - user = "user:bob", - action = "custom_action", - resource = "resource:123" - }); - -var response = await executor.ExecuteAsync>( - request, "CustomEndpoint" -); - -if (response.IsSuccessful) { - Console.WriteLine($"Status: {response.StatusCode}"); - Console.WriteLine($"Raw JSON: {response.RawResponse}"); - Console.WriteLine($"Data: {response.Data}"); -} else { - Console.WriteLine($"Request failed: {response.StatusCode}"); -} -``` - - - - -```python -import asyncio -import os -from openfga_sdk import ClientConfiguration, OpenFgaClient - -FGA_API_URL = os.environ.get("FGA_API_URL") -FGA_STORE_ID = os.environ.get("FGA_STORE_ID") - -async def main(): - configuration = ClientConfiguration( - api_url=FGA_API_URL, - store_id=FGA_STORE_ID, - ) - - async with OpenFgaClient(configuration) as fga_client: - response = await fga_client.execute_api_request( - operation_name="CustomEndpoint", - method="POST", - path="/stores/{store_id}/custom-endpoint", - path_params={"store_id": FGA_STORE_ID}, - body={ - "user": "user:bob", - "action": "custom_action", - "resource": "resource:123", - }, - query_params={ - "page_size": 20, - }, - ) - - if response.status == 200: - result = response.json() - print(f"Response: {result}") - -asyncio.run(main()) -``` - - - - -```java -import dev.openfga.sdk.api.client.OpenFgaClient; -import dev.openfga.sdk.api.client.HttpMethod; -import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; -import dev.openfga.sdk.api.configuration.ClientConfiguration; - -import java.util.Map; - -public class Example { - public static void main(String[] args) throws Exception { - var config = new ClientConfiguration() - .apiUrl(System.getenv("FGA_API_URL")) - .storeId(System.getenv("FGA_STORE_ID")); - - var fgaClient = new OpenFgaClient(config); - String storeId = System.getenv("FGA_STORE_ID"); - - Map requestBody = Map.of( - "user", "user:bob", - "action", "custom_action", - "resource", "resource:123" - ); - - var request = ApiExecutorRequestBuilder.builder( - HttpMethod.POST, "/stores/{store_id}/custom-endpoint" - ) - .pathParam("store_id", storeId) - .queryParam("page_size", "20") - .body(requestBody) - .build(); - - // Get raw response - var rawResponse = fgaClient.apiExecutor().send(request).get(); - System.out.println("Status Code: " + rawResponse.getStatusCode()); - System.out.println("Response: " + rawResponse.getData()); - } -} -``` - - - + :::note The examples above use `POST`, but the API Executor supports any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.). For `GET` requests, simply omit the `body` parameter. @@ -266,478 +54,31 @@ The examples above use `POST`, but the API Executor supports any HTTP method (`G Path parameters are specified in the path using `{param_name}` syntax and must be provided via the path parameters option. They are URL-encoded automatically. - - - -```javascript -const response = await fgaClient.executeApiRequest({ - operationName: "GetAuthorizationModel", - method: "GET", - path: "/stores/{store_id}/authorization-models/{model_id}", - pathParams: { - store_id: "your-store-id", - model_id: "your-model-id", - }, -}); -``` - - - - -```go -request := openfga.NewAPIExecutorRequestBuilder( - "GetAuthorizationModel", http.MethodGet, - "/stores/{store_id}/authorization-models/{model_id}", -). - WithPathParameter("store_id", "your-store-id"). - WithPathParameter("model_id", "your-model-id"). - Build() -``` - - - - -```csharp -var request = RequestBuilder - .Create(HttpMethod.Get, configuration.ApiUrl, - "/stores/{store_id}/authorization-models/{model_id}") - .WithPathParameter("store_id", "your-store-id") - .WithPathParameter("model_id", "your-model-id"); -``` - - - - -```python -response = await fga_client.execute_api_request( - operation_name="GetAuthorizationModel", - method="GET", - path="/stores/{store_id}/authorization-models/{model_id}", - path_params={ - "store_id": "your-store-id", - "model_id": "your-model-id", - }, -) -``` - - - - -```java -var request = ApiExecutorRequestBuilder.builder( - HttpMethod.GET, "/stores/{store_id}/authorization-models/{model_id}" -) - .pathParam("store_id", "your-store-id") - .pathParam("model_id", "your-model-id") - .build(); -``` - - - + ### Decoding the response into a typed object Some SDKs let you decode the raw response directly into a typed struct or class, avoiding manual JSON parsing. - - - -The JavaScript SDK's `executeApiRequest` already returns a parsed JSON object by default — no additional decoding step is needed. - -```javascript -const response = await fgaClient.executeApiRequest({ - operationName: "CustomEndpoint", - method: "POST", - path: "/stores/{store_id}/custom-endpoint", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { user: "user:bob" }, -}); - -// response is already a parsed object -console.log("Allowed:", response.allowed); -console.log("Reason:", response.reason); -``` - - - - -Use `ExecuteWithDecode` to automatically unmarshal the response into a Go struct: - -```go -// Using ctx, executor, and request from the example above -type CustomEndpointResponse struct { - Allowed bool `json:"allowed"` - Reason string `json:"reason"` -} - -var customResponse CustomEndpointResponse - -rawResponse, err := executor.ExecuteWithDecode(ctx, request, &customResponse) -if err != nil { - log.Fatalf("Request failed: %v", err) -} - -fmt.Printf("Allowed: %v, Reason: %s\n", customResponse.Allowed, customResponse.Reason) -fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) -``` - - - - -Specify the response type as a generic parameter to `ExecuteAsync`: - -```csharp -// The response is automatically deserialized into the specified type -var response = await executor.ExecuteAsync( - request, "GetStore" -); - -if (response.IsSuccessful) { - Console.WriteLine($"Store Name: {response.Data.Name}"); - Console.WriteLine($"Raw JSON: {response.RawResponse}"); -} -``` - - - - -The Python SDK returns a response object whose `.json()` method gives you a parsed dictionary: - -```python -response = await fga_client.execute_api_request( - operation_name="CustomEndpoint", - method="POST", - path="/stores/{store_id}/custom-endpoint", - path_params={"store_id": FGA_STORE_ID}, - body={"user": "user:bob"}, -) - -result = response.json() -print(f"Allowed: {result['allowed']}, Reason: {result['reason']}") -``` - - - - -Pass a class to `send()` to have the response decoded automatically: - -```java -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; } -} - -var response = fgaClient.apiExecutor() - .send(request, CustomEndpointResponse.class) - .get(); - -CustomEndpointResponse data = response.getData(); -System.out.println("Allowed: " + data.isAllowed()); -System.out.println("Reason: " + data.getReason()); -System.out.println("Status Code: " + response.getStatusCode()); -``` - - - + ## Streaming requests For endpoints that stream results (such as `StreamedListObjects`), use the streaming variant of the API Executor. Streaming endpoints return results incrementally as they are computed, rather than waiting for all results before responding. - - - -Use `executeStreamedApiRequest` instead of `executeApiRequest`: - -```javascript -const stream = fgaClient.executeStreamedApiRequest({ - operationName: "StreamedListObjects", - method: "POST", - path: "/stores/{store_id}/streamed-list-objects", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { - type: "document", - relation: "viewer", - user: "user:anne", - authorization_model_id: process.env.FGA_MODEL_ID, - }, -}); - -for await (const chunk of stream) { - if (chunk.result) { - console.log("Object:", chunk.result.object); // e.g. "document:roadmap" - } -} -``` - - - - -Use `ExecuteStreaming` which returns an `APIExecutorStreamingChannel` with `Results` and `Errors` channels: - -```go -import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - - openfga "github.com/openfga/go-sdk" -) - -storeID := os.Getenv("FGA_STORE_ID") -modelID := os.Getenv("FGA_MODEL_ID") - -request := openfga.NewAPIExecutorRequestBuilder( - "StreamedListObjects", http.MethodPost, "/stores/{store_id}/streamed-list-objects", -). - WithPathParameter("store_id", storeID). - WithBody(openfga.ListObjectsRequest{ - AuthorizationModelId: openfga.PtrString(modelID), - Type: "document", - Relation: "viewer", - User: "user:alice", - }). - Build() - -channel, err := executor.ExecuteStreaming(ctx, request, openfga.DefaultStreamBufferSize) -if err != nil { - log.Fatalf("Streaming request failed: %v", err) -} -defer channel.Close() - -for { - select { - case result, ok := <-channel.Results: - if !ok { - // Stream completed — check for final errors - select { - case err := <-channel.Errors: - if err != nil { - log.Fatalf("Stream error: %v", err) - } - default: - } - fmt.Println("Stream completed successfully") - return - } - - var response openfga.StreamedListObjectsResponse - if err := json.Unmarshal(result, &response); err != nil { - log.Fatalf("Failed to decode: %v", err) - } - fmt.Printf("Object: %s\n", response.Object) - - case err := <-channel.Errors: - if err != nil { - log.Fatalf("Stream error: %v", err) - } - } -} -``` - - - - -Use `ExecuteStreamingAsync` which returns an `IAsyncEnumerable`: - -```csharp -using OpenFga.Sdk.Client; -using OpenFga.Sdk.ApiClient; - -var storeId = configuration.StoreId; -var authorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"); - -var request = RequestBuilder - .Create(HttpMethod.Post, configuration.ApiUrl, - "/stores/{store_id}/streamed-list-objects") - .WithPathParameter("store_id", storeId) - .WithBody(new { - user = "user:anne", - relation = "can_read", - type = "document", - authorization_model_id = authorizationModelId - }); - -await foreach (var item in executor.ExecuteStreamingAsync( - request, "StreamedListObjects")) -{ - Console.WriteLine($"Object: {item.Object}"); -} -``` - - - - -Use `execute_streamed_api_request` which returns an `AsyncIterator` (or `Iterator` in the sync client) that yields one parsed JSON object per chunk: - -```python -async for chunk in fga_client.execute_streamed_api_request( - operation_name="StreamedListObjects", - method="POST", - path="/stores/{store_id}/streamed-list-objects", - path_params={"store_id": FGA_STORE_ID}, - body={ - "type": "document", - "relation": "viewer", - "user": "user:anne", - "authorization_model_id": FGA_MODEL_ID, - }, -): - # Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}} - if "result" in chunk: - print(chunk["result"]["object"]) # e.g. "document:roadmap" -``` - - - - -Use `streamingApiExecutor` which delivers each response object to a consumer callback as it arrives: - -```java -import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; -import dev.openfga.sdk.api.model.ListObjectsRequest; -import dev.openfga.sdk.api.model.StreamedListObjectsResponse; -import dev.openfga.sdk.api.client.HttpMethod; - -var request = ApiExecutorRequestBuilder.builder( - HttpMethod.POST, "/stores/{store_id}/streamed-list-objects" -) - .body(new ListObjectsRequest() - .user("user:anne") - .relation("viewer") - .type("document")) - .build(); - -fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class) - .stream( - request, - response -> System.out.println("Object: " + response.getObject()), - error -> System.err.println("Stream error: " + error.getMessage()) - ) - .thenRun(() -> System.out.println("Streaming complete")) - .exceptionally(err -> { - System.err.println("Fatal error: " + err.getMessage()); - return null; - }); -``` - - - + ## Error handling Always check the response status and handle errors appropriately. The API Executor preserves the SDK's built-in error handling, but you may want to inspect status codes for custom endpoints. - - - -```javascript -try { - const response = await fgaClient.executeApiRequest({ - operationName: "CustomEndpoint", - method: "POST", - path: "/stores/{store_id}/custom-endpoint", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { user: "user:bob" }, - }); - console.log("Success:", response); -} catch (error) { - console.error("Request failed:", error.message); -} -``` - - - - -```go -rawResponse, err := executor.Execute(ctx, request) -if err != nil { - log.Fatalf("Request failed: %v", err) -} - -fmt.Printf("Status Code: %d\n", rawResponse.StatusCode) -fmt.Printf("Headers: %+v\n", rawResponse.Headers) -``` - - - - -```csharp -var response = await executor.ExecuteAsync( - request, "GetStore" -); - -if (!response.IsSuccessful) { - switch ((int)response.StatusCode) { - case 404: - Console.WriteLine("Not found"); - break; - case 401: - Console.WriteLine("Unauthorized — check your credentials"); - break; - case 429: - Console.WriteLine("Rate limited — retry after delay"); - break; - case >= 500: - Console.WriteLine($"Server error: {response.RawResponse}"); - break; - default: - Console.WriteLine($"Request failed: {response.StatusCode}"); - break; - } - return; -} - -Console.WriteLine($"Store Name: {response.Data.Name}"); -``` - - - - -```python -response = await fga_client.execute_api_request( - operation_name="CustomEndpoint", - method="POST", - path="/stores/{store_id}/custom-endpoint", - path_params={"store_id": FGA_STORE_ID}, - body={"user": "user:bob"}, -) - -if response.status == 200: - result = response.json() - print(f"Response: {result}") -else: - print(f"Request failed with status: {response.status}") -``` - - - - -```java -try { - var rawResponse = fgaClient.apiExecutor().send(request).get(); - System.out.println("Status Code: " + rawResponse.getStatusCode()); - System.out.println("Response: " + rawResponse.getData()); -} catch (Exception e) { - System.err.println("Request failed: " + e.getMessage()); - System.err.println("Cause: " + e.getCause().getClass().getSimpleName()); -} -``` - - - + ## SDK examples For complete working examples, refer to the API Executor examples in each SDK repository: +- [Node.js](https://github.com/openfga/js-sdk/tree/main/example/api-executor) - [Go](https://github.com/openfga/go-sdk/tree/main/example/api_executor) - [.NET](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample) - [Python](https://github.com/openfga/python-sdk/tree/main/example/execute-api-request) @@ -767,13 +108,3 @@ For complete working examples, refer to the API Executor examples in each SDK re ]} /> - - - - - - - - - - diff --git a/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx b/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx new file mode 100644 index 0000000000..90fb004697 --- /dev/null +++ b/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx @@ -0,0 +1,705 @@ +import { getFilteredAllowedLangs, SupportedLanguage } from './SupportedLanguage'; +import { defaultOperationsViewer } from './DefaultTabbedViewer'; +import assertNever from 'assert-never/index'; + +// --------------------------------------------------------------------------- +// Shared opts +// --------------------------------------------------------------------------- +interface ExecuteApiRequestViewerOpts { + skipSetup?: boolean; + allowedLanguages?: SupportedLanguage[]; +} + +// --------------------------------------------------------------------------- +// 1. ExecuteApiRequestViewer – POST custom endpoint (full setup) +// --------------------------------------------------------------------------- + +function executeApiRequestViewer(lang: SupportedLanguage, _opts: ExecuteApiRequestViewerOpts): string { + switch (lang) { + case SupportedLanguage.JS_SDK: + return `const { OpenFgaClient } = require("@openfga/sdk"); + +const fgaClient = new OpenFgaClient({ + apiUrl: process.env.FGA_API_URL, + storeId: process.env.FGA_STORE_ID, +}); + +// Call a custom endpoint using path parameters +const response = await fgaClient.executeApiRequest({ + operationName: "CustomEndpoint", // For telemetry/logging + method: "POST", + path: "/stores/{store_id}/custom-endpoint", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { + user: "user:bob", + action: "custom_action", + resource: "resource:123", + }, + queryParams: { + page_size: 20, + }, +}); + +console.log("Response:", response);`; + case SupportedLanguage.GO_SDK: + return `import ( + "context" + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + openfga "github.com/openfga/go-sdk" + . "github.com/openfga/go-sdk/client" +) + +func main() { + ctx := context.Background() + storeID := os.Getenv("FGA_STORE_ID") + + fgaClient, err := NewSdkClient(&ClientConfiguration{ + ApiUrl: os.Getenv("FGA_API_URL"), + StoreId: storeID, + }) + if err != nil { + log.Fatalf("Failed to create client: %v", err) + } + + // Get the generic API executor + executor := fgaClient.GetAPIExecutor() + + requestBody := map[string]interface{}{ + "user": "user:bob", + "action": "custom_action", + "resource": "resource:123", + } + + // Build and execute the request + request := openfga.NewAPIExecutorRequestBuilder( + "CustomEndpoint", http.MethodPost, "/stores/{store_id}/custom-endpoint", + ). + WithPathParameter("store_id", storeID). + WithQueryParameter("page_size", "20"). + WithBody(requestBody). + Build() + + rawResponse, err := executor.Execute(ctx, request) + if err != nil { + log.Fatalf("Request failed: %v", err) + } + + var result map[string]interface{} + if err := json.Unmarshal(rawResponse.Body, &result); err != nil { + log.Fatalf("Failed to decode: %v", err) + } + + fmt.Printf("Status Code: %d\\n", rawResponse.StatusCode) + fmt.Printf("Response: %+v\\n", result) +}`; + case SupportedLanguage.DOTNET_SDK: + return `using OpenFga.Sdk.Client; +using OpenFga.Sdk.ApiClient; + +var configuration = new ClientConfiguration() { + ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL") ?? "http://localhost:8080", + StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), +}; + +var fgaClient = new OpenFgaClient(configuration); +var executor = fgaClient.ApiExecutor; + +// Build the request using fluent API +var request = RequestBuilder + .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/custom-endpoint") + .WithPathParameter("store_id", configuration.StoreId) + .WithQueryParameter("page_size", "20") + .WithBody(new { + user = "user:bob", + action = "custom_action", + resource = "resource:123" + }); + +var response = await executor.ExecuteAsync>( + request, "CustomEndpoint" +); + +if (response.IsSuccessful) { + Console.WriteLine($"Status: {response.StatusCode}"); + Console.WriteLine($"Raw JSON: {response.RawResponse}"); + Console.WriteLine($"Data: {response.Data}"); +} else { + Console.WriteLine($"Request failed: {response.StatusCode}"); +}`; + case SupportedLanguage.PYTHON_SDK: + return `import asyncio +import os +from openfga_sdk import ClientConfiguration, OpenFgaClient + +FGA_API_URL = os.environ.get("FGA_API_URL") +FGA_STORE_ID = os.environ.get("FGA_STORE_ID") + +async def main(): + configuration = ClientConfiguration( + api_url=FGA_API_URL, + store_id=FGA_STORE_ID, + ) + + async with OpenFgaClient(configuration) as fga_client: + response = await fga_client.execute_api_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={ + "user": "user:bob", + "action": "custom_action", + "resource": "resource:123", + }, + query_params={ + "page_size": 20, + }, + ) + + if response.status == 200: + result = response.json() + print(f"Response: {result}") + +asyncio.run(main())`; + case SupportedLanguage.JAVA_SDK: + return `import dev.openfga.sdk.api.client.OpenFgaClient; +import dev.openfga.sdk.api.client.HttpMethod; +import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; +import dev.openfga.sdk.api.configuration.ClientConfiguration; + +import java.util.Map; + +public class Example { + public static void main(String[] args) throws Exception { + var config = new ClientConfiguration() + .apiUrl(System.getenv("FGA_API_URL")) + .storeId(System.getenv("FGA_STORE_ID")); + + var fgaClient = new OpenFgaClient(config); + String storeId = System.getenv("FGA_STORE_ID"); + + Map requestBody = Map.of( + "user", "user:bob", + "action", "custom_action", + "resource", "resource:123" + ); + + var request = ApiExecutorRequestBuilder.builder( + HttpMethod.POST, "/stores/{store_id}/custom-endpoint" + ) + .pathParam("store_id", storeId) + .queryParam("page_size", "20") + .body(requestBody) + .build(); + + // Get raw response + var rawResponse = fgaClient.apiExecutor().send(request).get(); + System.out.println("Status Code: " + rawResponse.getStatusCode()); + System.out.println("Response: " + rawResponse.getData()); + } +}`; + case SupportedLanguage.CLI: + case SupportedLanguage.CURL: + case SupportedLanguage.RPC: + case SupportedLanguage.PLAYGROUND: + return `# API Executor is only available through the SDKs`; + default: + assertNever(lang); + } +} + +export function ExecuteApiRequestViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { + const defaultLangs = [ + SupportedLanguage.JS_SDK, + SupportedLanguage.GO_SDK, + SupportedLanguage.DOTNET_SDK, + SupportedLanguage.PYTHON_SDK, + SupportedLanguage.JAVA_SDK, + ]; + const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); + return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestViewer); +} + +// --------------------------------------------------------------------------- +// 2. ExecuteApiRequestPathParamsViewer – path parameter example +// --------------------------------------------------------------------------- + +function executeApiRequestPathParamsViewer( + lang: SupportedLanguage, + _opts: ExecuteApiRequestViewerOpts, +): string { + switch (lang) { + case SupportedLanguage.JS_SDK: + return `const response = await fgaClient.executeApiRequest({ + operationName: "GetAuthorizationModel", + method: "GET", + path: "/stores/{store_id}/authorization-models/{model_id}", + pathParams: { + store_id: "your-store-id", + model_id: "your-model-id", + }, +});`; + case SupportedLanguage.GO_SDK: + return `request := openfga.NewAPIExecutorRequestBuilder( + "GetAuthorizationModel", http.MethodGet, + "/stores/{store_id}/authorization-models/{model_id}", +). + WithPathParameter("store_id", "your-store-id"). + WithPathParameter("model_id", "your-model-id"). + Build()`; + case SupportedLanguage.DOTNET_SDK: + return `var request = RequestBuilder + .Create(HttpMethod.Get, configuration.ApiUrl, + "/stores/{store_id}/authorization-models/{model_id}") + .WithPathParameter("store_id", "your-store-id") + .WithPathParameter("model_id", "your-model-id");`; + case SupportedLanguage.PYTHON_SDK: + return `response = await fga_client.execute_api_request( + operation_name="GetAuthorizationModel", + method="GET", + path="/stores/{store_id}/authorization-models/{model_id}", + path_params={ + "store_id": "your-store-id", + "model_id": "your-model-id", + }, +)`; + case SupportedLanguage.JAVA_SDK: + return `var request = ApiExecutorRequestBuilder.builder( + HttpMethod.GET, "/stores/{store_id}/authorization-models/{model_id}" +) + .pathParam("store_id", "your-store-id") + .pathParam("model_id", "your-model-id") + .build();`; + case SupportedLanguage.CLI: + case SupportedLanguage.CURL: + case SupportedLanguage.RPC: + case SupportedLanguage.PLAYGROUND: + return `# API Executor is only available through the SDKs`; + default: + assertNever(lang); + } +} + +export function ExecuteApiRequestPathParamsViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { + const defaultLangs = [ + SupportedLanguage.JS_SDK, + SupportedLanguage.GO_SDK, + SupportedLanguage.DOTNET_SDK, + SupportedLanguage.PYTHON_SDK, + SupportedLanguage.JAVA_SDK, + ]; + const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); + return defaultOperationsViewer( + allowedLanguages, + opts, + executeApiRequestPathParamsViewer, + ); +} + +// --------------------------------------------------------------------------- +// 3. ExecuteApiRequestDecodeViewer – typed response decoding +// --------------------------------------------------------------------------- + +function executeApiRequestDecodeViewer( + lang: SupportedLanguage, + _opts: ExecuteApiRequestViewerOpts, +): string { + switch (lang) { + case SupportedLanguage.JS_SDK: + return `// The JavaScript SDK's executeApiRequest already returns a parsed JSON +// object by default — no additional decoding step is needed. +const response = await fgaClient.executeApiRequest({ + operationName: "CustomEndpoint", + method: "POST", + path: "/stores/{store_id}/custom-endpoint", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { user: "user:bob" }, +}); + +// response is already a parsed object +console.log("Allowed:", response.allowed); +console.log("Reason:", response.reason);`; + case SupportedLanguage.GO_SDK: + return `// Using ctx, executor, and request from the example above +// Use ExecuteWithDecode to automatically unmarshal the response into a Go struct +type CustomEndpointResponse struct { + Allowed bool \`json:"allowed"\` + Reason string \`json:"reason"\` +} + +var customResponse CustomEndpointResponse + +rawResponse, err := executor.ExecuteWithDecode(ctx, request, &customResponse) +if err != nil { + log.Fatalf("Request failed: %v", err) +} + +fmt.Printf("Allowed: %v, Reason: %s\\n", customResponse.Allowed, customResponse.Reason) +fmt.Printf("Status Code: %d\\n", rawResponse.StatusCode)`; + case SupportedLanguage.DOTNET_SDK: + return `// Specify the response type as a generic parameter to ExecuteAsync +var response = await executor.ExecuteAsync( + request, "GetStore" +); + +if (response.IsSuccessful) { + Console.WriteLine($"Store Name: {response.Data.Name}"); + Console.WriteLine($"Raw JSON: {response.RawResponse}"); +}`; + case SupportedLanguage.PYTHON_SDK: + return `# The Python SDK returns a response object whose .json() method +# gives you a parsed dictionary +response = await fga_client.execute_api_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={"user": "user:bob"}, +) + +result = response.json() +print(f"Allowed: {result['allowed']}, Reason: {result['reason']}")`; + case SupportedLanguage.JAVA_SDK: + return `// Pass a class to send() to have the response decoded automatically +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; } +} + +var response = fgaClient.apiExecutor() + .send(request, CustomEndpointResponse.class) + .get(); + +CustomEndpointResponse data = response.getData(); +System.out.println("Allowed: " + data.isAllowed()); +System.out.println("Reason: " + data.getReason()); +System.out.println("Status Code: " + response.getStatusCode());`; + case SupportedLanguage.CLI: + case SupportedLanguage.CURL: + case SupportedLanguage.RPC: + case SupportedLanguage.PLAYGROUND: + return `# API Executor is only available through the SDKs`; + default: + assertNever(lang); + } +} + +export function ExecuteApiRequestDecodeViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { + const defaultLangs = [ + SupportedLanguage.JS_SDK, + SupportedLanguage.GO_SDK, + SupportedLanguage.DOTNET_SDK, + SupportedLanguage.PYTHON_SDK, + SupportedLanguage.JAVA_SDK, + ]; + const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); + return defaultOperationsViewer( + allowedLanguages, + opts, + executeApiRequestDecodeViewer, + ); +} + +// --------------------------------------------------------------------------- +// 4. ExecuteApiRequestStreamingViewer – streaming endpoint example +// --------------------------------------------------------------------------- + +function executeApiRequestStreamingViewer( + lang: SupportedLanguage, + _opts: ExecuteApiRequestViewerOpts, +): string { + switch (lang) { + case SupportedLanguage.JS_SDK: + return `// Use executeStreamedApiRequest instead of executeApiRequest +const stream = fgaClient.executeStreamedApiRequest({ + operationName: "StreamedListObjects", + method: "POST", + path: "/stores/{store_id}/streamed-list-objects", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { + type: "document", + relation: "viewer", + user: "user:anne", + authorization_model_id: process.env.FGA_MODEL_ID, + }, +}); + +for await (const chunk of stream) { + if (chunk.result) { + console.log("Object:", chunk.result.object); // e.g. "document:roadmap" + } +}`; + case SupportedLanguage.GO_SDK: + return `import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + + openfga "github.com/openfga/go-sdk" +) + +storeID := os.Getenv("FGA_STORE_ID") +modelID := os.Getenv("FGA_MODEL_ID") + +// Use ExecuteStreaming which returns an APIExecutorStreamingChannel +// with Results and Errors channels +request := openfga.NewAPIExecutorRequestBuilder( + "StreamedListObjects", http.MethodPost, "/stores/{store_id}/streamed-list-objects", +). + WithPathParameter("store_id", storeID). + WithBody(openfga.ListObjectsRequest{ + AuthorizationModelId: openfga.PtrString(modelID), + Type: "document", + Relation: "viewer", + User: "user:alice", + }). + Build() + +channel, err := executor.ExecuteStreaming(ctx, request, openfga.DefaultStreamBufferSize) +if err != nil { + log.Fatalf("Streaming request failed: %v", err) +} +defer channel.Close() + +for { + select { + case result, ok := <-channel.Results: + if !ok { + // Stream completed — check for final errors + select { + case err := <-channel.Errors: + if err != nil { + log.Fatalf("Stream error: %v", err) + } + default: + } + fmt.Println("Stream completed successfully") + return + } + + var response openfga.StreamedListObjectsResponse + if err := json.Unmarshal(result, &response); err != nil { + log.Fatalf("Failed to decode: %v", err) + } + fmt.Printf("Object: %s\\n", response.Object) + + case err := <-channel.Errors: + if err != nil { + log.Fatalf("Stream error: %v", err) + } + } +}`; + case SupportedLanguage.DOTNET_SDK: + return `using OpenFga.Sdk.Client; +using OpenFga.Sdk.ApiClient; + +var storeId = configuration.StoreId; +var authorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"); + +// Use ExecuteStreamingAsync which returns an IAsyncEnumerable +var request = RequestBuilder + .Create(HttpMethod.Post, configuration.ApiUrl, + "/stores/{store_id}/streamed-list-objects") + .WithPathParameter("store_id", storeId) + .WithBody(new { + user = "user:anne", + relation = "can_read", + type = "document", + authorization_model_id = authorizationModelId + }); + +await foreach (var item in executor.ExecuteStreamingAsync( + request, "StreamedListObjects")) +{ + Console.WriteLine($"Object: {item.Object}"); +}`; + case SupportedLanguage.PYTHON_SDK: + return `# Use execute_streamed_api_request which returns an AsyncIterator +# (or Iterator in the sync client) that yields one parsed JSON object per chunk +async for chunk in fga_client.execute_streamed_api_request( + operation_name="StreamedListObjects", + method="POST", + path="/stores/{store_id}/streamed-list-objects", + path_params={"store_id": FGA_STORE_ID}, + body={ + "type": "document", + "relation": "viewer", + "user": "user:anne", + "authorization_model_id": FGA_MODEL_ID, + }, +): + # Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}} + if "result" in chunk: + print(chunk["result"]["object"]) # e.g. "document:roadmap"`; + case SupportedLanguage.JAVA_SDK: + return `import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; +import dev.openfga.sdk.api.model.ListObjectsRequest; +import dev.openfga.sdk.api.model.StreamedListObjectsResponse; +import dev.openfga.sdk.api.client.HttpMethod; + +// Use streamingApiExecutor which delivers each response object +// to a consumer callback as it arrives +var request = ApiExecutorRequestBuilder.builder( + HttpMethod.POST, "/stores/{store_id}/streamed-list-objects" +) + .body(new ListObjectsRequest() + .user("user:anne") + .relation("viewer") + .type("document")) + .build(); + +fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class) + .stream( + request, + response -> System.out.println("Object: " + response.getObject()), + error -> System.err.println("Stream error: " + error.getMessage()) + ) + .thenRun(() -> System.out.println("Streaming complete")) + .exceptionally(err -> { + System.err.println("Fatal error: " + err.getMessage()); + return null; + });`; + case SupportedLanguage.CLI: + case SupportedLanguage.CURL: + case SupportedLanguage.RPC: + case SupportedLanguage.PLAYGROUND: + return `# API Executor is only available through the SDKs`; + default: + assertNever(lang); + } +} + +export function ExecuteApiRequestStreamingViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { + const defaultLangs = [ + SupportedLanguage.JS_SDK, + SupportedLanguage.GO_SDK, + SupportedLanguage.DOTNET_SDK, + SupportedLanguage.PYTHON_SDK, + SupportedLanguage.JAVA_SDK, + ]; + const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); + return defaultOperationsViewer( + allowedLanguages, + opts, + executeApiRequestStreamingViewer, + ); +} + +// --------------------------------------------------------------------------- +// 5. ExecuteApiRequestErrorViewer – error handling example +// --------------------------------------------------------------------------- + +function executeApiRequestErrorViewer( + lang: SupportedLanguage, + _opts: ExecuteApiRequestViewerOpts, +): string { + switch (lang) { + case SupportedLanguage.JS_SDK: + return `try { + const response = await fgaClient.executeApiRequest({ + operationName: "CustomEndpoint", + method: "POST", + path: "/stores/{store_id}/custom-endpoint", + pathParams: { store_id: process.env.FGA_STORE_ID }, + body: { user: "user:bob" }, + }); + console.log("Success:", response); +} catch (error) { + console.error("Request failed:", error.message); +}`; + case SupportedLanguage.GO_SDK: + return `rawResponse, err := executor.Execute(ctx, request) +if err != nil { + log.Fatalf("Request failed: %v", err) +} + +fmt.Printf("Status Code: %d\\n", rawResponse.StatusCode) +fmt.Printf("Headers: %+v\\n", rawResponse.Headers)`; + case SupportedLanguage.DOTNET_SDK: + return `var response = await executor.ExecuteAsync( + request, "GetStore" +); + +if (!response.IsSuccessful) { + switch ((int)response.StatusCode) { + case 404: + Console.WriteLine("Not found"); + break; + case 401: + Console.WriteLine("Unauthorized — check your credentials"); + break; + case 429: + Console.WriteLine("Rate limited — retry after delay"); + break; + case >= 500: + Console.WriteLine($"Server error: {response.RawResponse}"); + break; + default: + Console.WriteLine($"Request failed: {response.StatusCode}"); + break; + } + return; +} + +Console.WriteLine($"Store Name: {response.Data.Name}");`; + case SupportedLanguage.PYTHON_SDK: + return `response = await fga_client.execute_api_request( + operation_name="CustomEndpoint", + method="POST", + path="/stores/{store_id}/custom-endpoint", + path_params={"store_id": FGA_STORE_ID}, + body={"user": "user:bob"}, +) + +if response.status == 200: + result = response.json() + print(f"Response: {result}") +else: + print(f"Request failed with status: {response.status}")`; + case SupportedLanguage.JAVA_SDK: + return `try { + var rawResponse = fgaClient.apiExecutor().send(request).get(); + System.out.println("Status Code: " + rawResponse.getStatusCode()); + System.out.println("Response: " + rawResponse.getData()); +} catch (Exception e) { + System.err.println("Request failed: " + e.getMessage()); + System.err.println("Cause: " + e.getCause().getClass().getSimpleName()); +}`; + case SupportedLanguage.CLI: + case SupportedLanguage.CURL: + case SupportedLanguage.RPC: + case SupportedLanguage.PLAYGROUND: + return `# API Executor is only available through the SDKs`; + default: + assertNever(lang); + } +} + +export function ExecuteApiRequestErrorViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { + const defaultLangs = [ + SupportedLanguage.JS_SDK, + SupportedLanguage.GO_SDK, + SupportedLanguage.DOTNET_SDK, + SupportedLanguage.PYTHON_SDK, + SupportedLanguage.JAVA_SDK, + ]; + const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); + return defaultOperationsViewer( + allowedLanguages, + opts, + executeApiRequestErrorViewer, + ); +} + diff --git a/src/components/Docs/SnippetViewer/index.ts b/src/components/Docs/SnippetViewer/index.ts index 19efebe2d1..5aa14280bb 100644 --- a/src/components/Docs/SnippetViewer/index.ts +++ b/src/components/Docs/SnippetViewer/index.ts @@ -2,6 +2,7 @@ export * from './CheckRequestViewer'; export * from './BatchCheckRequestViewer'; export * from './DefaultTabbedViewer'; export * from './ExpandRequestViewer'; +export * from './ExecuteApiRequestViewer'; export * from './ListObjectsRequestViewer'; export * from './ListUsersRequestViewer'; export * from './ReadChangesRequestViewer'; From 2a4fed0511c51a1a8940db6561644180fbf8dd7a Mon Sep 17 00:00:00 2001 From: Anurag Bandyopadhyay Date: Wed, 15 Apr 2026 12:41:58 +0530 Subject: [PATCH 4/6] fix: apply suggestions from review Co-authored-by: Raghd Hamzeh --- docs/content/interacting/raw-api-requests.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/content/interacting/raw-api-requests.mdx b/docs/content/interacting/raw-api-requests.mdx index 739a2e18c0..9258d48848 100644 --- a/docs/content/interacting/raw-api-requests.mdx +++ b/docs/content/interacting/raw-api-requests.mdx @@ -29,7 +29,7 @@ In certain cases you may want to call API endpoints that are not yet available a This is useful when: - You want to call a **new endpoint** that is not yet supported by the SDK. -- You are using an **earlier version** of the SDK that doesn't yet have a particular method. +- You are using an **earlier version** of the SDK that doesn't support a particular endpoint, and can't update. - You have a **custom endpoint** deployed that extends the API. ## Before you start @@ -40,7 +40,7 @@ This is useful when: Initialize the SDK client as usual (see [Setup SDK Client](../getting-started/setup-sdk-client.mdx)), then use the API Executor to build and send a request. -The examples below call a hypothetical custom endpoint, but you can use the same pattern for **any** API path. +The examples below call a hypothetical custom endpoint (`/stores/{store_id}/custom-endpoint`), but you can use the same pattern for any API path. ### Calling a custom endpoint with POST From 193ed5c3c07b61968d345d5412eb896a0a5cb521 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Thu, 16 Apr 2026 12:56:38 +0530 Subject: [PATCH 5/6] fix: lint --- .../SnippetViewer/ExecuteApiRequestViewer.tsx | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx b/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx index 90fb004697..d4174296b2 100644 --- a/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx @@ -14,7 +14,8 @@ interface ExecuteApiRequestViewerOpts { // 1. ExecuteApiRequestViewer – POST custom endpoint (full setup) // --------------------------------------------------------------------------- -function executeApiRequestViewer(lang: SupportedLanguage, _opts: ExecuteApiRequestViewerOpts): string { +function executeApiRequestViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { + void opts; switch (lang) { case SupportedLanguage.JS_SDK: return `const { OpenFgaClient } = require("@openfga/sdk"); @@ -229,10 +230,8 @@ export function ExecuteApiRequestViewer(opts: ExecuteApiRequestViewerOpts): JSX. // 2. ExecuteApiRequestPathParamsViewer – path parameter example // --------------------------------------------------------------------------- -function executeApiRequestPathParamsViewer( - lang: SupportedLanguage, - _opts: ExecuteApiRequestViewerOpts, -): string { +function executeApiRequestPathParamsViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { + void opts; switch (lang) { case SupportedLanguage.JS_SDK: return `const response = await fgaClient.executeApiRequest({ @@ -305,10 +304,8 @@ export function ExecuteApiRequestPathParamsViewer(opts: ExecuteApiRequestViewerO // 3. ExecuteApiRequestDecodeViewer – typed response decoding // --------------------------------------------------------------------------- -function executeApiRequestDecodeViewer( - lang: SupportedLanguage, - _opts: ExecuteApiRequestViewerOpts, -): string { +function executeApiRequestDecodeViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { + void opts; switch (lang) { case SupportedLanguage.JS_SDK: return `// The JavaScript SDK's executeApiRequest already returns a parsed JSON @@ -403,21 +400,15 @@ export function ExecuteApiRequestDecodeViewer(opts: ExecuteApiRequestViewerOpts) SupportedLanguage.JAVA_SDK, ]; const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer( - allowedLanguages, - opts, - executeApiRequestDecodeViewer, - ); + return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestDecodeViewer); } // --------------------------------------------------------------------------- // 4. ExecuteApiRequestStreamingViewer – streaming endpoint example // --------------------------------------------------------------------------- -function executeApiRequestStreamingViewer( - lang: SupportedLanguage, - _opts: ExecuteApiRequestViewerOpts, -): string { +function executeApiRequestStreamingViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { + void opts; switch (lang) { case SupportedLanguage.JS_SDK: return `// Use executeStreamedApiRequest instead of executeApiRequest @@ -590,21 +581,15 @@ export function ExecuteApiRequestStreamingViewer(opts: ExecuteApiRequestViewerOp SupportedLanguage.JAVA_SDK, ]; const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer( - allowedLanguages, - opts, - executeApiRequestStreamingViewer, - ); + return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestStreamingViewer); } // --------------------------------------------------------------------------- // 5. ExecuteApiRequestErrorViewer – error handling example // --------------------------------------------------------------------------- -function executeApiRequestErrorViewer( - lang: SupportedLanguage, - _opts: ExecuteApiRequestViewerOpts, -): string { +function executeApiRequestErrorViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { + void opts; switch (lang) { case SupportedLanguage.JS_SDK: return `try { @@ -696,10 +681,5 @@ export function ExecuteApiRequestErrorViewer(opts: ExecuteApiRequestViewerOpts): SupportedLanguage.JAVA_SDK, ]; const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer( - allowedLanguages, - opts, - executeApiRequestErrorViewer, - ); + return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestErrorViewer); } - From d771a6a6c2bd3dd7a53825b4ec1f1ea289da3505 Mon Sep 17 00:00:00 2001 From: SoulPancake Date: Fri, 17 Apr 2026 19:12:08 +0530 Subject: [PATCH 6/6] feat: add configurable api request vieweer component --- docs/content/interacting/overview.mdx | 5 - docs/content/interacting/raw-api-requests.mdx | 110 -- docs/sidebars.js | 5 - .../SnippetViewer/ExecuteApiRequestViewer.tsx | 947 +++++++----------- 4 files changed, 350 insertions(+), 717 deletions(-) delete mode 100644 docs/content/interacting/raw-api-requests.mdx diff --git a/docs/content/interacting/overview.mdx b/docs/content/interacting/overview.mdx index 4e9cb8fd6a..9b45bf0c8f 100644 --- a/docs/content/interacting/overview.mdx +++ b/docs/content/interacting/overview.mdx @@ -52,10 +52,5 @@ This section helps you integrate diff --git a/docs/content/interacting/raw-api-requests.mdx b/docs/content/interacting/raw-api-requests.mdx deleted file mode 100644 index 9258d48848..0000000000 --- a/docs/content/interacting/raw-api-requests.mdx +++ /dev/null @@ -1,110 +0,0 @@ ---- -title: Calling Other Endpoints -sidebar_position: 9 -slug: /interacting/raw-api-requests -description: Making raw HTTP calls to OpenFGA endpoints not yet wrapped by the SDK ---- - -import { - DocumentationNotice, - ExecuteApiRequestViewer, - ExecuteApiRequestPathParamsViewer, - ExecuteApiRequestDecodeViewer, - ExecuteApiRequestStreamingViewer, - ExecuteApiRequestErrorViewer, - ProductName, - ProductNameFormat, - RelatedSection, - SdkSetupPrerequisite, -} from '@components/Docs'; - -# Calling Other Endpoints - - - -In certain cases you may want to call API endpoints that are not yet available as dedicated methods in the SDK. Every SDK provides an **API Executor** that lets you make raw HTTP calls to any endpoint while still honoring the client configuration — including authentication, telemetry, retries, and error handling. - -## When to use - -This is useful when: - -- You want to call a **new endpoint** that is not yet supported by the SDK. -- You are using an **earlier version** of the SDK that doesn't support a particular endpoint, and can't update. -- You have a **custom endpoint** deployed that extends the API. - -## Before you start - - - -## Making a request - -Initialize the SDK client as usual (see [Setup SDK Client](../getting-started/setup-sdk-client.mdx)), then use the API Executor to build and send a request. - -The examples below call a hypothetical custom endpoint (`/stores/{store_id}/custom-endpoint`), but you can use the same pattern for any API path. - -### Calling a custom endpoint with POST - - - -:::note -The examples above use `POST`, but the API Executor supports any HTTP method (`GET`, `POST`, `PUT`, `DELETE`, etc.). For `GET` requests, simply omit the `body` parameter. -::: - -### Using path parameters - -Path parameters are specified in the path using `{param_name}` syntax and must be provided via the path parameters option. They are URL-encoded automatically. - - - -### Decoding the response into a typed object - -Some SDKs let you decode the raw response directly into a typed struct or class, avoiding manual JSON parsing. - - - -## Streaming requests - -For endpoints that stream results (such as `StreamedListObjects`), use the streaming variant of the API Executor. Streaming endpoints return results incrementally as they are computed, rather than waiting for all results before responding. - - - -## Error handling - -Always check the response status and handle errors appropriately. The API Executor preserves the SDK's built-in error handling, but you may want to inspect status codes for custom endpoints. - - - -## SDK examples - -For complete working examples, refer to the API Executor examples in each SDK repository: - -- [Node.js](https://github.com/openfga/js-sdk/tree/main/example/api-executor) -- [Go](https://github.com/openfga/go-sdk/tree/main/example/api_executor) -- [.NET](https://github.com/openfga/dotnet-sdk/tree/main/example/ApiExecutorExample) -- [Python](https://github.com/openfga/python-sdk/tree/main/example/execute-api-request) -- [Java](https://github.com/openfga/java-sdk/tree/main/examples/api-executor) - - - diff --git a/docs/sidebars.js b/docs/sidebars.js index f6cd14e2da..03a5f542c7 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -425,11 +425,6 @@ const sidebars = { label: 'AuthZEN API', id: 'content/interacting/authzen', }, - { - type: 'doc', - label: 'Calling Other Endpoints', - id: 'content/interacting/raw-api-requests', - }, ], }, diff --git a/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx b/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx index d4174296b2..515eee9d37 100644 --- a/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx +++ b/src/components/Docs/SnippetViewer/ExecuteApiRequestViewer.tsx @@ -3,565 +3,271 @@ import { defaultOperationsViewer } from './DefaultTabbedViewer'; import assertNever from 'assert-never/index'; // --------------------------------------------------------------------------- -// Shared opts +// Helpers – value serialisation per language // --------------------------------------------------------------------------- -interface ExecuteApiRequestViewerOpts { - skipSetup?: boolean; - allowedLanguages?: SupportedLanguage[]; -} - -// --------------------------------------------------------------------------- -// 1. ExecuteApiRequestViewer – POST custom endpoint (full setup) -// --------------------------------------------------------------------------- - -function executeApiRequestViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { - void opts; - switch (lang) { - case SupportedLanguage.JS_SDK: - return `const { OpenFgaClient } = require("@openfga/sdk"); - -const fgaClient = new OpenFgaClient({ - apiUrl: process.env.FGA_API_URL, - storeId: process.env.FGA_STORE_ID, -}); - -// Call a custom endpoint using path parameters -const response = await fgaClient.executeApiRequest({ - operationName: "CustomEndpoint", // For telemetry/logging - method: "POST", - path: "/stores/{store_id}/custom-endpoint", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { - user: "user:bob", - action: "custom_action", - resource: "resource:123", - }, - queryParams: { - page_size: 20, - }, -}); - -console.log("Response:", response);`; - case SupportedLanguage.GO_SDK: - return `import ( - "context" - "encoding/json" - "fmt" - "log" - "net/http" - "os" - - openfga "github.com/openfga/go-sdk" - . "github.com/openfga/go-sdk/client" -) - -func main() { - ctx := context.Background() - storeID := os.Getenv("FGA_STORE_ID") - - fgaClient, err := NewSdkClient(&ClientConfiguration{ - ApiUrl: os.Getenv("FGA_API_URL"), - StoreId: storeID, - }) - if err != nil { - log.Fatalf("Failed to create client: %v", err) - } - - // Get the generic API executor - executor := fgaClient.GetAPIExecutor() - - requestBody := map[string]interface{}{ - "user": "user:bob", - "action": "custom_action", - "resource": "resource:123", - } - - // Build and execute the request - request := openfga.NewAPIExecutorRequestBuilder( - "CustomEndpoint", http.MethodPost, "/stores/{store_id}/custom-endpoint", - ). - WithPathParameter("store_id", storeID). - WithQueryParameter("page_size", "20"). - WithBody(requestBody). - Build() - - rawResponse, err := executor.Execute(ctx, request) - if err != nil { - log.Fatalf("Request failed: %v", err) - } - - var result map[string]interface{} - if err := json.Unmarshal(rawResponse.Body, &result); err != nil { - log.Fatalf("Failed to decode: %v", err) - } - - fmt.Printf("Status Code: %d\\n", rawResponse.StatusCode) - fmt.Printf("Response: %+v\\n", result) -}`; - case SupportedLanguage.DOTNET_SDK: - return `using OpenFga.Sdk.Client; -using OpenFga.Sdk.ApiClient; - -var configuration = new ClientConfiguration() { - ApiUrl = Environment.GetEnvironmentVariable("FGA_API_URL") ?? "http://localhost:8080", - StoreId = Environment.GetEnvironmentVariable("FGA_STORE_ID"), -}; - -var fgaClient = new OpenFgaClient(configuration); -var executor = fgaClient.ApiExecutor; - -// Build the request using fluent API -var request = RequestBuilder - .Create(HttpMethod.Post, configuration.ApiUrl, "/stores/{store_id}/custom-endpoint") - .WithPathParameter("store_id", configuration.StoreId) - .WithQueryParameter("page_size", "20") - .WithBody(new { - user = "user:bob", - action = "custom_action", - resource = "resource:123" - }); - -var response = await executor.ExecuteAsync>( - request, "CustomEndpoint" -); - -if (response.IsSuccessful) { - Console.WriteLine($"Status: {response.StatusCode}"); - Console.WriteLine($"Raw JSON: {response.RawResponse}"); - Console.WriteLine($"Data: {response.Data}"); -} else { - Console.WriteLine($"Request failed: {response.StatusCode}"); -}`; - case SupportedLanguage.PYTHON_SDK: - return `import asyncio -import os -from openfga_sdk import ClientConfiguration, OpenFgaClient -FGA_API_URL = os.environ.get("FGA_API_URL") -FGA_STORE_ID = os.environ.get("FGA_STORE_ID") - -async def main(): - configuration = ClientConfiguration( - api_url=FGA_API_URL, - store_id=FGA_STORE_ID, - ) - - async with OpenFgaClient(configuration) as fga_client: - response = await fga_client.execute_api_request( - operation_name="CustomEndpoint", - method="POST", - path="/stores/{store_id}/custom-endpoint", - path_params={"store_id": FGA_STORE_ID}, - body={ - "user": "user:bob", - "action": "custom_action", - "resource": "resource:123", - }, - query_params={ - "page_size": 20, - }, - ) - - if response.status == 200: - result = response.json() - print(f"Response: {result}") - -asyncio.run(main())`; - case SupportedLanguage.JAVA_SDK: - return `import dev.openfga.sdk.api.client.OpenFgaClient; -import dev.openfga.sdk.api.client.HttpMethod; -import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; -import dev.openfga.sdk.api.configuration.ClientConfiguration; - -import java.util.Map; - -public class Example { - public static void main(String[] args) throws Exception { - var config = new ClientConfiguration() - .apiUrl(System.getenv("FGA_API_URL")) - .storeId(System.getenv("FGA_STORE_ID")); - - var fgaClient = new OpenFgaClient(config); - String storeId = System.getenv("FGA_STORE_ID"); - - Map requestBody = Map.of( - "user", "user:bob", - "action", "custom_action", - "resource", "resource:123" - ); - - var request = ApiExecutorRequestBuilder.builder( - HttpMethod.POST, "/stores/{store_id}/custom-endpoint" - ) - .pathParam("store_id", storeId) - .queryParam("page_size", "20") - .body(requestBody) - .build(); - - // Get raw response - var rawResponse = fgaClient.apiExecutor().send(request).get(); - System.out.println("Status Code: " + rawResponse.getStatusCode()); - System.out.println("Response: " + rawResponse.getData()); - } -}`; - case SupportedLanguage.CLI: - case SupportedLanguage.CURL: - case SupportedLanguage.RPC: - case SupportedLanguage.PLAYGROUND: - return `# API Executor is only available through the SDKs`; - default: - assertNever(lang); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function jsValue(val: any, indent: number): string { + if (typeof val === 'string') return `"${val}"`; + if (typeof val === 'number' || typeof val === 'boolean') return String(val); + if (Array.isArray(val)) { + const items = val.map((v) => jsValue(v, indent + 2)); + return `[${items.join(', ')}]`; } + if (typeof val === 'object' && val !== null) { + const pad = ' '.repeat(indent); + const closePad = ' '.repeat(indent - 2); + const entries = Object.entries(val).map(([k, v]) => `${pad}${k}: ${jsValue(v, indent + 2)}`); + return `{\n${entries.join(',\n')}\n${closePad}}`; + } + return String(val); } -export function ExecuteApiRequestViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { - const defaultLangs = [ - SupportedLanguage.JS_SDK, - SupportedLanguage.GO_SDK, - SupportedLanguage.DOTNET_SDK, - SupportedLanguage.PYTHON_SDK, - SupportedLanguage.JAVA_SDK, - ]; - const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestViewer); -} - -// --------------------------------------------------------------------------- -// 2. ExecuteApiRequestPathParamsViewer – path parameter example -// --------------------------------------------------------------------------- - -function executeApiRequestPathParamsViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { - void opts; - switch (lang) { - case SupportedLanguage.JS_SDK: - return `const response = await fgaClient.executeApiRequest({ - operationName: "GetAuthorizationModel", - method: "GET", - path: "/stores/{store_id}/authorization-models/{model_id}", - pathParams: { - store_id: "your-store-id", - model_id: "your-model-id", - }, -});`; - case SupportedLanguage.GO_SDK: - return `request := openfga.NewAPIExecutorRequestBuilder( - "GetAuthorizationModel", http.MethodGet, - "/stores/{store_id}/authorization-models/{model_id}", -). - WithPathParameter("store_id", "your-store-id"). - WithPathParameter("model_id", "your-model-id"). - Build()`; - case SupportedLanguage.DOTNET_SDK: - return `var request = RequestBuilder - .Create(HttpMethod.Get, configuration.ApiUrl, - "/stores/{store_id}/authorization-models/{model_id}") - .WithPathParameter("store_id", "your-store-id") - .WithPathParameter("model_id", "your-model-id");`; - case SupportedLanguage.PYTHON_SDK: - return `response = await fga_client.execute_api_request( - operation_name="GetAuthorizationModel", - method="GET", - path="/stores/{store_id}/authorization-models/{model_id}", - path_params={ - "store_id": "your-store-id", - "model_id": "your-model-id", - }, -)`; - case SupportedLanguage.JAVA_SDK: - return `var request = ApiExecutorRequestBuilder.builder( - HttpMethod.GET, "/stores/{store_id}/authorization-models/{model_id}" -) - .pathParam("store_id", "your-store-id") - .pathParam("model_id", "your-model-id") - .build();`; - case SupportedLanguage.CLI: - case SupportedLanguage.CURL: - case SupportedLanguage.RPC: - case SupportedLanguage.PLAYGROUND: - return `# API Executor is only available through the SDKs`; - default: - assertNever(lang); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function pyValue(val: any, indent: number): string { + if (typeof val === 'string') return `"${val}"`; + if (typeof val === 'number') return String(val); + if (typeof val === 'boolean') return val ? 'True' : 'False'; + if (Array.isArray(val)) { + const items = val.map((v) => pyValue(v, indent + 4)); + return `[${items.join(', ')}]`; } + if (typeof val === 'object' && val !== null) { + const pad = ' '.repeat(indent); + const closePad = ' '.repeat(indent - 4); + const entries = Object.entries(val).map(([k, v]) => `${pad}"${k}": ${pyValue(v, indent + 4)}`); + return `{\n${entries.join(',\n')},\n${closePad}}`; + } + return String(val); } -export function ExecuteApiRequestPathParamsViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { - const defaultLangs = [ - SupportedLanguage.JS_SDK, - SupportedLanguage.GO_SDK, - SupportedLanguage.DOTNET_SDK, - SupportedLanguage.PYTHON_SDK, - SupportedLanguage.JAVA_SDK, - ]; - const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer( - allowedLanguages, - opts, - executeApiRequestPathParamsViewer, - ); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function goValue(val: any, indent: number): string { + if (typeof val === 'string') return `"${val}"`; + if (typeof val === 'number' || typeof val === 'boolean') return String(val); + if (typeof val === 'object' && val !== null) { + const pad = ' '.repeat(indent); + const closePad = ' '.repeat(indent - 4); + const entries = Object.entries(val).map(([k, v]) => `${pad}"${k}": ${goValue(v, indent + 4)}`); + return `map[string]interface{}{\n${entries.join(',\n')},\n${closePad}}`; + } + return String(val); } -// --------------------------------------------------------------------------- -// 3. ExecuteApiRequestDecodeViewer – typed response decoding -// --------------------------------------------------------------------------- - -function executeApiRequestDecodeViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { - void opts; - switch (lang) { - case SupportedLanguage.JS_SDK: - return `// The JavaScript SDK's executeApiRequest already returns a parsed JSON -// object by default — no additional decoding step is needed. -const response = await fgaClient.executeApiRequest({ - operationName: "CustomEndpoint", - method: "POST", - path: "/stores/{store_id}/custom-endpoint", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { user: "user:bob" }, -}); - -// response is already a parsed object -console.log("Allowed:", response.allowed); -console.log("Reason:", response.reason);`; - case SupportedLanguage.GO_SDK: - return `// Using ctx, executor, and request from the example above -// Use ExecuteWithDecode to automatically unmarshal the response into a Go struct -type CustomEndpointResponse struct { - Allowed bool \`json:"allowed"\` - Reason string \`json:"reason"\` +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function javaValue(val: any, indent: number): string { + if (typeof val === 'string') return `"${val}"`; + if (typeof val === 'number' || typeof val === 'boolean') return String(val); + if (typeof val === 'object' && val !== null) { + const pad = ' '.repeat(indent); + const entries = Object.entries(val); + const pairs = entries.map(([k, v]) => `${pad}"${k}", ${javaValue(v, indent + 4)}`); + return `Map.of(\n${pairs.join(',\n')}\n${' '.repeat(indent - 4)})`; + } + return String(val); } -var customResponse CustomEndpointResponse - -rawResponse, err := executor.ExecuteWithDecode(ctx, request, &customResponse) -if err != nil { - log.Fatalf("Request failed: %v", err) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function csharpValue(val: any, indent: number): string { + if (typeof val === 'string') return `"${val}"`; + if (typeof val === 'number' || typeof val === 'boolean') return String(val).toLowerCase(); + if (typeof val === 'object' && val !== null) { + const pad = ' '.repeat(indent); + const closePad = ' '.repeat(indent - 4); + const entries = Object.entries(val).map(([k, v]) => `${pad}${k} = ${csharpValue(v, indent + 4)}`); + return `new {\n${entries.join(',\n')}\n${closePad}}`; + } + return String(val); } -fmt.Printf("Allowed: %v, Reason: %s\\n", customResponse.Allowed, customResponse.Reason) -fmt.Printf("Status Code: %d\\n", rawResponse.StatusCode)`; - case SupportedLanguage.DOTNET_SDK: - return `// Specify the response type as a generic parameter to ExecuteAsync -var response = await executor.ExecuteAsync( - request, "GetStore" -); - -if (response.IsSuccessful) { - Console.WriteLine($"Store Name: {response.Data.Name}"); - Console.WriteLine($"Raw JSON: {response.RawResponse}"); -}`; - case SupportedLanguage.PYTHON_SDK: - return `# The Python SDK returns a response object whose .json() method -# gives you a parsed dictionary -response = await fga_client.execute_api_request( - operation_name="CustomEndpoint", - method="POST", - path="/stores/{store_id}/custom-endpoint", - path_params={"store_id": FGA_STORE_ID}, - body={"user": "user:bob"}, -) - -result = response.json() -print(f"Allowed: {result['allowed']}, Reason: {result['reason']}")`; - case SupportedLanguage.JAVA_SDK: - return `// Pass a class to send() to have the response decoded automatically -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; } +function goMethod(method: string): string { + const map: Record = { + GET: 'http.MethodGet', + POST: 'http.MethodPost', + PUT: 'http.MethodPut', + DELETE: 'http.MethodDelete', + PATCH: 'http.MethodPatch', + }; + return map[method.toUpperCase()] || `"${method}"`; } -var response = fgaClient.apiExecutor() - .send(request, CustomEndpointResponse.class) - .get(); - -CustomEndpointResponse data = response.getData(); -System.out.println("Allowed: " + data.isAllowed()); -System.out.println("Reason: " + data.getReason()); -System.out.println("Status Code: " + response.getStatusCode());`; - case SupportedLanguage.CLI: - case SupportedLanguage.CURL: - case SupportedLanguage.RPC: - case SupportedLanguage.PLAYGROUND: - return `# API Executor is only available through the SDKs`; - default: - assertNever(lang); - } +function csharpMethod(method: string): string { + const map: Record = { + GET: 'HttpMethod.Get', + POST: 'HttpMethod.Post', + PUT: 'HttpMethod.Put', + DELETE: 'HttpMethod.Delete', + PATCH: 'HttpMethod.Patch', + }; + return map[method.toUpperCase()] || `new HttpMethod("${method}")`; } -export function ExecuteApiRequestDecodeViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { - const defaultLangs = [ - SupportedLanguage.JS_SDK, - SupportedLanguage.GO_SDK, - SupportedLanguage.DOTNET_SDK, - SupportedLanguage.PYTHON_SDK, - SupportedLanguage.JAVA_SDK, - ]; - const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestDecodeViewer); +function javaMethod(method: string): string { + return `HttpMethod.${method.toUpperCase()}`; } // --------------------------------------------------------------------------- -// 4. ExecuteApiRequestStreamingViewer – streaming endpoint example +// 1. ExecuteApiRequestViewer – configurable non-streaming request // --------------------------------------------------------------------------- -function executeApiRequestStreamingViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { - void opts; - switch (lang) { - case SupportedLanguage.JS_SDK: - return `// Use executeStreamedApiRequest instead of executeApiRequest -const stream = fgaClient.executeStreamedApiRequest({ - operationName: "StreamedListObjects", - method: "POST", - path: "/stores/{store_id}/streamed-list-objects", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { - type: "document", - relation: "viewer", - user: "user:anne", - authorization_model_id: process.env.FGA_MODEL_ID, - }, -}); - -for await (const chunk of stream) { - if (chunk.result) { - console.log("Object:", chunk.result.object); // e.g. "document:roadmap" - } -}`; - case SupportedLanguage.GO_SDK: - return `import ( - "encoding/json" - "fmt" - "log" - "net/http" - "os" - - openfga "github.com/openfga/go-sdk" -) +interface ExecuteApiRequestViewerOpts { + /** Name used for telemetry / logging. */ + operationName: string; + /** HTTP method, e.g. "POST", "GET". */ + method: string; + /** URL path template, e.g. "/stores/{store_id}/check". */ + path: string; + /** Path parameter substitutions. */ + pathParams?: Record; + /** Request body (omit for GET). */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body?: Record; + /** Query parameter key-value pairs. */ + queryParams?: Record; + /** Optional example response shown in a trailing comment. */ + responseExample?: string; -storeID := os.Getenv("FGA_STORE_ID") -modelID := os.Getenv("FGA_MODEL_ID") + skipSetup?: boolean; + allowedLanguages?: SupportedLanguage[]; +} -// Use ExecuteStreaming which returns an APIExecutorStreamingChannel -// with Results and Errors channels -request := openfga.NewAPIExecutorRequestBuilder( - "StreamedListObjects", http.MethodPost, "/stores/{store_id}/streamed-list-objects", -). - WithPathParameter("store_id", storeID). - WithBody(openfga.ListObjectsRequest{ - AuthorizationModelId: openfga.PtrString(modelID), - Type: "document", - Relation: "viewer", - User: "user:alice", - }). - Build() +function executeApiRequestViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { + const { operationName, method, path, pathParams, body, queryParams, responseExample } = opts; -channel, err := executor.ExecuteStreaming(ctx, request, openfga.DefaultStreamBufferSize) -if err != nil { - log.Fatalf("Streaming request failed: %v", err) -} -defer channel.Close() + switch (lang) { + case SupportedLanguage.JS_SDK: { + const parts: string[] = []; + parts.push(` operationName: "${operationName}",`); + parts.push(` method: "${method}",`); + parts.push(` path: "${path}",`); + if (pathParams && Object.keys(pathParams).length > 0) { + const entries = Object.entries(pathParams) + .map(([k, v]) => `${k}: "${v}"`) + .join(', '); + parts.push(` pathParams: { ${entries} },`); + } + if (body) { + parts.push(` body: ${jsValue(body, 4)},`); + } + if (queryParams && Object.keys(queryParams).length > 0) { + const entries = Object.entries(queryParams) + .map(([k, v]) => `${k}: "${v}"`) + .join(', '); + parts.push(` queryParams: { ${entries} },`); + } + return `const response = await fgaClient.executeApiRequest({ +${parts.join('\n')} +});${responseExample ? `\n\n// response: ${responseExample}` : ''}`; + } -for { - select { - case result, ok := <-channel.Results: - if !ok { - // Stream completed — check for final errors - select { - case err := <-channel.Errors: - if err != nil { - log.Fatalf("Stream error: %v", err) - } - default: - } - fmt.Println("Stream completed successfully") - return + case SupportedLanguage.GO_SDK: { + let code = ''; + if (body) { + code += `requestBody := ${goValue(body, 8)}\n\n`; + } + code += `request := openfga.NewAPIExecutorRequestBuilder(\n`; + code += ` "${operationName}", ${goMethod(method)}, "${path}",\n)`; + if (pathParams) { + for (const [k, v] of Object.entries(pathParams)) { + code += `.\n WithPathParameter("${k}", "${v}")`; } - - var response openfga.StreamedListObjectsResponse - if err := json.Unmarshal(result, &response); err != nil { - log.Fatalf("Failed to decode: %v", err) + } + if (queryParams) { + for (const [k, v] of Object.entries(queryParams)) { + code += `.\n WithQueryParameter("${k}", "${v}")`; } - fmt.Printf("Object: %s\\n", response.Object) + } + if (body) { + code += `.\n WithBody(requestBody)`; + } + code += `.\n Build()\n\n`; + code += `rawResponse, err := executor.Execute(ctx, request)\nif err != nil {\n log.Fatalf("Request failed: %v", err)\n}\n\n`; + code += `var result map[string]interface{}\njson.Unmarshal(rawResponse.Body, &result)`; + if (responseExample) { + code += `\n\n// result: ${responseExample}`; + } + return code; + } - case err := <-channel.Errors: - if err != nil { - log.Fatalf("Stream error: %v", err) + case SupportedLanguage.DOTNET_SDK: { + let code = `var request = RequestBuilder\n`; + code += ` .Create(${csharpMethod(method)}, configuration.ApiUrl, "${path}")`; + if (pathParams) { + for (const [k, v] of Object.entries(pathParams)) { + code += `\n .WithPathParameter("${k}", "${v}")`; + } + } + if (queryParams) { + for (const [k, v] of Object.entries(queryParams)) { + code += `\n .WithQueryParameter("${k}", "${v}")`; } + } + if (body) { + code += `\n .WithBody(${csharpValue(body, 8)})`; + } + code += `;\n\nvar response = await executor.ExecuteAsync>(\n`; + code += ` request, "${operationName}"\n);`; + if (responseExample) { + code += `\n\n// response.Data: ${responseExample}`; + } + return code; } -}`; - case SupportedLanguage.DOTNET_SDK: - return `using OpenFga.Sdk.Client; -using OpenFga.Sdk.ApiClient; - -var storeId = configuration.StoreId; -var authorizationModelId = Environment.GetEnvironmentVariable("FGA_MODEL_ID"); - -// Use ExecuteStreamingAsync which returns an IAsyncEnumerable -var request = RequestBuilder - .Create(HttpMethod.Post, configuration.ApiUrl, - "/stores/{store_id}/streamed-list-objects") - .WithPathParameter("store_id", storeId) - .WithBody(new { - user = "user:anne", - relation = "can_read", - type = "document", - authorization_model_id = authorizationModelId - }); -await foreach (var item in executor.ExecuteStreamingAsync( - request, "StreamedListObjects")) -{ - Console.WriteLine($"Object: {item.Object}"); -}`; - case SupportedLanguage.PYTHON_SDK: - return `# Use execute_streamed_api_request which returns an AsyncIterator -# (or Iterator in the sync client) that yields one parsed JSON object per chunk -async for chunk in fga_client.execute_streamed_api_request( - operation_name="StreamedListObjects", - method="POST", - path="/stores/{store_id}/streamed-list-objects", - path_params={"store_id": FGA_STORE_ID}, - body={ - "type": "document", - "relation": "viewer", - "user": "user:anne", - "authorization_model_id": FGA_MODEL_ID, - }, -): - # Each chunk has the shape {"result": {"object": "..."}} or {"error": {...}} - if "result" in chunk: - print(chunk["result"]["object"]) # e.g. "document:roadmap"`; - case SupportedLanguage.JAVA_SDK: - return `import dev.openfga.sdk.api.client.ApiExecutorRequestBuilder; -import dev.openfga.sdk.api.model.ListObjectsRequest; -import dev.openfga.sdk.api.model.StreamedListObjectsResponse; -import dev.openfga.sdk.api.client.HttpMethod; + case SupportedLanguage.PYTHON_SDK: { + const parts: string[] = []; + parts.push(` operation_name="${operationName}",`); + parts.push(` method="${method}",`); + parts.push(` path="${path}",`); + if (pathParams && Object.keys(pathParams).length > 0) { + const entries = Object.entries(pathParams) + .map(([k, v]) => `"${k}": "${v}"`) + .join(', '); + parts.push(` path_params={${entries}},`); + } + if (body) { + parts.push(` body=${pyValue(body, 8)},`); + } + if (queryParams && Object.keys(queryParams).length > 0) { + const entries = Object.entries(queryParams) + .map(([k, v]) => `"${k}": "${v}"`) + .join(', '); + parts.push(` query_params={${entries}},`); + } + let code = `response = await fga_client.execute_api_request(\n${parts.join('\n')}\n)\n\nresult = response.json()`; + if (responseExample) { + code += `\n\n# result: ${responseExample}`; + } + return code; + } -// Use streamingApiExecutor which delivers each response object -// to a consumer callback as it arrives -var request = ApiExecutorRequestBuilder.builder( - HttpMethod.POST, "/stores/{store_id}/streamed-list-objects" -) - .body(new ListObjectsRequest() - .user("user:anne") - .relation("viewer") - .type("document")) - .build(); + case SupportedLanguage.JAVA_SDK: { + let code = ''; + if (body) { + code += `Map requestBody = ${javaValue(body, 8)};\n\n`; + } + code += `var request = ApiExecutorRequestBuilder.builder(\n`; + code += ` ${javaMethod(method)}, "${path}"\n)`; + if (pathParams) { + for (const [k, v] of Object.entries(pathParams)) { + code += `\n .pathParam("${k}", "${v}")`; + } + } + if (queryParams) { + for (const [k, v] of Object.entries(queryParams)) { + code += `\n .queryParam("${k}", "${v}")`; + } + } + if (body) { + code += `\n .body(requestBody)`; + } + code += `\n .build();\n\n`; + code += `var rawResponse = fgaClient.apiExecutor().send(request).get();`; + if (responseExample) { + code += `\n\n// rawResponse.getData(): ${responseExample}`; + } + return code; + } -fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class) - .stream( - request, - response -> System.out.println("Object: " + response.getObject()), - error -> System.err.println("Stream error: " + error.getMessage()) - ) - .thenRun(() -> System.out.println("Streaming complete")) - .exceptionally(err -> { - System.err.println("Fatal error: " + err.getMessage()); - return null; - });`; case SupportedLanguage.CLI: case SupportedLanguage.CURL: case SupportedLanguage.RPC: @@ -572,7 +278,7 @@ fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class) } } -export function ExecuteApiRequestStreamingViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { +export function ExecuteApiRequestViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { const defaultLangs = [ SupportedLanguage.JS_SDK, SupportedLanguage.GO_SDK, @@ -581,98 +287,141 @@ export function ExecuteApiRequestStreamingViewer(opts: ExecuteApiRequestViewerOp SupportedLanguage.JAVA_SDK, ]; const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestStreamingViewer); + return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestViewer); } // --------------------------------------------------------------------------- -// 5. ExecuteApiRequestErrorViewer – error handling example +// 2. ExecuteApiRequestStreamingViewer – configurable streaming request // --------------------------------------------------------------------------- -function executeApiRequestErrorViewer(lang: SupportedLanguage, opts: ExecuteApiRequestViewerOpts): string { - void opts; +interface ExecuteApiRequestStreamingViewerOpts { + /** Name used for telemetry / logging. */ + operationName: string; + /** URL path template, e.g. "/stores/{store_id}/streamed-list-objects". */ + path: string; + /** Path parameter substitutions. */ + pathParams?: Record; + /** Request body. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body: Record; + /** Field name to extract from each streamed result, e.g. "object". */ + responseField?: string; + + skipSetup?: boolean; + allowedLanguages?: SupportedLanguage[]; +} + +function executeApiRequestStreamingViewer(lang: SupportedLanguage, opts: ExecuteApiRequestStreamingViewerOpts): string { + const { operationName, path, pathParams, body, responseField } = opts; + const field = responseField || 'object'; + switch (lang) { - case SupportedLanguage.JS_SDK: - return `try { - const response = await fgaClient.executeApiRequest({ - operationName: "CustomEndpoint", - method: "POST", - path: "/stores/{store_id}/custom-endpoint", - pathParams: { store_id: process.env.FGA_STORE_ID }, - body: { user: "user:bob" }, - }); - console.log("Success:", response); -} catch (error) { - console.error("Request failed:", error.message); + case SupportedLanguage.JS_SDK: { + const parts: string[] = []; + parts.push(` operationName: "${operationName}",`); + parts.push(` method: "POST",`); + parts.push(` path: "${path}",`); + if (pathParams && Object.keys(pathParams).length > 0) { + const entries = Object.entries(pathParams) + .map(([k, v]) => `${k}: "${v}"`) + .join(', '); + parts.push(` pathParams: { ${entries} },`); + } + parts.push(` body: ${jsValue(body, 4)},`); + return `for await (const chunk of fgaClient.executeStreamedApiRequest({ +${parts.join('\n')} +})) { + if (chunk.result) { + console.log(chunk.result.${field}); + } }`; - case SupportedLanguage.GO_SDK: - return `rawResponse, err := executor.Execute(ctx, request) -if err != nil { - log.Fatalf("Request failed: %v", err) -} + } -fmt.Printf("Status Code: %d\\n", rawResponse.StatusCode) -fmt.Printf("Headers: %+v\\n", rawResponse.Headers)`; - case SupportedLanguage.DOTNET_SDK: - return `var response = await executor.ExecuteAsync( - request, "GetStore" -); + case SupportedLanguage.GO_SDK: { + let code = `requestBody := ${goValue(body, 8)}\n\n`; + code += `request := openfga.NewAPIExecutorRequestBuilder(\n`; + code += ` "${operationName}", http.MethodPost, "${path}",\n)`; + if (pathParams) { + for (const [k, v] of Object.entries(pathParams)) { + code += `.\n WithPathParameter("${k}", "${v}")`; + } + } + code += `.\n WithBody(requestBody).\n Build()\n\n`; + code += `channel, err := executor.ExecuteStreaming(ctx, request, openfga.DefaultStreamBufferSize)\n`; + code += `if err != nil {\n log.Fatalf("Streaming request failed: %v", err)\n}\ndefer channel.Close()\n\n`; + code += `for {\n select {\n case result, ok := <-channel.Results:\n if !ok {\n return\n }\n`; + code += ` var obj map[string]interface{}\n json.Unmarshal(result, &obj)\n`; + code += ` fmt.Println(obj["${field}"])\n`; + code += ` case err := <-channel.Errors:\n if err != nil {\n log.Fatalf("Stream error: %v", err)\n }\n }\n}`; + return code; + } -if (!response.IsSuccessful) { - switch ((int)response.StatusCode) { - case 404: - Console.WriteLine("Not found"); - break; - case 401: - Console.WriteLine("Unauthorized — check your credentials"); - break; - case 429: - Console.WriteLine("Rate limited — retry after delay"); - break; - case >= 500: - Console.WriteLine($"Server error: {response.RawResponse}"); - break; - default: - Console.WriteLine($"Request failed: {response.StatusCode}"); - break; + case SupportedLanguage.DOTNET_SDK: { + let code = `var request = RequestBuilder\n`; + code += ` .Create(HttpMethod.Post, configuration.ApiUrl, "${path}")`; + if (pathParams) { + for (const [k, v] of Object.entries(pathParams)) { + code += `\n .WithPathParameter("${k}", "${v}")`; + } + } + code += `\n .WithBody(${csharpValue(body, 8)});\n\n`; + code += `await foreach (var item in executor.ExecuteStreamingAsync(\n`; + code += ` request, "${operationName}"))\n{\n`; + code += ` Console.WriteLine(item.${field.charAt(0).toUpperCase() + field.slice(1)});`; + code += `\n}`; + return code; } - return; -} -Console.WriteLine($"Store Name: {response.Data.Name}");`; - case SupportedLanguage.PYTHON_SDK: - return `response = await fga_client.execute_api_request( - operation_name="CustomEndpoint", - method="POST", - path="/stores/{store_id}/custom-endpoint", - path_params={"store_id": FGA_STORE_ID}, - body={"user": "user:bob"}, -) + case SupportedLanguage.PYTHON_SDK: { + const parts: string[] = []; + parts.push(` operation_name="${operationName}",`); + parts.push(` method="POST",`); + parts.push(` path="${path}",`); + if (pathParams && Object.keys(pathParams).length > 0) { + const entries = Object.entries(pathParams) + .map(([k, v]) => `"${k}": "${v}"`) + .join(', '); + parts.push(` path_params={${entries}},`); + } + parts.push(` body=${pyValue(body, 8)},`); + return `async for chunk in fga_client.execute_streamed_api_request( +${parts.join('\n')} +): + if "result" in chunk: + print(chunk["result"]["${field}"])`; + } + + case SupportedLanguage.JAVA_SDK: { + let code = ''; + code += `Map requestBody = ${javaValue(body, 8)};\n\n`; + code += `var request = ApiExecutorRequestBuilder.builder(\n`; + code += ` HttpMethod.POST, "${path}"\n)`; + if (pathParams) { + for (const [k, v] of Object.entries(pathParams)) { + code += `\n .pathParam("${k}", "${v}")`; + } + } + code += `\n .body(requestBody)\n .build();\n\n`; + code += `fgaClient.streamingApiExecutor(StreamedListObjectsResponse.class)\n`; + code += ` .stream(\n`; + code += ` request,\n`; + code += ` response -> System.out.println(response.get${field.charAt(0).toUpperCase() + field.slice(1)}()),\n`; + code += ` error -> System.err.println("Stream error: " + error.getMessage())\n`; + code += ` )\n .get();`; + return code; + } -if response.status == 200: - result = response.json() - print(f"Response: {result}") -else: - print(f"Request failed with status: {response.status}")`; - case SupportedLanguage.JAVA_SDK: - return `try { - var rawResponse = fgaClient.apiExecutor().send(request).get(); - System.out.println("Status Code: " + rawResponse.getStatusCode()); - System.out.println("Response: " + rawResponse.getData()); -} catch (Exception e) { - System.err.println("Request failed: " + e.getMessage()); - System.err.println("Cause: " + e.getCause().getClass().getSimpleName()); -}`; case SupportedLanguage.CLI: case SupportedLanguage.CURL: case SupportedLanguage.RPC: case SupportedLanguage.PLAYGROUND: - return `# API Executor is only available through the SDKs`; + return `# API Executor streaming is only available through the SDKs`; default: assertNever(lang); } } -export function ExecuteApiRequestErrorViewer(opts: ExecuteApiRequestViewerOpts): JSX.Element { +export function ExecuteApiRequestStreamingViewer(opts: ExecuteApiRequestStreamingViewerOpts): JSX.Element { const defaultLangs = [ SupportedLanguage.JS_SDK, SupportedLanguage.GO_SDK, @@ -681,5 +430,9 @@ export function ExecuteApiRequestErrorViewer(opts: ExecuteApiRequestViewerOpts): SupportedLanguage.JAVA_SDK, ]; const allowedLanguages = getFilteredAllowedLangs(opts.allowedLanguages, defaultLangs); - return defaultOperationsViewer(allowedLanguages, opts, executeApiRequestErrorViewer); + return defaultOperationsViewer( + allowedLanguages, + opts, + executeApiRequestStreamingViewer, + ); }