Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2202Done

11.1.7 `/api/v1/me` and `/api/v1/workspaces` MAP through `lib/api/v1/identity/schema.ts` — the shape becomes the value the routes emit, not a description beside them

Repo: moooon-B-V/motir-core · ONE PR. The code half of MOTIR-2195's decision, carved out because it changes response-shaping code and is not a documentation edit (that card's own third acceptance criterion says so).

What was decided, and what is left

ADR Amendment 5 (docs/decisions/public-api-conventions.md, 2026-08-05, shipped by MOTIR-2195) settled two things. The first — Story 11.1 owns the identity and workspace shapes, in lib/api/v1/identity/ — is a record change and landed with the amendment. The second is this card:

A v1 route MAPS THROUGH its schema — the shape is the value the route emits, not a description written beside it.

Every other v1 resource already does it: app/api/v1/projects/route.ts hands presentProject to the pager, the work-item routes return presentWorkItemDetail / presentWorkItemSummary, the ready set maps through presentReadyItem. The two OLDEST endpoints in the API do not. meSchema and workspaceSummarySchema describe what app/api/v1/me/route.ts and app/api/v1/workspaces/route.ts return; the routes build their rows from inline object literals that no schema constrains.

The gap that closes. A field added to /me's inline literal does not fail typecheck — it fails only tests/api/v1/openapi-operations-coverage.test.ts's .strict() parse, at run time, in a suite about something else. Every other resource fails at the compiler, at the seam that decides what the public API says. That is the weaker guarantee this card removes: the document and the endpoint become the SAME expression rather than two a test says agree today.

What must NOT change: the reason the routes shape explicitly. app/api/v1/me/route.ts records it — "the response is shaped explicitly rather than spread, because verify returns the raw Prisma User row and a public API must never leak one" — and workspaces/route.ts says the same for its rows. The mapper does not weaken that instinct, it institutionalises it: field by field, never spread, in ONE place instead of in each handler. ADR Amendment 2's corollary is the same rule one level up.

Response bytes do not change. This is a refactor of WHERE the shape is expressed, not WHAT it is. /me keeps { user: { id, name, email }, workspaceId, scopes }; a workspace row keeps { id, name, slug, createdAt } with createdAt an ISO string. Any byte difference is a §8 breaking change and a defect in this card.

Acceptance criteria

  • lib/api/v1/identity/schema.ts exports presentMe and presentWorkspaceSummary mappers on the pattern of presentProject (lib/api/v1/projects/schema.ts:101): each shapes field by field and never spreads, takes the SERVICE-side value as its input type, and returns the z.infer type of its schema.
  • app/api/v1/me/route.ts returns presentMe(...)'s output and app/api/v1/workspaces/route.ts passes presentWorkspaceSummary as paginateKeyset's row mapper — neither handler contains an inline object literal for a response row any more, found by reading both files rather than by running until green.
  • A field added to the underlying Prisma row (User, Workspace) still cannot reach either response — the mapper's input type is the service's return value, so widening the row changes nothing until the mapper is edited.
  • The response BYTES are unchanged for both endpoints: a test asserts each endpoint's exact key set (/me: user.{id,name,email}, workspaceId, scopes; a workspace row: id, name, slug, createdAt as an ISO string) against a REAL response driven with a real PAT, not against a fixture built from the schema.
  • tests/api/v1/openapi-operations-coverage.test.ts and tests/api/v1/openapi-drift-guard.test.ts still pass — their assertions are preserved; neither is edited to accommodate the change.
  • The WHY comment moves with the shaping: the "must never leak a raw Prisma row" reasoning lives in the schema module beside the field list, and each route keeps a one-line pointer to it rather than a copy.
  • lib/api/v1/identity/schema.ts's header note — currently "These schemas DESCRIBE today; they will EMIT" — is updated to state that they now emit, and to drop the "until it lands" clause.
  • The per-file coverage floor (≥90%) holds on every file this card changes.

Context refs

  • lib/api/v1/identity/schema.tsmeSchema, workspaceSummarySchema, and the header this card finishes.
  • app/api/v1/me/route.ts · app/api/v1/workspaces/route.ts — the two inline literals, and the comments that explain why they are explicit.
  • lib/api/v1/projects/schema.ts:101 (presentProject) + app/api/v1/projects/route.ts — the owned-and-mapped-through pattern to mirror, including how a mapper is handed to a pager.
  • lib/api/v1/workItems/schema.tspresentWorkItemSummary / presentWorkItemDetail, the same pattern at a larger scale.
  • docs/decisions/public-api-conventions.md — Amendment 5 §4 (this decision) and Amendment 2's corollary (a v1 response is a schema's output).
  • tests/api/v1/openapi-operations-coverage.test.ts — drives both routes with a real PAT and parses what they actually return; the existing proof the shapes match.