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>
64 lines
2.0 KiB
TypeScript
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),
|
|
};
|
|
}
|