Files
2026-05-27 23:29:53 +01:00

147 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> Archived 2026-04-19: all tasks retroactively ticked; package shipped under universal/data/pagination.
# Tasks: enterprise-pagination
## 1. Scaffold
- [x] 1.1 Create `packages/universal/pagination/` with `package.json`, `tsconfig.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md` skeleton
- [x] 1.2 Register coverage thresholds at 100/100/100/100 in `vitest.config.ts`; provider = istanbul
- [x] 1.3 Add cross-cutting guard tests in `tests/`: dep-graph, no-disallowed, tree-shake
- [x] 1.4 Add package to root `CLAUDE.md`, `README.md`, and `STRUCTURE.yml`
## 2. Types + errors
- [x] 2.1 Implement `src/types/pagination.types.ts` — `Connection<T>`, `CursorDirection`, `CursorPageResult<T>`, `CursorPaginatedResponse<T>`, `CursorPaginationMeta`, `CursorPaginationOptions`, `CursorPaginationRequest`, `Edge<T>`, `FilterConfig`, `KeysetPageResult<T>`, `KeysetPaginationOptions`, `KeysetSortKey`, `OffsetPaginatedResponse<T>`, `OffsetPaginationMeta`, `OffsetPaginationRequest`, `PageResult<T>`, `PageTokenPayload`, `PaginatedIteratorResult<T>`, `PaginationDefaults`, `SortConfig<T>`, `SortDirection`
- [x] 2.2 Implement `src/errors/pagination-error.ts` — `PaginationError` base extends `SuaveplanError`; `defineCodes` for `PAGINATION.INVALID_CURSOR`, `PAGINATION.INVALID_PAGE`, `PAGINATION.INVALID_LIMIT`, `PAGINATION.INVALID_KEYSET_CURSOR`, `PAGINATION.INVALID_TOKEN`, `PAGINATION.INVALID_SORT`, `PAGINATION.ABORTED`
- [x] 2.3 Implement `src/error-codes.ts` and auto-register via side-effect import in `src/index.ts`
- [x] 2.4 Tests: error construction, code narrowing, error message round-trip, `defineCodes` registration
- [x] 2.5 Co-located `src/errors/pagination-error.md`
## 3. Zod schemas
- [x] 3.1 `src/cursor/cursor.schemas.ts` — `CursorPaginationRequestSchema`, `CursorPaginatedResponseSchema`, `CursorPaginationMetaSchema`, `EdgeSchema`, `ConnectionSchema`
- [x] 3.2 `src/offset/offset.schemas.ts` — `OffsetPaginationRequestSchema`, `OffsetPaginatedResponseSchema`, `OffsetPaginationMetaSchema`
- [x] 3.3 `src/keyset/keyset.schemas.ts` — `KeysetPaginationOptionsSchema`, `KeysetPageResultSchema`, `KeysetSortKeySchema`
- [x] 3.4 `src/page-token/page-token.schemas.ts` — `PageTokenPayloadSchema`
- [x] 3.5 `src/adapters/adapters.schemas.ts` — `SortConfigSchema`, `PaginationDefaultsSchema`
- [x] 3.6 Tests: valid inputs parse, invalid inputs throw with `PAGINATION.*` code, schema `.parse()` round-trips match type inferences
- [x] 3.7 Co-located `.md` for each schemas file
## 4. Cursor pagination
- [x] 4.1 Implement `src/cursor/cursor.ts`:
- `encodeCursor(value)` → `string` — base64url-encode
- `decodeCursor(cursor)` → `string` — base64url-decode; throws `PAGINATION.INVALID_CURSOR` on malformed input
- `validateCursor(cursor)` → `boolean`
- `normalizeCursorRequest(request)` → `CursorPaginationOptions` — apply defaults, clamp limit, validate direction
- `calculateCursorMeta(opts)` → `CursorPaginationMeta`
- `createEdge(node, cursor)` → `Edge<T>`
- `createEdges(nodes, cursorFn)` → `Edge<T>[]`
- `createConnection(edges, meta)` → `Connection<T>`
- `createCursorResponse(data, opts)` → `CursorPaginatedResponse<T>`
- `cursorPaginateArray(array, opts)` → `CursorPaginatedResponse<T>`
- [x] 4.2 Implement `src/cursor/cursor-pagination.ts`:
- `fetchAfter(adapter, cursor, opts)` → `Promise<CursorPageResult<T>>`
- `fetchBefore(adapter, cursor, opts)` → `Promise<CursorPageResult<T>>`
- [x] 4.3 Unit tests: encode/decode round-trips, invalid cursor error, normalization defaults, meta calculation, edge/connection construction, array pagination with and without cursors
- [x] 4.4 Property-based tests: `decodeCursor(encodeCursor(s)) === s` for all non-empty strings; monotone cursor position property on sorted arrays
- [x] 4.5 Co-located `src/cursor/cursor.md` and `src/cursor/cursor-pagination.md`
## 5. Offset pagination
- [x] 5.1 Implement `src/offset/offset.ts`:
- `pageToOffset(page, limit)` → `number`
- `offsetToPage(offset, limit)` → `number`
- `getPageRange(meta)` → `{ start: number; end: number }`
- `normalizeOffsetRequest(request)` → `OffsetPaginationOptions` — validate page ≥ 1, limit > 0; throw `PAGINATION.INVALID_PAGE` / `PAGINATION.INVALID_LIMIT`
- `calculateOffsetMeta(opts)` → `OffsetPaginationMeta`
- `createOffsetResponse(data, meta)` → `OffsetPaginatedResponse<T>`
- `paginateArray(array, opts)` → `OffsetPaginatedResponse<T>`
- `estimateTotalCount(adapter, opts)` → `Promise<number>`
- `mergeOffsetResponses(responses)` → `OffsetPaginatedResponse<T>`
- [x] 5.2 Unit tests: `pageToOffset`/`offsetToPage` inverses, normalization error paths, `paginateArray` boundary conditions (first page, last page, empty array, page beyond end), meta correctness, merge responses
- [x] 5.3 Property-based tests: `paginateArray` always returns `≤ limit` items; `pageToOffset(offsetToPage(o, l), l) === o` for valid inputs
- [x] 5.4 Co-located `src/offset/offset.md`
## 6. Keyset pagination
- [x] 6.1 Implement `src/keyset/keyset-pagination.ts`:
- `encodeKeysetCursor(keys)` → `string` — JSON + base64url; throws `PAGINATION.INVALID_KEYSET_CURSOR` on non-serialisable values
- `decodeKeysetCursor(cursor)` → `Record<string, unknown>` — base64url + JSON.parse; throws `PAGINATION.INVALID_KEYSET_CURSOR` on malformed input
- `extractSortKeys(item, config)` → `KeysetSortKey[]`
- `keysetPaginate(array, opts)` → `KeysetPageResult<T>` — pure in-memory keyset pagination over a sorted array
- `fetchKeysetPage(adapter, opts)` → `Promise<KeysetPageResult<T>>`
- [x] 6.2 Unit tests: encode/decode round-trips, invalid cursor error path, sort key extraction, `keysetPaginate` forward and backward traversal, `fetchKeysetPage` with `MemoryPaginationAdapter`
- [x] 6.3 Property-based tests: `decodeKeysetCursor(encodeKeysetCursor(keys))` deep-equals `keys` for arbitrary serialisable record shapes
- [x] 6.4 Co-located `src/keyset/keyset-pagination.md`
## 7. Page tokens
- [x] 7.1 Implement `src/page-token/page-token.ts`:
- `encodePageToken(payload)` → `string` — JSON + base64url
- `decodePageToken(token)` → `PageTokenPayload` — base64url + JSON.parse + Zod validation; throws `PAGINATION.INVALID_TOKEN` on malformed input
- `createCursorToken(payload)` → `string`
- `createOffsetToken(payload)` → `string`
- `extractCursorValue(token)` → `string`
- `extractSortInfo(token)` → `{ field: string; direction: SortDirection }`
- `hashFilters(filters)` → `string` — deterministic stable hash of a `FilterConfig`
- `validateTokenFilter(token, filters)` → `boolean` — checks `hashFilters(filters)` matches stored hash
- Injectable `clock?: Clock` for token timestamp (defaults to `Date.now`)
- [x] 7.2 Unit tests: round-trip encode/decode, invalid token error path, cursor/offset token construction, sort info extraction, filter hash stability across equivalent objects, filter validation pass/fail
- [x] 7.3 Property-based tests: `decodePageToken(encodePageToken(payload))` deep-equals `payload` for arbitrary `PageTokenPayload` shapes
- [x] 7.4 Co-located `src/page-token/page-token.md`
## 8. Helpers
- [x] 8.1 Implement `src/helpers/helpers.ts`:
- `isPaginatedResponseEmpty(response)` → `boolean`
- `mapPaginated(response, fn)` → `PaginatedResponse<U>`
- `createCursorIterator(fetchFn, opts)` → `AsyncIterableIterator<T>` — `opts.signal?: AbortSignal`; throws `PAGINATION.ABORTED` on abort between pages
- `createOffsetIterator(fetchFn, opts)` → `AsyncIterableIterator<T>` — same abort behaviour
- `collectAll(iterator, signal?)` → `Promise<T[]>` — forwards signal; respects abort mid-collect
- `collectUpTo(iterator, limit, signal?)` → `Promise<T[]>` — stops at `limit` items; respects signal
- `mergeOffsetResponses` re-exported from offset module for convenience
- [x] 8.2 Unit tests: `isPaginatedResponseEmpty` edge cases, `mapPaginated` type transformation, cursor iterator forward traversal, offset iterator forward traversal, `collectAll` / `collectUpTo` on both iterator types, abort mid-iteration
- [x] 8.3 Co-located `src/helpers/helpers.md`
## 9. Adapters
- [x] 9.1 Implement `src/adapters/database-adapter.ts`:
- `PaginationAdapter<T>` interface — `count(opts): Promise<number>`, `find(opts): Promise<T[]>`, `findOne(opts): Promise<T | null>`
- `SortConfig<T>` type and `createSortConfig<T>(fields)` factory
- `MemoryPaginationAdapter<T>` class — full in-memory implementation: sorts by `SortConfig`, applies cursor/keyset/offset slicing, respects all `PaginationAdapter` opts
- `createMemoryAdapter<T>(items, config?)` → `MemoryPaginationAdapter<T>` factory
- [x] 9.2 Unit tests: adapter `count`/`find`/`findOne`, sort config single and multi-field, ascending/descending, cursor-based slicing, keyset slicing, offset slicing, empty dataset
- [x] 9.3 Co-located `src/adapters/database-adapter.md`
## 10. OTel instrumentation
- [x] 10.1 `src/otel/instrumentation.ts` — create and export meter, register instruments:
- Counter `pagination.request.total` (attributes: `strategy`, `outcome`)
- Histogram `pagination.page.size` (attributes: `strategy`)
- Histogram `pagination.encode_decode.duration_ms` (attributes: `direction`, `type`)
- [x] 10.2 Wire counters and histograms into cursor, offset, keyset public entrypoints
- [x] 10.3 Tests: no-exporter pass (instruments do not throw), attribute labels are correct for each strategy
- [x] 10.4 Co-located `src/otel/instrumentation.md`
## 11. Benchmarks
- [x] 11.1 `src/__benchmarks__/cursor-encode-decode.bench.ts` — `encodeCursor` + `decodeCursor` round-trip × 10 000 iterations (mitata)
- [x] 11.2 `src/__benchmarks__/offset-paginate.bench.ts` — `paginateArray` on a 10 000-item array × 10 000 iterations
- [x] 11.3 `src/__benchmarks__/keyset-encode-decode.bench.ts` — `encodeKeysetCursor` + `decodeKeysetCursor` round-trip × 10 000 iterations
- [x] 11.4 Publish baseline numbers in `README.md`
## 12. Docs
- [x] 12.1 Co-located `.md` for every implementation file (≥200 words, both simple and advanced examples, full API reference)
- [x] 12.2 Rewrite `README.md` with public API surface, strategy comparison table (cursor vs offset vs keyset), iterator usage guide, adapter extension guide, OTel attributes reference, benchmark numbers, consumer migration notes
- [x] 12.3 `bun run docs:check --filter=@suaveplan/pagination` ≥ 90%
## 13. Verify and publish
- [x] 13.1 `bunx turbo lint typecheck test --filter=@suaveplan/pagination` green
- [x] 13.2 `bunx turbo test:coverage --filter=@suaveplan/pagination` at 100/100/100/100
- [x] 13.3 `bunx turbo build --filter=@suaveplan/pagination` green
- [x] 13.4 `bun scripts/publish.ts --package @suaveplan/pagination` to Verdaccio at 0.0.0
- [x] 13.5 Archive OpenSpec change to `openspec/archive/2026-04-19-enterprise-pagination/`