Skip to content

feat: shared pagination (PaginationQueryDto, Paginated<T>), weak ETags, TRUST_PROXY - #344

Merged
EmmanuelOchaje merged 4 commits into
StellarTickets:mainfrom
devgrace100:feat/pagination-etag-trust-proxy
Oct 4, 2026
Merged

EmmanuelOchaje merged 4 commits into
StellarTickets:mainfrom
devgrace100:feat/pagination-etag-trust-proxy

Conversation

@devgrace100

Copy link
Copy Markdown
Contributor

Closes #245
Closes #246
Closes #248
Closes #249

#245: shared PaginationQueryDto

  • src/common/dto/pagination-query.dto.ts handles ?page=&limit=. page defaults to 1, limit defaults to 20, and limit is capped at 100 (MAX_PAGE_LIMIT).
  • GET /events uses it. Results are ordered by startsAt, then id, so rows can't move between pages when two events start at the same time.
  • Spec: pagination-query.dto.spec.ts covers defaults, string-to-number conversion, and out-of-range, fractional and non-numeric values.

#246: Paginated<T> type, helper and interceptor

  • src/common/pagination/paginated.ts adds Paginated<T> = { items, total, page, limit }, plus the helpers paginate(items, total, query) and toSkipTake(query) (Prisma skip/take).
  • PaginatedResponseInterceptor converts a handler's [items, total] result into a Paginated<T> body. [items, total] is exactly what prisma.$transaction([findMany, count]) returns. If a handler returns anything else, the interceptor throws a clear error.
  • Documented in docs/API.md (new Pagination section), including how to paginate a new endpoint.

⚠️ Breaking: GET /events now returns { items, total, page, limit } instead of a bare array, and it rejects unknown query params with 400 because the global ValidationPipe sets forbidNonWhitelisted. This is recorded under Changed in CHANGELOG.md.

#248: weak ETags and 304

  • HTTP setup moved from main.ts into configureApp() (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 whose If-None-Match matches the current ETag gets 304 Not Modified.
  • src/app.setup.spec.ts boots the real EventsController through configureApp with supertest and asserts:
    • a W/"…" ETag is sent
    • a matching If-None-Match returns 304 with an empty body
    • once the listing changes, the same header returns 200 with a new ETag
  • Documented in docs/API.md (Conditional GET (ETags)).

#249: TRUST_PROXY

  • New optional env var, applied as Express's trust proxy setting. It accepts:
    • false or unset (the default): ignore X-Forwarded-For
    • a hop count, e.g. 1
    • a comma-separated list of IPs, CIDR ranges (or ip/netmask), and the presets loopback, linklocal, uniquelocal
    • true: trust every hop
  • Env validation rejects invalid values at boot (e.g. on, 10.0.0.0/33). A typo therefore can't silently leave the scan rate limiter keying every client on the proxy's IP.
  • Tests:
    • trust-proxy.spec.ts covers the parser.
    • env.validation.spec.ts covers accepting and rejecting values at boot.
    • app.setup.spec.ts checks that req.ip ignores X-Forwarded-For by default, uses it when the hop is trusted, and ignores it when the connecting address isn't in the trusted list.
  • Documented in docs/DEPLOYMENT.md (new checklist item and a Running behind a proxy section that explains when to use each value and the spoofing risk of true). Also updated .env.example, and docs/RATE_LIMITING.md now links to the new section.

Compatibility with #334

#334 (Papytee, #289) adds @Header('Cache-Control', …) to GET /events. That works together with the ETags added here. The two PRs will have a small, mechanical conflict on the import block and findPublished in events.controller.ts. This PR adds src/app.setup.spec.ts rather than events.controller.spec.ts, so the new spec files don't collide.

Verification

  • npm test: 35 suites, 194 tests pass (147 before)
  • npx eslint on all changed files: clean
  • npm run build: passes
  • npx tsc --noEmit: only the existing error in src/tickets/dto/transfer-ticket.dto.spec.ts:35, which is also on main

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
@drips-wave

drips-wave Bot commented Sep 24, 2026

Copy link
Copy Markdown

@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! 🚀

Learn more about application limits

@netlify

netlify Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for stellarticketsbackend ready!

Name Link
🔨 Latest commit cc4f44e
🔍 Latest deploy log https://app.netlify.com/projects/stellarticketsbackend/deploys/6ac26cea5307cd0008801d72
😎 Deploy Preview https://deploy-preview-344--stellarticketsbackend.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

devgrace100 and others added 2 commits October 2, 2026 11:11
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>
@EmmanuelOchaje
EmmanuelOchaje merged commit 7daadcd into StellarTickets:main Oct 4, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants