@ahmad_009/querycraft
v2.0.2
Published
A flexible production-ready React query builder with custom operators, SQL/JSON/MongoDB/GraphQL output, drag-and-drop, undo/redo, and a headless hook.
Downloads
255
Maintainers
Readme
QueryCraft
A flexible, production-ready React query builder library. Build visual filter UIs that export to SQL, JSON, MongoDB, or GraphQL — with custom operators, drag-and-drop, undo/redo, and a headless hook. The default UI lays out each rule as numbered steps (column → match → value) and optional IF / THEN / ELSE and formula flows in the same style.
Install
npm i @ahmad_009/querycraftQuick Start
import { QueryBuilder } from 'querycraft';
import type { Field, RuleGroup } from 'querycraft';
import { useState } from 'react';
const fields: Field[] = [
{ name: 'name', label: 'Name', type: 'string' },
{ name: 'age', label: 'Age', type: 'number' },
{ name: 'active', label: 'Active', type: 'boolean' },
{ name: 'role', label: 'Role', type: 'select',
options: [
{ label: 'Admin', value: 'admin' },
{ label: 'User', value: 'user' },
],
},
];
export default function App() {
const [query, setQuery] = useState<RuleGroup | undefined>();
return (
<QueryBuilder
fields={fields}
onChange={setQuery}
showOutput
draggable
/>
);
}Built-in UI layout
The default QueryBuilder uses a step hierarchy so filters read top to bottom instead of a wide grid of equal columns.
- Root group — Pick AND / OR / NOT to define how rules inside the group combine. A short structure line (root only) reminds you: combinator → list of rules → optional nested groups.
- Each rule — One card with three numbered steps:
- Column — Which field you are filtering.
- Match — The operator (equals, contains, greater than, …).
- Value — What to compare against (or a note when no value is needed).
- Header row — Drag handle (if
draggable) plus remove; a one-line hint explains the flow. - Nested groups — Indented blocks with their own combinator; rules inside use the same three-step layout.
Flexible operands (default enableFlexibleOperands !== false): for unary operators without a custom value editor, step Value includes a Compare to dropdown — text or number I type, another column, or saved value (from variables) — then the matching control.
Expression operators & visual formulas
With enableExpressionOperators (default true), two extra operators are merged in for supported field types:
| Operator (label) | What the UI looks like | |------------------|-------------------------| | IF / THEN / ELSE | Three numbered blocks: (1) When “{field}”… — condition (simple compare or optional custom expression), (2) Then show, (3) Otherwise show. Each branch can be a column, fixed text, a variable, or calculate (opens the visual formula builder). | | Calculate / Formula | Step 1 — Choose the kind of calculation (combine text, math, round, checks, nested IF, or type your own). Step 2 — Parameters in a nested card (only the fields that choice needs). |
Custom condition (link under the simple IF condition) reuses the same column → how it relates → compared to steps; Type formula yourself (expert) switches to a raw formula field.
You can reuse pieces in your own operators:
import {
EXPRESSION_EXTENSION_OPERATORS,
VisualFormulaBuilder,
IfThenElseValueEditor,
MathFormulaValueEditor,
} from 'querycraft';
// Turn off the built-in IF/formula operators
<QueryBuilder fields={fields} enableExpressionOperators={false} />
// Plain value inputs only (no “another column” / variables on unary rules)
<QueryBuilder fields={fields} enableFlexibleOperands={false} />Pass variables / variableValues on QueryBuilder so “saved value” modes and @[name] in formulas resolve correctly (see types: QueryVariable).
Field Types
| Type | Value editor rendered | Default operators |
|------------|--------------------------|-------------------------------|
| string | Text input | equals, contains, starts with… |
| number | Number input | =, !=, >, <, >=, <=, between… |
| boolean | True / False dropdown | is true, is false |
| date | Date picker | before, after, between… |
| datetime | Datetime picker | same as date |
| select | Options dropdown | equals, not equals, is one of |
| array | Text input | contains, is empty, length is |
| custom | Your own component | all operators |
Output Formats
// Auto-output on every change
<QueryBuilder
fields={fields}
outputFormat="sql" // 'sql' | 'json' | 'mongodb' | 'graphql'
onOutputChange={str => console.log(str)}
showOutput // built-in output panel
/>Or use the hook directly:
import { useQueryBuilder } from 'querycraft';
const qb = useQueryBuilder({ fields });
// SQL — parameterized (safe for DB drivers)
const { sql, params } = qb.toSQL('users', true);
// → SELECT * FROM users WHERE name = $1 AND age > $2
// params: ['Ali', 18]
// SQL — inline values (for display only)
const { rawSql } = qb.toSQL('users', false);
// → SELECT * FROM users WHERE name = 'Ali' AND age > 18
// JSON
const json = qb.toJSON();
// → { combinator: 'AND', rules: [{ field: 'age', operator: '>', value: 18 }] }
// MongoDB
const mongo = qb.toMongoDB();
// → { $and: [{ age: { $gt: 18 } }] }
// GraphQL
const gql = qb.toGraphQL();Custom Operators
Add extra operators (merged into defaults)
import type { Operator } from 'querycraft';
const extraOperators: Operator[] = [
{
name: 'soundsLike',
label: 'sounds like',
arity: 'unary',
fieldTypes: ['string'],
sqlTemplate: "SOUNDEX({field}) = SOUNDEX({value})",
evaluator: (fv, rv) => String(fv)[0]?.toLowerCase() === String(rv)[0]?.toLowerCase(),
},
{
name: 'withinKm',
label: 'within km',
arity: 'unary',
fieldTypes: ['number'],
sqlTemplate: "ST_Distance({field}, POINT({value})) < 1000",
},
];
<QueryBuilder fields={fields} extraOperators={extraOperators} />Replace all operators
const myOperators: Operator[] = [
{ name: '=', label: 'Is', arity: 'unary', fieldTypes: ['string', 'number'] },
{ name: '!=', label: 'Is not', arity: 'unary', fieldTypes: ['string', 'number'] },
{ name: 'like', label: 'Like', arity: 'unary', fieldTypes: ['string'],
sqlTemplate: "{field} LIKE '%{value}%'" },
];
<QueryBuilder fields={fields} operators={myOperators} />Restrict operators per field
const fields: Field[] = [
{
name: 'status',
label: 'Status',
type: 'string',
operators: ['=', '!=', 'in'], // only these operators shown for this field
},
];Operator Interface
interface Operator {
name: string; // unique key: '=', 'contains', 'withinKm'
label: string; // shown in UI: 'equals', 'contains', 'within km'
arity?:
| 'none' // no value input (e.g. 'is null', 'is today')
| 'unary' // single input (default)
| 'between'; // two inputs (from / to)
fieldTypes?: FieldType[]; // which field types this appears for
// omit = appears for ALL types
sqlTemplate?: string; // '{field} LIKE \'%{value}%\''
// use {value[0]}, {value[1]} for between
mongoOp?: string; // '$gt', '$regex', etc.
evaluator?: (fieldValue: unknown, ruleValue: RuleValue) => boolean;
// for client-side filterRecords()
valueEditor?: React.ComponentType<ValueEditorProps>;
// custom value input for this operator
}Custom Renderers
Custom value editor (per field)
import type { ValueEditorProps } from 'querycraft';
function SliderEditor({ value, onChange }: ValueEditorProps) {
return (
<input
type="range" min={0} max={100}
value={Number(value) || 0}
onChange={e => onChange(e.target.value)}
/>
);
}
const fields: Field[] = [
{ name: 'score', label: 'Score', type: 'number', valueEditor: SliderEditor },
];Custom value editor (per operator)
const operators: Operator[] = [
{ name: 'between', label: 'between', arity: 'between',
fieldTypes: ['number'], valueEditor: MyRangePickerComponent },
];Override priority
operator.valueEditor → field.valueEditor → props.valueEditor → built-in default
Custom field / operator selectors
import type { FieldSelectorProps, OperatorSelectorProps } from 'querycraft';
function MyFieldSelector({ fields, value, onChange }: FieldSelectorProps) {
return (
<div className="my-field-selector">
{fields.map(f => (
<button key={f.name} onClick={() => onChange(f.name)}
className={value === f.name ? 'active' : ''}>
{f.label}
</button>
))}
</div>
);
}
<QueryBuilder
fields={fields}
fieldSelector={MyFieldSelector}
operatorSelector={MyOperatorSelector}
combinatorSelector={MyCombinatorSelector}
/>Custom rule / group actions
<QueryBuilder
fields={fields}
ruleActions={({ rule, onRemove }) => (
<>
<button onClick={onRemove}>Delete</button>
<button onClick={() => duplicate(rule)}>Duplicate</button>
</>
)}
groupActions={({ group, onRemove }) => (
<button onClick={onRemove}>Remove group</button>
)}
/>Headless Mode
Use the engine without any UI:
import { useQueryBuilder } from 'querycraft';
function MyCustomUI() {
const qb = useQueryBuilder({
fields,
extraOperators: myOps,
onChange: q => console.log(q),
validator: (rule, field) => {
if (field?.type === 'number' && isNaN(Number(rule.value)))
return 'Must be a number';
return true;
},
});
return (
<div>
<p>Rules: {qb.ruleCount} · Valid: {String(qb.isValid)}</p>
{/* mutations */}
<button onClick={() => qb.addRule(qb.query.id)}>Add rule</button>
<button onClick={qb.undo} disabled={!qb.canUndo}>Undo</button>
<button onClick={qb.redo} disabled={!qb.canRedo}>Redo</button>
<button onClick={qb.clear}>Clear</button>
{/* outputs */}
<pre>{qb.toSQL().rawSql}</pre>
</div>
);
}Client-side Filtering
Filter data arrays in the browser without a database:
import { filterRecords, useQueryBuilder } from 'querycraft';
const qb = useQueryBuilder({ fields });
const records = [
{ name: 'Ali', age: 25, active: true },
{ name: 'Bob', age: 15, active: false },
];
const matched = filterRecords(records, qb.query, qb.operators);
// uses each operator's evaluator functionControlled vs Uncontrolled
// Uncontrolled — QueryBuilder manages state internally
<QueryBuilder fields={fields} onChange={handleChange} />
// Controlled — you own the state
const [query, setQuery] = useState<RuleGroup>(createDefaultQuery(fields));
<QueryBuilder
fields={fields}
query={query}
onChange={setQuery}
/>Validation
<QueryBuilder
fields={fields}
validator={(rule, field) => {
if (field?.type === 'number' && isNaN(Number(rule.value)))
return 'Value must be a valid number';
if (rule.operator === 'in' && !String(rule.value).includes(','))
return 'Use comma-separated values for "is one of"';
return true;
}}
/>All Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| fields | Field[] | required | Field definitions |
| operators | Operator[] | built-ins | Full operator replacement |
| extraOperators | Operator[] | — | Merged into defaults |
| combinators | Combinator[] | ['AND','OR','NOT'] | Shown combinators |
| defaultQuery | RuleGroup | auto | Initial query (uncontrolled) |
| query | RuleGroup | — | Controlled query |
| onChange | (q: RuleGroup) => void | — | Called on every change |
| outputFormat | OutputFormat | 'sql' | Auto-output format |
| onOutputChange | (s: string) => void | — | Called with rendered output |
| customExporter | CustomExporter | — | For outputFormat='custom' |
| showFormulaBar | boolean | true | Formula preview bar |
| showOutput | boolean | true | Output panel |
| outputFormats | OutputFormat[] | all | Tabs in output panel (e.g. omit expression) |
| showExpressionOutput | boolean | true | Include bracket-style expression tab |
| showAddGroup | boolean | true | Add Group button |
| maxDepth | number | unlimited | Max nesting depth |
| draggable | boolean | true | Drag to reorder rules |
| enableExpressionOperators | boolean | true | Merge IF/THEN/ELSE + Calculate operators |
| enableFlexibleOperands | boolean | true | Unary rules: compare to const / column / variable |
| variables | QueryVariable[] | — | Saved values for RHS + formulas |
| variableValues | Record<string, unknown> | — | Runtime values for SQL / preview |
| variablesFilterByFieldType | boolean | false | Restrict variable list by column type |
| searchableFields | boolean | auto | Searchable column picker (true / false / default when ≥20 fields) |
| compact | boolean | false | Denser padding and type size |
| variant | 'light' \| 'dark' | — | Light/dark preset |
| theme | Partial<QueryCraftTheme> | — | Override CSS variable colors |
| disabled | boolean | false | Disable all inputs |
| className | string | — | CSS class on root |
| style | CSSProperties | — | Inline style on root |
| fieldSelector | ComponentType | built-in | Custom field selector |
| operatorSelector | ComponentType | built-in | Custom operator selector |
| combinatorSelector | ComponentType | built-in | Custom combinator UI |
| valueEditor | ComponentType | built-in | Global value editor override |
| ruleActions | ComponentType | built-in | Extra actions per rule |
| groupActions | ComponentType | built-in | Extra actions per group |
| validator | Function | — | Per-rule validation |
| translations | Partial<Translations> | English | i18n strings |
i18n
<QueryBuilder
fields={fields}
translations={{
addRule: '+ Condition',
addGroup: '+ Group',
removeRule: 'Delete',
removeGroup:'Delete group',
emptyQuery: 'Add your first condition…',
}}
/>Undo / Redo
Available on the hook (also shown as ↩ ↪ buttons in the formula bar):
const qb = useQueryBuilder({ fields });
qb.undo(); // undo last change
qb.redo(); // redo
qb.canUndo; // boolean
qb.canRedo; // booleanProgrammatic Engine
All core functions are exported individually:
import {
createRule, createGroup, createDefaultQuery,
addRule, addGroup, removeNode, updateRule, updateGroup, moveRule,
flattenRules, countRules, isEmptyQuery,
validateQuery,
toSQL, toJSON, toMongoDB, toGraphQL,
evaluateQuery, filterRecords,
} from 'querycraft';Testing Locally (before publishing)
Method 1 — npm link
# In querycraft-final/:
npm install
npm run build
npm link
# In your React project:
npm link querycraftMethod 2 — file path in package.json
In your React project's package.json:
{
"dependencies": {
"querycraft": "file:../querycraft-final"
}
}Then npm install.
Method 3 — copy src folder
Copy src/ into your project:
your-app/src/querycraft/ ← paste the src/ folder hereImport directly:
import { QueryBuilder } from './querycraft';License
MIT
