json-expression-eval

evaluate a json described boolean expression using dynamic functions

Usage no npm install needed!

<script type="module">
  import jsonExpressionEval from 'https://cdn.skypack.dev/json-expression-eval';
</script>

README

Npm Version node Build Status Test Coverage Maintainability Known Vulnerabilities dependencies Status devDependencies Status

json-expression-eval

A Fully typed Node.js module that evaluates a json described boolean expressions using dynamic functions and a given context.

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 (to be saved in 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;
        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>; // We pass Moment here to avoid TS exhaustion

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, Moment> = {
    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 expression: IExampleExpression = {
    or: [
        {
            userId: 'a@b.com',
        },
        {
            and: [
                {
                    countRange: [2, 6],
                },
                {
                    'nested.nested2.value3': true,
                },
            ],
        },
    ],
};

// Example usage 1
const handler =
    new ExpressionHandler<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, functionsTable);
handler.validate(validationContext); // Should not throw
console.log(handler.evaluate(context)); // true

// Example usage 2
validate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, validationContext, functionsTable); // Should not throw
console.log(evaluate<IExampleContext, IExampleFunctionTable, IExampleContextIgnore>(expression, context, functionsTable)); // true

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 - !==
    • {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

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}
      },
      {  
         "country": "USA"
      }
   ]
}