@inixiative/json-rules
v1.2.0
Published
TypeScript-first JSON rules engine with intuitive syntax and detailed error messages
Maintainers
Readme
@inixiative/json-rules
A TypeScript-first JSON rules library for:
- runtime validation with custom error messages
- Prisma query planning
- PostgreSQL
WHEREgeneration
The same rule AST can be evaluated against in-memory data with check(), converted into a Prisma query plan with toPrisma(), or compiled into SQL with toSql().
Installation
npm install @inixiative/json-rules
# or
yarn add @inixiative/json-rules
# or
bun add @inixiative/json-rulesQuick Start
import { check, Operator } from '@inixiative/json-rules';
const rule = {
field: 'age',
operator: Operator.greaterThanEquals,
value: 18,
error: 'Must be 18 or older',
};
check(rule, { age: 21 }); // true
check(rule, { age: 16 }); // "Must be 18 or older"What It Supports
- scalar comparisons
- nested logical conditions with
all/any if/then/else- array validation against nested object elements
- date comparisons with timezone-aware runtime evaluation
- relative value references via
path - custom error messages on every rule
- compilation to Prisma and PostgreSQL for supported subsets
Operators
Field Operators
equalsnotEqualslessThanlessThanEqualsgreaterThangreaterThanEqualscontainsnotContainsinnotInmatchesnotMatchesbetweennotBetweenisEmptynotEmptyexistsnotExistsstartsWithendsWith
Array Operators
allanynoneatLeastatMostexactlyemptynotEmpty
Date Operators
beforeafteronOrBeforeonOrAfterbetweennotBetweendayIndayNotIn
Rule Shapes
Field Rule
{
field: 'status',
operator: Operator.equals,
value: 'active'
}Logical Rules
{
all: [
{ field: 'age', operator: Operator.greaterThanEquals, value: 18 },
{ field: 'hasLicense', operator: Operator.equals, value: true }
]
}
{
any: [
{ field: 'role', operator: Operator.equals, value: 'admin' },
{ field: 'isOwner', operator: Operator.equals, value: true }
]
}Conditional Rule
{
if: { field: 'type', operator: Operator.equals, value: 'premium' },
then: { field: 'discount', operator: Operator.greaterThan, value: 0 },
else: { field: 'discount', operator: Operator.equals, value: 0 }
}Array Rule
{
field: 'orders',
arrayOperator: ArrayOperator.all,
condition: {
field: 'total',
operator: Operator.lessThanEquals,
path: '$.maxBudget'
}
}Date Rule
{
field: 'expiryDate',
dateOperator: DateOperator.after,
value: '2026-01-01'
}Path Semantics
path lets a rule resolve its comparison value from somewhere other than value.
Root Context Reference
In runtime validation, a plain path is resolved from the root context:
{
field: 'confirmPassword',
operator: Operator.equals,
path: 'password'
}Current Array Element Reference
Inside array conditions, $. means "read from the current element":
{
field: 'orders',
arrayOperator: ArrayOperator.all,
condition: {
field: 'total',
operator: Operator.lessThanEquals,
path: '$.maxBudget'
}
}Runtime Validation
check() evaluates a rule against data and returns:
truewhen the rule passes- a string when the rule fails
import { ArrayOperator, check, Operator } from '@inixiative/json-rules';
const rule = {
all: [
{ field: 'status', operator: Operator.equals, value: 'active' },
{
field: 'orders',
arrayOperator: ArrayOperator.atLeast,
count: 2,
condition: { field: 'status', operator: Operator.equals, value: 'completed' },
},
],
};
check(rule, {
status: 'active',
orders: [
{ status: 'completed' },
{ status: 'pending' },
{ status: 'completed' },
],
}); // trueCustom Errors
Every rule can define its own error:
{
field: 'email',
operator: Operator.matches,
value: /^[^@]+@[^@]+\.[^@]+$/,
error: 'Please enter a valid email address'
}Prisma Query Planning
toPrisma() converts a rule into a Prisma query plan.
import { Operator, toPrisma } from '@inixiative/json-rules';
const plan = toPrisma({
field: 'status',
operator: Operator.equals,
value: 'active',
});
// plan.steps => [{ operation: 'where', where: { status: { equals: 'active' } } }]Count-based relation filters such as atLeast, atMost, and exactly can produce multi-step plans. Use executePrismaQueryPlan() to resolve groupBy step references before passing the final where into Prisma.
import {
ArrayOperator,
Operator,
executePrismaQueryPlan,
toPrisma,
} from '@inixiative/json-rules';
const plan = toPrisma(
{
field: 'posts',
arrayOperator: ArrayOperator.atLeast,
count: 3,
condition: {
field: 'published',
operator: Operator.equals,
value: true,
},
},
{ map, model: 'User' },
);
const where = await executePrismaQueryPlan(plan, { post: prisma.post });
await prisma.user.findMany({ where });PostgreSQL SQL Generation
toSql() converts a rule into a parameterized PostgreSQL WHERE clause.
import { Operator, toSql } from '@inixiative/json-rules';
const result = toSql({
field: 'status',
operator: Operator.equals,
value: 'active',
});
// {
// sql: '"status" = $1',
// params: ['active'],
// joins: []
// }With a field map and model, toSql() can generate LEFT JOINs for relation traversal:
const result = toSql(
{ field: 'author.email', operator: Operator.equals, value: '[email protected]' },
{ map, model: 'Post', alias: 't0' },
);
// result.sql => '"t1"."email" = $1'
// result.joins => ['LEFT JOIN "User" AS "t1" ON "t1"."id" = "t0"."authorId"']Backend Support Matrix
Not every backend supports every rule shape.
| Capability | check() | toPrisma() | toSql() |
| --- | --- | --- | --- |
| Field operators | Yes | Most | Yes |
| matches / notMatches | Yes | No | Yes |
| Logical operators | Yes | Yes | Yes |
| Array all / any / none | Yes | Yes | No |
| Array atLeast / atMost / exactly | Yes | Yes, with map + model | No |
| Array empty / notEmpty | Yes | Yes | Yes |
| Date comparisons | Yes | Most | Yes |
| dayIn / dayNotIn | Yes | No | Yes |
| path: '$.field' current-element / same-row refs | Yes | No | Yes |
Prisma Limitations
matchesandnotMatchesare not supported by Prisma outputdayInanddayNotInare not supported by Prisma outputpath: '$.field'column-to-column comparisons are not supported by PrismaWHERE- count-based relation operators require
{ map, model }
SQL Limitations
- complex array element operators are not supported in SQL output:
allanynoneatLeastatMostexactly
toSql()generatesWHEREfragments andLEFT JOINs, not complete queries
TypeScript Types
The public rule types are generic over comparison payloads:
type Condition<TRuleValue = RuleValue, TDateValue = DateRuleValue> =
| Rule<TRuleValue>
| ArrayRule<TRuleValue, TDateValue>
| DateRule<TDateValue>
| All<TRuleValue, TDateValue>
| Any<TRuleValue, TDateValue>
| IfThenElse<TRuleValue, TDateValue>
| boolean;Useful exports:
checktoPrismaexecutePrismaQueryPlantoSqlvalidateRuleassertValidRuleOperatorArrayOperatorDateOperatorConditionStrictConditionRuleArrayRuleDateRule
Error Handling
The library throws when a rule is structurally invalid, for example:
- array operators used against non-arrays
- missing
countfor count-based array rules - invalid date values
- unsupported backend translations
It returns string errors only from runtime check().
If rules come from JSON, a database, an API, or an editor, validate them first:
import { assertValidRule, validateRule } from '@inixiative/json-rules';
const result = validateRule(rule, { target: 'check' });
if (!result.ok) {
console.error(result.errors);
}
assertValidRule(rule, { target: 'toPrisma' });Examples
See examples/basic-validation.ts, examples/array-operations.ts, examples/date-operations.ts, and examples/advanced-features.ts.
License
MIT
