
json-expression-eval (and rule engine)
A Fully typed Node.js module that evaluates a json described boolean expressions using dynamic functions and a given context.
Expressions can also be evaluated in a rule engine
manner.
The module is strictly typed, ensuring that passed expressions are 100% valid at compile time.
This module is especially useful if you need to serialize complex expressions / rules (to be saved in a DB for example)
Installation
npm install json-expression-eval
Or
yarn add json-expression-eval
Usage
Please see tests and examples dir for more usages and examples (under /src)
import {evaluate, Expression, ExpressionHandler, validate, ValidationContext} from 'json-expression-eval';
import {Moment} from 'moment';
import moment = require('moment');
interface IExampleContext {
userId: string;
times: number | undefined;
date: Moment;
nested: {
value: number | null;
value4: number;
nested2: {
value2?: number;
value3: boolean;
};
};
}
type IExampleContextIgnore = Moment;
type IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }) => boolean;
}
type IExampleExpression = Expression<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>;
const context: IExampleContext = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: null,
value4: 5,
nested2: {
value3: true,
},
},
};
const validationContext: ValidationContext<IExampleContext, IExampleContextIgnore> = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: 5,
value4: 6,
nested2: {
value2: 6,
value3: true,
},
},
};
const functionsTable: IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }): boolean => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};
const expression: IExampleExpression = {
or: [
{
userId: 'a@b.com',
},
{
and: [
{
countRange: [2, 6],
},
{
'nested.nested2.value3': true,
},
{
times: {
lte: {
ref: 'nested.value4'
}
},
},
],
},
],
};
const handler =
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, functionsTable);
handler.validate(validationContext);
console.log(handler.evaluate(context));
validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, validationContext, functionsTable);
console.log(evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, context, functionsTable));
Expression
There are 4 types of operators you can use (evaluated in that order of precedence):
and
- accepts a non-empty list of expressions
or
- accepts a non-empty list of expressions
not
- accepts another expressions
<user defined funcs>
- accepts any type of argument and evaluated by the user defined functions and the given context.
<compare funcs>
- operates on one of the context properties and compares it to a given value.
{property: {op: value}}
- available ops:
gt
- >
gte
- >=
lt
- <
lte
- <=
eq
- ===
neq
- !==
regexp: RegExp
- True if matches the compiled regular expression.
regexpi: RegExp
- True if matches the compiled regular expression with the i
flag set.
nin: any[]
- True if not in an array of values. Comparison is done using the ===
operator
inq: any[]
- True if in an array of values. Comparison is done using the ===
operator
between: readonly [number, number] (as const)
- True if the value is between the two specified values: greater than or equal to first value and less than or equal to second value.
{property: value}
- compares the property to that value (shorthand to the
eq
op)
Nested properties in the context can also be accessed using a dot notation (see example above)
In each expression level, you can only define 1 operator, and 1 only
You can reference values (and nested values) from the context using the {"ref":""}
(see example above) on the right-hand side of expressions (not in parameters to user defined functions though)
Example expressions, assuming we have the user
and maxCount
user defined functions in place can be:
{
"or":[
{
"not":{
"user":"a@b.com"
}
},
{
"maxCount":1
},
{
"times": { "eq" : 5}
},
{
"times": { "eq" : { "ref": "nested.preoprty"}}
},
{
"country": "USA"
}
]
}
Rule Engine
Please see tests and examples dir for more usages and examples (under /src)
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence} from 'json-expression-eval';
import {Moment} from 'moment';
import moment = require('moment');
interface IExampleContext {
userId: string;
times: number | undefined;
date: Moment;
nested: {
value: number | null;
nested2: {
value2?: number;
value3: boolean;
};
};
}
type IExampleContextIgnore = Moment;
type IExamplePayload = number;
type IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }) => boolean;
}
type IExampleRuleFunctionTable = {
userRule: (user: string, ctx: IExampleContext) => void | ResolvedConsequence<IExamplePayload>;
}
type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
IExampleFunctionTable, IExampleContextIgnore>;
const context: IExampleContext = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: null,
nested2: {
value3: true,
},
},
};
const validationContext: ValidationContext<IExampleContext, IExampleContextIgnore> = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: 5,
nested2: {
value2: 6,
value3: true,
},
},
};
const functionsTable: IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined }): boolean => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};
const ruleFunctionsTable: IExampleRuleFunctionTable = {
userRule: (user: string, ctx: IExampleContext): void | ResolvedConsequence<number> => {
if (ctx.userId === user) {
return {
message: `Username ${user} is not allowed`,
custom: 543,
}
}
},
};
const rules: IExampleRule[] = [
{
condition: {
or: [
{
userId: 'a@b.com',
},
{
and: [
{
countRange: [2, 6],
},
{
'nested.nested2.value3': true,
},
],
},
],
},
consequence: {
message: ['user', {
ref: 'userId',
}, 'should not equal a@b.com'],
custom: 579,
},
},
{
userRule: 'b@c.com',
},
];
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(functionsTable, ruleFunctionsTable);
engine.validate(rules, validationContext);
console.log(JSON.stringify(engine.evaluateAll(rules, context)));
validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(rules, validationContext, functionsTable, ruleFunctionsTable);
console.log(JSON.stringify(evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore>(rules, context, functionsTable, ruleFunctionsTable, false)));