page.js (2413B)
1 // src/lib/page.js - windows over the tables that grow without bound 2 // 3 // A window is one page of rows plus the total behind them. The total is 4 // counted under the same conditions as the rows, from the same FROM and 5 // WHERE, so a visibility rule cannot promise a page that turns out empty. 6 // 7 // LIMIT and OFFSET are bound rather than pasted in. All three dialects 8 // accept that, and retention already relies on it. 9 10 export const API_LIMIT = 50; 11 export const API_MAX_LIMIT = 200; 12 13 // A query string gives strings, a json body gives numbers, and both are 14 // accepted so the two ways of asking cannot disagree. 15 function asInt(value) { 16 if (typeof value === 'number') return Number.isInteger(value) ? value : NaN; 17 return Number.parseInt(typeof value === 'string' ? value : '', 10); 18 } 19 20 // Anything unusable reads as the default, and the cap applies silently: a 21 // client asking for everything gets a page, not an error to learn about. 22 export function limitOf(query, fallback = API_LIMIT, max = API_MAX_LIMIT) { 23 const n = asInt(query?.limit); 24 if (!Number.isFinite(n) || n < 1) return fallback; 25 return Math.min(n, max); 26 } 27 28 export function offsetOfQuery(query, max = API_MAX_LIMIT) { 29 const n = asInt(query?.offset); 30 if (!Number.isFinite(n) || n < 0) return 0; 31 // Far enough out that offset + limit stays a safe integer. 32 return Math.min(n, Number.MAX_SAFE_INTEGER - max); 33 } 34 35 // next is absent on the last page, so following pages needs no special case. 36 export function pageBlock({ url, offset, limit, total }) { 37 const block = {}; 38 39 const after = offset + limit; 40 if (after < total) { 41 const next = new URL(url); 42 next.searchParams.set('offset', String(after)); 43 next.searchParams.set('limit', String(limit)); 44 block.next = next.toString(); 45 } 46 47 block.offset = offset; 48 block.total = total; 49 return block; 50 } 51 52 // The offset twin of paginate, sharing from and where for the same reason. 53 export async function paginateOffset(db, { 54 select, from, where = '1 = 1', order, params = {}, offset, limit, 55 }) { 56 const rows = await db.all( 57 `SELECT ${select} FROM ${from} WHERE ${where} ORDER BY ${order} 58 LIMIT {pageSize} OFFSET {pageFrom}`, 59 { ...params, pageSize: limit, pageFrom: offset } 60 ); 61 62 const counted = await db.get( 63 `SELECT COUNT(*) AS total FROM ${from} WHERE ${where}`, 64 params 65 ); 66 67 return { rows, total: Number(counted?.total ?? 0), offset, limit }; 68 }