147 lines
11 KiB
Markdown
147 lines
11 KiB
Markdown
> 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/`
|