# 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](/sdk/guides/scripts) registered under that name; if there is none, the query throws an `UnknownOperator` error (see [Custom operators](#custom-operators)).

---

## Comparison operators

### eq

Matches exact values. Works with all data types.

**Signature:**

```typescript
{ attribute: string, operator: 'eq', value: any }
```

**Examples:**

```typescript title="String equality"
{ attribute: 'status', operator: 'eq', value: 'active' }
```

```typescript title="Number equality"
{ attribute: 'price', operator: 'eq', value: 99.99 }
```

```typescript title="Boolean equality"
{ attribute: 'inStock', operator: 'eq', value: true }
```

```typescript title="Reference equality"
{ attribute: 'category', operator: 'eq', value: { key: 'electronics' } }
```

```typescript title="Date equality"
{ 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.

```typescript title="Array equality (exact match)"
{ 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).

---

### 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:**

```typescript
{ 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.invert` is NOT IN. Like `eq`, a missing value then matches.
- `options.transform` applies to the attribute value and to every listed value.
- `null` in the list matches `null`, like `eq` with `null`.
- An empty list matches nothing (and everything under `invert`).
- `value` must be an array; anything else throws `invalid query - in operator needs an array value`, and a list doesn't store the condition (its query stays as it was).

**Examples:**

```typescript title="Any of several values"
{ attribute: 'status', operator: 'in', value: ['active', 'pending'] }
```

```typescript title="Through references"
{ attribute: 'customer.address.city', operator: 'in', value: ['Vienna', 'Graz'] }
```

```typescript title="Reference keys, as strings or { key }"
{ attribute: 'category', operator: 'in', value: ['cat-1', { key: 'cat-2' }] }
```

```typescript title="Every tag is one of the listed ones"
{ attribute: 'tags', operator: 'in', value: ['sale', 'new'], options: { arrayValueHandling: 'every' } }
```

