Skip to content

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

Description

@chonilius

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.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignperformancePerformance/optimization issuevery hardVery difficult task, expert-level effort required

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions