@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.
134 lines • 5.71 kB
JavaScript
import { parseDistinct, parseSort, toColumnFilters } from './parse_request.js';
function toInt(value) {
if (typeof value === 'number' && Number.isFinite(value))
return value;
if (typeof value === 'string' && value.trim() !== '') {
const n = Number(value);
if (Number.isFinite(n))
return n;
}
return undefined;
}
/**
* Parse a decoded, Spatie-laravel-query-builder / JSON:API-style query object
* into a {@link SpatieInput}. This is the richer, additive counterpart to
* {@link parseFilterRequest}: it understands the same `filter`/`sort`/`search`
* shapes plus JSON:API `include`, sparse `fields[resource]` sets, and cursor
* pagination (`page[after]`/`page[before]`).
*
* | Query string | Decoded input | Mapped to |
* | ------------------------------- | ------------------------------------ | ------------------------------------------ |
* | `filter[name]=Al` | `{ filter: { name: 'Al' } }` | `equals` column filter |
* | `filter[id]=1,2,3` | `{ filter: { id: '1,2,3' } }` | `in` column filter |
* | `filter[age][gte]=18` | `{ filter: { age: { gte: '18' } } }` | operator column filter |
* | `sort=-createdAt,name` | `{ sort: '-createdAt,name' }` | sort items |
* | `distinct=city,tier` | `{ distinct: 'city,tier' }` | `distinct: ['city', 'tier']` |
* | `include=posts,comments` | `{ include: 'posts,comments' }` | `include: ['posts', 'comments']` |
* | `fields[users]=id,name` | `{ fields: { users: 'id,name' } }` | `select: ['id', 'name']` |
* | `page[number]=2&page[size]=10` | `{ page: { number, size } }` | offset (`page`/`size`) |
* | `page[after]=cur&page[size]=10`| `{ page: { after, size } }` | cursor (`after`/`first`) |
*
* A pure reshape — no validation or allow-listing here; that happens downstream
* in `applyFilter` / `applyCursor` against the `FilterConfig`.
*/
export function parseSpatieRequest(qs) {
if (qs == null || typeof qs !== 'object')
return {};
const src = qs;
const out = {};
if (src.filter != null && typeof src.filter === 'object' && !Array.isArray(src.filter)) {
const filters = [];
for (const [field, value] of Object.entries(src.filter)) {
filters.push(...toColumnFilters(field, value));
}
if (filters.length > 0)
out.filters = filters;
}
const sort = parseSort(src.sort);
if (sort.length > 0)
out.sort = sort;
const distinct = parseDistinct(src.distinct);
if (distinct.length > 0)
out.distinct = distinct;
if (typeof src.search === 'string' && src.search.length > 0)
out.search = src.search;
const include = parseInclude(src.include);
if (include.length > 0)
out.include = include;
const select = parseSparseFieldsets(src.fields);
if (select.length > 0)
out.select = select;
applyPage(src.page, out);
return out;
}
/** Parse `include=posts,comments` (string or array) into a relation-name list. */
function parseInclude(include) {
if (typeof include === 'string') {
return include
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
if (Array.isArray(include)) {
return include.filter((s) => typeof s === 'string' && s.length > 0);
}
return [];
}
/**
* Flatten JSON:API sparse fieldsets (`fields[resource]=a,b`) into a single
* order-preserving, de-duplicated list of column names. Per-resource keys are
* collapsed because a single SELECT applies to the primary entity.
*/
function parseSparseFieldsets(fields) {
if (fields == null || typeof fields !== 'object' || Array.isArray(fields))
return [];
const seen = new Set();
const out = [];
for (const value of Object.values(fields)) {
const cols = typeof value === 'string'
? value.split(',')
: Array.isArray(value)
? value.filter((s) => typeof s === 'string')
: [];
for (const raw of cols) {
const col = raw.trim();
if (col.length > 0 && !seen.has(col)) {
seen.add(col);
out.push(col);
}
}
}
return out;
}
/**
* Map a JSON:API `page` object onto `out`: `page[after]`/`page[before]` →
* cursor (`after`/`first`, `before`/`last`); otherwise `page[number]`/`page[size]`
* → offset (`page`/`size`). `after` wins if both cursor bounds are present.
*/
function applyPage(page, out) {
if (page == null || typeof page !== 'object' || Array.isArray(page))
return;
const p = page;
const after = typeof p.after === 'string' ? p.after : undefined;
const before = typeof p.before === 'string' ? p.before : undefined;
const size = toInt(p.size);
if (after !== undefined || before !== undefined) {
if (after !== undefined) {
out.after = after;
if (size !== undefined)
out.first = size;
}
else if (before !== undefined) {
out.before = before;
if (size !== undefined)
out.last = size;
}
return;
}
const number = toInt(p.number);
if (number !== undefined)
out.page = number;
if (size !== undefined)
out.size = size;
}
//# sourceMappingURL=spatie_parser.js.map