A TypeScript-first Rule Engine inspired by Drizzle ORM. Provides a type-safe way to define schemas, rules, and conditions, and evaluate them against hydrated data contexts.
- Schema: Defines fields, defaults, and hydration logic.
- Rules: Declarative conditions with optional
thencallbacks and priorities. - Engine: Evaluates rules against a context and computes matching scores.
- Operators: Comparison, logical, array, and database-inspired operators.
import {
text,
number,
boolean,
enumField,
createSchema,
createRules,
createEngine,
and,
not,
eq,
gte,
} from 'src'
// 1️⃣ Define schema
const context = createSchema({
id: text('id').notNull(),
age: number('age').default(18),
isActive: boolean('isActive').default(true),
role: enumField('role', ['admin', 'user', 'vip']).default('user'),
})
// 2️⃣ Define conditions
const conditionAdult = gte(context.age, 18)
const conditionNotUser = not(eq(context.role, 'user'))
const conditionActiveVip = and(
eq(context.isActive, true),
eq(context.role, 'vip'),
)
// 3️⃣ Create rules
const rulesHelper = createRules(context)
const promoRule = rulesHelper.create('PROMO_ADULT', {
when: [conditionAdult, conditionNotUser],
priority: 10,
})
const discountRule = rulesHelper.create('DISCOUNT_VIP', {
when: [conditionActiveVip],
priority: 20,
})
// 4️⃣ Create engine and register rules
const engine = createEngine(context)
engine.rules(promoRule, discountRule)
// 5️⃣ Hydrate data
const hydrated = context.hydrate({
id: '123',
age: 25,
isActive: true,
role: 'vip',
})
// 6️⃣ Evaluate
const results = await engine.evaluate(hydrated)
console.log(results)
// Output: [{ id: 'DISCOUNT_VIP', score: 2 }, { id: 'PROMO_ADULT', score: 2 }]Builds a type-safe schema from field builders and adds a hydrate() helper.
| Function | Description |
|---|---|
text(name) |
Create a string field |
number(name) |
Create a number field |
boolean(name) |
Create a boolean field |
date(name) |
Create a date field |
enumField(name, values) |
Create an enum field with literal union |
Generates a helper to create rules bound to a schema.
const helper = createRules(schema)
const rule = helper.create('RULE_ID', { when: [condition], priority: 5 })Creates a RuleEngine instance to register and evaluate rules.
Methods:
| Method | Description |
|---|---|
rule(r) |
Adds a single rule |
rules(...r) |
Adds multiple rules |
evaluate(ctx, id?) |
Returns all matching rules with { id, score } |
getFirst(ctx, id?) |
Returns the first matching rule by priority |
invalidate(id?) |
Clears cached condition evaluation |
Logical Operators
| Operator | Description |
|---|---|
and(...) |
Recursively AND conditions |
or(...) |
Recursively OR conditions |
not(cond) |
Negates a condition |
Comparison Operators
| Operator | SQL Equivalent | Example |
|---|---|---|
eq |
= | eq(context.age, 18) |
ne |
<> | ne(context.role, 'user') |
gt |
> | gt(context.age, 21) |
gte |
>= | gte(context.age, 18) |
lt |
< | lt(context.age, 65) |
lte |
<= | lte(context.age, 65) |
| Operator | Description |
|---|---|
inArray |
Checks value in array |
notInArray |
Checks value not in array |
arrayContains |
Checks if array contains values |
arrayOverlaps |
Checks if array has any overlap |
arrayContainedBy |
Checks if array is contained by another |
TODO: Add examples and further explanation for how these operators work in rules.
| Operator | Description |
|---|---|
between |
Value between min & max inclusive |
notBetween |
Value outside min & max |
like |
SQL LIKE pattern match |
ilike |
Case-insensitive LIKE |
TODO: Provide usage examples and clarify edge cases.
- Always hydrate your context before evaluation.
- Use
priorityto control rule order. - Use
invalidate()when reusing context IDs to prevent stale evaluation caching. - Combine operators with
and,or,notfor complex rules.
rule-engine/
├── src/
│ ├── engine/ # RuleEngine, Rule, ConditionEvaluator
│ ├── fields/ # Field builders (TextFieldBuilder, NumberFieldBuilder, etc.)
│ ├── operators/ # Logical, comparison, array, DB-like operators
│ ├── schema/ # createSchema helper
│ ├── types/ # Rule types, AST types, inferred types
│ └── index.ts # Public API exports
const complexRule = rulesHelper.create('COMPLEX', {
when: [
and(
gte(context.age, 18),
eq(context.role, 'vip'),
or(eq(context.isActive, true), inArray(context.id, ['123', '456'])),
),
],
priority: 30,
})
engine.rule(complexRule)- Supports async
thencallbacks for side-effects. - Scores are calculated as the number of matching conditions.
- First-match evaluation respects
priorityandflow: 'stop'. - Designed to be fully type-safe with inferred schema types.
- Add missing operators like
exists,notExists,notLike, etc. - Expand array operators for nested structures.
- Implement advanced AST optimizations for large rule sets.