Skip to content

chore(api): perf section 1 — compression, SNS fetch timeouts, pagination#201

Merged
kashkoool merged 2 commits into
mainfrom
chore/api-perf-section-1
May 11, 2026
Merged

chore(api): perf section 1 — compression, SNS fetch timeouts, pagination#201
kashkoool merged 2 commits into
mainfrom
chore/api-perf-section-1

Conversation

@kashkoool

@kashkoool kashkoool commented May 11, 2026

Copy link
Copy Markdown
Owner

Summary

Section 1 of the recent perf/reliability audit — six items in one PR. All low-risk, no DB migration, no frontend changes required (default-50 pagination keeps existing list shapes intact).

Tag What File
H1 gzip compression middleware apps/api/src/main.ts
H2 5 s AbortSignal timeout on SNS cert fetch sns-signature-validator.service.ts:208
H2 5 s AbortSignal timeout on SubscribeURL confirm fetch ses-events.controller.ts:231
H3 60 s app-level request timeout middleware main.ts (after CORS)
H4 CORS preflight maxAge: 86_400 + optionsSuccessStatus: 204 main.ts:267
DB1+DB2 Pagination on 6 list endpoints (see table below) admin / catalog / auth
DB3 include: { coupon: true } → explicit select of 8 used fields bookings.service.ts:1100

Pagination endpoints

Endpoint DTO Default Max
GET /admin/settings/countries PaginationDto (admin) 20 100
GET /admin/settings/categories PaginationDto (admin) 20 100
GET /admin/settings/trending PaginationDto (admin) 20 100
GET /admin/payouts/export ExportPaginationDto (new, common) 1000 5000
GET /offers PaginationDto (new, common) 50 200
GET /auth/sessions PaginationDto (new, common) 50 200

Response shape stays a bare array — no contract break. Clients that need more pass ?limit=…&page=….

Items not included

  • DB5 LoyaltyLedger @@index([bookingId]) — already present at schema.prisma:666; audit was stale.
  • O4 (nestjs-pino) + O1 (CLS request-id) — explicitly deferred at user request.

Test plan

  • npx tsc --noEmit clean
  • npx eslint on all 9 changed source files — no new warnings
  • Full unit suite npx jest test/unit — 701 tests pass, 33 suites
  • New regression test in sns-signature-validator.spec.ts pins the AbortSignal arg into the cert fetch() call so the 5 s timeout can't be silently dropped
  • Updated auth.controller.spec.ts callsites for the new getSessions(query) arg
  • Integration suite (CI gates this — local DB not running)
  • CI: lint + type-check + security-scan + Trivy

Risk

Low. Runtime-only changes. No schema migration, no SSM, no IAM. Pagination default-50 cap means clients calling the 6 affected endpoints get at most 50 items by default instead of "all" — every current list is well under 50 rows. Rollback: git revert.

Summary by CodeRabbit

Release Notes

  • New Features

    • Pagination support added to admin endpoints (countries, categories, trending events, payout exports) and auth sessions listing.
  • Performance

    • HTTP response compression enabled for faster load times.
  • Bug Fixes

    • Added request timeout protection (60 seconds) to prevent connection hangs.
    • Added external service call timeouts (5 seconds) for improved reliability during transient issues.

Review Change Stack

H1 — Add response compression (gzip/deflate) after Helmet in main.ts.
1 KB threshold; respects Cache-Control: no-transform. JSON bodies don't
mix secrets + user input, so BREACH-safe.

H2 — 5 s AbortSignal.timeout on both SNS fetches:
- sns-signature-validator.service.ts cert fetch
- ses-events.controller.ts SubscribeURL confirm fetch
Prevents an AWS edge blip from pinning a request slot indefinitely.

H3 — 60 s app-level request timeout middleware (matches ALB idle timeout).
Returns clean 503 instead of letting the ALB 504.

H4 — CORS preflight maxAge: 86_400 (24 h, browser ceiling) +
optionsSuccessStatus: 204.

