Optional pagination: if both page and limit are absent, return a plain array (legacy / small lists). If either is present, normalize inputs, cap limit, run offset pagination, return { items, pagination } with total from a separate count query. When LEFT JOIN + mapMany inflate rows, paginate IDs first (no joins), then re-fetch full graphs restricted to those IDs so LIMIT does not lie.
Canonical implementation today lives in backend-claim-dev (payment-request.helper.ts, api-response.interface.ts, list controllers).
page / limit) and scripts that want the full list (omit both).items + pagination.page|limit|total|total_pages.getMany(), but SQL LIMIT applies before that — so paged joins on one-to-many relations need a two-step fetch.Not paginated (both params omitted after resolution):
data: T[] (array only).Paginated (at least one param present):
data: PaginatedData<T>:interface PaginationMeta {
page: number;
limit: number;
total: number;
total_pages: number;
}
interface PaginatedData<T> {
items: T[];
pagination: PaginationMeta;
}
Types in the reference repo: src/common/interfaces/api-response.interface.ts.
| Rule | Implementation idea |
|---|---|
| Pagination off when both missing | Resolver returns undefined; fetcher skips limit/offset and returns []. |
| Either param can turn on pagination | If only page is sent, default limit (e.g. 20); if only limit, default page to 1. |
| page | Integer, >= 1 (Math.max(1, parsed)). |
| limit | Integer, >= 1, <= max (reference uses **FETCH_PAYMENT_REQUEST_MAX_LIMIT = 100**). |
Nest @Query() typing | Values may arrive as string / number; normalize with a resolve...FromQueryParams helper that stringifies non-empty values before parsing. |
Helpers in reference repo:
resolvePaymentRequestPagination(page?, limit?) — numeric / undefined after controller parsing.resolvePaymentRequestPaginationFromQuery(page?, limit?) — string query pair -> { page, limit } or undefined if both absent.resolvePaymentRequestPaginationFromQueryParams — tolerates string | number | null | ''.All in src/helpers/payment-request.helper.ts.
page / limit from @Query().listPagination = resolveFromQuery(...) — may be undefined.page / limit into the service only when defined (or pass the whole optional object).T[] or PaginatedData<T>; document which endpoints are which.Example list entry: GET /api/v1/requests and role lists that forward listPagination into getListByPage in payment-request-fetch.service.ts.
skip / limit / totalskip = (page - 1) * limit
limitClause + offsetClause when paginated (or two-phase path below).whereClause (no joins needed unless count must reflect join semantics — reference counts on root entity only).Reference: fetchPaymentRequest in src/helpers/payment-request.helper.ts — builds paginationResult with total_pages: ceil(total / limit).
Problem: leftJoinAndMapMany multiplies rows; LIMIT 20 can return fewer than 20 parents after dedupe.
Detection: any configured subquery with useMap: true and mapOne: false (or equivalent "collection mapping" joins).
Algorithm:
IN (...) with second query — order may follow orderClause on the second query).WHERE id IN (:...ids) with full joins / maps, same order clause.Reference: usePagedIdThenFetch branch inside fetchPaymentRequest when paginationResolved and mapManyJoins.length >= 1.
PaginationMeta + PaginatedData<T> (or align names with existing API standard).MAX_LIMIT and default page/limit when only one param is sent.resolvePagination returning undefined only when both params absent.meta).page / limit, max limit, and paginated response shape.Monorepo: backend-claim-dev
| Concern | Path |
|---|---|
| Caps + resolvers | src/helpers/payment-request.helper.ts |
| Paginated types | src/common/interfaces/api-response.interface.ts |
List wiring + PaginatedData guard | src/modules/payment-request/services/payment-request-fetch.service.ts |
| HTTP entry | src/modules/payment-request/controllers/payment-request-fetch.controller.ts |