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

nrl-rule-builder

v0.1.0

Published

Natural-rule-language RuleBuilder React component. If/Then/Else + Set-to rule authoring UI with JSON output. Zero runtime dependencies — styled with a prebuilt stylesheet, no antd/tailwind required in the consumer.

Readme

nrl-rule-builder

npm version license

Natural-rule-language RuleBuilder React component — an If / Then / Else + "Set to" rule-authoring UI that reads like a sentence and serializes to JSON.

Users compose rules like:

If Amount is greater than 1000 and Reference starts with INV then set to Approved, else set to Review

…and you get back a machine-readable RuleJSON payload plus a human-readable description, live as they edit.

  • Zero runtime dependencies. No antd, no dayjs, no uuid — selects, inputs, and date pickers are lightweight built-in components. Only react / react-dom are peer dependencies (>=17).
  • No state library required. Schemas and persistence are plain props/callbacks — no Redux, no context gymnastics.
  • Self-contained styling. One prebuilt, minified stylesheet; the consumer does not need Tailwind.
  • ESM + CJS + TypeScript. Ships dist/index.js (ESM), dist/index.cjs (CJS), and full .d.ts types.
  • Round-trippable. Saved JSON can be parsed back into the editor with parseRuleFromJSON for later editing.

Install

npm install nrl-rule-builder

Quick start

'use client';

import { RuleBuilder, useRuleBuilder } from 'nrl-rule-builder';
import 'nrl-rule-builder/styles.css';

const leftSchema = {
  schema_title: 'PPG',
  columns: [
    // datatype_uuid drives which columns appear in number/text field pickers
    { alias: 'Amount', uuid: 'col-amount', datatype_uuid: '...', isactive: true, parentuuid: null },
    { alias: 'Reference', uuid: 'col-ref', datatype_uuid: '...', isactive: true, parentuuid: null },
  ],
};

const rightSchema = { schema_title: 'Bank', columns: [/* … */] };

export function MyRulePanel({ selectedColumn }) {
  const handlers = useRuleBuilder({
    selectedSchema: selectedColumn,        // .uuid becomes target_field on Set actions
    leftSchema,
    rightSchema,
    // initialRuleJson: savedJson,         // resume editing a previously saved rule
    onSave: ({ json, description }) => {
      // persist however you like (API call, redux dispatch, …)
      console.log(description, json);
    },
    onCancel: () => {/* close the panel */},
    onDelete: () => {/* soft-delete the rule */},
  });

  return (
    <RuleBuilder
      selected_column={selectedColumn}
      props={handlers}
      leftSchema={leftSchema}
      rightSchema={rightSchema}
    />
  );
}

useRuleBuilder produces the exact props bundle the component consumes. Call handlers.handleSave() from your own Save button; handlers.generatedJson / handlers.generatedDescription update live as the user edits.

Next.js note: the components are client components (they ship 'use client' banners), so you can import them directly from a server component tree — but render them inside a client boundary as shown above. SSR-rendering the markup is supported (the smoke test SSR-renders the builder).

Concepts

A rule is a tree of a few node kinds:

| Node | Meaning | | --- | --- | | Primary rule (type: 'primary') | An If <conditions> Then <action> Else <action> block. The Else branch may itself be another primary rule, giving you else if chains of any depth. | | Group (type: 'group') | The condition list of a primary rule — conditions joined by AND / OR operators (click the connector pill to toggle). | | Condition (type: 'condition') | <left operand> <operator> <right operand>. | | Set action (type: 'set') | "Set to <value>" — writes a value to the target column (target_field). A rule can also be just a Set action with no conditions. | | Value (type: 'value') | A leaf operand: a field reference, literal text/number/boolean/date, an arithmetic expression, or a text-extraction expression. | | Placeholder | The dashed "Select…" pill shown for any part the user hasn't filled in yet. |

When the builder mounts with no rule, the user first picks If (start a conditional rule) or Set (a plain assignment).

Condition operators

is equal to · is not equal to · is greater than · is less than · is greater than or equal to · is less than or equal to · is one of · is not one of · is between · contains · contains one of · does not contain · does not contain one of · starts with · starts with one of · does not start with · does not start with any of · ends with · ends with one of · does not end with · does not end with any of