DB1+DB2 — Pagination on 6 unbounded findMany endpoints:
- GET /admin/settings/countries
- GET /admin/settings/categories
- GET /admin/settings/trending
- GET /admin/payouts/export  (ExportPaginationDto — 1000 default, 5000 max)
- GET /offers                (PaginationDto — 50 default, 200 max)
- GET /auth/sessions         (PaginationDto)

Reuses existing admin PaginationDto (page/limit/max=100) for the 3 admin
settings lists. New common/dto/pagination.dto.ts adds PaginationDto (50/200)
+ ExportPaginationDto (1000/5000) for non-admin routes and the payouts
export. Response shape stays as bare arrays — no contract break.

DB3 — bookings.service.ts:1100 voucher fetch tightened from
include: { coupon: true } to a select scoped to the 8 fields actually used
(id, code, status, validTo, minOrderAmount, discountType, discountValue,
maxDiscount). Type-check is the load-bearing guard against missed fields.

DB5 — LoyaltyLedger @@index([bookingId]) was already present in
schema.prisma:666; no migration needed.

Tests: 701 unit tests pass. Updated auth.controller.spec.ts callsites
for the new getSessions(query) arg. Added regression test asserting the
cert fetch carries an AbortSignal (so the 5 s timeout can't be silently
dropped in a future refactor).
@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@kashkoool has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 29 minutes and 1 second before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0f94246f-9ddf-46a7-9428-e9b88b8ca571

📥 Commits

Reviewing files that changed from the base of the PR and between 9057f33 and 6ca1313.

📒 Files selected for processing (4)
  • apps/api/src/admin/admin.service.ts
  • apps/api/src/auth/auth.controller.ts
  • apps/api/src/catalog/offers.controller.ts
  • apps/api/src/main.ts
📝 Walkthrough

Walkthrough

This PR adds pagination to multiple API endpoints (admin, auth, catalog) via new reusable DTOs, introduces HTTP compression and request timeout middleware, and hardens external fetch operations with 5-second timeouts to prevent indefinite hangs during transient failures.

Changes

Pagination Feature

Layer / File(s) Summary
Pagination Data Types
apps/api/src/common/dto/pagination.dto.ts
PaginationDto and ExportPaginationDto define optional page and limit query parameters with validation, defaults (1 and 50/1000), and upper bounds (200/5000).
Admin Endpoints Pagination
apps/api/src/admin/admin.controller.ts, apps/api/src/admin/admin.service.ts
exportPayouts, getCountries, getCategories, and getTrendingEvents now accept pagination queries and apply take/skip logic with computed defaults.
Auth Sessions Pagination
apps/api/src/auth/auth.controller.ts
getSessions endpoint now accepts PaginationDto query parameters and applies pagination to the refreshToken.findMany query.
Catalog Offers Pagination
apps/api/src/catalog/offers.controller.ts
listOffers now accepts pagination query parameters and applies them to coupon listing while preserving existing filtering.
Pagination Tests
apps/api/test/unit/auth.controller.spec.ts
Unit tests updated to pass pagination DTO argument to getSessions.

Server Infrastructure

Layer / File(s) Summary
Compression Dependency
apps/api/package.json
Added compression runtime dependency and @types/compression development dependency.
Middleware Configuration
apps/api/src/main.ts
Compression middleware registered with 1KB threshold and cache-control filter. CORS configuration extended with preflight cache (maxAge: 86400) and success status 204. Request timeout middleware (60s) added to return 503 JSON on timeout.

Resilience & Optimization