```typescript title="NOT IN"
{ 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:**

```typescript
{ attribute: string, operator: 'gt', value: number | string | Date }
```

**Examples:**

```typescript title="Number greater than"
{ attribute: 'price', operator: 'gt', value: 100 }
// Matches: 100.01, 150, 200 (not 100)
```

```typescript title="Date greater than"
{ attribute: 'orderDate', operator: 'gt', value: new Date('2025-01-01') }
// Matches: 2025-01-02 and later (not 2025-01-01)
```

```typescript title="String greater than (alphabetical)"
{ 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:**

```typescript
{ attribute: string, operator: 'gte', value: number | string | Date }
```

**Examples:**

```typescript title="Number greater than or equal"
{ attribute: 'price', operator: 'gte', value: 100 }
// Matches: 100, 100.01, 150, 200
```

```typescript title="Date greater than or equal"
{ attribute: 'orderDate', operator: 'gte', value: new Date('2025-01-01') }
// Matches: 2025-01-01 and later
```

---

### lt

Matches values less than the target (exclusive).

**Signature:**

```typescript
{ attribute: string, operator: 'lt', value: number | string | Date }
```

**Examples:**

```typescript title="Number less than"
{ attribute: 'stock', operator: 'lt', value: 10 }
// Matches: 0, 1, 9 (not 10)
```

```typescript title="Date less than"
{ attribute: 'expiresAt', operator: 'lt', value: new Date() }
// Matches: Past dates only
```

---

### lte

Matches values less than or equal to the target (inclusive).

**Signature:**

```typescript
{ attribute: string, operator: 'lte', value: number | string | Date }
```

**Examples:**

```typescript title="Number less than or equal"
{ attribute: 'price', operator: 'lte', value: 100 }
// Matches: 0, 50, 99.99, 100
```

```typescript title="Price range (combined)"
productList.catalog.query.addGroup({
  groupKey: 'price',
  groupType: 'and',
  queries: [
    { attribute: 'price', operator: 'gte', value: 50 },
    { attribute: 'price', operator: 'lte', value: 150 },
  ],
});
// Matches: 50 to 150 inclusive
```

Put 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 in `options.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:**

```typescript
{
  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](/sdk/queries/query-search).

**Examples:**

```typescript title="Substring search"
{ attribute: 'description', operator: 'find', value: '/wireless/' }
// Matches: "Bluetooth wireless headphones" (not "Wireless Mouse")
```

```typescript title="Case-insensitive substring search"
{
  attribute: 'name',
  operator: 'find',
  value: '/laptop/',
  options: { regexFlags: 'i' }
}
// Matches: "Laptop", "LAPTOP", "Gaming laptop"
```

```typescript title="Exact match of the whole value"
{ attribute: 'status', operator: 'find', value: 'active' }
```

```typescript title="Find in nested objects"
{ attribute: 'metadata', operator: 'find', value: { key: 'value' } }
// Matches items with specific nested property
```

---

## Type operators

### typeof

Matches values by their JavaScript type.

**Signature:**

```typescript
{ 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:**

```typescript title="Find undefined values"
{ attribute: 'description', operator: 'typeof', value: 'undefined' }
// Matches items whose description is absent (not null or '')
```

```typescript title="Find defined values (with invert)"
{
  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)
```

```typescript title="Find null values (use eq, not typeof)"
{ attribute: 'deletedAt', operator: 'eq', value: null }
// Matches items whose deletedAt is null (not items without a deletedAt value)
```

```typescript title="Find numeric values"
{ attribute: 'discount', operator: 'typeof', value: 'number' }
// Matches items with a numeric discount
```

---

### isTruthy

Evaluates JavaScript truthiness.

**Signature:**

```typescript
{ 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`](#arrayvaluehandling)); an empty array matches neither `true` nor `false`.

**Examples:**

```typescript title="Find truthy values"
{ attribute: 'inStock', operator: 'isTruthy', value: true }
// Matches: inStock=true, inStock=1, inStock="yes"
```

```typescript title="Find falsy values"
{ attribute: 'discontinued', operator: 'isTruthy', value: false }
// Matches: discontinued=false, discontinued=0, discontinued=null
```

---

### size

Matches arrays or strings by their length.

**Signature:**

```typescript
{ attribute: string, operator: 'size', value: number }
```

**Examples:**

```typescript title="Exact size match"
{ attribute: 'images', operator: 'size', value: 3 }
// Matches items with exactly 3 images
```

```typescript title="Empty arrays"
{ 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)
```

```typescript title="String length"
{ attribute: 'sku', operator: 'size', value: 8 }
// Matches SKUs with exactly 8 characters
```

`size` 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

```typescript title="Case-insensitive exact match"
{
  attribute: 'name',
  operator: 'find',
  value: 'laptop',
  options: { transform: ['lowercase'] }
}
// Matches: "Laptop", "LAPTOP" (not "Gaming laptop": a bare string matches the whole value)
```

```typescript title="Multiple transforms"
{
  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](/sdk/api/query-operators#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.

```typescript title="NOT equals"
{
  attribute: 'status',
  operator: 'eq',
  value: 'discontinued',
  options: { invert: true }
}
// Matches items where status is NOT 'discontinued'
```

```typescript title="NOT in array"
{
  attribute: 'tags',
  operator: 'find',
  value: 'hidden',
  options: { invert: true }
}
// Matches items WITHOUT 'hidden' tag
```

---

### arrayValueHandling

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

```typescript title="Match any element (default)"
{
  attribute: 'tags',
  operator: 'eq',
  value: 'featured',
  options: { arrayValueHandling: 'some' }
}
// Matches if ANY tag equals 'featured'
```

```typescript title="Match all elements"
{
  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.

```typescript title="Case-insensitive substring"
{
  attribute: 'name',
  operator: 'find',
  value: '/laptop/',
  options: { regexFlags: 'i' }
}
// Matches: "Laptop", "LAPTOP", "Gaming laptop"
```

---

## Query structure

### Single condition

```typescript
productList.catalog.query.addCondition({
  operator: 'eq',
  attribute: 'status',
  value: 'active',
});
```

### Multiple conditions (AND)

```typescript
productList.catalog.query.addCondition([
  { operator: 'eq', attribute: 'status', value: 'active' },
  { operator: 'gte', attribute: 'price', value: 50 },
]);
// Result: status = 'active' AND price >= 50
```

`addCondition` 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](#lte)).

### Grouped conditions (OR)

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

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

```typescript title="Single-valued reference"
{ attribute: 'customer.name', operator: 'eq', value: 'John' }
// Orders whose customer's name is exactly 'John' (order -> customer reference)
```

```typescript title="Nested reference"
{ attribute: 'customer.address.city', operator: 'eq', value: 'New York' }
// Query nested properties through references
```

```typescript title="Multi-valued reference"
{ 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](/sdk/guides/scripts) 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](/sdk/guides/error-handling#error-subclasses)). 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.

```typescript
dcupl.query.registerCustomOperator(
  operator: string,
  fn: (query: DcuplQuery, listItem: ListItem) => boolean
): void
```

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

```typescript title="Between operator"
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] }],
});
```

```typescript title="The same as an operator script"
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

```typescript
type DcuplQuery = {
  attribute: string;
  operator: DcuplQueryOperator;
  value: any;
  queryKey?: any;
  options?: DcuplQueryOptions;
};
```

### DcuplQueryOperator

```typescript
type DcuplQueryOperator = 'eq' | 'in' | 'find' | 'gt' | 'gte' | 'lt' | 'lte' | 'typeof' | 'isTruthy' | 'size';
```

### DcuplQueryOptions

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

```typescript
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. When `addGroup` extends an existing keyed group, that group keeps its current `groupType`.
- A group that reaches evaluation without a `groupType` (the root query, or a nested group passed inside a full query to `apply({ groupKey: 'root', ... })`, `execute` or `dcupl.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:**

```typescript
// 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](/sdk/queries/query-filter) - Learn filtering fundamentals
- [Query logic](/sdk/queries/query-logic) - Combine filters with AND/OR
- [Deep queries](/sdk/queries/query-deep) - Query across references
- [Catalog API](/sdk/api/catalog) - Full catalog query methods
