--- phase: 03-event-write-back-pwa-install plan: 03 subsystem: api tags: [hono, drizzle, zod, calendarOutbox, write-back, outbox-pattern, access-control, tdd] requires: - phase: 03-event-write-back-pwa-install/03-01 provides: calendarOutbox schema + calendarEvents.objectUrl + Wave-0 RED test scaffold - phase: 03-event-write-back-pwa-install/03-02 provides: broker primitives (vevent.ts, write.ts) — not used by routes but confirm broker boundary provides: - POST /api/events/create — validates, checks D-03 ownership, enqueues pending outbox row, returns 202 with uid - PATCH /api/events/:uid/edit — looks up event, checks ownership, enqueues update or transaction-paired delete+create for calendar moves - DELETE /api/events/:uid — looks up event, checks ownership, enqueues delete row with server-side etag - GET /api/events/sync-status — member-scoped outbox status poll (D-09) - GET /api/events/writable-calendars — authoritative D-03 writable set (own personal + shared Family; never other member's personal) - zod schemas for event fields (title 255, location/description 2000 — T-03-08 bounds) affects: - 03-04 (outbox worker drains rows these endpoints enqueue) - 03-05 (EventForm + client.ts consume these endpoints + writable-calendars) tech-stack: added: [] patterns: - "resolveUserId(c): dev-bypass c.get('user') first, fallback to getAuth(c) for OIDC — same pattern as me.ts" - 'Enqueue-only write endpoints: no Fastmail call in routes; db.insert(calendarOutbox) is the only side effect' - 'Edit-as-move: db.transaction with paired delete+create sharing a groupId (D-04)' - 'sync-status: .orderBy(desc(createdAt)).limit(1) to get latest outbox row; userId-scoped (T-03-07)' - 'writable-calendars: WHERE userId=currentUser.id OR isShared=1 — authoritative D-03 enforcement (T-03-11)' - 'Test mock pattern for db.transaction: factory fn cb receives mock tx with insert; vi.mock hoisted factory captures mutable refs' - "devAuthBypass mock in tests: vi.mock('../auth/devBypass.js') injects dev user so write tests get authenticated context" key-files: created: [] modified: - apps/api/src/routes/events.ts - apps/api/tests/routes/events.test.ts key-decisions: - 'resolveUserId helper uses any type to avoid Hono context generic complexity — acceptable for internal helper' - 'Two-query ownership check for edit/delete (get event, then check calendar isShared) to maintain simple from().where() chain that test mocks can intercept without innerJoin complexity' - 'Writable-calendars response maps to { url, displayName, color, isShared } — the Plan 05 WritableCalendar shape' - "sync-status returns { uid, status: 'done' } when no outbox row found (nothing pending = settled)" patterns-established: - 'Enqueue-only write route: validate → check ownership → db.insert(calendarOutbox) → return 202; no broker call' - 'D-03 ownership enforcement at two layers: write endpoints AND writable-calendars listing' - 'vi.mock devAuthBypass for write-endpoint tests avoids needing ENV manipulation or OIDC infrastructure' requirements-completed: [CAL-04, CAL-05, CAL-06, CAL-07] duration: 7min completed: 2026-06-05 --- # Phase 03 Plan 03: Write API Surface Summary **Hono write endpoints (create/edit/delete + sync-status + writable-calendars) enqueue to calendarOutbox with D-03 ownership enforcement; zod-validated, 202 optimistic-accept, no Fastmail call** ## Performance - **Duration:** ~7 min - **Started:** 2026-06-05T17:51:00Z - **Completed:** 2026-06-05T21:58:08Z - **Tasks:** 3 (Tasks 1-2-3 implemented in one feat commit; TDD RED gate committed separately) - **Files modified:** 2 ## Accomplishments - All five write/status/writable-calendars endpoints enqueue-only and member-scoped - D-03 ownership enforced on both the write path and the writable-calendars listing - D-04 edit-as-move pair implemented transactionally (db.transaction with shared groupId) - D-09 polling endpoint (sync-status) live with strict userId scoping (T-03-07) - Broker boundary preserved: no tsdav import in routes/events.ts ## Task Commits 1. **RED gate** — `e14c5da` (test): extend events tests — write/sync-status/writable-calendars endpoints (9 new failing tests) 2. **GREEN + Tasks 1/2/3** — `0a82223` (feat): implement write API surface — all 69 events tests GREEN, tsc clean ## Files Created/Modified - `apps/api/src/routes/events.ts` — extended with POST /create, PATCH /:uid/edit, DELETE /:uid, GET /sync-status, GET /writable-calendars; auth helper; zod schemas; `db.transaction` for edit-as-move - `apps/api/tests/routes/events.test.ts` — extended with 9 new write-endpoint tests; wired db.insert + db.transaction into vi.mock; added devAuthBypass mock for auth injection ## Decisions Made - **resolveUserId uses `any` type:** Hono's generic context type is complex to thread through a standalone helper; `any` is acceptable for an internal module-private helper that does a simple property access. - **Two-query ownership check for edit/delete:** Rather than innerJoin (which would break the flat from().where() mock chain in tests), the implementation does a second query on calendars to check isShared when the event's userId doesn't match. Both queries share the same mock chain in tests, which works because both return the seeded mockDbRows. - **writable-calendars response shape:** `{ url, displayName, color, isShared }` matches the `WritableCalendar` shape Plan 05's `fetchWritableCalendars` expects. - **sync-status default to 'done':** When no outbox row exists for a UID, the endpoint returns `{ uid, status: 'done' }` — nothing pending means the event is settled. ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 1 - Bug] TypeScript error on resolveUserId helper** - **Found during:** Task 1 (implementation) — tsc --noEmit reported TS2493/TS2339 on complex Hono context type inference - **Issue:** The helper function tried to infer the Hono context type from `eventsRouter.get` parameters, which failed due to tuple type length mismatch - **Fix:** Changed helper parameter to `any` with inline cast; added clarifying comment - **Files modified:** apps/api/src/routes/events.ts - **Verification:** `tsc --noEmit` passes clean - **Committed in:** 0a82223 --- **Total deviations:** 1 auto-fixed (Rule 1 - type error) **Impact on plan:** Minor typing accommodation; no behavior change. ## Issues Encountered - Test mock architecture required careful design: the existing `vi.mock` for db/client.js only mocked `db.select`; extending it to include `db.insert` and `db.transaction` required restructuring the mock factory to use mutable `vi.fn()` references that can be reassigned in `beforeEach`. The devAuthBypass mock was added to give write-endpoint tests an authenticated user context without ENV manipulation. ## Known Stubs None — all endpoints are fully wired to the DB schema. The outbox rows they insert will be drained by the Plan 04 worker; until that plan runs, rows accumulate in pending state (correct behavior). ## Threat Flags No new network endpoints or auth paths beyond what is in the plan's threat model. All T-03-06 through T-03-11 mitigations are implemented. ## Self-Check - [x] `apps/api/src/routes/events.ts` exists and includes all 5 endpoints - [x] `apps/api/tests/routes/events.test.ts` exists and tests are GREEN (69 passed) - [x] Commits e14c5da (test RED) and 0a82223 (feat GREEN) exist - [x] `grep -c "tsdav\|createFastmailClient" apps/api/src/routes/events.ts` = 1 (comment only, not import) - [x] `grep -c "db.transaction" apps/api/src/routes/events.ts` = 1 - [x] tsc --noEmit passes clean ## Self-Check: PASSED ## Next Phase Readiness - Plan 04 (outbox worker): `calendarOutbox` rows are being enqueued; worker can now drain them - Plan 05 (EventForm + client.ts): POST /create, PATCH /:uid/edit, DELETE /:uid endpoints are live; GET /writable-calendars provides the picker data --- _Phase: 03-event-write-back-pwa-install_ _Completed: 2026-06-05_