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
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.
Overview
The primary
list()methods behind this API's coreGETcollection endpoints fetch every row in the table with noLIMIT/OFFSETand no way for a caller to request a page:Each is wired straight to a public, unpaginated
GETroute (GET /bounties,GET /milestones,GET /maintenance-pools,GET /users) with no query parameters accepted forlimit/offset/cursoranywhere in the corresponding controllers.BountiesController.listdoes accept astatusfilter, 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/milestonesare 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 namesBountiesController.list,MilestonesController.list,MaintenancePoolController.list, orUsersController.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
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).take/skip(or keyset equivalent) to each service's underlyingfind()/query-builder call.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).Acceptance Criteria
GET /bounties,GET /milestones,GET /maintenance-pools, andGET /usersall accept and correctly applylimit/offset(or equivalent) parameters.list()) are audited and updated as needed rather than silently truncated.Additional Notes
Precise references:
src/bounties/bounties.service.ts:174-176andsrc/bounties/bounties.controller.ts:25-28;src/milestones/milestones.service.ts:131-133andsrc/milestones/milestones.controller.ts:32-35;src/maintenance-pool/maintenance-pool.service.ts:109-111andsrc/maintenance-pool/maintenance-pool.controller.ts:39-42;src/users/users.service.ts:101-103andsrc/users/users.controller.ts:17-20.Test/reproduction plan:
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.