Skip to content

feat(api): answer the membership lists a page at a time - #673

Merged
alukach merged 2 commits into
mainfrom
feat/api-page-membership-lists
Oct 16, 2026
Merged

alukach merged 2 commits into
mainfrom
feat/api-page-membership-lists

Conversation

@alukach

@alukach alukach commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Item 1 of the follow-ups discussed on #667: the membership lists answer the same paged shape as every other list.

What

Since #651, a list answers { "items": [...], "next_cursor": ... }, and a client follows next_cursor until it is null. The three membership lists from #642 still answered a bare array:

Endpoint Before Now
GET /memberships Membership[] MembershipPage
GET /accounts/{account_id}/members Membership[] MembershipPage
GET /products/{account_id}/{product_id}/members Membership[] MembershipPage

A bare array can't grow a cursor later without breaking whoever reads it, and nothing reads these yet: no caller in the app's API clients, the CLI (main or its open PRs) or the data proxy, and no production traffic in the last day (the window Vercel's metrics allow). The CLI is being built against the API now, so this is the cheapest moment.

Every item still comes back on one page, with next_cursor: null, and the routes take no cursor or limit yet. The membership tables are read whole today, as they were. That is the same step #665 takes for its service-account and trust lists: the shape lets a client follow next_cursor now, so real pages can come later without breaking it.

  • Page<T>, pageSchema(name, item) and onePage(items) live in src/lib/operations/result.ts, beside the other result helpers, so a list that is read whole says so in one call. feat(api): manage service accounts, their trusts and their API keys #665 defines the same page helper locally in operations/service-accounts.ts; whichever merges second can use this one. ProductPage is built from it too, so the products and membership lists share one definition of the page contract.
  • getPendingInvitation, the server action behind the invitation banner, reads value.items.

Docs, ADRs and llms.txt

Checked public/llms.txt: it names no membership endpoint. Checked docs.source.coop: no page names these endpoints. The data proxy doesn't call them, so no ADR is affected.

Testing

  • npm run type-check passes.
  • Full npx jest: 91 suites, 966 tests pass.
  • operations/memberships.test.ts expects the page shape from listMembers and listMemberships; actions/memberships.test.ts feeds getPendingInvitation a page.
  • The route-coverage test validates the regenerated document as OpenAPI 3.
  • After the review follow-up (d9354fb): npm run type-check passes, and npx jest src/lib/operations src/app/api/v1 passes 12 suites, 127 tests.

🤖 Generated with Claude Code

GET /memberships, GET /accounts/{account_id}/members and GET /products/{account_id}/{product_id}/members answer { items, next_cursor } like every other list, rather than a bare array that couldn't grow a cursor without breaking its readers. Every item is still on one page, with next_cursor null; Page, pageSchema and onePage in operations/result.ts say so. The invitation banner reads the page's items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
source-coop-ui Ready Ready Preview Oct 16, 2026 2:42am UTC
source-cooperative Ready Ready Preview Oct 16, 2026 2:42am UTC

Request Review

@claude

claude Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @alukach's task in 12s —— View job


✅ No blocking issues — safe to merge.

All three membership list endpoints and their only in-app caller (getPendingInvitation in src/lib/actions/memberships.ts) now use the page shape. I found no other callers of listMembers or listMemberships. I read the diff only; I did not run the tests or type-check.

Simplify (ponytail)

  • src/lib/operations/result.ts: result.ts now calls extendZodWithOpenApi(z) at module level. This is probably redundant if the other schema modules already do it. It is harmless, but check whether it is needed.
  • src/lib/operations/products.ts: page() still builds {items, next_cursor} by hand. That is fine, because it carries a real cursor.

Docs

The description names llms.txt, docs.source.coop and the data-proxy ADRs, and says why each one still holds. Nothing further is needed.


💰 Estimated review cost: $0.11 · 0m11s · 6 turns

ProductPageSchema now comes from the shared pageSchema(), so ProductPage and MembershipPage share one definition of the page contract. result.ts extends zod with OpenAPI itself instead of relying on an importer to have done it, and the Revoked-memberships comment is back above ListMembershipsQuerySchema, which it explains.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alukach added a commit that referenced this pull request Oct 16, 2026
Follows #667, now merged. Writes down the API conventions this round of
PRs settled on, so the next endpoint follows them without anyone
reconstructing them from the code.

## What

- **`API.md`** at the repository root, beside `CONTRIBUTING.md`:
- **One operation, thin adapters:** what an operation decides, what a
route and a server action may do, and the exceptions for routes no
session can call.
- **Documented where it's defined:** `registerPath` beside the handler,
the coverage test and its `UNREGISTERED` allowlist, published prose,
explicit `HEAD`, `bearer` and `optionalBearer`, `internal`, and
`deprecated`.
- **Resources and methods:** URL and field naming, and a table of what
each method means and answers. Deactivating is `PATCH { "disabled": true
}`, never `DELETE`, and one resource has one edit endpoint.
  - **Lists:** `{items, next_cursor}`, never a bare array.
- **Errors:** the shared shape and when each status applies, the 500
`internal` error and the trust exchange's `{trusted: false}` 403,
authorizing before validating where the permission doesn't depend on the
input, and answering 404 for things the caller may not know of.
- **Writes:** carry only the fields that changed, and what
`accountsTable.update` always writes.
- **Compatibility:** the data proxy is pinned by a test, the CLI follows
the reference, and anyone else gets a traffic check, then deprecation
rather than removal.
- **`CLAUDE.md`** (and `AGENTS.md`, which links to it) gets a short
**API** section telling an agent to read `API.md` before touching
`/api/v1`.
- **`CONTRIBUTING.md`**'s "Server Actions & API Routes" paragraph said
both call `isAuthorized()` themselves. It now describes them as adapters
over one operation and points at `API.md`.

## Where it lives

A root `API.md` rather than more of `CLAUDE.md`: these are conventions
for whoever writes an endpoint, person or agent, and much of it is
visible in the code once you know where to look, which is what
`CLAUDE.md` says it isn't for. `CLAUDE.md` carries the pointer, so
agents still load it at the right moment. `CONTRIBUTING.md` links to it.

## Merge order

The guide describes the API as it stands once the open PRs from this
round merge, and names things they add:

- `optionalBearer`, `internal` and `deprecated` endpoints (#667)
- `data-proxy-contract.test.ts` (#671)
- explicit `HEAD` (#672)
- `pageSchema` and `onePage` (#673)
- one `PATCH` per account (#674)
- `DELETE` that only deletes (#675)

Merge it after them, or trim the line that names whichever of them
doesn't land.

## Where the code doesn't follow the guide yet

The guide states the conventions, not the code's progress toward them.
Known gaps, tracked outside the docs:

- The `/data-connections` routes and the data-connection actions still
read tables and call `isAuthorized()` directly: #593.
- The credentials, profile-image and `createAccount` actions still call
`isAuthorized()` directly: #592 and #594.
- `updateProduct`, `deleteProduct` and `updateMembership` validate the
body before authorizing, so a caller who may not act sees field errors
instead of a 403.
- `accountsTable.update` always writes `name`, `type` and whole metadata
maps, so `updateAccount` can undo a concurrent edit.

## Docs, ADRs and `llms.txt`

Repository-internal guidance only: no user-facing flow changes, so
docs.source.coop and `llms.txt` are unaffected, and it records no
decision shared with the data proxy beyond the existing contract test,
so no data.source.coop ADR changes.

## Testing

Documentation only; no code changed. Each claim in `API.md` was checked
against the code after a review pass found several the code didn't hold
to (`route-coverage.test.ts`, `accountsTable.update`, `withApiSession`'s
error handling, the revocations and trust-exchange routes). Checked that
`AGENTS.md` still resolves to `CLAUDE.md` and that the prose in `API.md`
wraps at 80, as the repository's Markdown does.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@alukach
alukach marked this pull request as ready for review October 16, 2026 03:27
@alukach
alukach merged commit 488275a into main Oct 16, 2026
10 checks passed
@alukach
alukach deleted the feat/api-page-membership-lists branch October 16, 2026 03:27

This branch was successfully deployed

2 active deployments
Preview – source-cooperative — d9354fb4 Deployed Oct 16, 2026 by vercel[bot]
Preview – source-coop-ui — d9354fb4 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant