cover-image

Rules Engine TS

About

Rules Engine TS is a strongly typed rules engine for evaluating deep and complex rules. With the power of TypeScript, you can create type-safe rules that are easy to read and maintain.

The motivation for this project was to create an out-of-the-box solution that had a simple data structure that was easy to mutate and easy to persist in a database. The general intended use case was to build the rules engine using a user interface, write the rules into a database and in the future retrieve the rules back from the database and run the rules against a target object.

The recommended way to consume rules-engine-ts is in a TypeScript environment. TypeScript will warn you when your rules are missing properties or if the types of your properties are incorrect. That isn't to say that rules-engine-ts can't be run with JavaScript. You will still get autocomplete on the available properties, but you will not get any warnings if you are missing properties or if the types of your properties are incorrect.

Basic usage

A rules engine can be configured and run like so:

1import { addRuleToUnion, addRulesToUnion, addUnionToUnion, createRoot, run } from 'rules-engine-ts';
2
3// Create root union
4const root = createRoot({ connector: 'and' });
5
6// Add a rule to the root union
7addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
8
9// Add a union to the root union (creates a nested ruleset)
10const union = addUnionToUnion(root, { connector: 'or' });
11
12// Add nested rules to the nested union
13addRulesToUnion(union, [
14  { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true },
15  { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true },
16]);
17
18// Run the rules engine
19const pass = run(root, { age: 19, name: 'Bob' });
20const fail = run(root, { age: 19, name: 'Carol' });
21
22console.log(pass); // true
23console.log(fail); // false

If we console log the root we can see what our rules look like:

1{
2  "entity": "root_union",
3  "id": "0d7428af-10e4-481b-84a7-056946bd4f12",
4  "connector": "and",
5  "rules": [
6    {
7      "entity": "rule",
8      "id": "82e96b0d-886e-4a2e-bf8c-f81b02ef11ce",
9      "parent_id": "0d7428af-10e4-481b-84a7-056946bd4f12",
10      "type": "number",
11      "field": "age",
12      "operator": "greater_than",
13      "value": 18
14    },
15    {
16      "entity": "union",
17      "id": "7c493486-409b-48df-bd66-7f4a16500c5e",
18      "parent_id": "0d7428af-10e4-481b-84a7-056946bd4f12",
19      "connector": "or",
20      "rules": [
21        {
22          "entity": "rule",
23          "id": "3abc4e64-d6c8-4303-9d07-b573a571f19a",
24          "parent_id": "7c493486-409b-48df-bd66-7f4a16500c5e",
25          "type": "string",
26          "field": "name",
27          "operator": "equals_to",
28          "value": "bob",
29          "ignore_case": true
30        },
31        {
32          "entity": "rule",
33          "id": "a3995445-55ca-49b2-8381-3d6758750413",
34          "parent_id": "7c493486-409b-48df-bd66-7f4a16500c5e",
35          "type": "string",
36          "field": "name",
37          "operator": "equals_to",
38          "value": "alice",
39          "ignore_case": true
40        }
41      ]
42    }
43  ]
44}

UI implementation example

image-8bfa7089181e8a3cea381bd707604d79846dfd50-750x640-gif

The rules can then be persisted into a database in JSON format:

1{
2  "entity": "root_union",
3  "id": "598444ae-032c-4ae5-85da-644cf90ab920",
4  "connector": "or",
5  "rules": [
6    {
7      "entity": "rule",
8      "id": "03fcb9b5-a3fe-4d63-97f3-dfce431c331d",
9      "parent_id": "598444ae-032c-4ae5-85da-644cf90ab920",
10      "type": "string",
11      "field": "user_display_name",
12      "operator": "equals_to",
13      "value": "Alice",
14      "ignore_case": true
15    },
16    {
17      "id": "d60639aa-8239-40c7-9cc3-ec89f8f8c58d",
18      "entity": "union",
19      "connector": "and",
20      "parent_id": "598444ae-032c-4ae5-85da-644cf90ab920",
21      "rules": [
22        {
23          "entity": "rule",
24          "id": "1821d9da-9f37-4689-a118-bf436ca37e89",
25          "parent_id": "d60639aa-8239-40c7-9cc3-ec89f8f8c58d",
26          "type": "string",
27          "field": "user_display_name",
28          "operator": "equals_to",
29          "value": "Bob",
30          "ignore_case": true
31        },
32        {
33          "entity": "rule",
34          "id": "c2a058ab-6005-44a4-94ae-b75736dce536",
35          "parent_id": "d60639aa-8239-40c7-9cc3-ec89f8f8c58d",
36          "type": "number",
37          "field": "total_challenges",
38          "operator": "greater_than_or_equal_to",
39          "value": 5
40        }
41      ]
42    }
43  ]
44}

At a later date, the rules can retrieved from the database and can be run by the rules engine like this:

1import { run } from 'rules-engine-ts';
2
3const rules = getRulesFromDatabase();
4
5const pass = run(rules, { user_display_name: 'alice', total_challenges: 0 });
6const fail = run(rules, { user_display_name: 'bob', total_challenges: 0 });
7
8if (pass) {
9  // do something
10}
11
12if (fail) {
13  //do somehting else
14}
15

Open source

The project is Open Source and can be viewed on GitHub & NPM.

  • GitHub

Features

  • User-friendly API to add complex rules.
  • API comes with helper functions to add, remove or update rules and unions.
  • Recursively search for any nested rule or union.
  • Uses a simple concept called Unions to join rules.
  • Decision tree-like data structure.
  • No limitations on the levels of nesting.
  • Rules are strongly typed with the help of discriminated unions.
  • Validator function that verifies all properties are correct.

Technologies used

To do

  • Create recipe examples.
  • Create a function to detect conflicting or redundant rules.
  • Create a UI builder tool.