Value types

Operands and Set values can be any of:

| Value type | Description | | --- | --- | | field | A column from the left/right schema. | | text, number, boolean, date, date-range | Literals (date pickers are built in). | | absolute | Absolute value of a numeric field. | | first_embedded_number | First number embedded in a text field (e.g. INV-10421042). | | field_as_number | Cast a text field to a number. | | text_before_something / text_after_something | Substring before/after a delimiter. | | text_between_something_and_something | Substring between two delimiters. | | First/Last N characters, remove leading characters | Character-level text extraction. | | something_plus_something, something_minus_something, something_into_something, something_by_something | Arithmetic (+, , ×, ÷) between two operands — operands can themselves be fields, numbers, or nested expressions. |

Generated JSON

generateJSON(rule, selectedSchema) (called automatically by the hook) produces a nested RuleJSON:

{
  "AND": [
    {
      "row_id": "3f6c…",
      "operator": "is greater than",
      "operands": {
        "left_something": { "type": "FIELD",  "FIELD":  { "value": "col-amount" } },
        "right_something": { "type": "NUMBER", "NUMBER": { "value": "1000" } }
      }
    },
    {
      "row_id": "9a1b…",
      "operator": "starts with",
      "operands": {
        "left_something": { "type": "FIELD", "FIELD": { "value": "col-ref" } },
        "right_something": { "type": "TEXT", "TEXT":  { "value": "INV" } }
      }
    }
  ],
  "set":  { "target_field": "col-status", "type": "TEXT", "TEXT": { "value": "Approved" } },
  "else": { "target_field": "col-status", "type": "TEXT", "TEXT": { "value": "Review" } }
}
  • Conditions nest right-associatively under AND / OR keys, mirroring the connector pills.
  • Each operand is { type: <TYPE>, <TYPE>: { value: … } } where <TYPE> is the uppercase mapping of the value type (FIELD, TEXT, NUMBER, DATE, SOMETHING_PLUS_SOMETHING, …). Arithmetic operands nest their own left/right operands recursively.
  • set carries the action; else is present only when the Else branch is filled, and is itself a full RuleJSON when the Else branch is another If-rule (else if).
  • A conditionless rule serializes to just { "set": { … } }.

The same shape feeds back in through initialRuleJson / parseRuleFromJSON to resume editing.

API reference

<RuleBuilder />

| Prop | Type | Description | | --- | --- | --- | | selected_column | { uuid: string } | Column the rule targets. Its uuid becomes target_field on Set actions. | | props | RuleBuilderHandlers | State + handler bundle (from useRuleBuilder, or your own with the same shape). | | leftSchema | Schema | Left-side schema backing the field dropdowns. | | rightSchema | Schema | Right-side schema backing the field dropdowns. | | dataTypeUuids | Partial<DataTypeUuids> | Optional override of the datatype-UUID catalog used to filter number/text columns (see below). | | fieldSide | 'left' \| 'right' \| null | Optional: scope field pickers to one schema side (for filter-rule builders). |

Schema is { schema_title: string, columns: SchemaColumn[] } and SchemaColumn is { alias, uuid, datatype_uuid?, isactive?, parentuuid? }. Only columns with isactive: true (and parentuuid: null for the general field picker) are offered.

useRuleBuilder(options): RuleBuilderHandlers

| Option | Type | Description | | --- | --- | --- | | selectedSchema | { uuid: string } | Column the rule writes to; .uuid becomes target_field. | | leftSchema / rightSchema | Schema | Schemas used for JSON parsing and description generation. | | initialRule | Rule | Start from an existing in-memory rule tree… | | initialRuleJson | RuleJSON | …or from previously saved JSON (parsed via parseRuleFromJSON). | | onSave | ({ json, description }) => void | Called by handleSave with the generated JSON + description. | | onCancel | () => void | Called by handleCancel after the rule resets to its initial state. | | onDelete | () => void | Called by handleDelete. |

Returned RuleBuilderHandlers:

