Pagination
import 'package:gisila/gisila.dart';PageParams
Section titled “PageParams”Validated limit / offset / cursor values.
class PageParams { PageParams({ int? limit, int? offset, String? cursor, int defaultLimit = 25, int maxLimit = 100, int maxOffset = 100000, }); final int limit; final int offset; final String? cursor;}When to use: on every list endpoint, so clients can’t ask for everything at once. Declare limit/offset/cursor as nullable query parameters on the route, then pass them in.
| Parameter | Default | Behaviour |
|---|---|---|
limit |
defaultLimit (25) |
Above maxLimit (100) it is cut down to maxLimit. Below 1 is a 400 (invalid_limit). |
offset |
0 |
Negative or above maxOffset (100000) is a 400 (invalid_offset). |
cursor |
null |
Stored as-is; decode it with Cursor.decode. |
One page of results, rendered as {"items": [...], "page": {...}}.
class Page<T> { Page( Iterable<T> items, { PageParams? params, int? limit, int? offset, int? total, String? nextCursor, bool? hasMore, }); final List<T> items; final int? limit; final int? offset; final int? total; final String? nextCursor; final bool hasMore; Map<String, Object?> toJson();}When to use: as the return type of list routes, so every list in your API has the same shape.
| Parameter | Description |
|---|---|
items |
The rows; encoded through their toJson(). |
params |
Fills limit, and offset when no cursor was used. |
limit, offset |
Override what params provides. |
total |
Total count, when you have it. |
nextCursor |
Cursor for the next page. |
hasMore |
Defaults to: true when nextCursor is set; else offset + items < total when total is known; else items.length >= limit. |
toJson() produces page.limit, page.offset, page.total, page.next_cursor (each only when set) and page.has_more.
{"items": [{"id": 51}, {"id": 52}], "page": {"limit": 25, "offset": 50, "total": 240, "has_more": true}}Example: offset pagination
@Get('/')@Permissions({'posts:read'})Future<Page<Post>> list(PostService posts, {int? limit, int? offset}) async { final page = PageParams(limit: limit, offset: offset); final rows = await posts.list(limit: page.limit, offset: page.offset); return Page(rows, params: page, total: await posts.count());}Cursor
Section titled “Cursor”Opaque cursors for keyset pagination.
abstract final class Cursor { static String encode(Map<String, Object?> position); static Map<String, Object?> decode(String cursor);}encode JSON-encodes the position (for example the last row’s sort key and id) and base64url-encodes it without padding. decode reverses it; a malformed cursor is a 400 (invalid_cursor).
When to use: large or fast-changing tables, where offsets get slow and skip or repeat rows. Cursors are not signed: decode them only into values you would accept from the client anyway, and keep authorization in the query.
Example: cursor pagination
@Get('/feed')Future<Page<Post>> feed(PostService posts, {int? limit, String? cursor}) async { final page = PageParams(limit: limit, cursor: cursor); final after = cursor == null ? null : Cursor.decode(cursor); final rows = await posts.after(id: after?['id'] as int?, limit: page.limit + 1); final more = rows.length > page.limit; final items = rows.take(page.limit).toList(); return Page(items, params: page, nextCursor: more ? Cursor.encode({'id': items.last.id}) : null);}See also: Build a CRUD API.