.md

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.

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.

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.

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

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
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 }
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
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
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 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 for full documentation.

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
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' },
});