# Query methods & operators

Guide to building and changing a list query. The full method reference is the Catalog API.

## Query object

Access the query API through `list.catalog.query`:

- Every `DcuplList` has a `catalog` with a `query` object
- The query object manages conditions, groups, and options
- Changes to the query automatically update filtered results

The query API provides methods to **add**, **set**, **remove**, and **execute** queries.

```typescript filename="query-object.ts"
import { Dcupl } from '@dcupl/core';

const dcupl = new Dcupl();
dcupl.models.set({
  key: 'Product',
  properties: [{ key: 'status', type: 'string' }],
  data: [{ key: 'p1', status: 'active' }],
});
await dcupl.init();

// Create a list
const list = dcupl.lists.create({
  modelKey: 'Product',
});

// Access the query object
const query = list.catalog.query;

// Add conditions, get results
query.addCondition({
  attribute: 'status',
  operator: 'eq',
  value: 'active',
});

const items = query.items();
```

## Adding conditions

`addCondition()` adds filter conditions to the query:

- Pass a single `QueryCondition` object
- Or pass an array of conditions
- Each condition goes into a group named after its attribute
- Conditions on **different** attributes combine with **AND**
- Conditions on the **same** attribute combine with **OR**

Use `addCondition` when building queries incrementally, like adding filters one at a time based on user selections. For a range on one attribute (AND) or an OR across attributes, use `addGroup()` with an explicit `groupType`.

```typescript filename="add-condition.ts"
// Single condition
query.addCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics',
});

// Different attributes combine with AND
query.addCondition([
  { attribute: 'inStock', operator: 'eq', value: true },
  { attribute: 'rating', operator: 'gte', value: 4 },
]);
// Result: category=Electronics AND inStock=true AND rating>=4

// The same attribute combines with OR
query.addCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Computers',
});
// Result: (category=Electronics OR category=Computers)
//         AND inStock=true AND rating>=4

// A range needs an 'and' group - two addCondition calls
// on 'price' would mean price>=100 OR price<=500
query.addGroup({
  groupKey: 'price',
  groupType: 'and',
  queries: [
    { attribute: 'price', operator: 'gte', value: 100 },
    { attribute: 'price', operator: 'lte', value: 500 },
  ],
});
```

## Setting conditions

`setCondition()` replaces the conditions on one attribute:

- Replaces the conditions in the group of the condition's attribute
- Keeps the conditions on all other attributes
- Useful for single-choice filters, like a category dropdown

To start over with a completely different filter, call `clear()` first.

```typescript filename="set-condition.ts"
// Add some conditions first
query.addCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Electronics',
});
query.addCondition({
  attribute: 'price',
  operator: 'gte',
  value: 100,
});

// setCondition replaces the 'category' conditions only
query.setCondition({
  attribute: 'category',
  operator: 'eq',
  value: 'Furniture',
});

// Now: category=Furniture AND price>=100

// Start over with a different filter
query.clear();
query.setCondition([
  { attribute: 'status', operator: 'eq', value: 'active' },
  { attribute: 'onSale', operator: 'eq', value: true },
]);
// Now: status=active AND onSale=true
```

## Query groups

Use `addGroup()` and `setGroup()` for complex boolean logic:

- `groupType: 'and'` - All conditions must match
- `groupType: 'or'` - Any condition can match
- `groupKey` - Identifier to extend, replace, or remove the group later

Groups are combined with AND. `addGroup()` with an existing `groupKey` extends that group; `setGroup()` replaces it. `addGroup()` always adds a group without a `groupKey` as a new group, while `setGroup()` without a key replaces the first keyless group. Give groups a key to keep them apart.

Always set `groupType`: a group created or replaced (`setGroup()`) without one is stored as `'or'`.

Groups can be nested for arbitrary boolean expressions like `(A AND B) OR (C AND D)`.

```typescript filename="query-groups.ts"
// Add OR group: Electronics OR Computers
query.addGroup({
  groupType: 'or',
  groupKey: 'category-filter',
  queries: [
    { attribute: 'category', operator: 'eq', value: 'Electronics' },
    { attribute: 'category', operator: 'eq', value: 'Computers' },
  ],
});

// setGroup replaces the group with the same groupKey
query.setGroup({
  groupType: 'or',
  groupKey: 'category-filter',
  queries: [
    { attribute: 'category', operator: 'eq', value: 'Furniture' },
  ],
});

// Nested group: inStock=true AND (brand=Apple OR brand=Samsung)
query.addGroup({
  groupType: 'and',
  groupKey: 'stock-and-brand',
  queries: [
    { attribute: 'inStock', operator: 'eq', value: true },
    {
      groupType: 'or',
      queries: [
        { attribute: 'brand', operator: 'eq', value: 'Apple' },
        { attribute: 'brand', operator: 'eq', value: 'Samsung' },
      ],
    },
  ],
});

// Result: category=Furniture
//         AND inStock=true AND (brand=Apple OR brand=Samsung)
```

