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-2060Done

11.3.3 The v1 PROJECT resource schema + `GET /api/v1/projects` and `GET /api/v1/projects/{projectKey}`

The entry point of the whole planning journey: an integration that holds a PAT knows its workspace but not what is in it. These two endpoints answer "which projects can I reach, and what is this one?" — and every other path in 11.3 and 11.2 is scoped by the projectKey they return.

Ships the project resource schema module as well as the two routes — per the ADR's Amendment 2, 11.3 owns the project, sprint, backlog and ready-set schemas in its own per-resource modules, on the pattern lib/api/v1/workItems/schema.ts set.

What to build

  • lib/api/v1/projects/schema.ts — the zod response schema for a project, plus its mapper. Field by field, never a spread, exactly as 11.2.2 established: ProjectDTO is an internal shape that changes whenever a page needs it to, and §8's additive-only promise cannot ride something nobody promised to keep still. Decide field by field what is public; accessLevel is a closed vocabulary and takes the same satisfies + AssertTotal compile-time totality guard the work-item vocabularies use, so a value added to the DTO union breaks the build here rather than shipping as a response the schema rejects at runtime.
  • GET /api/v1/projects — the token workspace's browsable projects, cursor-paged via 11.3.2. projectsService.listProjects(workspaceId, actorUserId) already applies the browse-access checks and returns only non-archived rows; the route pages over that bounded read in memory, the way GET /api/v1/workspaces does — this is a workspace's own project list, not a 1800-row collection. Note the ordering constraint that forced the generalized cursor: ProjectDTO.createdAt is optional and NOT loaded on this path, so the position must be one the list read actually carries.
  • GET /api/v1/projects/{projectKey} — one project. projectsService.getDetails(key, ctx) is the read that loads the details-path fields (createdAt, previousKeys); getByKey is the hot read that does not. Pick one deliberately and say which fields the choice makes available — the two return the SAME DTO type with different fields populated, which is exactly the kind of difference a spread would hide.

The project identifier (MOTIR) is what the path takes and what the response leads with; the internal cuid does not cross the wire (ADR §7).

Scope

read on both. This card ships no project WRITE — create, rename, archive, key change, membership and access-level are privileged administration the story's boundary excludes and no card here defers work to them.

Acceptance criteria

  • Both endpoints exist, declare scope: 'read', go through withV1Route, and carry the request-id and rate-limit headers on every exit path (inherited from the wrapper — asserted by 11.1's shipped tree-wide guard, not re-tested here).
  • Every response body is produced by the declared zod schema, not shaped inline, and carries no internal cuid for the project.
  • The project vocabulary (accessLevel) is TOTAL over its DTO union by a COMPILE-time guard, not a runtime test.
  • GET /api/v1/projects pages by cursor over a position the list read actually carries, defaults to 50 and clamps at 100; the last page reports nextCursor: null rather than requiring an extra empty round trip; an empty workspace is 200 with empty items, never a 404.
  • A project the token owner cannot browse does not appear in the list, and reading it by key is a 404, not a 403 — asserted for both the cross-tenant case and the same-tenant-not-browsable case, since those are different code paths that must give the same answer.
  • An unknown projectKey is a 404 with { code, error }, not a 500.
  • The detail response's field set is justified against the read chosen (getDetails vs getByKey), so a field is never null merely because the hot read did not load it.
  • Unit tests ship with the routes; every new file holds the ≥90% per-file coverage floor.
  • ONE PR against motir-core.

Context refs

  • lib/services/projectsService.tslistProjects(workspaceId, actorUserId), getByKey(key, ctx), getDetails(key, ctx), and ProjectNotFoundError.
  • lib/dto/projects.tsProjectDTO (note createdAt? and previousKeys? are load-path-dependent) and PreviousKeyDTO.
  • lib/api/v1/workItems/schema.ts — the module pattern to mirror: field-by-field mappers, the satisfies + AssertTotal vocabulary guards, and the recorded reasoning for every deliberate omission.
  • app/api/v1/workspaces/route.ts — the precedent for paging a bounded, already-read collection.
  • app/api/v1/projects/[projectKey]/work-items/route.ts — the shipped project-scoped route shape (parse first, resolve the key, present through a schema).
  • Blockers: 11.3.1 (the schema + cursor contract), 11.3.2 (the cursor primitive). Pattern precedent: 11.2.2. Parent story: 11.3.