@rapiq/parser-expression
Parses an expression language for filters: useful when a single string must carry a complex condition tree (search boxes, saved filters, CLI flags). The expression URL codec builds on it.
npm install @rapiq/core @rapiq/parser-simple @rapiq/parser-expressionThe language
Conditions are function-call expressions; and / or / not compose them:
and(eq(name, 'John'), gte(age, '18'))
or(in(status, 'active', 'pending'), gt(age, '65'))
not(eq(name, 'foo'))
contains(user.name, 'Bob')
elemMatch(items, and(eq(id, '1'), eq(active, 'true')))
elemMatch(scores, gt($this, '5'))| Function | Meaning | AST operator |
|---|---|---|
eq(field, value) | equal | EQUAL |
lt / lte / gt / gte | comparisons | LESS_THAN, … |
in(field, v1, v2, …) | in list | IN |
nin(field, v1, v2, …) | not in list | NOT_IN |
contains(field, value) | substring | CONTAINS |
startsWith(field, value) | prefix | STARTS_WITH |
endsWith(field, value) | suffix | ENDS_WITH |
elemMatch(field, expr) | array element match | ELEM_MATCH |
size(field, n) | array length (non-negative integer) | SIZE |
and(expr, …) / or(expr, …) | compound | Filters node |
not(expr) | negation: the exact complement of the interior | a single leaf with a negated twin normalizes to it (not(eq(…)) → NOT_EQUAL, not(in(…)) → NOT_IN, …), a double not cancels; everything else (ordering comparisons, size, elemMatch, groups) stays a first-class NOT Filters node |
Rules:
- Values are always single-quoted:
gte(age, '18'), notgte(age, 18). Quoted numerals are coerced to numbers,'true'/'false'to booleans,'null'tonull. Escape a quote by doubling it ('it''s'). - Quoted values are never comma-split:
eq(name, 'a,b')is the literal string'a,b'; lists are separate arguments. - Field names allow
[A-Za-z0-9_-]with dots for relation paths (user.name). - Inside an
elemMatchinterior, field paths are relative to the array element; the reserved$thismarker (core'sITSELFconstant) addresses the element itself. Outside an interior,$thisis an error.
For the functions above, the language mirrors the condition helpers: eq('name', 'John') in code ≙ eq(name, 'John') on the wire. The negated leaf helpers (ne, notContains, notStartsWith, notEndsWith) have no function spelling of their own (only nin does): write not(eq(…)), not(contains(…)), …, which normalize back to the twin on decode.
Usage
import { ExpressionParser } from '@rapiq/parser-expression';
const parser = new ExpressionParser(registry);
const query = parser.parse({
filters: "and(eq(name, 'John'), gte(age, '18'))",
sorts: '-age',
pagination: { limit: 25 },
}, { schema: 'user' });Only the filters parameter uses the expression language; fields, relations, pagination and sorts accept the same input as the simple parser, and the whole thing returns the same Query.
There is also a standalone parseFilters(input, options) returning just the Filters node. For schemas with asynchronous filter validators, use parseAsync() / parseFiltersAsync() on the query parser, or parseAsync() / parseExactAsync() on ExpressionFiltersParser.
Errors
Malformed syntax and invalid or disallowed expression keys always fail the parse, whatever the schema's policy: the expression parser has no silent-drop mode for them, because an expression cannot be partially reinterpreted. The raised error is the aggregated inputRejected failure (FiltersParseError from parseFilters, the general ParseError from a whole-query parse), and the issue trace on error.issues carries the specific code: SYNTAX_INVALID for a malformed expression, the key-related codes (keyNotAllowed, keyPathInvalid, …) for key violations.
Policy rejections are different: a filters.validate hook declining a leaf, or the relations hook declining a relation an expression traverses, follow throwOnFailure like every other dialect. Without it the leaf is dropped and the schema defaults apply when nothing survives; with it the rejection is recorded (KEY_VALIDATE_REJECTED) and fails the parse. See Error Handling.