Operators
Every built-in operator, the value it takes, and when a rule counts as complete.
An operator is the middle part of a rule: the is in "Status is Paid". Each field type offers its own list (see Fields).
Built-in operators
| Label | Id | Value | Offered on |
|---|---|---|---|
| is | eq | one | text, number, date, time, boolean, select |
| is not | ne | one | text, number, select |
| contains | contains | one | text |
| does not contain | notContains | one | text |
| starts with | startsWith | one | text |
| ends with | endsWith | one | text |
| is greater than | gt | one | number, date, datetime, time |
| is greater than or equal to | gte | one | number, date, datetime, time |
| is less than | lt | one | number, date, datetime, time |
| is less than or equal to | lte | one | number, date, datetime, time |
| is between | between | two (from, to) | number, date, datetime, time |
| is any of | in | a list | multiSelect |
| is none of | notIn | a list | multiSelect |
| is empty | isEmpty | none | every type except boolean |
| is not empty | isNotEmpty | none | every type except boolean |
The id is what the URL and your code use: status__eq=paid.
On date and datetime fields, the labels read like dates: is on, is after, is on or after, is before, is on or before. On time fields they read like times: is at, is at or after, is at or before. You can change any label with messages.
is between includes both ends. On a date field in the browser, between Sep 1 and Sep 30 means all of September. Your backend may see it differently: 2026-09-30 compared with a timestamp column usually means midnight at the start of that day. mapRule can fix that.
Value shapes
The Value column above is the operator's arity, the shape of the value it needs. The UI uses it to pick the input:
| Arity | Value in code | Example | In the URL |
|---|---|---|---|
none | null | Deleted at is empty | deletedAt__isEmpty |
single | one value | Status is Paid | status__eq=paid |
range | a [from, to] pair | Amount is between 10 and 50 | amount__between=10,50 |
multi | a list, at least one item | Tags is any of urgent, vip | tags__in=urgent,vip |
When users switch operators, the value is kept if the shape stays the same (is → is not), and cleared otherwise (is → is between).
Complete rules
While users edit, a rule can be half-finished: no operator yet, half a range. On Apply, only complete rules are kept. A rule is complete when:
- its field is one of your fields,
- its operator is offered for that field,
- its value has the right shape, with no empty parts, and each part is valid for the type (a real date, a number…).
The same check runs when a URL is read. That's why a hand-edited or outdated link never crashes anything: rules that don't pass are dropped one by one.
Values are also cleaned up on the way: "42" in a number field becomes 42, "true" in a boolean field becomes true.
You can run the check yourself:
import { isRuleComplete, normalizeRule, normalizeState } from "@querycn/filter-core"
const context = { fields: orderFields }
normalizeRule({ id: "1", field: "amount", operator: "gt", value: "42" }, context)
// → { id: "1", field: "amount", operator: "gt", value: 42 }
normalizeRule({ id: "2", field: "amount", operator: "between", value: ["10", ""] }, context)
// → null (half a range)
isRuleComplete({ id: "3", field: "status", operator: "contains", value: "p" }, context)
// → false (select fields don't offer `contains`)
normalizeState(draft, context) // drops incomplete rules, cleans up the restWarnings
Some rules are valid but probably not what the user meant. They're still applied, with a warning icon next to them:
| Warning | When |
|---|---|
reversedRange | An is between whose start is after its end, like 50 – 10. Shown while editing. |
conflict | The request sends the same key twice, and most backends only read one. Shown after applying. |
unsupported | The request can't express the rule, so your backend never sees it. Shown after applying. |
conflict and unsupported come from the serializer: see Warnings in the UI. In your own UI, useRuleWarnings(rule) returns all three.
Your own operators
Operators are data, so you can add your own, like is not between or a full-text matches. See Add an operator.