## Removing conditions

Remove specific conditions or clear the entire query:

| Method | Description |
| --- | --- |
| `removeCondition(condition)` | Remove one condition (pass the full condition) |
| `removeGroup(groupKey)` | Remove a group by its key. Attribute groups from `addCondition` are keyed by the attribute name. |
| `clear()` | Remove all conditions and groups, and the sort and pagination options |

```typescript filename="removing-conditions.ts"
// Setup: add conditions
query.addCondition({ attribute: 'category', operator: 'eq', value: 'Electronics' });
query.addCondition({ attribute: 'price', operator: 'gte', value: 100 });
query.addGroup({
  groupKey: 'brand-filter',
  groupType: 'or',
  queries: [
    { attribute: 'brand', operator: 'eq', value: 'Apple' },
    { attribute: 'brand', operator: 'eq', value: 'Samsung' },
  ],
});

// Remove one condition
query.removeCondition({ attribute: 'price', operator: 'gte', value: 100 });

// Remove all conditions on an attribute
query.removeGroup('category');

// Remove group by key
query.removeGroup('brand-filter');

// Clear everything
query.clear();

// Query is now empty
```

## Query options

`applyOptions()` configures pagination, sorting, and projection:

| Option | Description |
| --- | --- |
| `start` | Skip N items (offset for pagination) |
| `count` | Limit results to N items |
| `sort` | `{ attributes, order }` - one `'ASC'` or `'DESC'` per attribute (uppercase) |
| `projection` | Select specific properties to return, e.g. `{ $: false, name: true }` |

```typescript filename="apply-options.ts"
// Pagination: page 2, 20 items per page
query.applyOptions({
  start: 20,
  count: 20,
});

// Sorting: by price descending, then by name
query.applyOptions({
  sort: { attributes: ['price', 'name'], order: ['DESC', 'ASC'] },
});

// Projection: only return specific fields ($: false drops the rest;
// key is always included)
query.applyOptions({
  projection: { $: false, name: true, price: true, category: true },
});

// Combine all options
query.applyOptions({
  start: 0,
  count: 50,
  sort: { attributes: ['createdAt'], order: ['DESC'] },
  projection: { $: false, name: true, price: true },
});
```

## Getting results

Retrieve query results with these methods:

| Method | Returns | Use Case |
| --- | --- | --- |
| `items()` | Array of matching items | Display lists, tables |
| `first()` | First item of the current page (an applied `start` counts) or undefined | Single item lookup |
| `any()` | Boolean: any item on the current page (`false` when `start` is past the last match) | Check existence |
| `count()` | Number of items on the current page | Page size checks |
| `fn.metadata().currentSize` | Number of all matching items | Display total count |

```typescript filename="results.ts"
// Get all matching items (respects start/count options)
const products = query.items();
// => [{ key: 'p1', name: 'Laptop', ... }, ...]

// Get just the first match
const firstProduct = query.first();
// => { key: 'p1', name: 'Laptop', ... } or undefined

// Check if any items match
const hasResults = query.any();
// => true or false

// Count items on the current page (respects start/count)
const pageCount = query.count();
// => 20

// Total number of matching items (ignores start/count)
const totalCount = list.catalog.fn.metadata().currentSize;
// => 42

// Common pattern: pagination info
const items = query.items();
const total = list.catalog.fn.metadata().currentSize;
console.log(`Showing ${items.length} of ${total} results`);
```

## Query state

Inspect the current state of a query:

| Method | Returns | Use Case |
| --- | --- | --- |
| `current()` | QueryExecutionContext | Serialize, debug, or restore query |
| `isEmpty()` | Boolean | Check if query has conditions |
| `has({ groupKey })` | Boolean | Check for a specific group |

