first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,105 @@
# Cursor Pagination
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The cursor module provides the in-memory building blocks for Relay-style cursor-based pagination. It offers utilities for constructing edges, connections, and metadata from any sorted data source without requiring a database adapter. All functions are pure — they accept plain arrays and return plain objects — making them trivially testable and completely portable across runtimes.
Cursor-based pagination is preferred over offset pagination for large datasets or real-time feeds because it avoids the "jumping rows" problem: items inserted or deleted between page fetches cannot corrupt a cursor-based window.
## Features
- `calculateCursorMeta` — compute `CursorPaginationMeta` (start/end cursor, hasNextPage, hasPreviousPage, optional totalCount) from an edge array.
- `createCursorResponse` — build a `CursorPaginatedResponse<T>` in one call.
- `createConnection` — build a Relay-style `Connection<T>` with `edges` and `pageInfo`.
- `createEdge` / `createEdges` — wrap nodes with their cursor strings.
- `normalizeCursorRequest` — clamp limit, default direction, from raw input.
- `cursorPaginateArray` — paginate an in-memory array with forward/backward cursor support.
## Usage: Basic Example
```typescript
import { cursorPaginateArray } from "@suaveplan/pagination/cursor";
const items = [{ id: "a" }, { id: "b" }, { id: "c" }, { id: "d" }];
// First page — no cursor, limit 2
const page1 = cursorPaginateArray(items, { limit: 2 }, (item) => item.id);
// page1.data → [{ id: "a" }, { id: "b" }]
// page1.meta.endCursor → "b"
// page1.meta.hasNextPage → true
// Next page — use endCursor from previous response
const page2 = cursorPaginateArray(
items,
{ cursor: page1.meta.endCursor ?? undefined, limit: 2 },
(item) => item.id
);
// page2.data → [{ id: "c" }, { id: "d" }]
// page2.meta.hasNextPage → false
```
## Usage: Advanced Example
```typescript
import {
createEdges,
createConnection,
normalizeCursorRequest,
} from "@suaveplan/pagination/cursor";
// Relay-style connection with totalCount
const users = [
{ id: "u1", name: "Alice" },
{ id: "u2", name: "Bob" },
];
const req = normalizeCursorRequest({ limit: 10 }, { maxLimit: 50 });
const edges = createEdges(users, (u) => u.id);
const connection = createConnection(edges, false, "forward", 2);
// connection.edges[0].cursor → "u1"
// connection.pageInfo.hasNextPage → false
// connection.totalCount → 2
```
## API Reference
### `calculateCursorMeta<T>(edges, hasMore, direction?, totalCount?)`
| Parameter | Type | Default |
|-------------|-----------------------------|-------------|
| `edges` | `Edge<T>[]` | — |
| `hasMore` | `boolean` | — |
| `direction` | `"forward" \| "backward"` | `"forward"` |
| `totalCount` | `number \| undefined` | — |
Returns `CursorPaginationMeta`.
### `createCursorResponse<T>(edges, hasMore, direction?, totalCount?)`
Returns `CursorPaginatedResponse<T>` — the `data` field contains the unwrapped nodes.
### `createConnection<T>(edges, hasMore, direction?, totalCount?)`
Returns `Connection<T>` — the Relay-style form with `edges`, `pageInfo`, and optional `totalCount`.
### `createEdge<T>(node, cursor)`
Returns `Edge<T>` — `{ node, cursor }`.
### `createEdges<T>(nodes, getCursor)`
Maps `nodes` to `Edge<T>[]` using the provided cursor extractor. The extractor receives `(node, index)`.
### `normalizeCursorRequest(input, defaults?)`
Clamps `limit` to `[1, maxLimit]` (default `maxLimit = 100`). Defaults `direction` to `"forward"`. Returns a full `CursorPaginationRequest`.
### `cursorPaginateArray<T>(items, request, getCursor?)`
Slices `items` according to `cursor`, `limit`, and `direction`. Default cursor extractor is `(_, i) => String(i)`. Returns `CursorPaginatedResponse<T>`.
## Implementation Notes
All cursor positions are string values matched by strict equality. The default extractor uses array indices as cursors; callers are encouraged to pass a stable domain-key extractor (e.g., `item.id`) in production. For backward pagination, `startIndex` is clamped to `0` to avoid negative slices. The `hasNextPage`/`hasPreviousPage` logic follows the GraphQL Relay Cursor Connections Specification.