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

11.1.3 Opaque-cursor pagination + the shared list envelope, proven by `GET /api/v1/workspaces`

Repo: motir-core. One PR. The collection half of the /api/v1 envelope: ONE pagination shape every list endpoint in 11.2 / 11.3 reuses, plus the first real paginated endpoint.

blocked_by 11.1.2 — it composes into that wrapper rather than wrapping it again.

What to build

1. The cursor + list envelope primitive in lib/api/v1/: parse and validate ?cursor=&limit=, clamp limit (default 50, hard max 100), encode/decode the OPAQUE cursor, and shape the response as one list envelope (items + the next cursor) every collection returns.

Reuse the encoding idiom already shipped in lib/mcp/searchCursor.ts — the CLI's search_work_items paging already rides it, so the shape is proven. The cursor stays opaque: a client must not be able to hand-craft one, because that would freeze the underlying keyset as public API and make a future index change a breaking change.

2. GET /api/v1/workspaces — the token owner's workspaces, paginated. Chosen as the proving endpoint because it is a genuinely unbounded collection whose access rules are already enforced by the service, so the card tests PAGINATION rather than resource modelling.

Why cursor and not offset — the property to actually test

The ADR rejects offset pagination because Motir's collections mutate while a client pages them, and offset silently skips and duplicates rows when a row is inserted or removed mid-scan. That is not a theoretical concern for a PM tool where an agent loop writes while another reads. So this card does not merely implement a cursor — it proves the property: a test inserts a row between two page fetches and asserts no row is skipped and none is returned twice. A pagination card whose tests only walk a static fixture has not tested pagination.

Completeness

Real-product shape, not the green path: an empty collection returns an empty items with no next cursor (not a 404); the final page reports no next cursor rather than an empty-page round trip; a malformed or foreign cursor is a 422 with a code, never a 500 and never a silent reset to page one — a silent reset is the failure mode that makes a client loop forever.

Scope BOUNDARY

Ends at the pagination primitive + /api/v1/workspaces. It does NOT add filtering or sorting (the FilterAST arrives with the work-item list in 11.2); does NOT add rate limiting (11.1.4); does NOT add any other resource endpoint; does NOT change lib/mcp/searchCursor.ts or any MCP tool (reuse the idiom; if the code is genuinely shareable, extract it WITHOUT changing MCP behaviour, and if that is not clean, duplicate the ~20 lines rather than refactoring a shipped surface inside this card); does NOT change any service, repository or migration; writes no OpenAPI spec (11.4).

Acceptance criteria

  • GET /api/v1/workspaces returns the token owner's workspaces in the shared list envelope, gated on read.
  • limit defaults to 50 and is CLAMPED at 100; limit=0, a negative and a non-numeric value are each rejected as 422 with a code, not silently coerced.
  • Paging the whole collection with the returned cursor visits every row exactly once, and the last page reports no next cursor.
  • The mutation case: inserting a row between two page fetches skips no row and duplicates none — the property offset pagination cannot provide, asserted directly.
  • An empty collection returns an empty items and no next cursor, with a 200.
  • A malformed, truncated or foreign cursor returns 422 with a code — never a 500, and never a silent reset to the first page.
  • The cursor is opaque: the test asserts a client cannot construct a valid one from row data, so the keyset is not accidentally public API.
  • The envelope is reusable — a second fixture collection adopts it with no copied parsing logic.
  • Cross-tenant isolation holds: only the token's own workspaces appear.
  • The route contains no db.* and no $transaction; one service call.
  • Unit + integration tests ship in the same PR against real Postgres; the per-file coverage floor (≥90%) holds on every new file.

Context refs

  • lib/mcp/searchCursor.tsencodeSearchCursor / decodeSearchCursor, the shipped opaque-cursor idiom to reuse.
  • lib/mcp/tools/searchWorkItems.ts — how the shipped tool wraps a service page in a cursor (FILTER_ROW_CAP, the 50-row server cap).
  • lib/services/workspacesService.ts — the service the endpoint adapts.
  • lib/api/v1/ — the wrapper from 11.1.2 this composes into.
  • Decision: 11.1.1 (the pagination axis). Parent story: 11.1.