UNPKG

@adonis-agora/filter

Version:

Server-side query filtering/sorting/pagination for AdonisJS — Spatie-style input, a Lucid adapter, and field allow-listing. Part of the Agora ecosystem.

107 lines 4.42 kB
/** * Encodes keyset cursor values into a compact, URL-safe opaque string * (base64url of a JSON array). The shape is intentionally opaque to clients — * only this module reads it back. * * `Date` values are encoded as `{ $d: <iso> }` so they round-trip to `Date` * instances on decode (plain JSON would yield a string and break date keyset * comparisons). */ export function encodeCursor(values) { // Pre-map Date values to a tagged form. We cannot detect dates in the // JSON.stringify replacer because Date.toJSON() has already converted them to // strings by the time the replacer sees the value. const tagged = values.map((v) => (v instanceof Date ? { $d: v.toISOString() } : v)); const json = JSON.stringify(tagged); return Buffer.from(json, 'utf8').toString('base64url'); } /** * Decodes an opaque cursor string back into its keyset values. Returns `null` * when the cursor is malformed (bad base64, bad JSON, or not an array) so the * caller can ignore an invalid cursor instead of crashing. */ export function decodeCursor(cursor) { try { const json = Buffer.from(cursor, 'base64url').toString('utf8'); const parsed = JSON.parse(json, (_key, value) => { if (value && typeof value === 'object' && typeof value.$d === 'string') { return new Date(value.$d); } return value; }); return Array.isArray(parsed) ? parsed : null; } catch { return null; } } /** * Builds the keyset {@link SortItem}[] for cursor pagination: the caller's * effective sorts, with a stable primary-key tiebreaker appended if it is not * already present. The tiebreaker inherits the direction of the last sort column * so the overall ordering stays monotonic (important for a correct * `(cols, pk) > (...)` comparison). */ export function buildKeyset(sorts, primaryKey) { const hasPk = sorts.some((s) => s.field === primaryKey); if (hasPk) return sorts; const lastDirection = sorts[sorts.length - 1]?.direction ?? 'asc'; return [...sorts, { field: primaryKey, direction: lastDirection }]; } /** Flips every keyset column's direction (for backward cursor paging). */ export function reverseKeyset(keyset) { return keyset.map((s) => ({ field: s.field, direction: s.direction === 'asc' ? 'desc' : 'asc', })); } /** * Extracts the keyset values from a fetched row, in keyset column order. * Supports dotted relation paths (e.g. `author.name`) by walking the object. */ export function extractCursorValues(row, keyset) { return keyset.map((s) => { if (!s.field.includes('.')) return row[s.field]; let current = row; for (const segment of s.field.split('.')) { if (current == null || typeof current !== 'object') return undefined; current = current[segment]; } return current; }); } /** * Assembles a {@link CursorPage} from the rows fetched for a keyset query. * * The query is expected to have been built by {@link applyCursor}, which fetches * one extra row (`limit = size + 1`) so we can detect a further page. For * backward paging the rows come back reversed and are flipped here to restore * the caller's requested order. Boundary cursors are computed from the base * keyset so they round-trip regardless of paging direction. */ export function buildCursorPage(rows, resolved) { const { keyset, size, backward, hasCursor } = resolved; const hasExtra = rows.length > size; let pageRows = hasExtra ? rows.slice(0, size) : rows; if (backward) pageRows = pageRows.slice().reverse(); const firstRow = pageRows[0]; const lastRow = pageRows[pageRows.length - 1]; const startCursor = firstRow ? encodeCursor(extractCursorValues(firstRow, keyset)) : null; const endCursor = lastRow ? encodeCursor(extractCursorValues(lastRow, keyset)) : null; // With a cursor present, the opposite-direction page is known to exist; the // same-direction page exists iff we saw the extra row. const hasNext = backward ? hasCursor : hasExtra; const hasPrev = backward ? hasExtra : hasCursor; return { items: pageRows, nextCursor: hasNext ? endCursor : null, prevCursor: hasPrev ? startCursor : null, hasNext, hasPrev, }; } //# sourceMappingURL=cursor.js.map