Skip to content

@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.

sh
npm install @rapiq/core @rapiq/parser-simple @rapiq/parser-expression

The language

Conditions are function-call expressions; and / or / not compose them:

txt
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'))
FunctionMeaningAST operator
eq(field, value)equalEQUAL
lt / lte / gt / gtecomparisonsLESS_THAN, …
in(field, v1, v2, …)in listIN
nin(field, v1, v2, …)not in listNOT_IN
contains(field, value)substringCONTAINS
startsWith(field, value)prefixSTARTS_WITH
endsWith(field, value)suffixENDS_WITH
elemMatch(field, expr)array element matchELEM_MATCH
size(field, n)array length (non-negative integer)SIZE
and(expr, …) / or(expr, …)compoundFilters node
not(expr)negation: the exact complement of the interiora 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'), not gte(age, 18). Quoted numerals are coerced to numbers, 'true'/'false' to booleans, 'null' to null. 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 elemMatch interior, field paths are relative to the array element; the reserved $this marker (core's ITSELF constant) addresses the element itself. Outside an interior, $this is 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

typescript
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.

Released under the MIT License.