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 @@
# 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.