Packages
ash_typescript
0.16.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/guides/querying-data.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
-->
# Querying Data
This guide covers pagination, sorting, and filtering when working with AshTypescript RPC actions.
## Pagination
AshTypescript supports both offset-based and keyset (cursor-based) pagination.
### Offset-based Pagination
Use offset and limit for traditional page-based pagination:
```typescript
import { listTodos } from './ash_rpc';
// First page
const page1 = await listTodos({
fields: ["id", "title", "completed"],
page: { offset: 0, limit: 20 }
});
if (page1.success) {
console.log("Total items:", page1.data.count);
console.log("Items:", page1.data.results);
console.log("Has more:", page1.data.hasMore);
}
// Second page
const page2 = await listTodos({
fields: ["id", "title", "completed"],
page: { offset: 20, limit: 20 }
});
```
**Response includes:**
- `results`: Array of items for the current page
- `count`: Total number of items
- `hasMore`: Boolean indicating if more results exist
### Keyset (Cursor-based) Pagination
For better performance with large datasets:
```typescript
// First page
const page1 = await listTodos({
fields: ["id", "title", "completed"],
page: { limit: 20 }
});
if (page1.success && page1.data.hasMore) {
// Next page using 'after' cursor
const page2 = await listTodos({
fields: ["id", "title", "completed"],
page: { after: page1.data.nextPage, limit: 20 }
});
}
```
**Response includes:**
- `results`: Array of items
- `previousPage`: Cursor for backwards pagination
- `nextPage`: Cursor for forwards pagination
- `hasMore`: Boolean indicating if more results exist
### When to Use Each Type
| Pagination Type | Use When | Advantages |
|----------------|----------|------------|
| **Offset** | Small/medium datasets, page numbers needed | Simple, direct page access |
| **Keyset** | Large datasets, infinite scroll | Consistent performance, no skipped items |
### Optional vs Required Pagination
Actions can have **required** or **optional** pagination:
```typescript
// Optional pagination - return type changes based on usage
const simpleResult = await listTodos({
fields: ["id", "title"]
// No page parameter - returns simple array
});
const paginatedResult = await listTodos({
fields: ["id", "title"],
page: { offset: 0, limit: 20 }
// With page parameter - returns paginated response
});
```
TypeScript automatically infers the correct return type.
## Sorting
Sort results using a comma-separated string with direction prefixes.
### Basic Sorting
```typescript
// Sort by priority descending
const byPriority = await listTodos({
fields: ["id", "title", "priority"],
sort: "-priority"
});
// Sort by created date ascending
const byDate = await listTodos({
fields: ["id", "title", "createdAt"],
sort: "+createdAt"
});
```
**Sort syntax:**
- `+` prefix: ascending order (default)
- `-` prefix: descending order
### Multiple Sort Fields
```typescript
// Sort by priority (desc), then by title (asc)
const sorted = await listTodos({
fields: ["id", "title", "priority"],
sort: "-priority,+title"
});
```
### Disabling Client-Side Sorting
Use `enable_sort?: false` when the server should control ordering:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Standard action with sorting
rpc_action :list_todos, :read
# Server-controlled order - no client sorting
rpc_action :list_ranked_todos, :read, enable_sort?: false
end
end
```
When disabled:
- The `sort` parameter is **not included** in TypeScript types
- Any sort sent by client is **silently ignored**
- Filtering and pagination remain available
```typescript
// With enable_sort?: false
const rankedTodos = await listRankedTodos({
fields: ["id", "title", "rank"],
filter: { status: { eq: "active" } }, // Still available
page: { limit: 20 } // Still available
// sort: "-rank" // Not available in types
});
```
## Filtering
Filter results using type-safe filter objects.
### Basic Filters
```typescript
// Filter by completed status
const completedTodos = await listTodos({
fields: ["id", "title", "completed"],
filter: { completed: { eq: true } }
});
// Filter using "in" operator
const highPriorityTodos = await listTodos({
fields: ["id", "title", "priority"],
filter: { priority: { in: ["high", "urgent"] } }
});
```
### Comparison Operators
```typescript
// Find overdue tasks
const overdueTodos = await listTodos({
fields: ["id", "title", "dueDate"],
filter: {
dueDate: { lessThan: new Date().toISOString() }
}
});
```
**Available operators:**
- `eq`, `notEq`: Equals, not equals
- `in`: Value in array
- `greaterThan`, `greaterThanOrEqual`: Greater than (numbers, dates)
- `lessThan`, `lessThanOrEqual`: Less than (numbers, dates)
### Logical Operators
```typescript
// AND: High priority AND not completed
const activePriority = await listTodos({
fields: ["id", "title"],
filter: {
and: [
{ priority: { in: ["high", "urgent"] } },
{ completed: { eq: false } }
]
}
});
// OR: Completed OR high priority
const completedOrPriority = await listTodos({
fields: ["id", "title"],
filter: {
or: [
{ completed: { eq: true } },
{ priority: { eq: "high" } }
]
}
});
// NOT: Exclude completed
const incomplete = await listTodos({
fields: ["id", "title"],
filter: {
not: [{ completed: { eq: true } }]
}
});
```
### Filtering on Relationships
```typescript
// Filter by related user's name
const johnsTodos = await listTodos({
fields: ["id", "title", { user: ["name"] }],
filter: {
user: { name: { eq: "John Doe" } }
}
});
```
### Disabling Client-Side Filtering
Use `enable_filter?: false` when filtering should be server-controlled:
```elixir
typescript_rpc do
resource MyApp.Todo do
# Standard action with filtering
rpc_action :list_todos, :read
# Server applies filtering via action arguments
rpc_action :list_recent_todos, :list_recent, enable_filter?: false
end
end
```
When disabled:
- 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 - use action arguments instead
const recentTodos = await listRecentTodos({
fields: ["id", "title"],
input: { daysBack: 14 }, // Server-side filtering via argument
sort: "-createdAt" // Sorting still available
});
```
### Disabling Both Sorting and Filtering
```elixir
# Curated list with server-controlled order and filtering
rpc_action :list_curated_todos, :read,
enable_filter?: false,
enable_sort?: false
```
## Combining All Features
```typescript
const result = await listTodos({
fields: ["id", "title", "priority", "dueDate", "completed"],
filter: {
and: [
{ completed: { eq: false } },
{ priority: { in: ["high", "urgent"] } }
]
},
sort: "-priority,+dueDate",
page: { offset: 0, limit: 20 }
});
if (result.success) {
console.log(`Showing ${result.data.results.length} of ${result.data.count}`);
}
```
## Custom Filtering with Action Arguments
For advanced filtering (text search, pattern matching), use action arguments:
```elixir
# In your Ash resource
read :read do
argument :search, :string, allow_nil?: true
prepare fn query, _context ->
case Ash.Query.get_argument(query, :search) do
nil -> query
term -> Ash.Query.filter(query, contains(name, ^term) or contains(email, ^term))
end
end
end
```
```typescript
// Use action argument for text search
const results = await listUsers({
fields: ["id", "name", "email"],
input: { search: "john" },
filter: { active: { eq: true } } // Combine with standard filters
});
```
## Type Safety
All filter operators are fully type-safe:
```typescript
const result = await listTodos({
fields: ["id", "title"],
filter: {
priority: { eq: "invalid" } // TypeScript error if not valid enum value
}
});
```
## Next Steps
- [Field Selection](field-selection.md) - Advanced field selection patterns
- [Typed Queries](typed-queries.md) - Predefined queries for SSR
- [RPC Action Options](../features/rpc-action-options.md) - Configure action behavior
- [Error Handling](error-handling.md) - Handle query errors