first commit
This commit is contained in:
95
docs/foundation/data/pagination/src/tokens/page-tokens.md
Normal file
95
docs/foundation/data/pagination/src/tokens/page-tokens.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Page Tokens
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The page tokens module provides opaque, URL-safe token strings that carry all the state needed to resume a paginated query: the cursor or offset value, sort field and direction, and an optional filter hash for cache invalidation. Tokens are encoded as base64url JSON so they survive URL query-string round-trips without additional percent-encoding.
|
||||
|
||||
This module sits above the raw cursor encoding in `cursor-pagination.ts`: where that module encodes a single value, page tokens encode a structured payload that includes sort and filter metadata — making them suitable for stateless pagination APIs where the client must carry all context.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodePageToken` / `decodePageToken` — full payload round-trip with type validation.
|
||||
- `createCursorToken` — create a token from a domain value (ID, score, timestamp) and optional sort/filter info.
|
||||
- `createOffsetToken` — shortcut for offset-based tokens.
|
||||
- `extractCursorValue` — pull just the value field from a token.
|
||||
- `extractSortInfo` — pull sort field and direction from a token.
|
||||
- `hashFilters` — stable djb2 hash of a filter map for cache-invalidation.
|
||||
- `validateTokenFilter` — check that a token's filter hash matches an expected value.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { createCursorToken, decodePageToken } from "@suaveplan/pagination/tokens";
|
||||
|
||||
const token = createCursorToken("user_99", "createdAt", "desc");
|
||||
// → opaque base64url string
|
||||
|
||||
const payload = decodePageToken(token);
|
||||
// → { v: "user_99", s: "createdAt", d: "desc" }
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
createCursorToken,
|
||||
hashFilters,
|
||||
validateTokenFilter,
|
||||
extractSortInfo,
|
||||
} from "@suaveplan/pagination/tokens";
|
||||
|
||||
const filters = { status: "active", role: "admin" };
|
||||
const hash = hashFilters(filters);
|
||||
// → stable hash string, e.g. "1q7f9k"
|
||||
|
||||
const token = createCursorToken("cursor_abc", "name", "asc", hash);
|
||||
|
||||
// On the next request, verify the filter hasn't changed
|
||||
const valid = validateTokenFilter(token, hash); // → true
|
||||
const invalid = validateTokenFilter(token, "differenthash"); // → false
|
||||
|
||||
// Extract sort metadata to rebuild the query
|
||||
const sort = extractSortInfo(token);
|
||||
// → { sortBy: "name", sortDirection: "asc" }
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encodePageToken(payload: PageTokenPayload): string`
|
||||
|
||||
Serialises the payload to JSON and encodes as base64url. The payload must have a `v` field (string or number); `s`, `d`, `f` are optional.
|
||||
|
||||
### `decodePageToken(token: string): PageTokenPayload | null`
|
||||
|
||||
Decodes and validates a token. Returns `null` for any invalid input, never throws.
|
||||
|
||||
### `createCursorToken(value, sortBy?, sortDirection?, filterHash?): string`
|
||||
|
||||
Builds and encodes a `PageTokenPayload` from a cursor value and optional sort/filter metadata.
|
||||
|
||||
### `createOffsetToken(offset, sortBy?, sortDirection?, filterHash?): string`
|
||||
|
||||
Alias of `createCursorToken` with `offset: number` as the first argument.
|
||||
|
||||
### `extractCursorValue(token: string): string | number | null`
|
||||
|
||||
Returns the `v` field of the decoded token, or `null` if the token is invalid.
|
||||
|
||||
### `extractSortInfo(token: string): { sortBy?: string; sortDirection?: SortDirection } | null`
|
||||
|
||||
Returns the `s` and `d` fields of the decoded token, or `null` if the token is invalid.
|
||||
|
||||
### `hashFilters(filters: Record<string, unknown>): string`
|
||||
|
||||
Sorts keys lexicographically, concatenates `key=value` pairs, and applies a djb2 hash. Returns the absolute hash as a base-36 string. Suitable for cache-invalidation only — not cryptographic.
|
||||
|
||||
### `validateTokenFilter(token: string, expectedFilterHash: string): boolean`
|
||||
|
||||
Returns `true` when the token is valid and its `f` field equals `expectedFilterHash`. Returns `true` (unconditional) when the token has no `f` field. Returns `false` for invalid tokens or mismatched hashes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The djb2 hash in `hashFilters` uses bitwise OR with `0` to keep the accumulator within signed 32-bit integer range — preventing floating-point precision loss for long filter strings. The filter hash is NOT a cryptographic MAC; it cannot prevent token forgery. Use HMAC-signed tokens if forgery resistance is required.
|
||||
Reference in New Issue
Block a user