# Sources

Decide where a paginator's items come from, whether that's an array you already hold or one page at a time from your database. Covers a source without a total, the page every source returns, and writing one of your own.

A source decides which items belong on page N. [Pagination](/components/pagination) used the simpler of the two sources seedcord ships. This page covers the other one, plus writing your own.

[`ArraySource`](https://docs.seedcord.org/packages/gateway/latest/array-source) loads the whole list on every click and slices it. For a mod log with fifty thousand rows, that's fifty thousand rows read to show ten.

[`CursorSource`](https://docs.seedcord.org/packages/gateway/latest/cursor-source) fetches one page at a time and reports whether another follows, which is what a SQL `LIMIT`/`OFFSET` query or a paged HTTP API gives you.

```ts title="src/handlers/History.ts"
import {
    ButtonRoute,
    CursorSource,
    Paginator,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';

interface Entry {
    at: string;
    action: string;
}

export const History = new Paginator({
    prefix: 'history',
    source: new CursorSource(
        (ctx, page, perPage) =>
            readAuditPage(ctx.guild?.id, page * perPage, perPage),
        { perPage: 10 }
    ),
    renderItem: (entry) => `\`${entry.at}\` ${entry.action}`
});

@ButtonRoute(History.cursor)
export class HistoryNav extends History.Handler {}

@SlashRoute('history')
export class HistoryList extends SlashHandler<'history'> {
    public async execute(): Promise<void> {
        await History.start(this);
    }
}
```

The fetcher receives the page number and the page size, so `page * perPage` above is the offset. Return the slice and a `hasNext` flag, as a promise or a plain object.

A cursor source can't see where the list ends, so it passes a page past the last one to your fetcher unchanged. Return an empty slice with `hasNext: false` there. A `true` there leaves **Next** enabled, so someone can keep clicking into empty pages.

> **Gateway and http differ**
>
> The fetcher's first argument is a [`PageContext`](https://docs.seedcord.org/packages/gateway/latest/page-context). Gateway's carries `guild: Guild | null` while http's carries `guildId: string | null`, so `ctx.guild` fails to compile there.

## Without a total

`CursorSource` never asks for a total, because counting every row costs a second query. Three things follow.

* `totalPages` comes back `undefined`.
* The nav row drops **First** and **Last**, leaving **Prev**, the indicator, and **Next**.
* The indicator reads `Page 3` with no total after it.

Use [`ArraySource`](https://docs.seedcord.org/packages/gateway/latest/array-source) when the list is small enough to load whole and you want the count.

## The page a source returns

Every source returns a [`PageView`](https://docs.seedcord.org/packages/core/latest/page-view), the two shipped ones and yours alike.

{/* prettier-ignore-start */}

| field        | what it holds                                                         |
| ------------ | --------------------------------------------------------------------- |
| `items`      | the items on this page                                                |
| `page`       | the zero-based page number, clamped to the last page by `ArraySource` |
| `perPage`    | the page size the source used                                         |
| `totalPages` | the count, or `undefined` when the source cannot get one cheaply      |
| `hasPrev`    | whether a page comes before this one                                  |
| `hasNext`    | whether a page comes after it                                         |

{/* prettier-ignore-end */}

`perPage` defaults to 10 on both sources. A zero, a fraction, or a negative throws `PaginationInvalidPerPage` while you construct the source.

## Writing your own

[`PageSource`](https://docs.seedcord.org/packages/gateway/latest/page-source) declares one method. Write your own for something neither shipped source does, like reporting a real total on a query you still fetch one slice at a time.

```ts
export class AuditSource implements PageSource<Entry> {
    constructor(private readonly perPage: number) {}

    public async page(
        ctx: PageContext,
        n: number
    ): Promise<PageView<Entry>> {
        const total = await countAuditRows(ctx.guild?.id);
        const totalPages = Math.max(
            1,
            Math.ceil(total / this.perPage)
        );
        const page = Math.min(
            Math.max(0, Math.trunc(n)),
            totalPages - 1
        );

        return {
            items: await readAuditRows(
                ctx.guild?.id,
                page * this.perPage,
                this.perPage
            ),
            page,
            perPage: this.perPage,
            totalPages,
            hasPrev: page > 0,
            hasNext: page < totalPages - 1
        };
    }
}
```

One `COUNT` beside the page query fills in `totalPages`, which puts **First** and **Last** back without loading the table.

Clamp `n` yourself, as the sample does. Say the table had eight pages when a button was minted. After someone deletes half the rows, that button still carries page 7. `start()` also passes any number you give it.

seedcord has rendered every page so far. [Custom rendering](/components/pagination-render) hands you the whole message.