Layer / File(s) Summary
Fetch Timeout Protection
apps/api/src/email/ses-events.controller.ts, apps/api/src/email/sns-signature-validator.service.ts
Both fetch operations now include 5-second AbortSignal.timeout(5_000) to prevent indefinite hangs.
Booking Query Optimization
apps/api/src/bookings/bookings.service.ts
Coupon lookup during booking creation now projects specific fields instead of loading the entire related record.
SNS Timeout Test
apps/api/test/unit/sns-signature-validator.spec.ts
New test validates that fetch includes AbortSignal and redirect: 'manual', and correctly handles malformed certificate responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • kashkoool/Jadwal#94: Both PRs modify voucher/coupon handling in BookingsService, including query projections and lookup logic.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main performance improvements in the PR: compression, SNS fetch timeouts, and pagination across multiple endpoints.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/api-perf-section-1

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/api/src/common/dto/pagination.dto.ts (1)

16-49: ⚡ Quick win

Consider bounding page to defang offset-scan attacks.

limit is capped at 200 / 5 000, but page is unbounded. A client sending ?page=999999999&limit=200 forces Prisma to issue an OFFSET 199_999_999_800 against the underlying table — Postgres still has to walk and discard those rows before returning the (empty) slice, which is essentially a free DoS knob on every paginated endpoint that adopts these DTOs. Since bookings.service.ts:getMyBookings already uses a Math.min(..., 1000) page cap, mirroring that here keeps the policy consistent across the codebase.

