Skip to content

forPageAfterId

Build the SQL for a single keyset page: WHERE id > <lastSeenId> ORDER BY id LIMIT N. The building block underneath chunkById and eachById.

Signature

ts
FedacoBuilder<T>.forPageAfterId(
  perPage: number,
  lastId: number | string | null,
  column?: string,
): this

Parameters

NameDefaultDescription
perPageRows per page.
lastIdThe cursor — pass the last id from the previous page. Use 0 (or the smallest possible value) for the first page.
columnprimary keyThe cursor column. Must be unique and monotonic.

Returns this — chainable. Consume with .get() / .first().

Real-World Use Cases

1. Manual cursor pagination

ts
let cursor = 0;
while (true) {
  const page = await User.createQuery()
    .forPageAfterId(500, cursor)
    .get();

  if (page.length === 0) break;

  for (const user of page) {
    await processUser(user);
  }

  cursor = page[page.length - 1].id;
}

2. From an HTTP ?after=... parameter

ts
async function listAfter(req: Request) {
  const after = req.query.after ? Number(req.query.after) : 0;
  const limit = 50;

  const rows = await User.createQuery()
    .forPageAfterId(limit, after)
    .get();

  return {
    items: rows,
    nextCursor: rows.length === limit ? rows[rows.length - 1].id : null,
  };
}

This is the cursor-based pattern most APIs (GitHub, Stripe) use — stable under writes, no OFFSET cost growing with page number.

3. Custom cursor column

ts
const rows = await Event.createQuery()
  .forPageAfterId(100, lastEventTimestamp, 'created_at')
  .get();

When the table is naturally time-ordered. The column must be unique — if two events share a created_at, one will be skipped at a page boundary. Use a (created_at, id) composite cursor when ties happen.

4. Combined with WHERE clauses

ts
const rows = await Order.createQuery()
  .where('status', 'paid')
  .forPageAfterId(200, after)
  .get();

forPageAfterId adds its own WHERE id > X ORDER BY id ASC — fedaco preserves your other filters.

forPageAfterId vs paginate vs chunkById

ToolStrategyUI / cursorStable under writes?
paginateOFFSET + COUNT(*)page number
forPageAfterIdkeyset (manual)cursor
chunkByIdkeyset (auto-loop)n/a

Common Pitfalls

  • Always pass an integer/scalar lastIdnull typically resolves to 0, but be explicit.
  • The cursor column must be ordered ASC internally. Don't add a conflicting orderBy(column, 'desc').
  • Compound keys aren't directly supported. For composite cursors (timestamp + id), encode the comparison via whereRaw.

See Also

Released under the MIT License.