npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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/querycraft

Quick 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:
    1. Column — Which field you are filtering.
    2. Match — The operator (equals, contains, greater than, …).
    3. 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.valueEditorfield.valueEditorprops.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 function

Controlled 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;     // boolean

Programmatic 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 querycraft

Method 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 here

Import directly:

import { QueryBuilder } from './querycraft';

License

MIT