# Docyrus Query Builder
URL: /docs/web/docyrus/docyrus-query-builder
Visual query builder for Docyrus data sources — filters, columns, sorting, calculations, formulas, pivots, and child queries.
**Demo:**
```tsx
'use client';
import { useState } from 'react';
import {
type EnumOption,
type IField
} from '@docyrus/ui/components/form-fields';
import { useForm } from '@tanstack/react-form';
import { CalendarClock, FileBarChart, Send } from 'lucide-react';
import { Button } from '@docyrus/ui/primitives/ui/button';
import { Switch } from '@docyrus/ui/primitives/ui/switch';
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger
} from '@docyrus/ui/primitives/ui/tabs';
import { cn } from '@docyrus/ui/primitives/lib/utils';
import { Field, FieldLabel } from '@docyrus/ui/primitives/ui/field';
import { CheckboxFormField } from '@docyrus/ui/components/form-fields/checkbox-form-field';
import { SelectFormField } from '@docyrus/ui/components/form-fields/select-form-field';
import { TextFormField } from '@docyrus/ui/components/form-fields/text-form-field';
import { TextareaFormField } from '@docyrus/ui/components/form-fields/textarea-form-field';
import {
DocyrusDataSourceQueryBuilder,
type IDataSourceReference,
type ISelectQueryParams
} from '@docyrus/ui/components/docyrus-query-builder';
const tasksFields = [
{
id: 't-1', name: 'Title', slug: 'title', type: 'field-text' as const
},
{
id: 't-2', name: 'Status', slug: 'status', type: 'field-status' as const
},
{
id: 't-3', name: 'Owner', slug: 'owner', type: 'field-userSelect' as const
},
{
id: 't-4', name: 'Due Date', slug: 'due_date', type: 'field-date' as const
},
{
id: 't-5', name: 'Priority', slug: 'priority', type: 'field-select' as const
},
{
id: 't-6', name: 'Created At', slug: 'created_at', type: 'field-dateTime' as const
},
{
id: 't-7', name: 'Done', slug: 'is_done', type: 'field-checkbox' as const
}
];
const dealsFields = [
{
id: 'd-1', name: 'Name', slug: 'name', type: 'field-text' as const
},
{
id: 'd-2', name: 'Amount', slug: 'amount', type: 'field-money' as const
},
{
id: 'd-3', name: 'Stage', slug: 'stage', type: 'field-status' as const
},
{
id: 'd-4', name: 'Account', slug: 'account', type: 'field-relation' as const
},
{
id: 'd-5', name: 'Close Date', slug: 'close_date', type: 'field-date' as const
}
];
const dataSources: IDataSourceReference[] = [
{
id: 'crm.tasks',
name: 'Tasks',
title: 'Tasks',
description: 'Things to get done.',
slug: 'tasks',
appSlug: 'crm',
appName: 'CRM',
fullSlug: 'crm.tasks',
type: 'standard',
fields: tasksFields
},
{
id: 'crm.deals',
name: 'Deals',
title: 'Deals',
description: 'Sales pipeline records.',
slug: 'deals',
appSlug: 'crm',
appName: 'CRM',
fullSlug: 'crm.deals',
type: 'standard',
fields: dealsFields
}
];
const initialQuery: Partial
)}
Submitted value is shown to the right ↦
);
}
/* ────────────────────────────────────────────────────────────────────────── */
/* Root */
/* ────────────────────────────────────────────────────────────────────────── */
export function DocyrusQueryBuilderDemo() {
return (
);
}
```
## Installation
```bash
pnpm dlx @docyrus/cli add @docyrus/ui-docyrus-query-builder
```
**Dependencies:**
- [class-variance-authority](https://www.npmjs.com/package/class-variance-authority)
- [lucide-react](https://www.npmjs.com/package/lucide-react)
- [react-querybuilder](https://www.npmjs.com/package/react-querybuilder)
## Usage
```tsx
'use client';
import { useState } from 'react';
import {
DocyrusQueryBuilder,
type ISelectQueryParams
} from '@docyrus/ui/components/docyrus-query-builder';
export function MyQueryBuilder() {
const [query, setQuery] = useState>({});
return (
);
}
```
### With a Docyrus API client
Pass an authenticated `RestApiClient` from `@docyrus/api-client` (or any object that implements the `DocyrusQueryBuilderClient` shape) to enable data-source discovery and query previews.
```tsx
import { useDocyrusClient } from '@docyrus/signin';
export function MyAuthedQueryBuilder() {
const client = useDocyrusClient();
const [query, setQuery] = useState>({});
return (
);
}
```
### Without a client (offline / preset data)
Skip the client and supply `dataSources` and `fields` yourself for offline editing and form-driven workflows.
```tsx
```
## EditorAgent integration
Pair `useApplyQuery` with `renderAiAssistant` to let an LLM author Docyrus query specs. The hook owns the controlled `value` and ships an `applyQuery` tool that the agent calls to commit a new query spec — the tool guards against primitives / arrays / null so a malformed payload cannot crash the builder.
```tsx
import { EditorAgent } from '@docyrus/ui/components/editor-agent';
import {
DocyrusDataSourceQueryBuilder,
useApplyQuery
} from '@docyrus/ui/components/docyrus-query-builder';
export function QueryBuilderPlayground({ client, user, agentId }) {
const query = useApplyQuery();
const [aiOpen, setAiOpen] = useState(false);
return (
(
)}
/>
);
}
```
The matching backend agent must register a tool named `applyQuery` whose input schema mirrors `{ value: object (required), explanation?: string }`.
## Columns — current + parent data sources
The **Columns** section is a single grouped, multi-select tree (no separate "selected columns" pane). In both the current and parent groups, **user** fields (`field-userSelect` / `field-userMultiSelect`) and **enum** fields (`field-select`, `field-radioGroup`, `field-enum`, `field-status`, `field-multiSelect`, `field-tagSelect`) are expandable into a fixed sub-field catalog — user → `id, firstname, lastname, email, photo`; enum → `id, autonumber_id, name, slug, icon, color`. A checked sub-field nests at the correct depth, e.g. `status(id, name)` or `company(owner(firstname, email))` (parent).
The header carries a **Parent Data Sources** switch (its state lives in the hook, so it survives switching to Run & Preview / JSON and back). It pulls fields from the data sources referenced by the current data source's `field-relation` columns and nests a checked field under its relation column, e.g. `company(title, industry)`. It requires a `client`; fields come from `GET /v1/dev/data-sources/:id/fields?expand=parent` (the `parent` expansion returns each parent's full field list keyed by data source id). The same wiring is exposed on the hook as `fetchRelatedFields(['parent', 'child'])` for custom UIs.
## Child Queries
The **Child Queries** panel (enabled via `enableChildQueries`) fetches related records as nested arrays, one per child data source. It uses the `child` expansion of the same endpoint (`GET /v1/dev/data-sources/:id/fields?expand=child`) to discover the data sources whose own `field-relation` points back to the current one:
1. Pick a child data source from the **Add child data source** dropdown — this creates an entry whose **From** and **Using** are auto-filled and read-only (`from` = the child data source as a `{appSlug}_{slug}` reference, e.g. `base_callcenter_call`; `using` = its back-reference relation field slug).
2. Select the columns to return via a multi-select tree-view of the child data source's own fields (with the same user / enum sub-field expansion as the Columns section).
3. Optionally set **Order By** (a field dropdown + asc/desc direction) and **Limit**.
`value.childQueries` is an **array** of `{ alias, from, using, columns, orderBy?, limit? }`. The `alias` (the result key the child rows are returned under) defaults to the child data source slug, and the editor automatically keeps it listed in the parent `value.columns` — the items endpoint **drops the child block unless its alias is also a parent column** (so the emitted request is `?columns=…,alias&childQueries=[{ alias, from, using, … }]`). Removing an entry strips its alias from the columns again. Child data sources load when the panel opens (it flips `relatedExpand.child`); the fetch is de-duped per `(dataSource, expand)` signature.
## API Reference
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `value` | `Partial` | required | Current query payload |
| `onChange` | `(value: Partial) => void` | required | Called whenever the query is edited |
| `client` | `DocyrusQueryBuilderClient \| null` | `null` | Authenticated Docyrus API client. Enables data-source discovery and query previews. |
| `fields` | `IField[]` | `[]` | Available fields for the selected data source. Overridden by the resolved data source when set. |
| `dataSources` | `IDataSourceReference[]` | `[]` | Pre-loaded data sources. Used as a fallback when no client is provided. |
| `locale` | `UiI18nLocale` | `'en'` | UI locale token forwarded to translation-aware sub-components |
| `variant` | `'default' \| 'bordered' \| 'compact'` | `'default'` | Visual variant |
| `size` | `'sm' \| 'default' \| 'lg'` | `'default'` | Component size |
| `className` | `string` | — | Additional CSS class |
| `defaultSection` | `DSQBSection` | `'dataSource'` | Initially active configure section |
| `lockDataSource` | `boolean` | `false` | Lock the builder to the data source supplied via `value` (`dataSourceId` / `dataSourceFullSlug`). Hides the "Select Data Source" step and the clear button so the user can't switch data sources. |
| `enableFormulas` | `boolean` | `true` | Show the formula editor section |
| `enablePivot` | `boolean` | `true` | Show the pivot editor section |
| `enableChildQueries` | `boolean` | `true` | Show the child queries editor section |
| `enableCalculations` | `boolean` | `true` | Show the calculations editor section |
| `aiAssistantOpen` | `boolean` | — | Controlled open state for the AI Assistant drawer. When provided the builder stops managing the open state internally — pair with `onAiAssistantOpenChange`. |
| `onAiAssistantOpenChange` | `(open: boolean) => void` | — | Fired when the AI Assistant toolbar button toggles the drawer. |
| `renderAiAssistant` | `(ctx: IQueryBuilderAiAssistantRenderContext) => ReactNode` | — | Mounts a custom AI Assistant drawer body. When set, the toolbar shows a Bot toggle that opens/closes the drawer; this render fn supplies the body (typically a `` or `` wrapper). The context exposes the live `value` payload and the currently-`selectedDataSource` so the slot can wire the agent's `dataSourceId` without re-deriving it. |
| `aiAssistantWidth` | `number` | `380` | Width in pixels the drawer animates to when open. |
## Components
| Component | Description |
|-----------|-------------|
| `DocyrusDataSourceQueryBuilder` | Top-level component — renders the Configure / Run & Preview / JSON tabs |
| `useApplyQuery` | Hook that owns `value` state and exposes the `applyQuery` client-side tool for `` — the tool replaces the builder's query spec with the agent's draft. Refuses primitives / arrays / null so a bad LLM call cannot crash the builder. |
| `DSQBDataSourceSelector` | Data source picker pane |
| `DSQBColumnsEditor` | Column selection + ordering pane |
| `DSQBFiltersEditor` | Filter rule builder pane |
| `DSQBOrderByEditor` | Sort builder pane |
| `DSQBPaginationEditor` | Limit + offset editor |
| `DSQBCalculationsEditor` | Aggregation / calculation rules editor |
| `DSQBFormulaEditor` | Formula composer (block AST) |
| `DSQBChildQueriesEditor` | Child queries editor |
| `DSQBPivotEditor` | Pivot matrix editor |
| `DSQBSettingsEditor` | Query mode + expand toggles |
| `DSQBJsonPreview` | Read-only JSON view of the current query |
| `DSQBRunPreview` | Run results table |
| `DSQBFieldSelector` | Reusable field picker primitive |
## Type Exports
| Type | Description |
|------|-------------|
| `DocyrusQueryBuilderProps` | Top-level component props |
| `DocyrusQueryBuilderClient` | Minimal HTTP client shape consumed by the component |
| `DSQBContextValue` | Shape of the internal context exposed by `useDSQB()` |
| `DSQBPrimaryTab` | `'configure' \| 'preview' \| 'json'` |
| `DSQBSection` | Configure section identifiers (data source, columns, filters, …) |
| `DSQBPreviewState` | Run-preview state (rows, columns, status, error) |
| `ISelectQueryParams` | Full query payload sent to the items endpoint |
| `IQueryFilterGroup` / `IQueryFilterRule` / `QueryFilterType` | Filter tree primitives |
| `ISelectQueryOrderBy` | Sort specifier |
| `ISelectQueryCalculationRule` / `AggregateFunction` / `NumberType` | Calculation rule primitives |
| `IQueryFormula` / `IQueryFormulaBlock` / `BlockKind` | Formula AST primitives |
| `IQueryChildQueryParams` | Child query specifier |
| `ISelectPivot` / `ISelectPivotMatrixQuery` / `DateRangeInterval` | Pivot primitives |
| `IDataSourceReference` | Discovered or supplied data source descriptor |
| `DSQBRelatedFields` / `DSQBParentFieldGroup` / `DSQBChildFieldGroup` | Parent / child field groups returned by `fetchRelatedFields()` |
| `DSQBRelatedExpand` | `{ parent, child }` expansion flags (`relatedExpand` / `setRelatedExpand` on the context) |
| `IField` / `IFieldType` | Docyrus field descriptor + supported field-type tokens |
| `QueryMode` / `ExpandType` | Execution flag tokens |
| `IQueryValidationError` | Validation error shape returned by `validateQuery()` |
| `ParsedColumn` | Parsed column descriptor returned by `parseColumnString()` |
| `UiI18nLocale` | Supported locale tokens |
| `IQueryBuilderAiAssistantRenderContext` | Argument passed to `renderAiAssistant` — `{ open, width, onClose, value, selectedDataSource }`. |
| `IUseApplyQueryResult` | Return shape of `useApplyQuery` — `{ query, setQuery, tools }`. |
## Helpers
| Export | Description |
|--------|-------------|
| `parseColumnString(columns)` | Parse a Docyrus columns string into `ParsedColumn[]` |
| `serializeColumns(columns)` | Inverse of `parseColumnString` |
| `normalizeOrderBy(orderBy)` | Coerce any orderBy value into an array |
| `serializeOrderBy(orderBy)` | Serialize orderBy into the API string form |
| `buildDefaultColumns(fields)` | Build a default columns string from a field list |
| `cleanPayload(query)` | Strip null / empty values from a query payload before sending |
| `getFieldTypeCategory(type)` | Map an `IFieldType` to its filter category |
| `getOperatorsForFieldType(type)` | Get the operator list applicable to a field type |
| `validateQuery(query)` | Run static validation, returning `IQueryValidationError[]` |
| `countFilterRules(filters)` | Count leaves in a filter tree |
| `createBlock(kind)` | Create a default formula block |
| `updateBlockAt(root, path, next)` | Immutably update a formula block at a path |
| `removeBlockAt(root, path)` | Immutably remove a formula block at a path |
| `insertBlockAt(root, path, next)` | Immutably insert a formula block at a path |
| `getBlockLabel(block)` | Render a human-readable label for a formula block |