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