# Querying data

> The pagination, filtering and sorting patterns every list query in the Tidal Control API shares. Learn them once, plus counting records and handling errors.

Source: https://tidalcontrol.com/docs/api/querying-data

All list queries in the Tidal Control API follow the same patterns for pagination, filtering, and sorting. Learn them once and they apply everywhere.

## Request structure

Every GraphQL request is a `POST` to `https://portal.tidalcontrol.com/graphql` with a JSON body:

```json
{
  "query": "...",
  "variables": { ... }
}
```

Always use `variables` instead of inlining values into the query string. It keeps queries readable and avoids injection issues.

## Pagination

All `*_paged` queries use **cursor-based pagination** following the Relay Connection spec. Request a page with `first` (page size) and `after` (cursor from the previous page):

```graphql
query ExportRisks($first: Int, $after: String) {
  risks_paged(first: $first, after: $after) {
    edges {
      node {
        id
        name
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

Variables:

```json
{ "first": 50, "after": null }
```

When `pageInfo.hasNextPage` is `true`, pass `pageInfo.endCursor` as `after` in the next request.

### Fetching all pages (Python)

```python
def fetch_all(graphql_fn, query, page_size=50):
    """Fetch all pages of a paged query. graphql_fn(query, variables) must return data."""
    results = []
    cursor = None

    while True:
        data = graphql_fn(query, {"first": page_size, "after": cursor})
        # Extract the first (and only) key from the response
        key = next(iter(data))
        page = data[key]
        results.extend(edge["node"] for edge in page["edges"])

        if not page["pageInfo"]["hasNextPage"]:
            break
        cursor = page["pageInfo"]["endCursor"]

    return results
```

> **tip:**

A page size of `50` is a good default. You can go higher, but very large pages may increase response time.

## Filtering

Pass a `filter` argument to narrow results. Each entity has its own filter type with relevant fields. All filter fields are optional. Omit any field you don't need.

This example fetches only open, high-priority issues:

```graphql
query FilteredIssues($filter: IssueFilter) {
  issues_paged(first: 50, filter: $filter) {
    edges {
      node {
        id
        name
        priority
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

Variables:

```json
{
  "filter": {
    "closed": false,
    "priority": ["HIGH"]
  }
}
```

### Common filter fields

A handful of fields work the same way across almost every entity.

**Available on most entities:**

- `archived: Boolean`: include or exclude archived records
- `assignees: [UUID!]`: filter by assigned user IDs
- `search: String`: free-text search on name/description

**Activities (issues, tasks):**

- `closed: Boolean`: open or closed activities
- `controls: [UUID!]`: activities linked to specific controls
- `assets: [UUID!]`: activities linked to specific assets
- `status: [ActivityDueStatus!]`: `NOT_DUE_SOON`, `DUE_SOON`, `OVERDUE`, `NOT_SET`

**Issues only:**

- `priority: [Priority!]`: `LOW`, `MEDIUM`, `HIGH`
- `type: [IssueType!]`: e.g. `AUDIT_FINDING`, `INCIDENT`, `CONTROL_GAP`

**Risks and controls:**

- `frameworks: [UUID!]`: linked to specific frameworks
- `references: [UUID!]`: linked to specific framework references

## Sorting

Pass a `sort` array to control the order of results. Each sort item specifies a `field` name and an optional `direction` (`ASC` or `DESC`):

```graphql
query SortedRisks {
  risks_paged(
    first: 50,
    sort: [{ field: "sequenceId", direction: ASC }]
  ) {
    edges {
      node {
        sequenceId
        name
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

You can pass multiple sort fields. Common sortable fields include `name`, `sequenceId`, `createdDate`, `notBefore`, and `expires`.

## Counting records

Use `*_count` queries to get a total count without fetching all data:

```graphql
query {
  risks_count
  controls_count(filter: { archived: false })
  issues_count(filter: { closed: false, priority: ["HIGH"] })
}
```

## Error handling

A successful response always has HTTP status `200`, even when the GraphQL query itself has errors. Check the `errors` array in the response body:

```json
{
  "data": null,
  "errors": [
    {
      "message": "Unauthorized",
      "locations": [{ "line": 1, "column": 1 }]
    }
  ]
}
```

Common errors:

- **Unauthorized**: your token is missing or expired; re-authenticate
- **Field does not exist**: check the field name against the schema
- **Variable … expected type …**: a filter variable has the wrong type
