Packages
ash_typescript
0.17.3
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 typed sort strings with direction prefixes. AshTypescript generates per-resource sort field types, so your IDE autocompletes valid field names and catches typos at compile time.
### Type-Safe Sort Fields
For each resource, AshTypescript generates:
- A `{Resource}SortField` union type of all sortable field names
- A `{resource}SortFields` runtime const array for iteration
The `sort` parameter accepts `SortString<TodoSortField>` — a template literal type that allows bare field names or prefixed variants:
```typescript
// Sort by priority descending
const byPriority = await listTodos({
fields: ["id", "title", "priority"],
sort: "-priority" // Type-checked: "priority" must be a valid TodoSortField
});
// Sort by created date ascending
const byDate = await listTodos({
fields: ["id", "title", "createdAt"],
sort: "+createdAt" // Autocompleted by your IDE
});
// sort: "-nonExistent" // TypeScript error: not a valid TodoSortField
```
**Sort syntax:**
- `fieldName` or `+fieldName`: ascending order
- `-fieldName`: descending order
- `++fieldName`: ascending, nulls first
- `--fieldName`: descending, nulls first
### Multiple Sort Fields
Pass an array for multi-field sorting:
```typescript
// Sort by priority (desc), then by title (asc)
const sorted = await listTodos({
fields: ["id", "title", "priority"],
sort: ["-priority", "+title"] // Each element is type-checked
});
```
### 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)
- `isNil`: Check for null/nil values (boolean)
### 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 } }]
}
});
```
### Null Checks with isNil
Use `isNil` to filter for null or non-null values:
```typescript
// Find todos without a due date
const noDueDate = await listTodos({
fields: ["id", "title"],
filter: { dueDate: { isNil: true } }
});
// Find todos that have a due date set
const hasDueDate = await listTodos({
fields: ["id", "title", "dueDate"],
filter: { dueDate: { isNil: false } }
});
```
The `isNil` operator is available on nullable fields and accepts a boolean value.
### Filtering on Aggregates
Aggregates (count, sum, avg, etc.) are filterable just like regular fields:
```typescript
// Find todos with highly-rated comments
const popularTodos = await listTodos({
fields: ["id", "title"],
filter: {
commentCount: { greaterThan: 10 }
}
});
```
### 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
### Filter Field Arrays
For each resource, AshTypescript generates runtime arrays and union types of all filterable field names:
```typescript
import type { TodoFilterField } from './ash_types';
import { todoFilterFields } from './ash_types';
// Runtime array for building dynamic filter UIs
todoFilterFields.forEach(field => {
console.log(`Can filter by: ${field}`);
});
// Type-safe field reference
const field: TodoFilterField = "priority"; // Autocompleted by IDE
```
These include attributes, relationships, and aggregates that are filterable on the resource.
### Filter Operators
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