Custom operators and functions
Add operators like "within distance", and functions like lower() or relativeDate(), with output for each format.
Functions
A function call can stand on either side of a rule. On the right it's call; on the left it
replaces the field name:
// signedUp > 30 days ago
{ kind: 'rule', field: 'signedUp', operator: 'gt',
call: { fn: 'relativeDate', args: { amount: { value: 30 }, unit: { value: 'day' }, direction: { value: 'ago' } } } }
// lower(name) = 'ada'
{ kind: 'rule', field: { fn: 'lower', args: { text: { ref: 'name' } } }, operator: 'eq', value: 'ada' }Each argument is { value }, { ref: 'field' } or another call, so calls nest.
builtinFunctions covers RAQB's basic set: lower, upper, length, now, today and
relativeDate. Register functions when you define the fields:
import { builtinFunctions, defineFields, defineFunction, f } from '@qnot/core';
const daysSince = defineFunction({
label: 'Days since',
returns: f.number(),
args: { date: f.date() },
evaluate: ({ date }) => Math.floor((Date.now() - Date.parse(String(date))) / 864e5),
formats: {
sql: ({ date }) => `(CURRENT_DATE - ${date})`,
text: ({ date }) => `days since ${date}`,
},
});
const fields = defineFields({ signedUp: f.date(), lastSeen: f.date() }, { functions: { ...builtinFunctions, daysSince } });returnsis a field schema: it decides which operators apply and which editor the UI shows.argsare field schemas too;required: falsemakes one optional.evaluatepowerscompile().formatsmaps a codec name (sql,mongo,jsonlogic,spel,elasticsearch,text) to an encoder that gets the arguments already encoded for that format, plus their raw values. Exporting a rule whose function has no format for that codec throws (or is skipped withunsupported: 'skip').
Function calls are typed loosely (FnCall with a string name); validation checks the function
exists, its return kind fits the field, and its arguments are valid. Turn them off in the UI with
settings.allowFunctions: false.
Custom operators
import { defineFields, defineOperator, f } from '@qnot/core';
const near = defineOperator({
id: 'near',
arity: 'one',
label: 'is within',
evaluate: (distance, km) => typeof distance === 'number' && typeof km === 'number' && distance <= km,
formats: {
sql: ({ field, value }) => `${field} <= ${value}`,
mongo: ({ field, raw }) => ({ [String(field)]: { $lte: raw } }),
},
});
const fields = defineFields(
{ distance: f.number({ extraOperators: ['near'] }) },
{ operators: [near] },
);extraOperators adds custom operators to a field's defaults and to its type, so
{ field: 'distance', operator: 'near', value: 5 } type-checks. arity decides the value shape
('none', 'one', 'two', 'many'). Codecs call formats[codecName] with the rendered field,
the rendered value and the raw value; without one, export codecs throw (or skip, with
unsupported: 'skip') and the UI-state codecs pass the operator through by id.
Built-in operators also record their inverse (eq/neq, in/notIn…), available from
operatorInfo(fields, op).inverse, for code that wants to push a NOT down instead of wrapping it.
On this page