Packages
ash_typescript
0.17.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.0
0.15.3
0.15.2
retired
0.15.1
retired
0.15.0
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
retired
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.2
0.10.1
0.10.0
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.1
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.0
0.3.3
0.3.2
0.3.1
0.2.0
0.1.2
0.1.0
Generate type-safe TypeScript clients directly from your Ash resources and actions, ensuring end-to-end type safety between your backend and frontend.
Current section
Files
Jump to
Current section
Files
documentation/features/rpc-action-options.md
<!--
SPDX-FileCopyrightText: 2025 Torkild G. Kjevik
SPDX-FileCopyrightText: 2025 ash_typescript contributors <https://github.com/ash-project/ash_typescript/graphs/contributors>
SPDX-License-Identifier: MIT
-->
# RPC Action Options
This guide covers all configuration options available for `rpc_action` declarations, including load restrictions, query controls, identity lookups, and more.
## Overview
Each `rpc_action` accepts two required arguments and optional configuration:
```elixir
rpc_action :function_name, :ash_action_name, options
```
| Argument | Description |
|----------|-------------|
| First | Name of the generated TypeScript function |
| Second | Name of the Ash action to execute |
| Options | Keyword list of configuration options |
## Load Restrictions
Control which relationships and calculations clients can request using `allowed_loads` and `denied_loads`.
### allowed_loads (Whitelist)
Only allow loading specific fields:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Only user and tags can be loaded
rpc_action :list_todos, :read, allowed_loads: [:user, :tags]
end
end
```
```typescript
// Allowed
const result = await listTodos({
fields: ["id", "title", { user: ["name"], tags: ["name"] }]
});
// Error: "comments" not in allowed_loads
const result = await listTodos({
fields: ["id", "title", { comments: ["text"] }]
});
```
### denied_loads (Blacklist)
Block specific fields while allowing all others:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Everything except internal_notes can be loaded
rpc_action :list_todos, :read, denied_loads: [:internal_notes, :audit_log]
end
end
```
```typescript
// Allowed (user is not denied)
const result = await listTodos({
fields: ["id", "title", { user: ["name"] }]
});
// Error: "internal_notes" is denied
const result = await listTodos({
fields: ["id", "title", { internal_notes: ["content"] }]
});
```
### Nested Load Restrictions
Restrict nested relationships using keyword list syntax:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Allow user, but only allow loading user's public_profile
rpc_action :list_todos, :read,
allowed_loads: [
:tags,
user: [:public_profile]
]
end
end
```
```typescript
// Allowed
const result = await listTodos({
fields: [
"id",
{ user: ["name", { public_profile: ["bio"] }] }
]
});
// Error: user.private_settings not allowed
const result = await listTodos({
fields: [
"id",
{ user: ["name", { private_settings: ["data"] }] }
]
});
```
### TypeScript Type Generation
Load restrictions affect the generated TypeScript types. With `allowed_loads`, only the allowed fields appear in the field selection types:
```elixir
rpc_action :list_todos, :read, allowed_loads: [:user]
```
```typescript
// Generated type only includes "user" as a loadable field
// "comments", "tags", etc. are not available in autocomplete
const result = await listTodos({
fields: ["id", "title", { user: ["name"] }] // Only user is available
});
```
### Error Responses
When a client requests a restricted field:
```typescript
// With allowed_loads: [:user]
const result = await listTodos({
fields: ["id", { comments: ["text"] }] // "comments" not allowed
});
// Returns:
// {
// success: false,
// errors: [{
// type: "load_not_allowed",
// message: "Field 'comments' is not in the allowed loads list",
// fields: ["comments"]
// }]
// }
```
```typescript
// With denied_loads: [:internal_notes]
const result = await listTodos({
fields: ["id", { internal_notes: ["content"] }]
});
// Returns:
// {
// success: false,
// errors: [{
// type: "load_denied",
// message: "Field 'internal_notes' is denied",
// fields: ["internal_notes"]
// }]
// }
```
### When to Use Each
| Option | Use When |
|--------|----------|
| `allowed_loads` | You want explicit control over a small set of loadable fields |
| `denied_loads` | You want to block a few sensitive fields while allowing most |
**Best practice**: Use `allowed_loads` for security-sensitive endpoints where you want explicit control. Use `denied_loads` when most fields are safe and you only need to block a few.
## Query Controls
### enable_filter?
Disable client-side filtering:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Standard action with filtering
rpc_action :list_todos, :read
# Server controls filtering via action arguments
rpc_action :list_recent_todos, :list_recent, enable_filter?: false
end
end
```
When `enable_filter?: false`:
- The `filter` parameter is **not included** in TypeScript types
- Filter types for this action are **not generated**
- Any filter sent by client is **silently ignored**
```typescript
// With enable_filter?: false
const result = await listRecentTodos({
fields: ["id", "title"],
input: { daysBack: 7 } // Use action arguments for filtering
// filter: { ... } // Not available in types
});
```
### enable_sort?
Disable client-side sorting:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Standard action with sorting
rpc_action :list_todos, :read
# Server controls ordering
rpc_action :list_ranked_todos, :read, enable_sort?: false
end
end
```
When `enable_sort?: false`:
- The `sort` parameter is **not included** in TypeScript types
- Any sort sent by client is **silently ignored**
```typescript
// With enable_sort?: false
const result = await listRankedTodos({
fields: ["id", "title", "rank"]
// sort: "-rank" // Not available in types
});
```
### Combining Controls
```elixir
# Fully server-controlled action
rpc_action :list_curated_todos, :read,
enable_filter?: false,
enable_sort?: false,
allowed_loads: [:user]
```
## Get Actions
### get?
Constrain a read action to return a single record:
```elixir
typescript_rpc do
resource MyApp.User do
rpc_action :get_current_user, :read, get?: true
end
end
```
Uses `Ash.read_one` instead of `Ash.read`, returning a single record or error.
### get_by
Look up a single record by specific fields:
```elixir
typescript_rpc do
resource MyApp.User do
rpc_action :get_user_by_email, :read, get_by: [:email]
end
end
```
```typescript
const result = await getUserByEmail({
getBy: { email: "user@example.com" },
fields: ["id", "name", "email"]
});
```
### not_found_error?
Control behavior when a get action finds no record:
```elixir
typescript_rpc do
resource MyApp.User do
# Returns error when not found (default)
rpc_action :get_user, :read, get_by: [:id]
# Returns null when not found
rpc_action :find_user, :read, get_by: [:email], not_found_error?: false
end
end
```
```typescript
// With not_found_error?: false
const result = await findUser({
getBy: { email: "maybe@example.com" },
fields: ["id", "name"]
});
if (result.success) {
if (result.data) {
console.log("Found:", result.data.name);
} else {
console.log("User not found"); // No error, just null
}
}
```
## Identity Lookups
Control how records are located for update and destroy actions.
### Primary Key (Default)
```elixir
rpc_action :update_user, :update
# Equivalent to: identities: [:_primary_key]
```
```typescript
await updateUser({
identity: "550e8400-e29b-41d4-a716-446655440000",
input: { name: "New Name" },
fields: ["id", "name"]
});
```
### Named Identity
First define the identity on your resource:
```elixir
defmodule MyApp.User do
use Ash.Resource
identities do
identity :unique_email, [:email]
end
end
```
Then configure the RPC action:
```elixir
rpc_action :update_user_by_email, :update, identities: [:unique_email]
```
```typescript
await updateUserByEmail({
identity: { email: "user@example.com" },
input: { name: "New Name" },
fields: ["id", "name"]
});
```
### Multiple Identities
Allow either primary key or named identity:
```elixir
rpc_action :update_user, :update, identities: [:_primary_key, :unique_email]
```
```typescript
// By primary key
await updateUser({
identity: "550e8400-e29b-41d4-a716-446655440000",
input: { name: "Via PK" },
fields: ["id"]
});
// By email
await updateUser({
identity: { email: "user@example.com" },
input: { name: "Via Email" },
fields: ["id"]
});
```
### Actor-Scoped Actions
For actions that operate on the current actor:
```elixir
# Action filters to current user
defmodule MyApp.User do
actions do
update :update_me do
change relate_actor(:id)
end
end
end
# No identity needed
rpc_action :update_me, :update_me, identities: []
```
```typescript
// No identity parameter - operates on authenticated user
await updateMe({
input: { name: "My New Name" },
fields: ["id", "name"]
});
```
### Composite Identities
Identities spanning multiple fields:
```elixir
identities do
identity :by_tenant_user, [:tenant_id, :user_id]
end
rpc_action :update_subscription, :update, identities: [:by_tenant_user]
```
```typescript
await updateSubscription({
identity: {
tenantId: "tenant-uuid",
userId: "user-uuid"
},
input: { status: "active" },
fields: ["id", "status"]
});
```
## Metadata Fields
Expose action metadata to clients:
```elixir
rpc_action :list_todos, :read, show_metadata: [:total_count, :has_more]
```
See [Action Metadata](action-metadata.md) for details.
## Quick Reference
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `allowed_loads` | `list(atom \| keyword)` | `nil` | Whitelist of loadable fields |
| `denied_loads` | `list(atom \| keyword)` | `nil` | Blacklist of loadable fields |
| `enable_filter?` | `boolean` | `true` | Enable client-side filtering |
| `enable_sort?` | `boolean` | `true` | Enable client-side sorting |
| `get?` | `boolean` | `false` | Return single record |
| `get_by` | `list(atom)` | `nil` | Fields for single-record lookup |
| `not_found_error?` | `boolean` | `true` | Error vs null on not found |
| `identities` | `list(atom)` | `[:_primary_key]` | Allowed identity lookups |
| `show_metadata` | `list(atom) \| false \| nil` | `nil` | Metadata fields to expose |
| `metadata_field_names` | `keyword` | `nil` | Metadata field name mappings |
| `namespace` | `atom \| string` | `nil` | Organize action into namespace |
| `description` | `string` | `nil` | Custom JSDoc description |
| `deprecated` | `boolean \| string` | `nil` | Mark as deprecated |
| `see` | `list(atom)` | `nil` | Related actions for @see tags |
## Next Steps
- [Querying Data](../guides/querying-data.md) - Filtering, sorting, pagination
- [CRUD Operations](../guides/crud-operations.md) - Update and destroy patterns
- [Action Metadata](action-metadata.md) - Exposing metadata to clients
- [Field Selection](../guides/field-selection.md) - Field selection patterns
- [Developer Experience](developer-experience.md) - Namespaces, JSDoc, and manifest generation