<!--
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