Skip to content

Repository files navigation

Origami — Visual Query Builder

A schema-driven visual query builder that lets you construct complex nested database queries through a graphical interface — no raw syntax required. Built for the Frontend Wizards Stage 8 challenge.

Live Demo


Overview

Origami lets you visually compose queries with nested AND/OR logic, preview the output in SQL, MongoDB, or JSON format, and execute against a live mock dataset — all in real time.

It supports unlimited nesting depth, schema-aware input controls, drag-and-drop reordering, query history, saved presets, export/import, and keyboard-first workflows.


Features

Core

  • Visual query builder — add rules, nest groups, combine AND/OR logic
  • Schema-driven rendering — fields, operators, and input types adapt per schema (Users, Orders, Products)
  • Live query preview — SQL, MongoDB filter, and JSON output update in real time
  • Query execution — filter mock datasets (50 rows each) and inspect results
  • Three output formats — SQL, MongoDB ($and/$or), JSON filter object

Advanced

  • Unlimited nesting — condition groups nest to any depth
  • Drag-and-drop reordering — reorder rules and groups within any parent
  • Query history — last 20 executed queries, persisted to localStorage
  • Saved presets — name, save, load, and delete query presets
  • Export / Import — export query as .json, import with structural validation
  • Keyboard shortcuts — execute, save, copy, undo, toggle panel, help modal
  • Collapsible bottom panel — preview and results collapse with Cmd+J
  • Collapsible sidebar — icon-only mode on desktop, off-canvas on mobile
  • Dark / Light mode — system default detection with manual toggle
  • Responsive — landing page fully responsive, builder gracefully degraded on mobile

Bonus operators

  • regex — with client-side pattern validation
  • isNull / isNotNull — fieldtype-independent
  • before / after / between — date comparison operators

Tech Stack

Concern Choice
Framework Next.js 16 (App Router)
Language TypeScript (strict)
Styling TailwindCSS v4 + CSS variables
Components shadcn/ui (Origami theme)
State Zustand + Immer
Drag and drop DnD Kit
Animation Framer Motion
Icons Hugeicons React
Font Geist Sans + Geist Mono
Virtualization TanStack Virtual (results > 100 rows)
Testing Vitest + React Testing Library
Deployment Vercel

Getting Started

npm install
npm run dev

Open http://localhost:3000


Running Tests

npm run test
npm run test:coverage

Routes

Route Description
/ Landing page
/builder Query builder
/builder/schema Schema reference — fields, types, valid operators
/builder/history Query history — last 20 executed queries
/builder/presets Saved queries — save, load, delete presets

Architecture

Folder Structure

src/
├── app/
│   ├── page.tsx                    # Landing page
│   ├── builder/
│   │   ├── page.tsx                # Query builder
│   │   ├── schema/page.tsx         # Schema reference
│   │   ├── history/page.tsx        # Query history
│   │   └── presets/page.tsx        # Saved presets
│   ├── layout.tsx
│   └── globals.css
├── components/
│   ├── builder/                    # Query builder UI components
│   ├── landing/                    # Landing page sections
│   ├── preview/                    # Query preview panel
│   ├── results/                    # Results table and states
│   ├── sidebar/                    # Sidebar panels (legacy)
│   └── ui/                         # Shared UI primitives
├── store/
│   ├── queryStore.ts               # Query tree state
│   ├── schemaStore.ts              # Active schema state
│   └── uiStore.ts                  # UI state, history, presets, results
├── lib/
│   ├── queryEngine/
│   │   ├── types.ts                # RuleNode, GroupNode, QueryTree
│   │   ├── generator.ts            # Tree → SQL / Mongo / JSON
│   │   ├── executor.ts             # Tree → filter dataset
│   │   ├── validator.ts            # Tree → ValidationError[]
│   │   └── serializer.ts           # Export / import JSON
│   ├── schemas/                    # Schema definitions
│   ├── mockData/                   # 50-row mock datasets
│   └── utils/
│       ├── operators.ts            # Type → operator compatibility map
│       └── nodeHelpers.ts          # Pure tree manipulation functions
├── hooks/
│   └── useKeyboardShortcuts.ts
└── __tests__/                      # Unit and integration tests

Recursive Rendering Strategy

The query builder is built around a recursive QueryNode type:

type QueryNode = RuleNode | GroupNode
type GroupNode = { id: string; type: 'group'; logic: 'AND' | 'OR'; children: QueryNode[] }
type RuleNode  = { id: string; type: 'rule'; field: string; operator: Operator; value: unknown }

