// PostgREST's .or() filter syntax treats "," "(" and ")" as structural // delimiters between conditions. A raw user-supplied search term containing // them (e.g. from a search box or ?q= param) can break out of the intended // column conditions and append arbitrary extra filters to the query. Strip // them before interpolating — harmless for real name/title searches, which // never legitimately contain them. export function sanitizeIlikeTerm(term: string): string { return term.replace(/[,()]/g, ""); } // PostgREST caps every response at db.max_rows (1000, see // supabase/config.toml) and does so *silently* — a query over ~800 employees // or the employee_history log just stops returning rows, and a report or // export built from it is quietly wrong rather than failing. Anything that // aggregates a whole table has to page explicitly; anything that renders a // bounded list (an employee page, the audit log) uses .range() directly and // does not need this. const PAGE_SIZE = 1000; type PagedQuery = { range: (from: number, to: number) => PromiseLike<{ data: Row[] | null; error: unknown }>; }; export async function fetchAllRows(buildQuery: () => PagedQuery): Promise { const rows: Row[] = []; for (let page = 0; ; page++) { const { data, error } = await buildQuery().range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1); if (error || !data) break; rows.push(...data); if (data.length < PAGE_SIZE) break; } return rows; }