feat(access-api): offset pagination and sorting for admin members list - #315
Merged
Lakes41 merged 1 commit intoJul 29, 2026
Conversation
DiegoMoron0102
force-pushed
the
feat/issue-259-members-pagination
branch
from
July 29, 2026 02:25
57e992b to
d70266e
Compare
Closes Adamantine-guild#259 Replace the cursor-based listing of `GET /v1/communities/:communityId/members` (added in Adamantine-guild#236) with an offset-paginated, sortable listing that returns a shared `PaginatedResponse` envelope: { data, total, page, pageSize, nextCursor } Offset pagination is required because the issue asks for a `total` count, which cursor pagination cannot produce cheaply. - Add `page` (>= 1, default 1) and `pageSize` (1..100, default 25) query params, validated with Zod at the route layer and again in the service, so out-of-range values return 400 VALIDATION_ERROR instead of being clamped. - Add `sort` (`joinedAt` | `role`, default `joinedAt`) with a stable `id ASC` tiebreaker, so pages never overlap or drop rows when members share a role or a join date. - `total` comes from a parallel `count()` so clients can render page counts without a second request. The extra COUNT(*) is deliberate: an approximate or cached number would mislead admins. - `nextCursor` is always null in offset mode but stays in the contract, so a later move to cursor pagination is not a breaking change for consumers. - Move the pre-existing `status` filter from post-fetch JS filtering into the Prisma `where` clause. Filtering after skip/take silently returned short pages; the SQL predicate mirrors getNormalizedMembershipState(). - Export a generic `PaginatedResponse<T>` from `@guildpass/shared-types` and update `API_CONTRACT`, `docs/openapi.json` (regenerated), the v1 contract snapshot, the sdk-lite client + README, and the README endpoint table. - Update service, integration, contract and rate-limit tests accordingly.
DiegoMoron0102
force-pushed
the
feat/issue-259-members-pagination
branch
from
July 29, 2026 02:36
d70266e to
0ca3725
Compare
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #259.
GET /v1/communities/:communityId/membersnow returns an offset-paginated, sortablelist using a shared
PaginatedResponseenvelope, replacing the cursor-based listingadded in #236:
{ "data": [ /* members */ ], "total": 42, "page": 1, "pageSize": 25, "nextCursor": null }Offset pagination is what the issue's acceptance criteria require, because they ask for
a
totalcount — cursor pagination cannot produce that cheaply.Query parameters
page1400 VALIDATION_ERRORon 0, negatives, NaNpageSize25400 VALIDATION_ERRORabove the capsortjoinedAt|rolejoinedAt400 VALIDATION_ERRORon anything elserolestatusValidation happens twice on purpose: Zod at the route layer (so bad input never reaches
the DB) and again in the service (so direct service callers get the same guarantees).
Behaviour
id ASCtiebreaker, so pages can never overlap or drop arow when several members share the same
joinedAtorrole.totalis the count of all members matching the filters, not just the current page,so clients can render page counts without a second request. That extra
COUNT(*)isa deliberate cost — an approximate or cached number would mislead admins.
nextCursoris alwaysnullin offset mode, but stays in the contract on purpose:moving to cursor-based pagination later will not be a breaking change for consumers.
200with an emptydataarray and the realtotal,rather than a 404.
preHandlerchain are unchanged.Drive-by fix:
statusfilter was breaking paginationThe
statusfilter was applied in JavaScript after the page had been fetched, so?status=active&pageSize=25could return fewer than 25 rows while more matches existedon later pages. It is now a Prisma
wherepredicate that mirrorsgetNormalizedMembershipState(), so filtering happens beforeskip/take.Contract, docs and clients
PaginatedResponse<T>exported from@guildpass/shared-types.API_CONTRACT,docs/openapi.json(regenerated viaopenapi:generate), thesdk-liteclient + README, and the README endpoint table all updated.packages/shared-types/contracts/v1/schemas.json— the members entry updated to thenew shape. Flagging this for maintainers: per
docs/API_VERSIONING.mda responsereshape is a breaking change that warrants a MAJOR bump. I did not bump
x-guildpass-api-version, following the precedent of Add pagination and filtering to the admin member listing endpoint #236 (7b885a2), which reshapedthis same endpoint's response without a bump or snapshot update. Happy to switch to a
contracts/v2/snapshot and a 2.0.0 bump if you'd prefer that instead.Testing
The full CI pipeline was run locally against this branch:
pnpm lint:cipnpm build(incl.tscon access-api)pnpm test:citest:cideliberately excludes a set of suites that need a live database, and three ofthe files this PR touches (
memberService.test.ts,routes.integration.test.ts,rateLimit.test.ts) fall inside that exclusion list. They were run separately andcompared against a baseline run of
origin/mainwith these changes removed: thefailing suites are byte-for-byte the same set on both sides, and this branch adds 3 net
passing tests. Those pre-existing failures are unrelated to pagination — they need a
real database or hit known breakage in the badges/roles/outbox paths.
eslinton the touched files goes from 9 errors onmainto 8 here (an unused importbecame used).
New and updated coverage for this change:
memberService.listMembersForAdmin— defaults,skip/takemath,totalcorrectness, 400 on
page < 1/pageSizeoutside[1,100],sort=roleorderingwith tiebreaker, active-roles-only, empty tail page, and both
statuspredicates.routes.integration— envelope defaults, param forwarding, 400 for out-of-rangevalues without hitting the service, and walking two pages.
openApiContractandapiContractCompatibility— validate against the regeneratedspec and the updated snapshot.
Breaking change
The response shape of this endpoint changed. Consumers reading
body.membersorbody.pagination.hasMoremust move tobody.data/body.total+body.page, and?limit=/?cursor=become?pageSize=/?page=.sdk-liteis updated in this PR.