Files
app/src/utils/pagination.ts
BOHA 907ee574e6 chore(lint): remove all 34 dead-code warnings (verified site by site)
unused-vars (25): leftover imports/types across pages, routes and services;
write-only sickHours accumulator; unused holidayCount pair; test helpers
authPatch/authDelete; TOut dropped from ApiMutationOptions (the hook keeps
its own generic — no call-site changes); requireAuth no longer importable
in customers/warehouse routes (matches the no-bare-auth-reads convention).

useless-assignment (5): initializers proven overwritten on every path
(systemQty x2, hours, writtenSize -> let x: number) and the seed's dead
adminUser reassignment (create kept, assignment dropped).

useless-escape (4): [eE+\-] -> [eE+-] and [\/...] -> [/...] — identical
semantics.

Behavior-neutral; lint 102 -> 68 warnings (0 errors), suite 641 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:13:52 +02:00

64 lines
2.0 KiB
TypeScript

import { PaginationMeta } from "../types";
/**
* Parse common pagination/sort/search query params.
*
* SECURITY NOTE (trust boundary): `page` and `limit` are clamped here
* (page ≥ 1, 1 ≤ limit ≤ 100), but `sort` and `search` are passed through
* VERBATIM and are NOT validated against any schema. They originate from the
* client. Callers that feed `sort` into a Prisma `orderBy` MUST allow-list it
* against the set of sortable columns for that entity (e.g.
* `const col = ALLOWED_SORTS.includes(sort) ? sort : "id"`), otherwise an
* attacker-supplied field name reaches the query layer. `search` is only safe
* when used through parameterized Prisma `contains` filters — never interpolate
* it into raw SQL.
*/
export function parsePagination(
query: Record<string, unknown>,
opts?: { defaultLimit?: number; maxLimit?: number },
): {
page: number;
limit: number;
skip: number;
sort: string;
order: "asc" | "desc";
search: string;
} {
// Defaults suit dense list views (25/page, capped at 100). Report endpoints
// that render their full result set pass a generous defaultLimit/maxLimit so
// they stay effectively complete while still bounded against a runaway scan.
const defaultLimit = opts?.defaultLimit ?? 25;
const maxLimit = opts?.maxLimit ?? 100;
const page = Math.max(1, parseInt(String(query.page || "1"), 10) || 1);
const limit = Math.min(
maxLimit,
Math.max(
1,
parseInt(
String(query.limit || query.per_page || String(defaultLimit)),
10,
) || defaultLimit,
),
);
const sort = String(query.sort || "id");
const order =
String(query.order || "").toLowerCase() === "asc" ? "asc" : "desc";
const search = String(query.search || "");
return { page, limit, skip: (page - 1) * limit, sort, order, search };
}
export function buildPaginationMeta(
total: number,
page: number,
limit: number,
): PaginationMeta {
return {
page,
limit,
per_page: limit,
total,
total_pages: Math.ceil(total / limit),
};
}