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