conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 }