Bounty/Milestone/MaintenancePool/User list endpoints have no pagination and return the entire table in one unbounded response
Maintainers usually reply within 1 day
@senmalong is already working on this.
Since Aug 16, 2026.
Assessment
This issue has not been assessed yet.
Description
Overview
The primary list() methods behind this API's core GET collection endpoints fetch every row in the table with no LIMIT/OFFSET and no way for a caller to request a page:
// src/bounties/bounties.service.ts:174-176
async list(status?: BountyStatus): Promise<Bounty[]> {
return this.bountyRepo.find({ where: status ? { status } : {} });
}
// src/milestones/milestones.service.ts:131-133
async list(): Promise<Milestone[]> {
return this.milestoneRepo.find();
}
// src/maintenance-pool/maintenance-pool.service.ts:109-111
async list(): Promise<MaintenancePool[]> {
return this.poolRepo.find();
}
// src/users/users.service.ts:101-103
async list(): Promise<User[]> {
return this.userRepo.find();
}
Each is wired straight to a public, unpaginated GET route (GET /bounties, GET /milestones, GET /maintenance-pools, GET /users) with no query parameters accepted for limit/offset/cursor anywhere in the corresponding controllers. BountiesController.list does accept a status filter, which narrows the result set somewhat, but every un-filtered call — and every call on the other three endpoints, which don't even offer a filter — returns literally every row that has ever existed in that table, in one response, every time. For a platform whose entire premise is an open, growing catalog of GitHub bounties across many repositories, bounties/milestones are exactly the tables expected to grow without bound over the platform's lifetime; this isn't a "might matter someday at extreme scale" concern, it's the shape every one of these tables is designed to take on by the time the platform has any meaningful usage history.
This is a different problem from the two related, already-tracked issues in this repo: the open "N+1 query and unbounded loop patterns across bounty expiry, milestone resolution, and reputation computation" issue is about repeated per-row queries inside a loop (an N+1 shape); the open "Analytics payout heatmap and top-clients queries likely load unbounded result sets into memory" issue is about AnalyticsService's own aggregation queries for a single user's history. Neither names BountiesController.list, MilestonesController.list, MaintenancePoolController.list, or UsersController.list — this issue is about the primary bulk-listing endpoints of four separate resources returning their entire backing table in a single unbounded query and a single unbounded HTTP response body, a distinct failure shape (one query, unboundedly large, not many small ones) from both of those.
Requirements
- Add standard
limit/offset(or cursor-based) pagination query parameters toGET /bounties,GET /milestones,GET /maintenance-pools, andGET /users, with a sane default page size and an enforced maximum (so a caller can't requestlimit=999999999and recreate the exact same problem via the query string). - Apply the corresponding
take/skip(or keyset equivalent) to each service's underlyingfind()/query-builder call. - Return enough metadata in the response for a client to know whether more pages exist (a
totalcount, ahasMoreflag, or anextCursor, per whatever pagination convention the rest of this API's forthcoming versioning work — see the related open "Add API versioning strategy" issue — ultimately settles on; this issue shouldn't block on that decision, but should use a reasonable, documented shape now rather than inventing something that has to change again later). - Add a test that seeds more rows than the default page size for at least one of the four endpoints and asserts the response is capped and paginated, not returning everything.
Acceptance Criteria
-
GET /bounties,GET /milestones,GET /maintenance-pools, andGET /usersall accept and correctly applylimit/offset(or equivalent) parameters. - Each has an enforced maximum page size that can't be bypassed via the query string.
- A test proves that seeding N rows greater than the default/max page size still returns a capped, paginated result.
- Existing callers relying on the current unpaginated shape (if any exist in this codebase, e.g. internal service-to-service calls to
list()) are audited and updated as needed rather than silently truncated.
Additional Notes
Precise references: src/bounties/bounties.service.ts:174-176 and src/bounties/bounties.controller.ts:25-28; src/milestones/milestones.service.ts:131-133 and src/milestones/milestones.controller.ts:32-35; src/maintenance-pool/maintenance-pool.service.ts:109-111 and src/maintenance-pool/maintenance-pool.controller.ts:39-42; src/users/users.service.ts:101-103 and src/users/users.controller.ts:17-20.
Test/reproduction plan:
for (let i = 0; i < 250; i++) {
await bountyRepo.save({ issueId: /* ... */, sponsorId, amount: '10.0000000', status: BountyStatus.OPEN });
}
const res = await request(app).get('/bounties').expect(200);
// pre-fix: res.body.length === 250 (or however many were seeded — unbounded)
// post-fix: res.body.length <= the enforced default/max page size, with pagination metadata present
Cross-references: distinct from the open "N+1 query and unbounded loop patterns across bounty expiry, milestone resolution, and reputation computation" issue (a different problem shape — repeated small queries in a loop, not one large unbounded one) and the open "Analytics payout heatmap and top-clients queries likely load unbounded result sets" issue (scoped to AnalyticsService's own per-user aggregation queries, not the primary resource-listing endpoints this issue covers). Loosely related to the open "Add API versioning strategy" issue, since the pagination response shape chosen here is exactly the kind of API contract decision that's painful to change later without versioning in place — worth a one-line acknowledgment in the PR, not a blocker.
- Dominant language
- TypeScript
- Stars
- 2
- Forks
- 90
- Avg merge
- 31m
- Merged PRs (30d)
- 20
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- No contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from MergeFi/backend
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug security
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
-
bug testing
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
bug security
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
zapcooking/frontend#946 ·
Maintainers usually reply within 1 day
-
enhancement good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
WiliamRosa/cosmos-semantic-platform#40 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
liberation-data/drivine#733 · 1 comment ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
makecindy/cindy#10062 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day