The <ConditionGroup> component renders itself recursively:

{group.children.map(child =>
  child.type === 'rule'
    ? <RuleRow key={child.id} ruleId={child.id} />
    : <ConditionGroup key={child.id} groupId={child.id} depth={depth + 1} />
)}

The same recursive pattern is used in the query engine — generateSQL, executeQuery, and validateTree all traverse the tree with typed recursive functions, avoiding switch-case sprawl and remaining extensible.


State Management

The application uses three Zustand stores:

queryStore — owns the query tree and validation state

  • Tree mutations use Immer for immutable updates
  • Validation runs after every mutation via revalidate(schema)
  • Touched nodes tracked separately so errors only show after user interaction

schemaStore — tracks the active schema key

  • Schema definitions and mock datasets are static imports, not stored state
  • Switching schema resets the query tree

uiStore — owns everything else

  • Query history (last 20, persisted to localStorage)
  • Saved presets (persisted to localStorage)
  • Execution result (persisted across panel collapse/expand)
  • Panel open/closed state
  • isPanelOpen drives the collapsible bottom panel

All store actions are pure functions. No side effects inside reducers.


Query Engine Design

The query engine lives entirely in src/lib/queryEngine/ and has zero UI dependencies. It is a set of pure functions over the QueryTree type.

Generator (generator.ts)

  • generateSQL(tree, tableName) — produces parenthesized SQL WHERE clause
  • generateMongo(tree) — produces $and/$or nested MongoDB filter
  • generateJSONFilter(tree) — produces normalized JSON filter object
  • All three skip incomplete rules (no field or no value) rather than erroring

Executor (executor.ts)

  • executeQuery(tree, dataset) — filters an array of rows against the query tree
  • Incomplete rules are pass-through (true) so partial forms don't silently filter out all rows
  • Invalid regex patterns return false rather than throwing

Validator (validator.ts)

  • validateTree(tree, schema) — returns ValidationError[] with nodeId and message per error
  • Checks: empty groups, missing fields/values, operator/type incompatibility, invalid regex, missing valueTo for between
  • Only errors for nodes in touchedNodes are surfaced in the UI

Serializer (serializer.ts)

  • exportQuery(tree, schemaKey) — JSON with version, schemaKey, exportedAt, tree
  • importQuery(json) — parses and structurally validates; returns ImportError on failure
  • Validates: required keys, node types, max nesting depth of 10, array children

Performance Optimizations

  • React.memo on all builder components — RuleRow, ConditionGroup, FieldSelector, OperatorSelector, ValueInput
  • useCallback on all event handlers in ConditionGroup to prevent child re-renders
  • useMemo for query generation in QueryPreview — only recomputes when tree changes
  • Stable nanoid node IDs prevent full subtree re-mounts on reorder
  • TanStack Virtual for result sets over 100 rows — only ~15 DOM nodes rendered at once
  • Validation filtered to touched nodes only — no error churn during tree construction

Keyboard Shortcuts

Shortcut Action
Cmd/Ctrl + Enter Execute query (opens panel first if collapsed)
Cmd/Ctrl + J Toggle output panel
Cmd/Ctrl + S Save preset
Cmd/Ctrl + Shift + C Copy current query
? Open shortcuts help modal

Trade-offs

No real database connectivity — the executor runs entirely in-memory against mock datasets. This keeps the app self-contained and deployable without a backend, at the cost of not being able to test against real data.

Validation on touch, not on submit — errors surface after a field is interacted with rather than on form submit. This is more forgiving during query construction but means a partially filled query can be executed with silent incomplete rules (which are treated as pass-through).

Single-file query tree — the entire query tree lives in one Zustand store slice. For very deep trees (10+ levels) with many rules, this means every tree mutation triggers a full re-render of the root. Mitigated with memoization but not eliminated entirely.

DnD within parent only — drag-and-drop reordering works within the same parent group. Cross-group drag (moving a rule from one group into another) is not supported. This keeps the DnD implementation straightforward while covering the most common reorder use case.

localStorage for persistence — history and presets persist to localStorage. This is simple and requires no backend, but data is lost if the user clears browser storage and is not shared across devices.


Deployment

Auto-deployed to Vercel on merge to main. Pull requests generate preview deployments automatically.

npm run build   # verify before merge

About

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages