Bounty/Milestone/MaintenancePool/User list endpoints have no pagination and return the entire table in one unbounded response

Open
#63 3 comments 0 reactions 1 assignee View on GitHub

Maintainers usually reply within 1 day

@senmalong is already working on this.

Since Aug 16, 2026.

  • #67 by @ghzhost — open
  • #73 by @senmalong — open

Assessment

This issue has not been assessed yet.

Description

GrantFox OSS Maybe Rewarded Official Campaign | FWC26 performance Third Campaign very hard

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 to GET /bounties, GET /milestones, GET /maintenance-pools, and GET /users, with a sane default page size and an enforced maximum (so a caller can't request limit=999999999 and recreate the exact same problem via the query string).
  • Apply the corresponding take/skip (or keyset equivalent) to each service's underlying find()/query-builder call.
  • Return enough metadata in the response for a client to know whether more pages exist (a total count, a hasMore flag, or a nextCursor, 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, and GET /users all accept and correctly apply limit/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

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from MergeFi/backend

All issues in MergeFi/backend

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.