Query operators reference
Complete reference of all query operators available in the dcupl SDK.
Quick reference
| Operator | Description | Supported Types |
|---|---|---|
eq |
Exact match | All |
in |
Matches any value of a list | All |
find |
Regex, exact or object match | string, object |
gt |
Greater than | string, number, date |
gte |
Greater than or equal | string, number, date |
lt |
Less than | string, number, date |
lte |
Less than or equal | string, number, date |
typeof |
Type checking | All |
isTruthy |
Boolean truthiness check | All |
size |
Array/string length | Array, string |
These ten are all the built-in operators; there is no neq, nin, exists or regex (use eq or in with invert, typeof and find instead). A condition with any other operator name runs the operator script registered under that name; if there is none, the query throws an UnknownOperator error (see Custom operators).
Comparison operators
eq
Matches exact values. Works with all data types.
Signature:
{ attribute: string, operator: 'eq', value: any }Examples:
{ attribute: 'status', operator: 'eq', value: 'active' }{ attribute: 'price', operator: 'eq', value: 99.99 }{ attribute: 'inStock', operator: 'eq', value: true }{ attribute: 'category', operator: 'eq', value: { key: 'electronics' } }{ attribute: 'createdAt', operator: 'eq', value: new Date('2025-01-01') }eq on a date property also matches the ISO string (date.toISOString()), but not a timestamp.
{ attribute: 'tags', operator: 'eq', value: ['sale', 'featured'] }eq with an array value compares the whole array. To match "any of" several values, use in.
in
Matches if the attribute's value equals any value of the list. It uses the same equality as eq: a string or { key } matches a reference by its key, and a date by its ISO string. It answers like an or group of one eq condition per listed value.
Signature:
{ attribute: string, operator: 'in', value: unknown[] }Rules:
- On an
Array<…>property or a dotted path through a multi-valued reference,arrayValueHandling: 'some'(default) matches if at least one value is listed;'every'matches if every value is listed (and there is at least one). options.invertis NOT IN. Likeeq, a missing value then matches.options.transformapplies to the attribute value and to every listed value.nullin the list matchesnull, likeeqwithnull.- An empty list matches nothing (and everything under
invert). valuemust be an array; anything else throwsinvalid query - in operator needs an array value, and a list doesn't store the condition (its query stays as it was).
Examples:
{ attribute: 'status', operator: 'in', value: ['active', 'pending'] }{ attribute: 'customer.address.city', operator: 'in', value: ['Vienna', 'Graz'] }{ attribute: 'category', operator: 'in', value: ['cat-1', { key: 'cat-2' }] }{ attribute: 'tags', operator: 'in', value: ['sale', 'new'], options: { arrayValueHandling: 'every' } }{ attribute: 'status', operator: 'in', value: ['archived', 'deleted'], options: { invert: true } }gt
Matches values greater than the target (exclusive).
The same value rules apply to gt, gte, lt and lte: numbers compare numerically and strings by character code, which is case-sensitive ('book' > 'M', because lowercase letters come after uppercase ones). On a date property, pass a Date (or a timestamp in milliseconds); an ISO string such as '2025-01-01' matches nothing. A missing (undefined/null) value never matches.
Signature:
{ attribute: string, operator: 'gt', value: number | string | Date }Examples:
{ attribute: 'price', operator: 'gt', value: 100 }
// Matches: 100.01, 150, 200 (not 100){ attribute: 'orderDate', operator: 'gt', value: new Date('2025-01-01') }
// Matches: 2025-01-02 and later (not 2025-01-01){ attribute: 'name', operator: 'gt', value: 'M' }
// Matches: "MacBook", "Nike", "Zenith" (not "Apple", "Laptop")gte
Matches values greater than or equal to the target (inclusive).
Signature:
{ attribute: string, operator: 'gte', value: number | string | Date }Examples:
{ attribute: 'price', operator: 'gte', value: 100 }
// Matches: 100, 100.01, 150, 200{ attribute: 'orderDate', operator: 'gte', value: new Date('2025-01-01') }
// Matches: 2025-01-01 and laterlt
Matches values less than the target (exclusive).
Signature:
{ attribute: string, operator: 'lt', value: number | string | Date }Examples:
{ attribute: 'stock', operator: 'lt', value: 10 }
// Matches: 0, 1, 9 (not 10){ attribute: 'expiresAt', operator: 'lt', value: new Date() }
// Matches: Past dates onlylte
Matches values less than or equal to the target (inclusive).
Signature:
{ attribute: string, operator: 'lte', value: number | string | Date }Examples:
{ attribute: 'price', operator: 'lte', value: 100 }
// Matches: 0, 50, 99.99, 100productList.catalog.query.addGroup({
groupKey: 'price',
groupType: 'and',
queries: [
{ attribute: 'price', operator: 'gte', value: 50 },
{ attribute: 'price', operator: 'lte', value: 150 },
],
});
// Matches: 50 to 150 inclusivePut both bounds in one 'and' group. Passed to addCondition, two conditions on the same attribute are combined with OR, which matches every price.
Search operators
find
Regex, exact or object match. How it matches depends on the shape of value:
- Slash-delimited regex (e.g.
'/wireless/','/^foo/'): the text between the slashes is a JavaScript regex. Use this for substring and pattern matching. Flags go inoptions.regexFlags, not after the closing slash:'/^foo/i'is a bare string and matches exactly. - Bare string (e.g.
'wireless'): exact equality against the whole field value, not a substring.'Ball'does not match'Soccer Ball'. - Object (e.g.
{ key: 'value' }): property match against the field.
A /regex/ is tested against non-string values as strings: '/^1/' on a number attribute matches 150 and 100.
Signature:
{
attribute: string,
operator: 'find',
value: Record<string, unknown> | string,
options?: { regexFlags?: string }
}regexFlags applies only to a /regex/ value; it is ignored for any other value, and invalid flags match nothing. When you build the pattern from user input, escape regex characters before wrapping it in slashes. For search UIs, see Text search.
Examples:
{ attribute: 'description', operator: 'find', value: '/wireless/' }
// Matches: "Bluetooth wireless headphones" (not "Wireless Mouse"){
attribute: 'name',
operator: 'find',
value: '/laptop/',
options: { regexFlags: 'i' }
}
// Matches: "Laptop", "LAPTOP", "Gaming laptop"{ attribute: 'status', operator: 'find', value: 'active' }{ attribute: 'metadata', operator: 'find', value: { key: 'value' } }
// Matches items with specific nested propertyType operators
typeof
Matches values by their JavaScript type.
Signature:
{ attribute: string, operator: 'typeof', value: TypeName }Valid Types: 'string', 'number', 'boolean', 'object', 'undefined'
Note: To find null values, use the eq operator with value: null instead of typeof (typeof 'object' matches null too, as in JavaScript).
Examples:
{ attribute: 'description', operator: 'typeof', value: 'undefined' }
// Matches items whose description is absent (not null or ''){
attribute: 'description',
operator: 'typeof',
value: 'undefined',
options: { invert: true }
}
// Matches items with any description value, null and '' included (use isTruthy true for a usable value){ attribute: 'deletedAt', operator: 'eq', value: null }
// Matches items whose deletedAt is null (not items without a deletedAt value){ attribute: 'discount', operator: 'typeof', value: 'number' }
// Matches items with a numeric discountisTruthy
Evaluates JavaScript truthiness.
Signature:
{ attribute: string, operator: 'isTruthy', value: boolean }Truthy Values: true, non-zero numbers, non-empty strings, objects
Falsy Values: false, 0, '', null, undefined, NaN
On an Array<…> property each element is checked (see arrayValueHandling); an empty array matches neither true nor false.
Examples:
{ attribute: 'inStock', operator: 'isTruthy', value: true }
// Matches: inStock=true, inStock=1, inStock="yes"{ attribute: 'discontinued', operator: 'isTruthy', value: false }
// Matches: discontinued=false, discontinued=0, discontinued=nullsize
Matches arrays or strings by their length.
Signature:
{ attribute: string, operator: 'size', value: number }Examples:
{ attribute: 'images', operator: 'size', value: 3 }
// Matches items with exactly 3 images{ attribute: 'reviews', operator: 'size', value: 0 }
// Matches items whose reviews value is an empty array,
// not items that have no reviews value at all (undefined/null never matches){ attribute: 'sku', operator: 'size', value: 8 }
// Matches SKUs with exactly 8 characterssize always measures the whole array or string; arrayValueHandling is ignored.
Operator options
transform and invert work with every operator, arrayValueHandling with every operator except size, and regexFlags only with find.
transform
An array of transformations, applied in order to both the attribute value and the query value before comparison.
Available Transforms:
'lowercase'- Convert to lowercase'trim'- Remove leading/trailing whitespace'removeWhitespace'- Remove all whitespace
{
attribute: 'name',
operator: 'find',
value: 'laptop',
options: { transform: ['lowercase'] }
}
// Matches: "Laptop", "LAPTOP" (not "Gaming laptop": a bare string matches the whole value){
attribute: 'email',
operator: 'eq',
value: 'user@example.com',
options: { transform: ['lowercase', 'trim'] }
}Only string values (and strings inside arrays) are transformed: on a number or date attribute a transform does nothing. A transform also makes eq and in check every item instead of using the attribute's index.
invert
Negates the operator result. Applies to every built-in operator, not to custom operators: their result is used as is, so negate inside the script or function. A missing (undefined/null) value matches neither way, except for eq, in, typeof and isTruthy, which compare against it. A list of values (an Array<…> property or a dotted path through a multi-valued reference) is checked per value: an empty list does not match the condition, so the inverted condition matches it.
{
attribute: 'status',
operator: 'eq',
value: 'discontinued',
options: { invert: true }
}
// Matches items where status is NOT 'discontinued'{
attribute: 'tags',
operator: 'find',
value: 'hidden',
options: { invert: true }
}
// Matches items WITHOUT 'hidden' tagarrayValueHandling
Controls how operators match against array values: Array<…> properties and dotted paths through a multi-valued reference (memberships.role compares the role of every membership).
Values:
'some'(default) - Match if ANY element matches'every'- Match only if ALL elements match, and there is at least one: an empty array (or a reference with no items) never matches
{
attribute: 'tags',
operator: 'eq',
value: 'featured',
options: { arrayValueHandling: 'some' }
}
// Matches if ANY tag equals 'featured'{
attribute: 'tags',
operator: 'find',
value: '/promo/',
options: { arrayValueHandling: 'every' }
}
// Matches only if ALL tags contain 'promo'regexFlags
Flags for a find condition with a /regex/ value, e.g. 'i' for case-insensitive. Ignored by every other operator and value shape; invalid flags match nothing. Never put flags after the closing slash: '/laptop/i' is a bare string and matches only that exact text.
{
attribute: 'name',
operator: 'find',
value: '/laptop/',
options: { regexFlags: 'i' }
}
// Matches: "Laptop", "LAPTOP", "Gaming laptop"Query structure
Single condition
productList.catalog.query.addCondition({
operator: 'eq',
attribute: 'status',
value: 'active',
});Multiple conditions (AND)
productList.catalog.query.addCondition([
{ operator: 'eq', attribute: 'status', value: 'active' },
{ operator: 'gte', attribute: 'price', value: 50 },
]);
// Result: status = 'active' AND price >= 50addCondition puts each condition into the group keyed by its attribute (created as an 'or' group). Conditions on different attributes are combined with AND; conditions on the same attribute are combined with OR. For a range on one attribute, use an 'and' group (see Price range).
Grouped conditions (OR)
productList.catalog.query.addGroup({
groupKey: 'categories',
groupType: 'or',
queries: [
{ operator: 'eq', attribute: 'category', value: { key: 'electronics' } },
{ operator: 'eq', attribute: 'category', value: { key: 'computers' } },
],
});Nested groups
A nested group goes into the queries array, next to the conditions.
productList.catalog.query.addGroup({
groupKey: 'main',
groupType: 'and',
queries: [
{ operator: 'eq', attribute: 'active', value: true },
{
groupKey: 'categories',
groupType: 'or',
queries: [
{ operator: 'eq', attribute: 'category', value: { key: 'electronics' } },
{ operator: 'eq', attribute: 'category', value: { key: 'computers' } },
],
},
],
});
// Result: active AND (electronics OR computers)Deep queries
Query through references using dot notation.
{ attribute: 'customer.name', operator: 'eq', value: 'John' }
// Orders whose customer's name is exactly 'John' (order -> customer reference){ attribute: 'customer.address.city', operator: 'eq', value: 'New York' }
// Query nested properties through references{ attribute: 'orders.status', operator: 'eq', value: 'completed' }
// Customers with at least one completed order (arrayValueHandling 'some')Custom operators
Any operator that is not built in runs the operator script registered under that name with dcupl.scripts.set(). Without one, the query throws an UnknownOperator error (invalid query - unknown operator "X"; expected a built-in operator (…) or a registered custom operator (…)) from dcupl.query.execute() and before a list stores the condition, so a misspelt operator fails instead of matching nothing. Check error.name === 'UnknownOperator', not instanceof (see Error handling). The older dcupl.query.registerCustomOperator() still works but is deprecated; a script with the same name takes precedence over it. options.invert is not applied to a custom operator's result: negate inside the script or function.
dcupl.query.registerCustomOperator(
operator: string,
fn: (query: DcuplQuery, listItem: ListItem) => boolean
): voidThe function receives the condition (attribute, operator, value) and the item, whose attribute values are top-level properties. It is only called for items that have a value for query.attribute; items without one never match.
dcupl.query.registerCustomOperator('$between', (query, listItem) => {
const value = listItem[query.attribute];
const [min, max] = query.value as [number, number];
return value >= min && value <= max;
});
dcupl.query.execute({
modelKey: 'Product',
queries: [{ attribute: 'price', operator: '$between', value: [100, 500] }],
});dcupl.scripts.set(
'$between',
`const [min, max] = query.value; const value = item[query.attribute]; return value >= min && value <= max;`,
);In TypeScript, DcuplQuery.operator only lists the built-in operators, so list.catalog.query.addCondition({ operator: '$between', ... }) does not type-check. Cast the condition ({ ... } as unknown as DcuplQuery); dcupl.query.execute() accepts it as is.
Type reference
DcuplQuery, DcuplQueryOperator and DcuplQueryGroup are exported from @dcupl/common, e.g. import type { DcuplQuery, DcuplQueryGroup } from '@dcupl/common';. DcuplQueryOptions is not exported (DcuplQuery declares its options inline); use NonNullable<DcuplQuery['options']> if you need it.
DcuplQuery
type DcuplQuery = {
attribute: string;
operator: DcuplQueryOperator;
value: any;
queryKey?: any;
options?: DcuplQueryOptions;
};DcuplQueryOperator
type DcuplQueryOperator = 'eq' | 'in' | 'find' | 'gt' | 'gte' | 'lt' | 'lte' | 'typeof' | 'isTruthy' | 'size';DcuplQueryOptions
type DcuplQueryOptions = {
transform?: ('lowercase' | 'trim' | 'removeWhitespace')[];
invert?: boolean;
arrayValueHandling?: 'some' | 'every';
regexFlags?: string; // find only
};DcuplQueryGroup
Groups combine multiple conditions with AND/OR logic. Nested groups are placed directly in the queries array.
type DcuplQueryGroup = {
groupKey?: string;
groupType?: 'and' | 'or'; // Set it explicitly (see below)
queries: (DcuplQuery | DcuplQueryGroup)[];
};Set groupType explicitly. When it is omitted, the effective behaviour depends on where the group ends up:
- Groups given to the query builder (
addGroup,setGroup,apply(group)) are stored as'or', both when they are created and when they replace an existing group. WhenaddGroupextends an existing keyed group, that group keeps its currentgroupType. - A group that reaches evaluation without a
groupType(the root query, or a nested group passed inside a full query toapply({ groupKey: 'root', ... }),executeordcupl.query.execute) is evaluated exactly like'and'.
In add mode (the default), a group without a groupKey is always appended as a new group. Only keyed groups merge with an existing group of the same groupKey. In set mode (setGroup), a group without a groupKey replaces the first top-level group that has no key and is only appended when there is none. A keyed group is found at any depth (a top-level match wins), so a group nested inside a keyless wrapper is replaced or extended in place instead of getting a second top-level copy.
Example with nested groups:
// Complex filter: active products that are either electronics OR books
const query: DcuplQueryGroup = {
groupType: 'and',
queries: [
{ operator: 'eq', attribute: 'status', value: 'active' },
{
groupType: 'or',
queries: [
{ operator: 'eq', attribute: 'category', value: 'electronics' },
{ operator: 'eq', attribute: 'category', value: 'books' },
],
},
],
};See also
- Basic filtering - Learn filtering fundamentals
- Query logic - Combine filters with AND/OR
- Deep queries - Query across references
- Catalog API - Full catalog query methods