WI-032: Log a play
WI-032: Log a play
The item ADR-0007 and ADR-0006 were written for. Until now the shelf’s rating and review have always been null, because nothing could create the sessions they project from.
In scope
POST /games/:slug/sessions— everything optional; no body at all is valid.GET /me/sessions— your plays, newest first.DELETE /me/sessions/:id— scoped to the author.- The query layer in
packages/db.
Out of scope
- Participants and winners.
playersandsession_participantsexist and need their own item — the Player/User claim flow is the delicate part of the schema and deserves more than a footnote here. - The logging UI. The routes come first, as with WI-030 before WI-031.
Schema depth is not form depth
Everything except the game is nullable, so POST with no body means “played this, today” and is a complete record rather than a draft. That is ADR-0007’s decision, and it is why an absent body is parsed as {} instead of rejected.
Logging is not upserting
Unlike the shelf, this deliberately inserts every time. Playing the same game twice in one day is two sessions, and ADR-0006 keeps every rating rather than overwriting because opinion changing over time is the product. Deduplicating would silently discard the second play.
The seeded version of that mistake fails five tests, including all three projection invariants — which is the right blast radius for it.
The projection is the point
Nothing here writes shelf_entries; it could not, the trigger raises (ADR-0021). So the shelf changing at all is evidence the projection fired.
Three invariants have tests:
- the newest rating wins, and the older session keeps its own
- rating and review project independently — a later play rated but not reviewed does not blank the review
- deleting a session re-projects, restoring the previous rating rather than blanking the shelf
Definition of done
- A
POSTwith no body records a play today. - The same game twice is two sessions.
- A rating reaches the shelf without anything writing
shelf_entries. - The newest rating wins; older sessions are untouched.
- Rating and review project independently.
- Deleting re-projects rather than blanking.
- One user cannot list or delete another’s sessions.
- Ratings outside 1–10, non-integer ratings, zero durations and malformed dates are 400s.
- A malformed session id is a 404, not a 400 — whether a session exists should not be inferable.
- Each check proven able to fail.
- Gate green.
Verification
pnpm --filter @tabletop/api testpnpm gateNotes
playedAt arrives as an ISO string and the query layer wants a Date. The first version spread the request body after converting it, which put the string back — caught by tsc, and it would have inserted a string into a timestamptz.
The response includes the shelf as it now stands, because logging a rating changes it and the client would otherwise have to ask again to find out.