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)
npm install json-expression-eval
Or
yarn add json-expression-eval
Please see tests and examples dir for more usages and examples (under /src)
import {evaluate, Expression, ExpressionHandler, validate, ValidationContext, EvaluatorFuncRunOptions} 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 IExampleCustomEvaluatorFuncRunOptions = {dryRun: boolean};
type IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOpts: EvaluatorFuncRunOptions<IExampleCustomEvaluatorFuncRunOptions>) => Promise<boolean>;
}
type IExampleExpression = Expression<IExampleContext, IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEvaluatorFuncRunOptions>; // We pass Moment here to avoid TS exhaustion
const context: IExampleContext = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: null,
value4: 5,
nested2: {
value3: true,
},
},
};
// For validation we must provide a full example context
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: async ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOpts: EvaluatorFuncRunOptions<IExampleCustomEvaluatorFuncRunOptions>): Promise<boolean> => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};
const expression: IExampleExpression = {
or: [
{
userId: 'a@b.com',
},
{
times: {
lte: {
op: '+',
lhs: {
ref: 'nested.value4',
},
rhs: 2,
},
},
},
{
and: [
{
countRange: [2, 6],
},
{
'nested.nested2.value3': true,
},
{
times: {
lte: {
ref: 'nested.value4',
},
},
},
],
},
],
};
(async () => {
// Example usage 1
const handler =
new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore,
IExampleCustomEvaluatorFuncRunOptions>(expression, functionsTable);
await handler.validate(validationContext, {dryRun: false}); // Should not throw
console.log(await handler.evaluate(context, {dryRun: true})); // true
// Example usage 2
await validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore,
IExampleCustomEvaluatorFuncRunOptions>(expression, validationContext, functionsTable, {dryRun: true}); // Should not throw
console.log(await evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEvaluatorFuncRunOptions>(expression, context, functionsTable, {dryRun: true})); // true
})()
There are 4 types of operators you can use (evaluated in that order of precedence):
and
- accepts a non-empty list of expressionsor
- accepts a non-empty list of expressionsnot
- accepts another expressions<user defined funcs>
- accepts any type of argument and evaluated by the user defined functions, and the given context (can be async) and run options (i.e. validation + custom defined value).<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 thei
flag set.nin: any[]
- True if not in an array of values. Comparison is done using the===
operatorinq: any[]
- True if in an array of values. Comparison is done using the===
operatorbetween: 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.
- available ops:
{property: value}
- compares the property to that value (shorthand to the
eq
op, without the option to user math or refs to other properties)
- compares the property to that value (shorthand to the
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
The right-hand side of compare (not user defined) functions can be a:
- literal - number/string/boolean (depending on the left-hand side of the function)
- reference to a property (or nested property) in the context.
This can be achieved by using{"ref":"<dot notation path>"}
- A math operation that can reference properties in the context.
The valid operations are+,-,*,/,%,pow
.
This can be achieved by usingwhich will be computed as{ "op": "<+,-,*,/,%,pow>", "lhs": {"ref": "<dot notation path>"}, // or a number literal "rhs": {"ref": "<dot notation path>"} // or a number literal }
<lhs> <op> <rhs>
where lhs is left-hand-side and rhs is right-hand-side. So for examplewill equal{ "op": "/", "lhs": 10, "rhs": 2 }
10 / 2 = 5
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"
}
]
}
Please see tests and examples dir for more usages and examples (under /src)
import {ValidationContext, validateRules, evaluateRules, RulesEngine, Rule, ResolvedConsequence, EngineRuleFuncRunOptions} 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 IExampleCustomEngineRuleFuncRunOptions = {dryRun: boolean};
type IExamplePayload = number;
type IExampleFunctionTable = {
countRange: ([min, max]: [min: number, max: number], ctx: { times: number | undefined },
runOpts: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>) => boolean;
}
type IExampleRuleFunctionTable = {
userRule: (user: string, ctx: IExampleContext,
runOpts: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>) =>
Promise<void | ResolvedConsequence<IExamplePayload>>;
}
type IExampleRule = Rule<IExamplePayload, IExampleRuleFunctionTable, IExampleContext,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>;
const context: IExampleContext = {
userId: 'a@b.com',
times: 3,
date: moment(),
nested: {
value: null,
nested2: {
value3: true,
},
},
};
// For validation we must provide a full example context
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 },
runOptions: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>): boolean => {
return ctx.times === undefined ? false : ctx.times >= min && ctx.times < max;
},
};
const ruleFunctionsTable: IExampleRuleFunctionTable = {
userRule: async (user: string, ctx: IExampleContext,
runOptions: EngineRuleFuncRunOptions<IExampleCustomEngineRuleFuncRunOptions>)
: Promise<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',
},
];
(async () => {
// Example usage 1
const engine = new RulesEngine<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(
functionsTable, ruleFunctionsTable);
await engine.validate(rules, validationContext, {dryRun: false}); // Should not throw
console.log(JSON.stringify(await engine.evaluateAll(rules, context, {dryRun: false}))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
// Example usage 2
await validateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(
rules, validationContext, functionsTable, ruleFunctionsTable, {dryRun: false}); // Should not throw
console.log(JSON.stringify(await evaluateRules<IExamplePayload, IExampleContext, IExampleRuleFunctionTable,
IExampleFunctionTable, IExampleContextIgnore, IExampleCustomEngineRuleFuncRunOptions>(rules, context, functionsTable, ruleFunctionsTable, false, {dryRun: false}))); // [{"message":"user a@b.com should not equal a@b.com","custom":579}]
})();