♻️ Suggested fix — cap `page` on both DTOs
 export class PaginationDto {
   `@IsOptional`()
   `@Type`(() => Number)
   `@IsInt`()
   `@Min`(1)
+  `@Max`(10_000)
   page?: number = 1;
 ...
 export class ExportPaginationDto {
   `@IsOptional`()
   `@Type`(() => Number)
   `@IsInt`()
   `@Min`(1)
+  `@Max`(10_000)
   page?: number = 1;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/common/dto/pagination.dto.ts` around lines 16 - 49, Add an upper
bound to the page field on both PaginationDto and ExportPaginationDto to prevent
huge OFFSET scans; specifically, annotate the page property with `@Max`(1000)
(matching the cap used in bookings.service.ts:getMyBookings) so page stays
within a safe range while preserving the existing defaults and other decorators.
apps/api/test/unit/auth.controller.spec.ts (1)

211-228: ⚡ Quick win

Add explicit assertions for default take/skip in the sessions query.

The test now exercises the new signature, but it still doesn’t verify that pagination is actually forwarded to Prisma. Adding this assertion will lock in the PR’s functional intent.

Proposed test assertion update
   const r = await ctrl.getSessions({ id: 'u1', role: 'VENDOR' } as any, req, {});

+  expect(prisma._client.refreshToken.findMany).toHaveBeenCalledWith(
+    expect.objectContaining({
+      take: 50,
+      skip: 0,
+    }),
+  );
+
   expect(authSvc.getTokenHash).toHaveBeenCalledWith('rtoken');
   expect(r).toHaveLength(2);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/test/unit/auth.controller.spec.ts` around lines 211 - 228, Add an
assertion that the sessions query forwards the controller's default pagination
to Prisma: after calling ctrl.getSessions, assert that
prisma._client.refreshToken.findMany was invoked with an argument containing the
default take and skip values (e.g., take: 10, skip: 0) so the test locks in
pagination behavior; use an object-containing matcher when checking the call to
refreshToken.findMany to avoid coupling to unrelated query fields and reference
ctrl.getSessions and prisma._client.refreshToken.findMany in the assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/admin/admin.service.ts`:
- Around line 1389-1393: The paginated Prisma queries (e.g., the
this.prisma.client.country.findMany call in admin.service.ts) use a non-unique
single-column orderBy (nameEn) which can produce unstable paging; update those
orderBy clauses to include a deterministic secondary key by adding id as a
tie-breaker (e.g., orderBy: [{ nameEn: 'asc' }, { id: 'asc' }]). Apply the same
change to the other similar findMany queries referenced in this review (the
calls around the other spots noted) so all paginated queries use a stable
composite ordering.

In `@apps/api/src/main.ts`:
- Around line 298-310: The middleware registering a per-request timeout via
req.setTimeout is leaving a persistent 'timeout' listener on keep-alive sockets;
change the middleware so you install an explicit timeout handler (e.g. const
onTimeout = () => { if (!res.headersSent) res.status(503).json({ message:
'Request timeout' }); req.destroy(); }), register it with req.setTimeout or
req.on('timeout', onTimeout) on entry, and then remove that listener on response
completion (res.on('finish', ...) and res.on('close', ...):
req.removeListener('timeout', onTimeout')) so the timeout callback is cleared
when a request completes; keep the same 60_000ms value and the existing behavior
inside onTimeout.

---

Nitpick comments:
In `@apps/api/src/common/dto/pagination.dto.ts`:
- Around line 16-49: Add an upper bound to the page field on both PaginationDto
and ExportPaginationDto to prevent huge OFFSET scans; specifically, annotate the
page property with `@Max`(1000) (matching the cap used in
bookings.service.ts:getMyBookings) so page stays within a safe range while
preserving the existing defaults and other decorators.

In `@apps/api/test/unit/auth.controller.spec.ts`:
- Around line 211-228: Add an assertion that the sessions query forwards the
controller's default pagination to Prisma: after calling ctrl.getSessions,
assert that prisma._client.refreshToken.findMany was invoked with an argument
containing the default take and skip values (e.g., take: 10, skip: 0) so the
test locks in pagination behavior; use an object-containing matcher when
checking the call to refreshToken.findMany to avoid coupling to unrelated query
fields and reference ctrl.getSessions and prisma._client.refreshToken.findMany
in the assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 707ac90f-2ab9-4ac5-95cc-b2e93c9a73b7

📥 Commits

Reviewing files that changed from the base of the PR and between 8bdc86a and 9057f33.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json, !**/package-lock.json
📒 Files selected for processing (12)
  • apps/api/package.json
  • apps/api/src/admin/admin.controller.ts
  • apps/api/src/admin/admin.service.ts
  • apps/api/src/auth/auth.controller.ts
  • apps/api/src/bookings/bookings.service.ts
  • apps/api/src/catalog/offers.controller.ts
  • apps/api/src/common/dto/pagination.dto.ts
  • apps/api/src/email/ses-events.controller.ts
  • apps/api/src/email/sns-signature-validator.service.ts
  • apps/api/src/main.ts
  • apps/api/test/unit/auth.controller.spec.ts
  • apps/api/test/unit/sns-signature-validator.spec.ts

Comment thread apps/api/src/admin/admin.service.ts
Comment thread apps/api/src/main.ts
Two follow-ups on the perf section-1 PR.

Finding 1 (Major) — request-timeout middleware was destroying healthy
keep-alive sockets 60s after the last response. req.setTimeout sets the
timeout on the underlying socket and the 'timeout' listener does not
clear when the response finishes, so an idle-but-healthy keep-alive
socket behind the ALB was forced to redo TCP+TLS handshake on every
new request. Each request was also stacking a fresh 'timeout' listener
on the same socket. Clear timeout + remove the listener on res 'finish'
and 'close' so the protection only fires on actually-hung requests.

Finding 2 (Minor) — add id as a secondary sort key on all 6 paginated
findMany calls. Single-column orderBy on a non-unique field (nameEn,
createdAt, lastUsedAt) lets Postgres return tied rows in arbitrary
order — page boundaries can duplicate or skip rows. Composite ordering
makes page boundaries deterministic across requests.

Endpoints updated:
- admin countries:        [nameEn asc, id asc]
- admin categories:       [nameEn asc, id asc]
- admin trending events:  [createdAt desc, id desc]
- admin payouts export:   [createdAt desc, id desc]
- /offers:                [createdAt desc, id desc]
- /auth/sessions:         [lastUsedAt desc, id desc]

Tests: 701/701 unit pass, type-check clean, lint clean.
@kashkoool kashkoool merged commit 124dcc3 into main May 11, 2026
22 checks passed
@kashkoool kashkoool deleted the chore/api-perf-section-1 branch May 11, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant