Quick start

A typed query builder in React, from schema to SQL.

Define your fields

The schema is the one place you describe what can be queried. Everything else, from the UI's editors to the types of rule values and the SQL, follows from it.

fields.ts
import { ,  } from '@qnot/core';

export const  = ({
  : .(),
  : .({ : 0, : true }),
  : .(['free', 'pro', 'team']),
  : .(),
  : .({ : 'Signed up' }),
});

Render a builder

<QueryBuilder> creates a store from your fields and renders the default, unstyled UI.

Filters.tsx
import '@qnot/core/styles.css';
import { QueryBuilder } from '@qnot/react';
import type { QueryOf } from '@qnot/core';
import { fields } from './fields';

export function Filters({ onChange }: { onChange: (q: QueryOf<typeof fields>) => void }) {
  return <QueryBuilder fields={fields} onChange={onChange} />;
}

Use the query

The value is qnot's own JSON: a tree of groups and rules, typed by your schema.

const : <typeof > = {
  : 'group',
  : 'and',
  : [
    { : 'rule', : 'age', : 'gte', : 21 },
    { : 'rule', : 'plan', : 'in', : ['pro', 'team'] },
  ],
};

Turn it into SQL, or filter rows directly:

import { compile, encode } from '@qnot/core';
import { sqlFormat } from '@qnot/core/formats/sql';

const { sql, params } = encode(sqlFormat({ dialect: 'postgres', params: 'numbered' }), query, fields);
// sql: "age >= $1 AND plan IN ($2, $3)", params: [21, 'pro', 'team']

const adults = users.filter(compile(fields, query));

Store-first or controlled

<QueryBuilder> accepts store options directly, which is enough for most forms. When other parts of the page need the query (a preview, a results table, undo buttons), create the store yourself and share it:

import { QueryBuilder, QueryProvider, useHistory, useQuery, useQueryStore } from '@qnot/react';

function Page() {
  const store = useQueryStore({ fields, initial: savedQuery });
  return (
    <QueryProvider store={store}>
      <QueryBuilder store={store} />
      <Preview />
    </QueryProvider>
  );
}

function Preview() {
  const query = useQuery<typeof fields>(); // re-renders on every change
  return <pre>{JSON.stringify(query, null, 2)}</pre>;
}

It also works controlled, like an input. Passing back the value it gave you costs nothing, because the store recognises its own output and skips reconciling:

const [query, setQuery] = useState(initialQuery);
<QueryBuilder fields={fields} value={query} onChange={setQuery} />;

Next