feat(api): parseSort + parseFilter query helpers (#192) #202
No reviewers
Labels
No labels
area:auth
area:ci
area:db
area:infra
area:native
area:pwa
area:service
epic
feature
foundation
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
james/carol!202
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "192-sort-filter-helpers"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Follow-up from #179. Adds two pure-function helpers for
?sort=and?filter[...]=query params, and smoke-adopts them on/api/notesend-to-end.Helpers
apps/api/lib/api/parseSort.ts?sort=name(asc),?sort=-name(desc),?sort=-createdAt,name(multi-field, left-to-right priority).invalid_sort_fieldvialib/api/errors.ts.{ ok: true, sort: [] }; caller picks the default.apps/api/lib/api/parseFilter.ts?filter[status]=open&filter[kind]=email.invalid_filter_field. Failed per-field validation → 400invalid_filter_value.Both helpers return a tagged
{ ok, response? }result rather than throwing — mirrorsparseCursorQueryinlib/api/pagination.ts, so the three plug together cleanly in a route.URL syntax decision: bracketed filters
I went with
?filter[status]=openover flat?status=open. The existingdocs/api-conventions.md§Sort and filter grammar already documented bracketed syntax (line 161 of main before this PR), so this PR is a pickup, not a fork. Bracketed namespacing also keeps filter params unambiguously separate fromcursor,limit, andsort— a flat scheme would force the parser to know about every reserved non-filter key.Smoke adoption:
/api/notes?sort=updatedAt | -updatedAt | createdAt | -createdAt— single-field for cursor stability; the leading entry drives the cursor, trailing entries are ignored.?filter[q]=<substring>— case-insensitive title contains. Implemented aslower(title) LIKE lower(?) ESCAPE '\'so it's portable across SQLite and Postgres; user input has% _ \escaped to prevent wildcard injection.The cursor's
tsfield carries whichever sort column is active, sozTimestampIdCursorstays unchanged.Out of scope
Rolling sort/filter out across the remaining endpoints — that's a follow-up.
/api/educations,/api/profile/contacts,/api/account/tokensstay flat for now.Test plan
pnpm install --frozen-lockfilepnpm -F @carol/api typecheckpnpm -F @carol/api lintpnpm -F @carol/api test— 554 passed / 107 skipped (Postgres leg runs in CI)pnpm -F @carol/api openapi:checkpnpm -F @carol/api openapi:coveragepnpm -F @carol/api-client check(spec changed → regenerated typed client)pnpm -r typecheckNew unit tests:
apps/api/tests/lib/api/parseSort.test.ts— 16 cases (happy, multi-field, unknown, empty, casing, trailing commas, repeated keys, lone-, content-type, type spot-check).apps/api/tests/lib/api/parseFilter.test.ts— 16 cases (happy, multi-field, coercion, transforms, repeated keys, unknown, casing, enum failure, length failure, malformed brackets, nested brackets, content-type, type spot-check).New integration tests on
tests/api/notes.test.ts:sort=createdAtandsort=-createdAtorder correctly.filter[q]is case-insensitive and escapes LIKE wildcards.filter[q]fails asinvalid_filter_value.Links
docs/api-conventions.md§Sort and filter grammar — updated in this PR with the three new error codes and the cursor-pagination interaction note.Smoke adoption of parseSort + parseFilter on the reference paginated endpoint. /api/notes now accepts: - ?sort=updatedAt | -updatedAt | createdAt | -createdAt (single-field for cursor stability; the leading entry drives the cursor, trailing entries are ignored) - ?filter[q]=<substring> — case-insensitive title contains The cursor's `ts` field carries whichever sort column is active so the existing zTimestampIdCursor stays unchanged. The substring filter uses `lower(title) LIKE lower(?) ESCAPE '\'` for SQLite + Postgres parity; user input has `% _ \` escaped so it can't inject wildcards. OpenAPI spec + the generated typed client are regenerated; conventions doc + error-code table updated for the new `invalid_sort_field`, `invalid_filter_field`, `invalid_filter_value` codes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>📊 Test coverage
Patch coverage: no testable lines changed.
Overall (
app/,lib/,db/, excluding UI per ADR-0019):Soft thresholds per ADR-0019. Coverage is informational and does not block merge.
Trivy (container image)
Threshold:
high· Total findings: 121 · At/above threshold: 16.27.0, 7.28.0, 8.5.0