```typescript filename="query-state.ts"
// Get complete query context
const context = query.current();
// => { modelKey: 'Product', groupKey: 'root', groupType: 'and',
//      queries: [...], start: 0, count: 50 }

// Useful for debugging or saving to URL/storage
console.log(JSON.stringify(context, null, 2));

// Check if query is empty
if (query.isEmpty()) {
  console.log('No filters applied');
}

// Check if an attribute is filtered
// (addCondition groups are keyed by the attribute name)
const hasCategory = query.has({ groupKey: 'category' });

// Check if specific group exists
const hasBrandGroup = query.has({ groupKey: 'brand-filter' });
// => true if a group with that key exists
```

## Operators

The `operator` property determines how values are compared:

| Category | Operators |
| --- | --- |
| Equality | `eq`, `in` |
| Comparison | `gt`, `gte`, `lt`, `lte` |
| Text and pattern | `find` |
| Type and value checks | `typeof`, `isTruthy`, `size` |

There are no separate "not" operators: negate any condition with `options: { invert: true }`. Text search uses `find` with a regular expression in slashes; a bare string must match the whole value. Ranges are an `'and'` group of `gte` and `lte`.

Any other `operator` name runs the [script](/sdk/guides/scripts) registered under that key as a custom operator. Without one, the query throws an `UnknownOperator` error naming the operator and the valid ones, so a typo like `'neq'` fails instead of silently returning no items.

See [Operators Reference](/sdk/api/query-operators) for full documentation.

```typescript filename="operators.ts"
// Equality
{ attribute: 'status', operator: 'eq', value: 'active' }

// Not equal: invert any condition
{ attribute: 'status', operator: 'eq', value: 'deleted', options: { invert: true } }

// Comparison
{ attribute: 'price', operator: 'gte', value: 100 }
{ attribute: 'price', operator: 'lt', value: 1000 }

// Contains: wrap the term in slashes (a bare string matches the whole value)
{ attribute: 'name', operator: 'find', value: '/laptop/', options: { regexFlags: 'i' } }

// Starts with / Ends with
{ attribute: 'sku', operator: 'find', value: '/^SKU-/' }
{ attribute: 'email', operator: 'find', value: '/@company\\.com$/' }

// In list
{ attribute: 'category', operator: 'in', value: ['Electronics', 'Computers'] }

// Not in list
{ attribute: 'status', operator: 'in', value: ['archived', 'deleted'], options: { invert: true } }

// Has a value (null counts as a value here: typeof null is 'object')
{ attribute: 'description', operator: 'typeof', value: 'undefined', options: { invert: true } }

// Has a usable value (not undefined, null or '')
{ attribute: 'description', operator: 'isTruthy', value: true }

// Array or string length
{ attribute: 'tags', operator: 'size', value: 0 }
```

## Condition options

Fine-tune condition behavior with the `options` property:

| Option | Description |
| --- | --- |
| `transform` | Array of transforms applied to both sides before comparing: `'lowercase'`, `'trim'`, `'removeWhitespace'` |
| `invert` | Negate the condition result |
| `arrayValueHandling` | How to match array values (`'some'`, the default, or `'every'`) |
| `regexFlags` | `find` only: flags for a `'/regex/'` value, e.g. `'i'` for case-insensitive |

```typescript filename="condition-options.ts"
// Case-insensitive exact match with transform
query.addCondition({
  attribute: 'name',
  operator: 'eq',
  value: 'laptop',
  options: { transform: ['lowercase'] },
});

// Case-insensitive substring search with regexFlags
query.addCondition({
  attribute: 'name',
  operator: 'find',
  value: '/lap/',
  options: { regexFlags: 'i' },
});

// Invert: find items NOT matching
query.addCondition({
  attribute: 'status',
  operator: 'eq',
  value: 'archived',
  options: { invert: true },
});
// Equivalent to: status != 'archived'

// Array handling: at least one tag is 'sale' or 'featured'
query.addCondition({
  attribute: 'tags',
  operator: 'in',
  value: ['sale', 'featured'],
  options: { arrayValueHandling: 'some' },
});

// Array handling: every tag is 'premium' or 'verified'
// (an empty tags array never matches 'every')
query.addCondition({
  attribute: 'tags',
  operator: 'in',
  value: ['premium', 'verified'],
  options: { arrayValueHandling: 'every' },
});
```

## Next steps

- [Catalog API](/sdk/api/catalog) — Full reference of every query method (apply, remove, execute, one, many, ...)
- [Basic filtering](/sdk/queries/query-filter) — Interactive filtering with facets
- [Query logic](/sdk/queries/query-logic) — Complex boolean expressions
- [Results & Pagination](/sdk/queries/query-results) — Pagination, sorting, and projections
- [Operators Reference](/sdk/api/query-operators) — Complete list of all operators
