docs(03-03): complete write API surface plan — enqueue-only endpoints GREEN

This commit is contained in:
Lucas Berger
2026-06-05 17:59:53 -04:00
parent 0a8222329e
commit 7398946aa8
16 changed files with 2168 additions and 10 deletions
+3 -3
View File
@@ -102,7 +102,7 @@ Plans:
**Wave 2** *(blocked on Wave 1)*
- [x] 03-02-PLAN.md — TDD: VEVENT builder (vevent.ts, D-13 DATE/DATETIME + RRULE) + tsdav write wrappers (write.ts, D-12 broker boundary)
- [ ] 03-03-PLAN.md — Write API: POST/PATCH/DELETE events + GET sync-status, enqueue-only, D-03 ownership, D-04 edit-as-move pair (CAL-04/05/06/07)
- [x] 03-03-PLAN.md — Write API: POST/PATCH/DELETE events + GET sync-status, enqueue-only, D-03 ownership, D-04 edit-as-move pair (CAL-04/05/06/07)
**Wave 3** *(blocked on Wave 2)*
@@ -161,7 +161,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
|-------|----------------|--------|-----------|
| 1. Foundation + Broker Spike | 4/4 | Complete | 2026-06-04 |
| 2. Calendar Display | 5/5 | Complete | 2026-06-05 |
| 3. Event Write-Back + PWA Install | 2/8 | In Progress| |
| 3. Event Write-Back + PWA Install | 3/8 | In Progress| |
| 4. Shared Lists + Live Sync | 0/? | Not started | - |
| 5. Web Push Notifications | 0/? | Not started | - |
@@ -171,7 +171,7 @@ Note: Phase 4 depends only on Phase 1 and can begin as soon as Phase 1 is comple
**Goal:** [Captured for future planning] Abstract the calendar backend behind a provider interface so Fastmail/CalDAV is one implementation among potentially many. Shipping with a single provider is fine, but the broker, sync, and event-expansion layers should be structured so additional providers (e.g. other CalDAV hosts, Google Calendar, generic ICS feeds) can be added without rework. Captures the "provider" seam as an explicit architectural concern.
**Requirements:** TBD
**Plans:** 1/8 plans executed
**Plans:** 3/8 plans executed
Plans:
+10 -6
View File
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: executing
stopped_at: Completed 03-02-PLAN.md
last_updated: "2026-06-05T21:50:11.190Z"
stopped_at: Completed 03-03-PLAN.md
last_updated: "2026-06-05T21:59:40.013Z"
last_activity: 2026-06-05
progress:
total_phases: 6
completed_phases: 2
total_plans: 17
completed_plans: 11
completed_plans: 12
percent: 33
---
@@ -26,7 +26,7 @@ See: .planning/PROJECT.md (updated 2026-06-03)
## Current Position
Phase: 03 (event-write-back-pwa-install) — EXECUTING
Plan: 3 of 8
Plan: 4 of 8
Status: Ready to execute
Last activity: 2026-06-05
@@ -54,6 +54,7 @@ Progress: [███████░░░] 65%
*Updated after each plan completion*
| Phase 03 P01 | 45 | 5 tasks | 8 files |
| Phase 03 P03-02 | 4 | 2 tasks | 2 files |
| Phase 03 P03-03 | 7 | 3 tasks | 2 files |
## Accumulated Context
@@ -71,6 +72,9 @@ Recent decisions affecting current work:
- Recurring events: Create + display only in v1 (CALDAV:expand on server side). Single-occurrence edit deferred to v1.x.
- [Phase ?]: D-Task5-DDL: drizzle-kit push unsafe on MariaDB 11 (mysql dialect misreads metadata, schedules truncate). Additive DDL hand-applied; adopt drizzle-kit generate+migrate before next schema change.
- [Phase ?]: D-02-RRULE: use ICAL.Recur.fromString + ICAL.Property for RRULE serialization — addPropertyWithValue produces char-split output for RECUR types
- [Phase ?]: resolveUserId helper uses any type to avoid Hono context generic complexity — internal helper only
- [Phase ?]: Write endpoints enqueue-only (calendarOutbox) with 202 optimistic-accept; no Fastmail call in routes (D-12/D-05)
- [Phase ?]: GET /writable-calendars is authoritative D-03 writable-set endpoint: WHERE userId=currentUser.id OR isShared=1
### Pending Todos
@@ -99,6 +103,6 @@ Recent decisions affecting current work:
## Session Continuity
Last session: 2026-06-05T21:50:11.159Z
Stopped at: Completed 03-02-PLAN.md
Last session: 2026-06-05T21:59:40.006Z
Stopped at: Completed 03-03-PLAN.md
Resume file: None
@@ -0,0 +1,143 @@
---
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*