# 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