Initial Commit
This commit is contained in:
105
packages/foundation/data/pagination/src/offset/offset.md
Normal file
105
packages/foundation/data/pagination/src/offset/offset.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# Offset Pagination
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
The offset pagination module provides the computational building blocks for traditional page-number-based pagination. It handles request normalisation, metadata calculation, page-to-offset conversion, and in-memory array slicing. All functions are pure and synchronous except `estimateTotalCount`, which calls an adapter to sample the dataset.
|
||||
|
||||
Offset pagination is the correct choice when total count is cheap to compute, pages are shallow (< a few thousand rows), or the UI must render absolute page numbers. For deep pagination on large datasets, prefer keyset or cursor pagination to avoid scanning skipped rows.
|
||||
|
||||
## Features
|
||||
|
||||
- `calculateOffsetMeta` — compute full metadata (page, limit, total, totalPages, hasNext, hasPrevious).
|
||||
- `createOffsetResponse` — assemble a typed `OffsetPaginatedResponse<T>` envelope.
|
||||
- `normalizeOffsetRequest` — clamp page/limit to configured defaults, default sortOrder.
|
||||
- `pageToOffset` — convert 1-based page number to a zero-based array/SQL offset.
|
||||
- `offsetToPage` — convert a zero-based offset back to a 1-based page number.
|
||||
- `getPageRange` — produce a windowed list of page numbers for UI navigation controls.
|
||||
- `paginateArray` — slice an in-memory array and return a full `OffsetPaginatedResponse<T>`.
|
||||
- `estimateTotalCount` — sample-based approximate total count via adapter.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { paginateArray } from "@suaveplan/pagination/offset";
|
||||
|
||||
const items = Array.from({ length: 50 }, (_, i) => ({ id: i + 1 }));
|
||||
const page2 = paginateArray(items, 2, 10);
|
||||
// page2.data → items 11–20
|
||||
// page2.meta.page → 2
|
||||
// page2.meta.totalPages → 5
|
||||
// page2.meta.hasNext → true
|
||||
// page2.meta.hasPrevious → true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import {
|
||||
normalizeOffsetRequest,
|
||||
getPageRange,
|
||||
pageToOffset,
|
||||
offsetToPage,
|
||||
} from "@suaveplan/pagination/offset";
|
||||
|
||||
// Normalise untrusted input from a query string
|
||||
const req = normalizeOffsetRequest(
|
||||
{ page: -5, limit: 9999 },
|
||||
{ maxLimit: 100, defaultLimit: 20 }
|
||||
);
|
||||
// req.page → 1 (clamped), req.limit → 100 (clamped)
|
||||
|
||||
// Render page buttons for a 300-item result set
|
||||
const pages = getPageRange(15, 30, 7);
|
||||
// → [12, 13, 14, 15, 16, 17, 18]
|
||||
|
||||
// Convert offset 40 at limit 10 → page 5
|
||||
const page = offsetToPage(40, 10); // → 5
|
||||
const offset = pageToOffset(5, 10); // → 40
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `calculateOffsetMeta(total, page, limit): OffsetPaginationMeta`
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|----------|--------------------------------------|
|
||||
| `total` | `number` | Total item count across all pages |
|
||||
| `page` | `number` | Current 1-based page number |
|
||||
| `limit` | `number` | Items per page |
|
||||
|
||||
Returns `OffsetPaginationMeta` with `totalPages`, `hasNext`, and `hasPrevious`.
|
||||
|
||||
### `createOffsetResponse<T>(data, total, page, limit): OffsetPaginatedResponse<T>`
|
||||
|
||||
Wraps `data` and computed metadata into the standard response envelope.
|
||||
|
||||
### `normalizeOffsetRequest(input, defaults?): OffsetPaginationRequest`
|
||||
|
||||
Clamps `page` to `≥ 1` and `limit` to `[minLimit, maxLimit]`. Defaults: minLimit=1, maxLimit=100, defaultLimit=20, defaultPage=1.
|
||||
|
||||
### `pageToOffset(page, limit): number`
|
||||
|
||||
Returns `Math.max(0, (page - 1) * limit)`.
|
||||
|
||||
### `offsetToPage(offset, limit): number`
|
||||
|
||||
Returns `Math.floor(offset / limit) + 1`. Returns `1` if `limit ≤ 0`.
|
||||
|
||||
### `getPageRange(currentPage, totalPages, windowSize?): number[]`
|
||||
|
||||
Default `windowSize` is 5. The window is centred on `currentPage` and never exceeds `[1, totalPages]`.
|
||||
|
||||
### `paginateArray<T>(items, page, limit): OffsetPaginatedResponse<T>`
|
||||
|
||||
Slices `items[offset..offset+limit]` and builds the full response with computed metadata.
|
||||
|
||||
### `estimateTotalCount(adapter, opts): Promise<number>`
|
||||
|
||||
Samples up to `opts.sampleSize` (default 1000) items from the adapter and extrapolates. Returns `0` if the adapter reports zero total.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`normalizeOffsetRequest` uses `Math.floor` to discard fractional page/limit values from untrusted API input. The `sortOrder` field defaults to `"desc"` because most read-heavy UIs show newest items first. `getPageRange` is intentionally view-agnostic — it returns page numbers only; rendering (ellipsis, prev/next buttons) belongs in the UI layer.
|
||||
Reference in New Issue
Block a user