# Lists, filters and ordering

Every collection reads the same way. Four parameters — `page_size`, `page_token`,
`order_by`, `filter` — and one envelope, whose array is named after the collection it came
from.

```sh title="quickstart.sh"
curl_json -G "${auth[@]}" "$site/cms_pages" \
  --data-urlencode 'page_size=5' \
  --data-urlencode 'order_by=create_time desc' \
  --data-urlencode 'filter=is_active = true AND title:"Hello"' |
  jq '{total_size, next_page_token, titles: [.cms_pages[].title]}'
```

Two things worth knowing before you write the query. **Which fields you may filter and
sort by is per resource**, and the reference page for each one lists them: they are the
fields with an index behind them, so a query cannot quietly table-scan the platform. And
**an unknown field is refused**, not ignored — a typo comes back as an
[`INVALID_ARGUMENT`](/concepts/errors) naming the field, rather than as a full unfiltered
page that looks like an answer.

For paging, `page_token` is the path to take: it survives rows being inserted while you
walk. `skip` exists for a grid that has to jump straight to page 40, and it is capped.

## 8. Lists

### Response shape

```json
{
  "cms_pages": [ { "name": "websites/6a5e.../cms_pages/507f...", "...": "..." } ],
  "next_page_token": "eyJvIjoxMDB9",
  "total_size": 137
}
```

- The array field is named **exactly the collection id** (AIP-132). Clients
  derive it from the last path segment; they do not need a per-endpoint map.
- `next_page_token` is an **opaque** string. Empty or absent means "no more".
  Clients MUST NOT parse it; servers MAY encode an offset, a sort key, or a
  Typesense cursor in it, and MAY change that encoding without a version bump.
- `total_size` is the count of the **whole filtered collection**, not of this
  page.

### Pagination

| Param | Type | Meaning |
|---|---|---|
| `page_size` | int | Max results. Default **50**, server max **100**; a resource MAY lower its max. Over the max is clamped, not rejected. |
| `page_token` | string | Opaque token from a previous `next_page_token`. |
| `skip` | int | Results to skip before the page (AIP-158). |

`skip` exists because the admin grid offers direct page jumps and cannot walk
tokens to reach page 40. It is bounded (`skip + page_size ≤ 10000`; beyond that
is `OUT_OF_RANGE`) because a deep Mongo `skip` is a linear scan. `page_token`
is the path a batch consumer MUST take.

### Ordering

`order_by=create_time desc, title`

- Comma-separated `field [asc|desc]`, default `asc`, at most **3** keys.
- Only fields the resource declares **sortable** are accepted; anything else is
  `INVALID_ARGUMENT`. A field is sortable only if it is backed by an index —
  this rule exists so that an admin clicking a column header cannot table-scan
  production.
- Default order is per resource and MUST be documented and indexed. It is
  `create_time desc` unless the resource says otherwise.

### Filtering

`filter=is_active = true AND title:"launch"`

A deliberately small subset of AIP-160:

```
expr       := term (("AND" | "OR") term)*        ; AND binds tighter than OR
term       := ["NOT"] comparison | "(" expr ")"
comparison := field op value
op         := "=" | "!=" | "<" | "<=" | ">" | ">=" | ":"
field      := ident ("." ident)*
value      := string | number | true | false | null | rfc3339-timestamp
```

- `:` is **has**: case-insensitive substring on a string, membership on an
  array (`visible_view_ids:"68b0c1"`).
- Only fields the resource declares **filterable** are accepted; anything else
  is `INVALID_ARGUMENT` naming the field. Functions, wildcards and traversals
  into unindexed subdocuments are not supported.
- A parse failure returns `INVALID_ARGUMENT` with the byte offset in
  `details[].field_violations[].description`.

Free text has no special parameter: the client composes it, e.g.
`filter=title:"foo" OR content:"foo"`. Ranked full-text search is
a different thing with a different backend (Typesense) and is a custom method,
not a filter ([§10](/concepts/methods)).