| Key | Description | | --- | --- | | rule / setRule | Current Rule tree (null shows the initial "If / Set" selector). | | handleReplace(path, newNode) | Immutably replaces a node in the rule tree (keeps group operators in sync). | | handleInitialSelection(value, selected_column) | Creates the initial rule from the 'if' / 'set' choice. | | handleSave / handleCancel / handleDelete | Wired to your onSave / onCancel / onDelete callbacks. | | generatedJson, generatedDescription | Live serialized JSON + human-readable sentence. |

Providers

If you render several builders, wrap once instead of passing schemas to each:

import { RuleBuilderProvider } from 'nrl-rule-builder';

<RuleBuilderProvider leftSchema={leftSchema} rightSchema={rightSchema} dataTypeUuids={overrides}>
  <RuleBuilder selected_column={col} props={handlers} />
</RuleBuilderProvider>

FilterFieldSideProvider (with useFilterFieldSide) scopes nested field pickers to one schema side — the provider equivalent of the fieldSide prop.

Datatype catalog

Field pickers filter columns into "number fields" / "text fields" lists by datatype_uuid. The defaults (DEFAULT_DATA_TYPE_UUIDS) match the original host platform's catalog — pass your own IDs via the dataTypeUuids prop on RuleBuilder or RuleBuilderProvider:

const overrides = { AMOUNT: 'your-amount-uuid', TEXT_50: 'your-text-uuid', /* … */ };

Keys: DATE, DATETIME, TEXT_200, TEXT_50, CURRENCY, NUMBER_INT, NUMBER_DECIMAL, AMOUNT, BOOLEAN.

Utilities

All pure rule-JSON helpers are exported:

| Function | Description | | --- | --- | | generateJSON(rule, selectedSchema) | Serialize a Rule tree to RuleJSON. | | parseRuleFromJSON(json, leftSchema?, rightSchema?) | Inverse of generateJSON — rebuild an editable Rule tree from saved JSON. | | generateRuleDescription(json, leftSchema?, rightSchema?) | Human-readable sentence for a RuleJSON (field UUIDs resolved to aliases). | | buildRuleOutline(json, leftSchema?, rightSchema?) | Indented outline (RuleOutlineLine[]) for previews/summaries. | | findEmptyFields(json) | Validation — lists unfilled parts of a rule (useful to gate Save). | | toggleImmediateOperator(rule, path) | Flip an ANDOR connector at a path. | | generateOperand / generateAction / generateRowId | Lower-level serialization helpers. |

Constants: CASE_TYPE (per-value-type pill color catalog), TEXT_BETWEEN_SEPARATOR, DEFAULT_DATA_TYPE_UUIDS.

All TypeScript types are exported too: Rule, PrimaryRule, SetAction, Condition, Group, Value, Placeholder, RuleJSON, ConditionJSON, OperandJSON, ActionJSON, Schema, SchemaColumn, SelectedColumn, DataTypeUuids, RuleBuilderProps, RuleBuilderHandlers, UseRuleBuilderOptions, …

UI primitives

The built-in controls are exported for consumers who want matching UI around the builder: Select (with option groups), Input, Button, DatePicker, DateRangePicker, Pill, InlineEditable, plus EditModeProvider / useEditMode.

Styling

Import the prebuilt stylesheet once, anywhere in your app:

import 'nrl-rule-builder/styles.css';

It is generated from Tailwind at build time and minified, but your app does not need Tailwind — it's plain CSS. The package marks CSS as sideEffects, so bundlers won't tree-shake the import away.

Development

npm install
npm run dev         # local playground at http://localhost:4321 (live reload)
npm run typecheck   # tsc --noEmit
npm run build       # tsup (ESM + CJS + d.ts) + tailwind → dist/styles.css
npm run smoke       # SSR render + JSON round-trip checks against dist/

npm run dev serves demo/ — a playground wired to sample schemas with live panels for the generated JSON, description, and validation issues. It imports straight from src/, so component edits reload instantly (set PORT=xxxx to change the port).

Publishing runs the full pipeline automatically: npm publish triggers prepublishOnly → typecheck + build + smoke.

License

MIT