feat(api+client): projects feature (#26) #265

Merged
james merged 5 commits from 26-projects-feature into main 2026-06-23 23:35:36 +00:00
Owner

Summary

Brings the Projects feature live across the full stack — a per-user list of independent / outside-of-work projects with name, optional description, and optional link. Mirrors the Notes / Education CRUD shape end to end:

  • DB (#016): projects table (user-scoped, ON DELETE CASCADE). display_order reserved for a future reordering ticket. Index on user_id. ProjectsRepository ships cursor-paginated list, unbounded list, findById, create, update, delete.
  • API: GET/POST /api/projects + PATCH/DELETE /api/projects/{id}. Cursor pagination via the shared timestamp+id envelope. zod DTOs in lib/dto/project.ts validate name (1..200), description (≤2000, nullable), and link (http(s):// only, nullable — javascript: and friends rejected).
  • OpenAPI: four routes registered in lib/api/openapi-routes.ts; openapi.json regenerated; drift gate green; coverage 69 (path, method) pairs.
  • @carol/api-client: useProjects, useCreateProject, useUpdateProject, useDeleteProject (mutations patch the list cache in place); keys.projects added to the central registry.
  • Client: /projects screen replaces the placeholder. Inline create + edit-in-place, link tappable via Linking.openURL, KeyboardAwareScrollView + Enter chains, theme-aware styles pre-flattened to plain objects per #239.
  • i18n: full projects.* namespace in en.json (sentence case, first person, voice-guide compliant).

Routes

Method Path Description
GET /api/projects List (cursor-paginated)
POST /api/projects Create
PATCH /api/projects/{id} Update (partial)
DELETE /api/projects/{id} Delete

Test plan

  • pnpm install --frozen-lockfile
  • pnpm -F @carol/api typecheck
  • pnpm -F @carol/api lint
  • pnpm -F @carol/api test — 680 passed (53 files; sqlite leg; postgres-leg tests skipped here, will run on CI)
  • pnpm -F @carol/api openapi:check — up to date
  • pnpm -F @carol/api openapi:coverage — 69 (path, method) pairs registered
  • pnpm -F @carol/api-client check — generated client up to date
  • pnpm -F @carol/api-client typecheck
  • pnpm -F @carol/api-client lint
  • pnpm -F @carol/api-client test
  • pnpm -F @carol/client typecheck
  • pnpm -F @carol/client lint
  • pnpm -F @carol/client test
  • pnpm -F @carol/client export:web — 24 routes including /projects

Notes for the reviewer

  • projects in KYSELY_TABLES: added BEFORE users in tests/db/_engines.ts to keep the Postgres drop order FK-safe (the trap from #259).
  • Link validation strictness: the zod schema accepts only well-formed URLs and only http:// / https:// schemes (refine on top of z.url()). The client mirrors that contract via isValidProjectLink in projects.tsx. Tests cover both "not a url" and "javascript:alert(1)".
  • display_order: defaulted to 0 by the repo on insert, untouched by update. Reserved for the eventual reordering feature; no client surface today.
  • Out of scope as per the ticket: reordering, linking projects to jobs/contracts, attachments, external import.

Linked: #26. See idea.mdExperience for the product context.

Closes #26.

## Summary Brings the Projects feature live across the full stack — a per-user list of independent / outside-of-work projects with name, optional description, and optional link. Mirrors the Notes / Education CRUD shape end to end: - **DB (#016)**: `projects` table (user-scoped, ON DELETE CASCADE). `display_order` reserved for a future reordering ticket. Index on `user_id`. `ProjectsRepository` ships cursor-paginated list, unbounded list, findById, create, update, delete. - **API**: `GET/POST /api/projects` + `PATCH/DELETE /api/projects/{id}`. Cursor pagination via the shared timestamp+id envelope. zod DTOs in `lib/dto/project.ts` validate name (1..200), description (≤2000, nullable), and link (http(s):// only, nullable — `javascript:` and friends rejected). - **OpenAPI**: four routes registered in `lib/api/openapi-routes.ts`; `openapi.json` regenerated; drift gate green; coverage 69 (path, method) pairs. - **@carol/api-client**: `useProjects`, `useCreateProject`, `useUpdateProject`, `useDeleteProject` (mutations patch the list cache in place); `keys.projects` added to the central registry. - **Client**: `/projects` screen replaces the placeholder. Inline create + edit-in-place, link tappable via `Linking.openURL`, KeyboardAwareScrollView + Enter chains, theme-aware styles pre-flattened to plain objects per #239. - **i18n**: full `projects.*` namespace in `en.json` (sentence case, first person, voice-guide compliant). ## Routes | Method | Path | Description | | --- | --- | --- | | GET | /api/projects | List (cursor-paginated) | | POST | /api/projects | Create | | PATCH | /api/projects/{id} | Update (partial) | | DELETE | /api/projects/{id} | Delete | ## Test plan - [x] `pnpm install --frozen-lockfile` - [x] `pnpm -F @carol/api typecheck` - [x] `pnpm -F @carol/api lint` - [x] `pnpm -F @carol/api test` — 680 passed (53 files; sqlite leg; postgres-leg tests skipped here, will run on CI) - [x] `pnpm -F @carol/api openapi:check` — up to date - [x] `pnpm -F @carol/api openapi:coverage` — 69 (path, method) pairs registered - [x] `pnpm -F @carol/api-client check` — generated client up to date - [x] `pnpm -F @carol/api-client typecheck` - [x] `pnpm -F @carol/api-client lint` - [x] `pnpm -F @carol/api-client test` - [x] `pnpm -F @carol/client typecheck` - [x] `pnpm -F @carol/client lint` - [x] `pnpm -F @carol/client test` - [x] `pnpm -F @carol/client export:web` — 24 routes including `/projects` ## Notes for the reviewer - **`projects` in `KYSELY_TABLES`**: added BEFORE `users` in `tests/db/_engines.ts` to keep the Postgres drop order FK-safe (the trap from #259). - **Link validation strictness**: the zod schema accepts only well-formed URLs and only `http://` / `https://` schemes (refine on top of `z.url()`). The client mirrors that contract via `isValidProjectLink` in `projects.tsx`. Tests cover both `"not a url"` and `"javascript:alert(1)"`. - **`display_order`**: defaulted to 0 by the repo on insert, untouched by update. Reserved for the eventual reordering feature; no client surface today. - **Out of scope** as per the ticket: reordering, linking projects to jobs/contracts, attachments, external import. Linked: [#26](https://forge.wynning.tech/james/carol/issues/26). See [`idea.md`](https://forge.wynning.tech/james/carol/src/branch/main/idea.md) → _Experience_ for the product context. Closes #26.
Adds the `projects` table (per-user, cascading on user delete) plus
`ProjectsRepository` mirroring the EducationsRepository / NotesRepository
pattern: cursor-paginated list, unbounded list, findById, create,
partial-update, deleteById. `display_order` is reserved for a future
reordering feature; the rest of the columns back the standalone-project
CRUD surface from idea.md's Experience section.

Dual-engine tests under tests/db/projects.test.ts; `projects` added to
KYSELY_TABLES before `users` for the Postgres drop order (#259).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the four-handler shape (list, create, patch, delete) mirroring
/api/educations. Cross-user lookups 404 per the don't-leak-existence
rule; cursor-pagination uses the shared timestamp+id envelope from
lib/api/pagination.ts.

zod DTOs in lib/dto/project.ts validate name (1..200), description
(nullable, ≤2000), and link (nullable, http(s):// only — javascript:
and friends rejected at the zod layer). Update DTO requires at least
one field via the shared refine convention.

OpenAPI 3.1 registry entries added in lib/api/openapi-routes.ts;
openapi.json regenerated and drift-checked. HTTP-layer tests under
tests/api/projects.test.ts cover auth, ownership 404s, pagination,
URL validation, and the empty-patch 400.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`useProjects`, `useCreateProject`, `useUpdateProject`,
`useDeleteProject` — same shape as the educations hooks. Mutations
patch the cached list in place to avoid edit-cycle flicker.

`keys.projects` added to the central key registry; the
distinct-first-segment invariant test updated to 10 buckets.
Generated schema regenerated from the bumped openapi.json.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the projects.* keys the new CRUD screen consumes — title/lede,
field labels and placeholders, button states, loading/empty/error
strings, and a client-side linkInvalid hint. Sentence-case + first-
person per the design system voice guide. es.json keeps falling back
to English per-key (per the existing partial-locale convention).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat(client): projects screen — list, create, edit, delete (#26)
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 14s
PR / Static analysis (pull_request) Successful in 1m36s
PR / OSV-Scanner (pull_request) Successful in 1m30s
PR / OpenAPI (pull_request) Successful in 2m2s
PR / pnpm audit (pull_request) Successful in 2m19s
PR / Package age policy (soft) (pull_request) Successful in 20s
PR / Typecheck (pull_request) Successful in 3m18s
PR / Client (web export smoke) (pull_request) Successful in 3m57s
PR / Lint (pull_request) Successful in 4m6s
Secrets / gitleaks (pull_request) Successful in 1m43s
PR / Build (pull_request) Successful in 4m18s
PR / Test (postgres) (pull_request) Successful in 4m18s
PR / Test (sqlite) (pull_request) Successful in 4m28s
PR / Trivy (image) (pull_request) Successful in 2m50s
PR / Coverage (soft) (pull_request) Successful in 4m36s
f701111b1f
Replaces the placeholder /projects route with a real CRUD surface.
Inline create-and-edit form (mirrors notes.tsx), per-row Edit /
Delete actions (mirrors education in experience.tsx). Loading,
load-failed, and empty states all surfaced via the projects.*
catalog.

Enter chains the create + edit forms (name → description →
link → submit) under KeyboardAwareScrollView so the focused input
auto-scrolls into view on small screens (#258, #255). Link
validation runs client-side via lib/serverUrlValidate.ts-shaped
isValidProjectLink — http(s) only, same gate as the zod DTO.

Theme tokens are pre-flattened into plain objects once per render
(via useMemo) and applied to leaf primitives directly, per #239's
style-array audit. Tappable links call Linking.openURL — same
external-open helper account.tsx and login.tsx use.

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):

Metric Value Soft target
Lines 82.9% ≥ 50%
Branches 75.6% ≥ 75%
Functions 91.9% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 82.9% ✅ | ≥ 50% | | Branches | 75.6% ✅ | ≥ 75% | | Functions | 91.9% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit e8ebebf920 into main 2026-06-23 23:35:36 +00:00
james deleted branch 26-projects-feature 2026-06-23 23:35:36 +00:00
Sign in to join this conversation.
No description provided.