feat: shared pagination (PaginationQueryDto, Paginated<T>), weak ETags, TRUST_PROXY - #344
Merged
EmmanuelOchaje merged 4 commits intoOct 4, 2026
Conversation
Add PaginationQueryDto in src/common (page default 1, limit default 20,
max 100) and a Paginated<T> type with paginate() / toSkipTake() helpers.
PaginatedResponseInterceptor turns a handler's [items, total] result,
the shape of prisma.$transaction([findMany, count]), into
{ items, total, page, limit }.
GET /events now uses both: it takes ?page=&limit=, orders by startsAt
then id so rows can't shift between pages, and returns a Paginated body
instead of a bare array. Documented in docs/API.md.
Closes StellarTickets#245
Closes StellarTickets#246
Move HTTP setup from main.ts into configureApp() (src/app.setup.ts) so
specs exercise the same middleware and Express settings as production.
- Enable weak ETags explicitly (app.set('etag', 'weak')). A matching
If-None-Match gets 304 Not Modified; the spec asserts the 304 and a
fresh ETag once the listing changes. Documented in docs/API.md.
- Add an optional TRUST_PROXY env var, applied as Express 'trust proxy'
so req.ip (which the scan rate limiter keys on) is the real client IP
behind a load balancer. Accepts true/false, a hop count, or a list of
IPs, CIDR ranges and presets; invalid values fail env validation at
boot. Documented in docs/DEPLOYMENT.md, .env.example and
docs/RATE_LIMITING.md.
Closes StellarTickets#248
Closes StellarTickets#249
|
@devgrace100 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
✅ Deploy Preview for stellarticketsbackend ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Keeps upstream's tracing bootstrap, CORS_ORIGINS, soft-delete filters, audit logging and response cache, and layers this PR's pagination, weak ETags and TRUST_PROXY on top. configureApp now carries the helmet/CSP, body limit, CORS, versioning and validation setup from main.ts.
Keep configureApp() as the shared HTTP setup and call upstream's new applyApiPrefix() from it; keep both sets of env.validation imports. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.
Closes #245
Closes #246
Closes #248
Closes #249
#245: shared
PaginationQueryDtosrc/common/dto/pagination-query.dto.tshandles?page=&limit=.pagedefaults to 1,limitdefaults to 20, andlimitis capped at 100 (MAX_PAGE_LIMIT).GET /eventsuses it. Results are ordered bystartsAt, thenid, so rows can't move between pages when two events start at the same time.pagination-query.dto.spec.tscovers defaults, string-to-number conversion, and out-of-range, fractional and non-numeric values.#246:
Paginated<T>type, helper and interceptorsrc/common/pagination/paginated.tsaddsPaginated<T> = { items, total, page, limit }, plus the helperspaginate(items, total, query)andtoSkipTake(query)(Prismaskip/take).PaginatedResponseInterceptorconverts a handler's[items, total]result into aPaginated<T>body.[items, total]is exactly whatprisma.$transaction([findMany, count])returns. If a handler returns anything else, the interceptor throws a clear error.docs/API.md(new Pagination section), including how to paginate a new endpoint.GET /eventsnow returns{ items, total, page, limit }instead of a bare array, and it rejects unknown query params with400because the globalValidationPipesetsforbidNonWhitelisted. This is recorded under Changed inCHANGELOG.md.#248: weak ETags and 304
main.tsintoconfigureApp()(src/app.setup.ts), so specs run the same middleware and Express settings as production.app.set('etag', 'weak')makes weak ETags an explicit setting rather than a framework default that could change. A request whoseIf-None-Matchmatches the current ETag gets304 Not Modified.src/app.setup.spec.tsboots the realEventsControllerthroughconfigureAppwith supertest and asserts:W/"…"ETag is sentIf-None-Matchreturns 304 with an empty bodydocs/API.md(Conditional GET (ETags)).#249:
TRUST_PROXYtrust proxysetting. It accepts:falseor unset (the default): ignoreX-Forwarded-For1ip/netmask), and the presetsloopback,linklocal,uniquelocaltrue: trust every hopon,10.0.0.0/33). A typo therefore can't silently leave the scan rate limiter keying every client on the proxy's IP.trust-proxy.spec.tscovers the parser.env.validation.spec.tscovers accepting and rejecting values at boot.app.setup.spec.tschecks thatreq.ipignoresX-Forwarded-Forby default, uses it when the hop is trusted, and ignores it when the connecting address isn't in the trusted list.docs/DEPLOYMENT.md(new checklist item and a Running behind a proxy section that explains when to use each value and the spoofing risk oftrue). Also updated.env.example, anddocs/RATE_LIMITING.mdnow links to the new section.Compatibility with #334
#334 (Papytee, #289) adds
@Header('Cache-Control', …)toGET /events. That works together with the ETags added here. The two PRs will have a small, mechanical conflict on the import block andfindPublishedinevents.controller.ts. This PR addssrc/app.setup.spec.tsrather thanevents.controller.spec.ts, so the new spec files don't collide.Verification
npm test: 35 suites, 194 tests pass (147 before)npx eslinton all changed files: cleannpm run build: passesnpx tsc --noEmit: only the existing error insrc/tickets/dto/transfer-ticket.dto.spec.ts:35, which is also onmain