144 lines
7.9 KiB
Markdown
144 lines
7.9 KiB
Markdown
---
|
|
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*
|