├── docs ├── _Footer.md ├── Overview.md ├── _Sidebar.md ├── Facts.md ├── Home.md ├── Examples.md ├── Flow-Control-API.md ├── Dynamic-Control.md └── Rules.md ├── jest.config.js ├── .gitignore ├── examples ├── runner.sh ├── node.js │ ├── 5.RecurssionWithRules.js │ ├── 1.SimpleRule.js │ ├── 4.PrioritizedRules.js │ ├── 3.CascadingRules.js │ ├── 2.MultipleRules.js │ └── 6.MoreRulesAndFacts.js └── web │ └── index.html ├── tsconfig.json ├── rollup.config.mjs ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml ├── lib ├── types.ts └── index.ts ├── LICENSE ├── dist ├── index.d.ts ├── index.js └── node-rules.min.js ├── package.json ├── CODE_OF_CONDUCT.md ├── README.md └── test └── index.test.ts /docs/_Footer.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs 4 | npm-debug.log 5 | .DS_Store 6 | .nyc_output 7 | *.tgz -------------------------------------------------------------------------------- /examples/runner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | THIS_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 3 | 4 | cd $THIS_DIR/.. # project root 5 | npm run build 6 | npm link 7 | cd $THIS_DIR/../examples/node.js 8 | npm link node-rules 9 | for i in *.js; do node $i; done; -------------------------------------------------------------------------------- /docs/Overview.md: -------------------------------------------------------------------------------- 1 | Node-rules takes rules written in JSON friendly format as input. You can register different rules on the rule engine after initiating it. Once the rule engine is running with registered rules, you can feed it with different fact objects and the rule engine will process them with the various rules registered on it. 2 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "lib": [ 6 | "esnext" 7 | ], 8 | "strict": true, 9 | "typeRoots": [ 10 | "types", 11 | "node_modules/@types" 12 | ], 13 | "esModuleInterop": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "allowSyntheticDefaultImports": true 16 | } 17 | } -------------------------------------------------------------------------------- /docs/_Sidebar.md: -------------------------------------------------------------------------------- 1 | * [Index](https://github.com/mithunsatheesh/node-rules/wiki/) 2 | * [Overview](https://github.com/mithunsatheesh/node-rules/wiki/Overview) 3 | * [Rules](https://github.com/mithunsatheesh/node-rules/wiki/Rules) 4 | * [Facts](https://github.com/mithunsatheesh/node-rules/wiki/Facts) 5 | * [Flow Control API](https://github.com/mithunsatheesh/node-rules/wiki/Flow-Control-API) 6 | * [Dynamic control](https://github.com/mithunsatheesh/node-rules/wiki/Dynamic-Control) 7 | * [Examples](https://github.com/mithunsatheesh/node-rules/wiki/Examples) 8 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from "@rollup/plugin-commonjs"; 2 | import globals from "rollup-plugin-node-globals"; 3 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 4 | import terser from "@rollup/plugin-terser"; 5 | 6 | export default [ 7 | { 8 | input: "dist/index.js", 9 | plugins: [nodeResolve(), commonjs(), globals(), terser()], 10 | output: { 11 | name: "NodeRules", 12 | format: "umd", 13 | extend: true, 14 | exports: "named", 15 | file: `dist/node-rules.min.js`, 16 | }, 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /docs/Facts.md: -------------------------------------------------------------------------------- 1 | Facts are those input json values on which the rule engine applies its rule to obtain results. A fact can have multiple attributes as you decide. 2 | 3 | A sample Fact may look like 4 | 5 | { 6 | "userIP": "27.3.4.5", 7 | "name":"user4", 8 | "application":"MOB2", 9 | "userLoggedIn":true, 10 | "transactionTotal":400, 11 | "cardType":"Credit Card", 12 | } 13 | 14 | The above fact goes through the rule engine when its executed. The conditions inside each rule will inspect the attributes again user defined conditions and consequences will be applied if they match for the fact. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Consequence { 2 | (API: API, fact: Fact): void; 3 | ruleRef?: string | undefined; 4 | } 5 | 6 | export type Rule = { 7 | id?: string; 8 | index?: number; 9 | name?: string; 10 | on?: boolean; 11 | priority?: number; 12 | condition: (API: API, fact: Fact) => void; 13 | consequence: Consequence; 14 | }; 15 | 16 | export type Fact = { 17 | [key: string]: any; 18 | matchPath?: string[]; 19 | }; 20 | 21 | export type Options = { 22 | ignoreFactChanges?: boolean; 23 | }; 24 | 25 | export interface API { 26 | rule: () => Rule; 27 | when: (outcome: any) => void; 28 | restart: () => void; 29 | stop: () => void; 30 | next: () => void; 31 | } 32 | -------------------------------------------------------------------------------- /examples/node.js/5.RecurssionWithRules.js: -------------------------------------------------------------------------------- 1 | const { RuleEngine } = require("node-rules"); 2 | 3 | /* Sample Rule to block a transaction if its below 500 */ 4 | /* Also validates if the legacy syntax of operating on this is supported in Rule engine */ 5 | var rule = { 6 | condition: function (R) { 7 | R.when(this.someval < 10); 8 | }, 9 | consequence: function (R) { 10 | console.log(++this.someval, " : incrementing again till 10"); 11 | R.restart(); 12 | }, 13 | }; 14 | /* Creating Rule Engine instance and registering rule */ 15 | var R = new RuleEngine(); 16 | R.register(rule); 17 | /* some val is 0 here, rules will recursively run till it becomes 10. 18 | This just a mock to demo the restart feature. */ 19 | var fact = { 20 | someval: 0, 21 | }; 22 | R.execute(fact, function (data) { 23 | console.log("Finished with value", data.someval); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /examples/node.js/1.SimpleRule.js: -------------------------------------------------------------------------------- 1 | const { RuleEngine } = require("node-rules"); 2 | 3 | /* Sample Rule to block a transaction if its below 500 */ 4 | var rule = { 5 | condition: function (R, fact) { 6 | R.when(fact.transactionTotal < 500); 7 | }, 8 | consequence: function (R, fact) { 9 | fact.result = false; 10 | fact.reason = `The transaction was blocked as the transaction total of ${fact.transactionTotal} was less than threshold 500`; 11 | R.stop(); 12 | }, 13 | }; 14 | 15 | /* Creating Rule Engine instance and registering rule */ 16 | var R = new RuleEngine(); 17 | R.register(rule); 18 | /* Fact with less than 500 as transaction, and this should be blocked */ 19 | var fact = { 20 | name: "user4", 21 | application: "MOB2", 22 | transactionTotal: 400, 23 | cardType: "Credit Card", 24 | }; 25 | 26 | R.execute(fact, function (data) { 27 | if (data.result !== false) { 28 | console.log("Valid transaction"); 29 | } else { 30 | console.log("Blocked Reason:" + data.reason); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /docs/Home.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://api.travis-ci.org/mithunsatheesh/node-rules.svg?branch=main)](https://travis-ci.org/mithunsatheesh/node-rules) 2 | [![npm](https://img.shields.io/npm/l/express.svg?style=flat-square)]() 3 | [![npm version](https://badge.fury.io/js/node-rules.svg)](http://badge.fury.io/js/node-rules) 4 | 5 | 6 | Node-rules 7 | ===== 8 | Node-rules is a light weight forward chaining Rule Engine, written on node.js. 9 | 10 | 11 | ###Wiki 12 | * [Overview](https://github.com/mithunsatheesh/node-rules/wiki/Overview) 13 | * [Rules](https://github.com/mithunsatheesh/node-rules/wiki/Rules) 14 | * [Facts](https://github.com/mithunsatheesh/node-rules/wiki/Facts) 15 | * [Flow Control API](https://github.com/mithunsatheesh/node-rules/wiki/Flow-Control-API) 16 | * [Dynamic control](https://github.com/mithunsatheesh/node-rules/wiki/Dynamic-Control) 17 | * [Export/Import Rules](https://github.com/mithunsatheesh/node-rules/wiki/Exporting-and-Importing-Rules) 18 | * [Examples](https://github.com/mithunsatheesh/node-rules/wiki/Examples) 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015 Mithun Satheesh 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | The example below shows how to use the rule engine to apply a sample rule on a specific fact. Rules fed into the rule engine may be as Array of rules or as individual rule objects. 2 | 3 | ```js 4 | //import the package 5 | const { RuleEngine } = require("node-rules"); 6 | 7 | //define the rules 8 | const rules = [ 9 | { 10 | condition: (R, fact) => { 11 | R.when(fact && fact.transactionTotal < 500); 12 | }, 13 | consequence: (R, fact) => { 14 | fact.result = false; 15 | R.stop(); 16 | }, 17 | }, 18 | ]; 19 | /*as you can see above we removed the priority 20 | and on properties for this example as they are optional.*/ 21 | 22 | //sample fact to run the rules on 23 | let fact = { 24 | userIP: "27.3.4.5", 25 | name: "user4", 26 | application: "MOB2", 27 | userLoggedIn: true, 28 | transactionTotal: 400, 29 | cardType: "Credit Card", 30 | }; 31 | 32 | //initialize the rule engine 33 | const R = new RuleEngine(rules); 34 | 35 | //Now pass the fact on to the rule engine for results 36 | R.execute(fact, (result) => { 37 | if (result.result) console.log("\n-----Payment Accepted----\n"); 38 | else console.log("\n-----Payment Rejected----\n"); 39 | }); 40 | ``` 41 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | FORCE_COLOR: 2 11 | NODE_COV: lts/* 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node: 22 | - 14 23 | - 16 24 | - 18 25 | - lts/* 26 | 27 | steps: 28 | - name: Clone repository 29 | uses: actions/checkout@v3 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node-version }} 35 | cache: 'npm' 36 | 37 | - name: Install npm dependencies 38 | run: npm ci 39 | 40 | - name: Build 41 | run: npm run build --if-present 42 | 43 | - name: Run Jest 44 | run: npm test 45 | 46 | - name: Run Jest with coverage 47 | run: npm run cover 48 | if: matrix.node == env.NODE_COV 49 | 50 | - name: Run Coveralls 51 | uses: coverallsapp/github-action@1.1.3 52 | if: matrix.node == env.NODE_COV 53 | continue-on-error: true 54 | with: 55 | github-token: '${{ secrets.GITHUB_TOKEN }}' -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | interface Consequence { 2 | (API: API, fact: Fact): void; 3 | ruleRef?: string | undefined; 4 | } 5 | type Rule = { 6 | id?: string; 7 | index?: number; 8 | name?: string; 9 | on?: boolean; 10 | priority?: number; 11 | condition: (API: API, fact: Fact) => void; 12 | consequence: Consequence; 13 | }; 14 | type Fact = { 15 | [key: string]: any; 16 | matchPath?: string[]; 17 | }; 18 | type Options = { 19 | ignoreFactChanges?: boolean; 20 | }; 21 | interface API { 22 | rule: () => Rule; 23 | when: (outcome: any) => void; 24 | restart: () => void; 25 | stop: () => void; 26 | next: () => void; 27 | } 28 | 29 | declare class RuleEngine { 30 | rules: Rule[]; 31 | activeRules: Rule[]; 32 | private ignoreFactChanges; 33 | constructor(rules?: Rule | Rule[], options?: Options); 34 | init(): void; 35 | register(rules: Rule | Rule[]): void; 36 | sync(): void; 37 | execute(fact: Fact, callback: (fact: Fact) => void): void; 38 | nextTick(callback: () => void): void; 39 | findRules(query?: Record): Rule[]; 40 | turn(state: string, filter?: Record): void; 41 | prioritize(priority: number, filter?: Record): void; 42 | } 43 | 44 | export { RuleEngine }; 45 | -------------------------------------------------------------------------------- /examples/node.js/4.PrioritizedRules.js: -------------------------------------------------------------------------------- 1 | const { RuleEngine } = require("node-rules"); 2 | 3 | /* Set of Rules to be applied */ 4 | var rules = [ 5 | { 6 | priority: 4, 7 | condition: function (R, fact) { 8 | R.when(fact.transactionTotal < 500); 9 | }, 10 | consequence: function (R, fact) { 11 | fact.result = false; 12 | fact.reason = "The transaction was blocked as it was less than 500"; 13 | R.stop(); 14 | }, 15 | }, 16 | { 17 | priority: 10, // this will apply first 18 | condition: function (R, fact) { 19 | R.when(fact.cardType === "Debit"); 20 | }, 21 | consequence: function (R, fact) { 22 | fact.result = false; 23 | fact.reason = 24 | "The transaction was blocked as debit cards are not allowed"; 25 | R.stop(); 26 | }, 27 | }, 28 | ]; 29 | /* Creating Rule Engine instance and registering rule */ 30 | var R = new RuleEngine(); 31 | R.register(rules); 32 | /* Fact with more than 500 as transaction but a Debit card, and this should be blocked */ 33 | var fact = { 34 | name: "user4", 35 | application: "MOB2", 36 | transactionTotal: 600, 37 | cardType: "Debit", 38 | }; 39 | /* This fact will be blocked by the Debit card rule as its of more priority */ 40 | R.execute(fact, function (data) { 41 | if (data.result !== false) { 42 | console.log("Valid transaction"); 43 | } else { 44 | console.log("Blocked Reason:" + data.reason); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /examples/node.js/3.CascadingRules.js: -------------------------------------------------------------------------------- 1 | const { RuleEngine } = require("node-rules"); 2 | 3 | /* Here we can see a rule which upon matching its condition, 4 | does some processing and passes it to other rules for processing */ 5 | var rules = [ 6 | { 7 | condition: function (R, fact) { 8 | R.when(fact.application === "MOB"); 9 | }, 10 | consequence: function (R, fact) { 11 | fact.isMobile = true; 12 | R.next(); //we just set a value on to fact, now lests process rest of rules 13 | }, 14 | }, 15 | { 16 | condition: function (R, fact) { 17 | R.when(fact.cardType === "Debit"); 18 | }, 19 | consequence: function (R, fact) { 20 | fact.result = false; 21 | fact.reason = 22 | "The transaction was blocked as debit cards are not allowed"; 23 | R.stop(); 24 | }, 25 | }, 26 | ]; 27 | /* Creating Rule Engine instance and registering rule */ 28 | var R = new RuleEngine(); 29 | R.register(rules); 30 | 31 | /* Fact is mobile with Credit card type. This should go through */ 32 | var fact = { 33 | name: "user4", 34 | application: "MOB", 35 | transactionTotal: 600, 36 | cardType: "Credit", 37 | }; 38 | R.execute(fact, function (data) { 39 | if (data.result !== false) { 40 | console.log("Valid transaction"); 41 | } else { 42 | console.log("Blocked Reason:" + data.reason); 43 | } 44 | 45 | if (data.isMobile) { 46 | console.log("It was from a mobile device too!!"); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /examples/node.js/2.MultipleRules.js: -------------------------------------------------------------------------------- 1 | const { RuleEngine } = require("node-rules"); 2 | 3 | /* Set of Rules to be applied 4 | First blocks a transaction if less than 500 5 | Second blocks a debit card transaction.*/ 6 | /*Note that here we are not specifying which rule to apply first. 7 | Rules will be applied as per their index in the array. 8 | If you need to enforce priority manually, then see examples with prioritized rules */ 9 | var rules = [ 10 | { 11 | condition: function (R, fact) { 12 | R.when(fact.transactionTotal < 500); 13 | }, 14 | consequence: function (R, fact) { 15 | fact.result = false; 16 | fact.reason = "The transaction was blocked as it was less than 500"; 17 | R.stop(); //stop if matched. no need to process next rule. 18 | }, 19 | }, 20 | { 21 | condition: function (R, fact) { 22 | R.when(fact.cardType === "Debit"); 23 | }, 24 | consequence: function (R, fact) { 25 | fact.result = false; 26 | fact.reason = 27 | "The transaction was blocked as debit cards are not allowed"; 28 | R.stop(); 29 | }, 30 | }, 31 | ]; 32 | /* Creating Rule Engine instance and registering rule */ 33 | var R = new RuleEngine(); 34 | R.register(rules); 35 | /* Fact with more than 500 as transaction but a Debit card, and this should be blocked */ 36 | var fact = { 37 | name: "user4", 38 | application: "MOB2", 39 | transactionTotal: 600, 40 | cardType: "Debit", 41 | }; 42 | R.execute(fact, function (data) { 43 | if (data.result !== false) { 44 | console.log("Valid transaction"); 45 | } else { 46 | console.log("Blocked Reason:" + data.reason); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /examples/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Node Rules Web example 7 | 8 | 9 | 10 | 11 | 17 | 18 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /docs/Flow-Control-API.md: -------------------------------------------------------------------------------- 1 | This is an object injected into the condition and consequence functions defined by the user to make it easy for the user to define the Rule Engine flow. 2 | 3 | If you look at the below rule example. 4 | 5 | { 6 | "name": "transaction minimum", 7 | "priority": 3, 8 | "on" : true, 9 | "condition": function(R) { 10 | R.when(this.transactionTotal < 500); 11 | }, 12 | "consequence": function(R) { 13 | this.result = false; 14 | R.stop(); 15 | } 16 | } 17 | 18 | The `R` object injected in both condition and consequence refers to the API we are talking about. 19 | 20 | 21 | Below are the functions available via the Flow Control API. 22 | 23 | #### R.when 24 | This function is used to pass the condition expression that we want to evaluate. In the above expression we pass the expression to check whether the transactionTotal attribute of the fact in context is below 500 or not. If the expression passed to `R.when` evaluates to true, then the condition will execute. Else the rule engine will move to next rule or may terminate if there are no rules left to apply. 25 | 26 | #### R.next 27 | This function is used inside consequence functions. This is used to instruct the rule engine to start applying the next rule on the fact if any. 28 | 29 | #### R.stop 30 | This function is used inside consequence functions to instruct the Rule Engine to stop processing the fact. If this function is called, even if rules are left to be applied, the rule engine will not apply rest of rules on the fact. It is used mostly when we arrive a conclusion on a particular fact and there is no need of any further process on it to generate a result. 31 | 32 | As you can see above example, when the transaction is less than 500, we no longer need to process the rule. So stores false in result attribute and calls the stop immediately inside consequence. 33 | 34 | #### R.restart 35 | This function is used inside consequence functions to instruct the rule engine to begin applying the Rules on the fact from first. This function is also internally used by the Rule engine when the fact object is modified by a consequence function and it needs to go through all the rules once gain. 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /docs/Dynamic-Control.md: -------------------------------------------------------------------------------- 1 | Dynamic Control is needed when the rule engine is in running state with a set of rules and you need to manipulate the rules which are running on it. There are a number of functions exposed by the library for this purpose. 2 | 3 | The various functions for dynamic control are 4 | 5 | 1. Turn 6 | 2. Prioritize 7 | 3. Register 8 | 4. FindRules 9 | 5. Init 10 | 11 | ##### 1. `RuleEngine.turn(,)` 12 | This function is used to dynamically activate or deactivate a rule. The syntax for using this function is shown in below example. 13 | 14 | RuleEngine.turn("OFF", { 15 | "id": "one" 16 | }); 17 | 18 | Here `RuleEngine` is the rule engine instance. The first parameter to turn function indicates whether we need to turn the rule ON or OFF. The second parameter passed to the function is a filter. It should be a key which can be used to uniquely distinguish the targeted rule or set of rules from the other rules running in the Rule Engine. Here the above example will deactivate all the rules where the `id` attribute equals "one". 19 | 20 | ##### 2. `RuleEngine.prioritize(,)` 21 | This function is used to dynamically change the priority of a rule while rule engine is running. It works similar to the Turn function and just that instead of the ON/OFF state we will be passing a priority number value to the function. See example below 22 | 23 | RuleEngine.prioritize(10, { 24 | "id": "one" 25 | }); 26 | 27 | The above `prioritize` call will give priority to Rule with id "one" over all the rules which are having lesser priority than 10. 28 | 29 | 30 | ##### 3. `RuleEngine.register()` 31 | We know that we can pass Rules as parameter into the Rule Engine constructor while we create the Rule Engine object like below. 32 | 33 | const RuleEngine = new RuleEngine(rules); 34 | 35 | Where `rules` can be either an array of rule objects or a single array. But what if we need to add some rules later to the Rule Engine. Register can be used any time to append new rules into the Rule Engine. It can be used like. 36 | 37 | const RuleEngine = new RuleEngine(); 38 | RuleEngine.register(newrule); 39 | RuleEngine.register(newrule); 40 | 41 | 42 | ##### 4. `RuleEngine.findRules()` 43 | This function is used to retrieve the Rules which are registered on the Rule engine which matches the filter we pass as its parameter. A sample usage can be like below. 44 | 45 | const rules = RuleEngine.findRules({"id": "one"}); 46 | 47 | ##### 5. `RuleEngine.init()` 48 | This function is used to remove all the rules registered on the Rule Engine. This is mostly used for rule clean up purposes by internal functions. A sample usage can be like below. 49 | 50 | const RuleEngine = new RuleEngine(); 51 | RuleEngine.register(badrule); 52 | RuleEngine.init();//removes the bad rule and cleans up 53 | RuleEngine.register(newrule); 54 | 55 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-rules", 3 | "version": "9.2.0", 4 | "description": "Business Rules Engine for JavaScript", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/mithunsatheesh/node-rules" 8 | }, 9 | "dependencies": { 10 | "lodash.clonedeep": "^4.5.0", 11 | "lodash.isequal": "^4.5.0" 12 | }, 13 | "devDependencies": { 14 | "@rollup/plugin-commonjs": "^24.0.1", 15 | "@rollup/plugin-node-resolve": "^15.0.1", 16 | "@rollup/plugin-terser": "^0.4.0", 17 | "@types/jest": "^27.0.0", 18 | "@types/lodash.clonedeep": "^4.5.6", 19 | "@types/lodash.isequal": "^4.5.5", 20 | "@types/node": "^16.3.3", 21 | "jest": "^27.0.6", 22 | "jest-cli": "^27.0.6", 23 | "npm": "^9.6.0", 24 | "rollup": "^3.7.0", 25 | "rollup-plugin-node-globals": "^1.4.0", 26 | "ts-jest": "^27.0.3", 27 | "tsup": "^4.12.5", 28 | "typescript": "^4.3.5" 29 | }, 30 | "main": "dist/index.js", 31 | "types": "dist/index.d.ts", 32 | "files": [ 33 | "dist" 34 | ], 35 | "engines": { 36 | "node": ">=12" 37 | }, 38 | "scripts": { 39 | "prepare": "npm run build", 40 | "test": "jest", 41 | "build": "tsup ./lib/index.ts --dts && rollup --config ./rollup.config.mjs", 42 | "cover": "jest --coverage", 43 | "clean": "rm -rf ./dist ./node_modules && rm package-lock.json", 44 | "examples": "./examples/runner.sh" 45 | }, 46 | "readmeFilename": "README.md", 47 | "license": "MIT", 48 | "author": { 49 | "name": "Mithun Satheesh", 50 | "url": "https://github.com/mithunsatheesh" 51 | }, 52 | "contributors": [ 53 | { 54 | "name": "Abdul Munim Kazia", 55 | "url": "http://munimkazia.com" 56 | }, 57 | { 58 | "name": "ramanaveli2i", 59 | "url": "https://github.com/ramanaveli2i" 60 | }, 61 | { 62 | "name": "Alexis Tyler", 63 | "url": "https://github.com/OmgImAlexis" 64 | }, 65 | { 66 | "name": "Joseph Heck", 67 | "url": "https://github.com/heckj" 68 | }, 69 | { 70 | "name": "Jacob Jewell", 71 | "url": "https://github.com/jakesjews" 72 | }, 73 | { 74 | "name": "pdapel", 75 | "url": "https://github.com/pdapel" 76 | }, 77 | { 78 | "name": "Dirk Rejahl", 79 | "url": "https://github.com/drejahl" 80 | }, 81 | { 82 | "name": "Matija Munjaković", 83 | "url": "https://github.com/matija" 84 | }, 85 | { 86 | "name": "Alex Velikanov", 87 | "url": "https://github.com/Alec2435" 88 | } 89 | ], 90 | "keywords": [ 91 | "bre", 92 | "rete", 93 | "rule", 94 | "rules", 95 | "engine", 96 | "rule engine", 97 | "rules engine", 98 | "javascript rule engine", 99 | "js rule engine", 100 | "forward chaining rule engine", 101 | "inference system" 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mithunsatish@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /docs/Rules.md: -------------------------------------------------------------------------------- 1 | A rule will consist of a condition and its corresponding consequence. If the fact you feed into the engine satisfies the condition, then the consequence will run. Also optionally user may choose to define the priority of a rule applied. The rule engine will be applying the rules on the fact according to the priority defined. 2 | 3 | Lets see how a sample rule will look like and then proceed to explain the different attributes of a rule. 4 | 5 | { 6 | "name": "transaction minimum", 7 | "priority": 3, 8 | "on" : true, 9 | "condition": (R, fact) => { 10 | R.when(fact.transactionTotal < 500); 11 | }, 12 | "consequence": (R, fact) => { 13 | fact.result = false; 14 | R.stop(); 15 | } 16 | } 17 | 18 | Above is a sample rule which has mandatory as well as optional parameters. You can choose to use which all attributes you need to use while defining your rule. Now lets look into the attributes one by one. 19 | 20 | ###### 1. condition 21 | Condition is a function where the user can do the checks on the fact provided. The fact variable will be available in `this` context of the condition function or as second function argument incase you are using arrow functions. Lets see a sample condition below. 22 | 23 | "condition": (R, fact) => { 24 | R.when(this.transactionTotal < 500); 25 | } 26 | 27 | As you can see, the we have to pass an expression on to the `R.when` function which is a part of the [Flow Control API](https://github.com/mithunsatheesh/node-rules/wiki/Flow-Control-API). You can read more about the API [Flow Control API](https://github.com/mithunsatheesh/node-rules/wiki/Flow-Control-API). If the expression evaluates to true for a fact, the corresponding consequence will execute. 28 | 29 | Its mandatory to have this field. 30 | 31 | ###### 2. consequence 32 | The consequence is the part where we define what happens when the condition evaluates to true for a particular fact. Just like in condition, fact variable will be available in `this` context or as second function argument incase you are using arrow functions. You may utilize it to add extra result attributes if needed. 33 | 34 | "consequence": (R, fact) { 35 | fact.result = false; 36 | R.stop(); 37 | } 38 | 39 | In the above example we use an additional parameter `result` to communicate to the code outside the rule engine that the fact has succeeded. Also the Rule API provides a number of functions here to control the flow of the rule engine. They are `R.stop()`, `R.restart()` and `R.next()`. Stop refers to stop processing the rule engine. Restart tells the rule engine to start applying all the rules again to the fact. Next is to instruct the rule engine to continue applying the rest of the rules to the fact before stopping. Check [Flow Control API](https://github.com/mithunsatheesh/node-rules/wiki/Flow-Control-API) in wiki to read more about this. 40 | 41 | You can read more about flow control API here. 42 | 43 | Its mandatory to have this field. 44 | 45 | ###### 3. priority 46 | This field is used to specify the priority of a rule. The rules with higher priority will be applied on the fact first and then followed by lower priority rules. You can have multiple rules with same priority and the engine will not ensure the order in that case. 47 | 48 | Its not mandatory to have this field. 49 | 50 | ###### 4. on 51 | This field is used to store the state of a rule. This is used to activate and deactivate rules at run time. Rules with `on` set to `false` will not be applied on the facts. 52 | 53 | It is not mandatory to have this field. 54 | 55 | ###### 5. add a unique attribute 56 | It is suggested that you should add a property which can be used as a unique identifier for a rule. Why it is because when you need to dynamically turn on/off or change priority of a rule, you will need a filter to select a rule from the engine via the APIs. That time you may use the unique property as a key for the filter for selection process. 57 | 58 | Suppose that in the above example `name` is unique for each rule. Then for changing state or re prioritizing a rule at run time, you may use a filter like `{"name":"transaction minimum"}`. 59 | 60 | Again its optional to add a unique identifier. You may ignore adding it to your rules if you are not changing rule states at run time. -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from "lodash.clonedeep"; 2 | import isEqual from "lodash.isequal"; 3 | 4 | import { Rule, Options, Fact, API } from "./types"; 5 | 6 | export {Rule, Options, Fact, API} 7 | 8 | export class RuleEngine { 9 | public rules: Rule[] = []; 10 | public activeRules: Rule[] = []; 11 | private ignoreFactChanges: boolean = false; 12 | 13 | constructor(rules?: Rule | Rule[], options?: Options) { 14 | if (rules) { 15 | this.register(rules); 16 | } 17 | if (options) { 18 | this.ignoreFactChanges = options.ignoreFactChanges || false; 19 | } 20 | } 21 | 22 | init(): void { 23 | this.rules = []; 24 | this.activeRules = []; 25 | } 26 | 27 | register(rules: Rule | Rule[]): void { 28 | if (Array.isArray(rules)) { 29 | this.rules.push(...rules); 30 | } else if (rules !== null && typeof rules === "object") { 31 | this.rules.push(rules); 32 | } 33 | this.sync(); 34 | } 35 | 36 | sync(): void { 37 | this.activeRules = this.rules.filter((a) => { 38 | if (typeof a.on === "undefined") { 39 | a.on = true; 40 | } 41 | if (a.on === true) { 42 | return a; 43 | } 44 | }); 45 | this.activeRules.sort((a, b) => { 46 | if (a.priority && b.priority) { 47 | return b.priority - a.priority; 48 | } else { 49 | return 0; 50 | } 51 | }); 52 | } 53 | 54 | execute(fact: Fact, callback: (fact: Fact) => void): void { 55 | const thisHolder = this; 56 | let complete = false; 57 | const session = cloneDeep(fact); 58 | let lastSession = cloneDeep(fact); 59 | let rules = this.activeRules; 60 | const matchPath: string[] = []; 61 | const ignoreFactChanges = this.ignoreFactChanges; 62 | 63 | function FnRuleLoop(x: number) { 64 | const API: API = { 65 | rule: () => rules[x], 66 | when: (outcome: boolean) => { 67 | if (outcome) { 68 | const _consequence = rules[x].consequence; 69 | _consequence.ruleRef = rules[x].id || rules[x].name || `index_${x}`; 70 | thisHolder.nextTick(() => { 71 | matchPath.push(_consequence.ruleRef as string); 72 | _consequence.call(session, API, session); 73 | }); 74 | } else { 75 | thisHolder.nextTick(() => { 76 | API.next(); 77 | }); 78 | } 79 | }, 80 | restart: () => FnRuleLoop(0), 81 | stop: () => { 82 | complete = true; 83 | return FnRuleLoop(0); 84 | }, 85 | next: () => { 86 | if (!ignoreFactChanges && !isEqual(lastSession, session)) { 87 | lastSession = cloneDeep(session); 88 | thisHolder.nextTick(() => { 89 | API.restart(); 90 | }); 91 | } else { 92 | thisHolder.nextTick(() => { 93 | return FnRuleLoop(x + 1); 94 | }); 95 | } 96 | }, 97 | }; 98 | 99 | rules = thisHolder.activeRules; 100 | if (x < rules.length && !complete) { 101 | const _rule = rules[x].condition; 102 | _rule.call(session, API, session); 103 | } else { 104 | thisHolder.nextTick(() => { 105 | session.matchPath = matchPath; 106 | callback(session); 107 | }); 108 | } 109 | } 110 | FnRuleLoop(0); 111 | } 112 | 113 | nextTick(callback: () => void) { 114 | process?.nextTick ? process?.nextTick(callback) : setTimeout(callback, 0); 115 | } 116 | 117 | findRules(query?: Record) { 118 | if (typeof query === "undefined") { 119 | return this.rules; 120 | } 121 | 122 | // Clean the properties set to undefined in the search query if any to prevent miss match issues. 123 | Object.keys(query).forEach( 124 | (key) => query[key] === undefined && delete query[key] 125 | ); 126 | 127 | // Return rules in the registered rules array which match partially to the query. 128 | return this.rules.filter((rule: any) => { 129 | return Object.keys(query).some((key: any) => { 130 | return query[key] === rule[key]; 131 | }); 132 | }); 133 | } 134 | 135 | turn(state: string, filter?: Record) { 136 | const rules = this.findRules(filter); 137 | for (let i = 0, j = rules.length; i < j; i++) { 138 | rules[i].on = state.toLowerCase() === "on"; 139 | } 140 | this.sync(); 141 | } 142 | 143 | prioritize(priority: number, filter?: Record) { 144 | const rules = this.findRules(filter); 145 | for (let i = 0, j = rules.length; i < j; i++) { 146 | rules[i].priority = priority; 147 | } 148 | this.sync(); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }var __defProp = Object.defineProperty; 2 | var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; 3 | var __publicField = (obj, key, value) => { 4 | __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); 5 | return value; 6 | }; 7 | 8 | // lib/index.ts 9 | var _lodashclonedeep = require('lodash.clonedeep'); var _lodashclonedeep2 = _interopRequireDefault(_lodashclonedeep); 10 | var _lodashisequal = require('lodash.isequal'); var _lodashisequal2 = _interopRequireDefault(_lodashisequal); 11 | var RuleEngine = class { 12 | constructor(rules, options) { 13 | __publicField(this, "rules", []); 14 | __publicField(this, "activeRules", []); 15 | __publicField(this, "ignoreFactChanges", false); 16 | if (rules) { 17 | this.register(rules); 18 | } 19 | if (options) { 20 | this.ignoreFactChanges = options.ignoreFactChanges || false; 21 | } 22 | } 23 | init() { 24 | this.rules = []; 25 | this.activeRules = []; 26 | } 27 | register(rules) { 28 | if (Array.isArray(rules)) { 29 | this.rules.push(...rules); 30 | } else if (rules !== null && typeof rules === "object") { 31 | this.rules.push(rules); 32 | } 33 | this.sync(); 34 | } 35 | sync() { 36 | this.activeRules = this.rules.filter((a) => { 37 | if (typeof a.on === "undefined") { 38 | a.on = true; 39 | } 40 | if (a.on === true) { 41 | return a; 42 | } 43 | }); 44 | this.activeRules.sort((a, b) => { 45 | if (a.priority && b.priority) { 46 | return b.priority - a.priority; 47 | } else { 48 | return 0; 49 | } 50 | }); 51 | } 52 | execute(fact, callback) { 53 | const thisHolder = this; 54 | let complete = false; 55 | const session = _lodashclonedeep2.default.call(void 0, fact); 56 | let lastSession = _lodashclonedeep2.default.call(void 0, fact); 57 | let rules = this.activeRules; 58 | const matchPath = []; 59 | const ignoreFactChanges = this.ignoreFactChanges; 60 | function FnRuleLoop(x) { 61 | const API = { 62 | rule: () => rules[x], 63 | when: (outcome) => { 64 | if (outcome) { 65 | const _consequence = rules[x].consequence; 66 | _consequence.ruleRef = rules[x].id || rules[x].name || `index_${x}`; 67 | thisHolder.nextTick(() => { 68 | matchPath.push(_consequence.ruleRef); 69 | _consequence.call(session, API, session); 70 | }); 71 | } else { 72 | thisHolder.nextTick(() => { 73 | API.next(); 74 | }); 75 | } 76 | }, 77 | restart: () => FnRuleLoop(0), 78 | stop: () => { 79 | complete = true; 80 | return FnRuleLoop(0); 81 | }, 82 | next: () => { 83 | if (!ignoreFactChanges && !_lodashisequal2.default.call(void 0, lastSession, session)) { 84 | lastSession = _lodashclonedeep2.default.call(void 0, session); 85 | thisHolder.nextTick(() => { 86 | API.restart(); 87 | }); 88 | } else { 89 | thisHolder.nextTick(() => { 90 | return FnRuleLoop(x + 1); 91 | }); 92 | } 93 | } 94 | }; 95 | rules = thisHolder.activeRules; 96 | if (x < rules.length && !complete) { 97 | const _rule = rules[x].condition; 98 | _rule.call(session, API, session); 99 | } else { 100 | thisHolder.nextTick(() => { 101 | session.matchPath = matchPath; 102 | callback(session); 103 | }); 104 | } 105 | } 106 | FnRuleLoop(0); 107 | } 108 | nextTick(callback) { 109 | (process == null ? void 0 : process.nextTick) ? process == null ? void 0 : process.nextTick(callback) : setTimeout(callback, 0); 110 | } 111 | findRules(query) { 112 | if (typeof query === "undefined") { 113 | return this.rules; 114 | } 115 | Object.keys(query).forEach((key) => query[key] === void 0 && delete query[key]); 116 | return this.rules.filter((rule) => { 117 | return Object.keys(query).some((key) => { 118 | return query[key] === rule[key]; 119 | }); 120 | }); 121 | } 122 | turn(state, filter) { 123 | const rules = this.findRules(filter); 124 | for (let i = 0, j = rules.length; i < j; i++) { 125 | rules[i].on = state.toLowerCase() === "on"; 126 | } 127 | this.sync(); 128 | } 129 | prioritize(priority, filter) { 130 | const rules = this.findRules(filter); 131 | for (let i = 0, j = rules.length; i < j; i++) { 132 | rules[i].priority = priority; 133 | } 134 | this.sync(); 135 | } 136 | }; 137 | 138 | 139 | exports.RuleEngine = RuleEngine; 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/mithunsatheesh/node-rules/actions/workflows/main.yml/badge.svg)](https://github.com/mithunsatheesh/node-rules/actions/workflows/main.yml) 2 | [![npm](https://img.shields.io/npm/l/express.svg?style=flat-square)]() 3 | [![npm version](https://badge.fury.io/js/node-rules.svg)](http://badge.fury.io/js/node-rules) 4 | [![Coverage Status](https://coveralls.io/repos/github/mithunsatheesh/node-rules/badge.svg?branch=main)](https://coveralls.io/github/mithunsatheesh/node-rules?branch=main) 5 | [![npm downloads](https://img.shields.io/npm/dm/node-rules.svg)](https://img.shields.io/npm/dm/node-rules.svg) 6 | [![install size](https://img.shields.io/github/size/mithunsatheesh/node-rules/dist/node-rules.min.js)](https://github.com/mithunsatheesh/node-rules/blob/main/dist/node-rules.min.js) 7 | [![install size](https://packagephobia.com/badge?p=node-rules)](https://packagephobia.com/result?p=node-rules) 8 | [![Known Vulnerabilities](https://snyk.io/test/npm/node-rules/badge.svg)](https://snyk.io/test/npm/node-rules) 9 | [![CDNJS](https://img.shields.io/cdnjs/v/node-rules?color=orange&style=flat-square)](https://cdnjs.com/libraries/node-rules) 10 | 11 | # Node Rules 12 | 13 | Node-rules is a light weight forward chaining Rule Engine, written in JavaScript for both browser and node.js environments. 14 | 15 | #### Installation 16 | 17 | npm install node-rules 18 | 19 | ![Sample Screencast](https://raw.githubusercontent.com/mithunsatheesh/node-rules/gh-pages/images/screencast.gif "See it in action") 20 | 21 | #### Try This Out! 22 | 23 | You can see this in action in a node.js environment using [this RunKit example](https://runkit.com/mithunsatheesh/node-rules-9.0.0). If you are interested to use it in the browser, use [this JSFiddle](https://jsfiddle.net/mithunsatheesh/6pwohf3g/) for a quick start. 24 | 25 | #### Overview 26 | 27 | Node-rules takes rules written in JSON friendly format as input. Once the rule engine is running with rules registered on it, you can feed it facts and the rules will be applied one by one to generate an outcome. 28 | 29 | ###### 1. Defining a Rule 30 | 31 | A rule will consist of a condition and its corresponding consequence. You can find the explanation for various mandatory and optional parameters of a rule in [this wiki](https://github.com/mithunsatheesh/node-rules/wiki/Rules). 32 | 33 | ```js 34 | { 35 | "condition" : (R, fact) => { 36 | R.when(fact.transactionTotal < 500); 37 | }, 38 | "consequence" : (R, fact) => { 39 | fact.result = false; 40 | R.stop(); 41 | } 42 | } 43 | ``` 44 | 45 | Here priority is an optional parameter which will be used to specify priority of a rule over other rules when there are multiple rules running. In the above rule `R.when` evaluates the condition expression and `R.stop` used to stop further processing of the fact as we have arrived at a result. 46 | 47 | The functions `R.stop`, `R.when`, `R.next`, `R.restart` are part of the Flow Control API which allows user to control the Engine Flow. Read more about [Flow Controls](https://github.com/mithunsatheesh/node-rules/wiki/Flow-Control-API) in [wiki](https://github.com/mithunsatheesh/node-rules/wiki). 48 | 49 | ###### 2. Defining a Fact 50 | 51 | Facts are those input json values on which the rule engine applies its rule to obtain results. A fact can have multiple attributes as you decide. 52 | 53 | A sample Fact may look like 54 | 55 | ```json 56 | { 57 | "name": "user4", 58 | "application": "MOB2", 59 | "transactionTotal": 400, 60 | "cardType": "Credit Card" 61 | } 62 | ``` 63 | 64 | ###### 3. Using the Rule Engine 65 | 66 | The example below shows how to use the rule engine to apply a sample rule on a specific fact. Rules can be fed into the rule engine as Array of rules or as an individual rule object. 67 | 68 | ```js 69 | const { RuleEngine } = require("node-rules"); 70 | 71 | /* Creating Rule Engine instance */ 72 | const R = new RuleEngine(); 73 | 74 | /* Add a rule */ 75 | const rule = { 76 | condition: (R, fact) => { 77 | R.when(fact.transactionTotal < 500); 78 | }, 79 | consequence: (R, fact) => { 80 | fact.result = false; 81 | fact.reason = "The transaction was blocked as it was less than 500"; 82 | R.stop(); 83 | }, 84 | }; 85 | 86 | /* Register Rule */ 87 | R.register(rule); 88 | 89 | /* Add a Fact with less than 500 as transaction, and this should be blocked */ 90 | let fact = { 91 | name: "user4", 92 | application: "MOB2", 93 | transactionTotal: 400, 94 | cardType: "Credit Card", 95 | }; 96 | 97 | /* Check if the engine blocks it! */ 98 | R.execute(fact, (data) => { 99 | if (data.result !== false) { 100 | console.log("Valid transaction"); 101 | } else { 102 | console.log("Blocked Reason:" + data.reason); 103 | } 104 | }); 105 | ``` 106 | 107 | ###### 4. Controlling Rules running on the Rule Engine 108 | 109 | If you are looking for ways to specify the order in which the rules get applied on a fact, it can be done via using the `priority` parameter. Read more about it in the [Rule wiki](https://github.com/mithunsatheesh/node-rules/wiki/Rules). If you need to know about how to change priority of rules or remove add new rules to a Running Rule Engine, you may read more about it in [Dynamic Control Wiki](https://github.com/mithunsatheesh/node-rules/wiki/Dynamic-Control). 110 | 111 | ###### 5. Exporting Rules to an external storage 112 | 113 | To read more about storing rules running on the engine to an external DB, refer this [wiki article](https://github.com/mithunsatheesh/node-rules/wiki/Exporting-and-Importing-Rules). 114 | 115 | #### Wiki 116 | 117 | To read more about the Rule engine functions, please read [the wiki here](https://github.com/mithunsatheesh/node-rules/wiki)!. To find more examples of implementation please look in the [examples](https://github.com/mithunsatheesh/node-rules/tree/main/examples) folder. 118 | 119 | #### Issues 120 | 121 | Got issues with the implementation?. Feel free to open an issue [here](https://github.com/mithunsatheesh/node-rules/issues/new). 122 | 123 | #### Licence 124 | 125 | Node rules is distributed under the MIT License. 126 | 127 | #### External References 128 | 129 | - https://ieeexplore.ieee.org/document/7968566 130 | 131 | #### Credits 132 | 133 | The JSON friendly rule formats used in version 2.x.x of this module were initially based on the node module [jools](https://github.com/tdegrunt/jools). 134 | The screencast image shown in this page is taken from [nmotv.in](http://nmotw.in/node-rules/) which has a pretty nice article on how to use this module! 135 | -------------------------------------------------------------------------------- /examples/node.js/6.MoreRulesAndFacts.js: -------------------------------------------------------------------------------- 1 | const { RuleEngine } = require("node-rules"); 2 | 3 | const COLORS = { 4 | red: "\x1b[31m", 5 | green: "\x1b[32m", 6 | yellow: "\x1b[33m", 7 | }; 8 | 9 | const rules = [ 10 | /**** Rule 1 ****/ 11 | { 12 | name: "transaction minimum 500", 13 | priority: 3, 14 | on: true, 15 | condition: function (R, f) { 16 | R.when(f.transactionTotal < 500); 17 | }, 18 | consequence: function (R, f) { 19 | console.log( 20 | "Rule 1 matched - blocks transactions below value 500. Rejecting payment." 21 | ); 22 | f.result = false; 23 | R.stop(); 24 | }, 25 | }, 26 | /**** Rule 2 ****/ 27 | { 28 | name: "high credibility customer - avoid checks and bypass", 29 | priority: 2, 30 | on: true, 31 | condition: function (R, f) { 32 | R.when(f.userCredibility && f.userCredibility > 5); 33 | }, 34 | consequence: function (R, f) { 35 | console.log( 36 | "Rule 2 matched - user credibility is more, then avoid further check. Accepting payment." 37 | ); 38 | f.result = true; 39 | R.stop(); 40 | }, 41 | }, 42 | /**** Rule 3 ****/ 43 | { 44 | name: "block AME > 10000", 45 | priority: 4, 46 | on: true, 47 | condition: function (R, f) { 48 | R.when( 49 | f.cardType == "Credit Card" && 50 | f.cardIssuer == "American Express" && 51 | f.transactionTotal > 1000 52 | ); 53 | }, 54 | consequence: function (R, f) { 55 | console.log( 56 | "Rule 3 matched - filter American Express payment above 10000. Rejecting payment." 57 | ); 58 | f.result = false; 59 | R.stop(); 60 | }, 61 | }, 62 | /**** Rule 4 ****/ 63 | { 64 | name: "block Cashcard Payment", 65 | priority: 8, 66 | on: true, 67 | condition: function (R, f) { 68 | R.when(f.cardType == "Cash Card"); 69 | }, 70 | consequence: function (R, f) { 71 | console.log( 72 | "Rule 4 matched - reject the payment if cash card. Rejecting payment." 73 | ); 74 | f.result = false; 75 | R.stop(); 76 | }, 77 | }, 78 | /**** Rule 5 ****/ 79 | { 80 | name: "block guest payment above 10000", 81 | priority: 6, 82 | on: true, 83 | condition: function (R, f) { 84 | R.when( 85 | f.customerType && 86 | f.transactionTotal > 10000 && 87 | f.customerType == "guest" 88 | ); 89 | }, 90 | consequence: function (R, f) { 91 | console.log( 92 | "Rule 5 matched - reject if above 10000 and customer type is guest. Rejecting payment." 93 | ); 94 | f.result = false; 95 | R.stop(); 96 | }, 97 | }, 98 | /**** Rule 6 ****/ 99 | { 100 | name: "is customer guest?", 101 | priority: 7, 102 | on: true, 103 | condition: function (R, f) { 104 | R.when(!f.userLoggedIn); 105 | }, 106 | consequence: function (R, f) { 107 | console.log( 108 | "Rule 6 matched - support rule written for blocking payment above 10000 from guests." 109 | ); 110 | console.log("Process left to chain with rule 5."); 111 | f.customerType = "guest"; 112 | R.next(); // the fact has been altered, so all rules will run again. No need to restart. 113 | }, 114 | }, 115 | /**** Rule 7 ****/ 116 | { 117 | name: "block payment from specific app", 118 | priority: 5, 119 | on: true, 120 | condition: function (R, f) { 121 | R.when(f.appCode && f.appCode === "MOBI4"); 122 | }, 123 | consequence: function (R, f) { 124 | console.log("Rule 7 matched - block payment for Mobile. Reject Payment."); 125 | f.result = false; 126 | R.stop(); 127 | }, 128 | }, 129 | /**** Rule 8 ****/ 130 | { 131 | name: "event risk score", 132 | priority: 2, 133 | on: true, 134 | condition: function (R, f) { 135 | R.when(f.eventRiskFactor && f.eventRiskFactor < 5); 136 | }, 137 | consequence: function (R, f) { 138 | console.log("Rule 8 matched - the event is not critical, so accept"); 139 | f.result = true; 140 | R.stop(); 141 | }, 142 | }, 143 | /**** Rule 9 ****/ 144 | { 145 | name: "block ip range set", 146 | priority: 3, 147 | on: true, 148 | condition: function (R, f) { 149 | var ipList = [ 150 | "10.X.X.X", 151 | "12.122.X.X", 152 | "12.211.X.X", 153 | "64.X.X.X", 154 | "64.23.X.X", 155 | "74.23.211.92", 156 | ]; 157 | var allowedRegexp = new RegExp( 158 | "^(?:" + 159 | ipList.join("|").replace(/\./g, "\\.").replace(/X/g, "[^.]+") + 160 | ")$" 161 | ); 162 | R.when(f.userIP && f.userIP.match(allowedRegexp)); 163 | }, 164 | consequence: function (R, f) { 165 | console.log( 166 | "Rule 9 matched - ip falls in the given list, then block. Rejecting payment." 167 | ); 168 | f.result = false; 169 | R.stop(); 170 | }, 171 | }, 172 | /**** Rule 10 ****/ 173 | { 174 | name: "check if user's name is blacklisted", 175 | priority: 1, 176 | on: true, 177 | condition: function (R, f) { 178 | var blacklist = ["user4"]; 179 | R.when(f && blacklist.indexOf(f.name) > -1); 180 | }, 181 | consequence: function (R, f) { 182 | console.log( 183 | "Rule 10 matched - the user is malicious, then block. Rejecting payment." 184 | ); 185 | f.result = false; 186 | R.stop(); 187 | }, 188 | }, 189 | ]; 190 | /** example of cash card user, so payment blocked. ****/ 191 | let user1 = { 192 | userIP: "10.3.4.5", 193 | name: "user1", 194 | eventRiskFactor: 6, 195 | userCredibility: 1, 196 | appCode: "WEB1", 197 | userLoggedIn: false, 198 | transactionTotal: 12000, 199 | cardType: "Cash Card", 200 | cardIssuer: "OXI", 201 | }; 202 | 203 | /** example of payment from blocked app, so payemnt blocked. ****/ 204 | let user2 = { 205 | userIP: "27.3.4.5", 206 | name: "user2", 207 | eventRiskFactor: 2, 208 | userCredibility: 2, 209 | appCode: "MOBI4", 210 | userLoggedIn: true, 211 | transactionTotal: 500, 212 | cardType: "Credit Card", 213 | cardIssuer: "VISA", 214 | }; 215 | 216 | /** example of low priority event, so skips frther checks. ****/ 217 | let user3 = { 218 | userIP: "27.3.4.5", 219 | name: "user3", 220 | eventRiskFactor: 2, 221 | userCredibility: 2, 222 | appCode: "WEB1", 223 | userLoggedIn: true, 224 | transactionTotal: 500, 225 | cardType: "Credit Card", 226 | cardIssuer: "VISA", 227 | }; 228 | 229 | /** malicious list of users in rule 10 matches and exists. ****/ 230 | let user4 = { 231 | userIP: "27.3.4.5", 232 | name: "user4", 233 | eventRiskFactor: 8, 234 | userCredibility: 2, 235 | appCode: "WEB1", 236 | userLoggedIn: true, 237 | transactionTotal: 500, 238 | cardType: "Credit Card", 239 | cardIssuer: "VISA", 240 | }; 241 | 242 | /** highly credible user exempted from further checks. ****/ 243 | let user5 = { 244 | userIP: "27.3.4.5", 245 | name: "user5", 246 | eventRiskFactor: 8, 247 | userCredibility: 8, 248 | appCode: "WEB1", 249 | userLoggedIn: true, 250 | transactionTotal: 500, 251 | cardType: "Credit Card", 252 | cardIssuer: "VISA", 253 | }; 254 | 255 | /** example of a user whose ip listed in malicious list. ****/ 256 | let user6 = { 257 | userIP: "10.3.4.5", 258 | name: "user6", 259 | eventRiskFactor: 8, 260 | userCredibility: 2, 261 | appCode: "WEB1", 262 | userLoggedIn: true, 263 | transactionTotal: 500, 264 | cardType: "Credit Card", 265 | cardIssuer: "VISA", 266 | }; 267 | 268 | /** example of a chaned up rule. will take two iterations. ****/ 269 | let user7 = { 270 | userIP: "27.3.4.5", 271 | name: "user7", 272 | eventRiskFactor: 2, 273 | userCredibility: 2, 274 | appCode: "WEB1", 275 | userLoggedIn: false, 276 | transactionTotal: 100000, 277 | cardType: "Credit Card", 278 | cardIssuer: "VISA", 279 | }; 280 | 281 | /** none of rule matches and fires exit clearance with accepted payment. ****/ 282 | let user8 = { 283 | userIP: "27.3.4.5", 284 | name: "user8", 285 | eventRiskFactor: 8, 286 | userCredibility: 2, 287 | appCode: "WEB1", 288 | userLoggedIn: true, 289 | transactionTotal: 500, 290 | cardType: "Credit Card", 291 | cardIssuer: "VISA", 292 | }; 293 | 294 | const R = new RuleEngine(rules); 295 | 296 | console.log(COLORS.yellow, "----------"); 297 | console.log(COLORS.yellow, "start execution of rules"); 298 | console.log(COLORS.yellow, "----------"); 299 | 300 | R.execute(user7, function (result) { 301 | if (result.result !== false) 302 | console.log(COLORS.green, "Completed", "User7 Accepted"); 303 | else console.log(COLORS.red, "Completed", "User7 Rejected"); 304 | }); 305 | R.execute(user1, function (result) { 306 | if (result.result !== false) 307 | console.log(COLORS.green, "Completed", "User1 Accepted"); 308 | else console.log(COLORS.red, "Completed", "User1 Rejected"); 309 | }); 310 | R.execute(user2, function (result) { 311 | if (result.result !== false) 312 | console.log(COLORS.green, "Completed", "User2 Accepted"); 313 | else console.log(COLORS.red, "Completed", "User2 Rejected"); 314 | }); 315 | R.execute(user3, function (result) { 316 | if (result.result !== false) 317 | console.log(COLORS.green, "Completed", "User3 Accepted"); 318 | else console.log(COLORS.red, "Completed", "User3 Rejected"); 319 | }); 320 | R.execute(user4, function (result) { 321 | if (result.result !== false) 322 | console.log(COLORS.green, "Completed", "User4 Accepted"); 323 | else console.log(COLORS.red, "Completed", "User4 Rejected"); 324 | }); 325 | R.execute(user5, function (result) { 326 | if (result.result !== false) 327 | console.log(COLORS.green, "Completed", "User5 Accepted"); 328 | else console.log(COLORS.red, "Completed", "User5 Rejected"); 329 | }); 330 | R.execute(user6, function (result) { 331 | if (result.result !== false) 332 | console.log(COLORS.green, "Completed", "User6 Accepted"); 333 | else console.log(COLORS.red, "Completed", "User6 Rejected"); 334 | }); 335 | R.execute(user8, function (result) { 336 | if (result.result !== false) 337 | console.log(COLORS.green, "Completed", "User8 Accepted"); 338 | else console.log(COLORS.red, "Completed", "User8 Rejected"); 339 | }); 340 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { RuleEngine } from "../lib/index"; 2 | import { API, Fact } from "../lib/types"; 3 | 4 | describe("Rules", function () { 5 | describe(".init()", function () { 6 | it("should empty the existing rule array", function () { 7 | var rules = [ 8 | { 9 | condition: function (R: API) { 10 | R.when(1); 11 | }, 12 | consequence: function (R: API) { 13 | R.stop(); 14 | }, 15 | }, 16 | ]; 17 | var R = new RuleEngine(rules); 18 | R.init(); 19 | expect(R.rules).toEqual([]); 20 | }); 21 | }); 22 | describe(".register()", function () { 23 | it("Rule should be turned on if the field - ON is absent in the rule", function () { 24 | var rules = [ 25 | { 26 | condition: function (R: API) { 27 | R.when(1); 28 | }, 29 | consequence: function (R: API) { 30 | R.stop(); 31 | }, 32 | }, 33 | ]; 34 | var R = new RuleEngine(rules); 35 | expect(R.rules[0].on).toEqual(true); 36 | }); 37 | it("Rule can be passed to register as both arrays and individual objects", function () { 38 | var rule = { 39 | condition: function (R: API) { 40 | R.when(1); 41 | }, 42 | consequence: function (R: API) { 43 | R.stop(); 44 | }, 45 | }; 46 | var R1 = new RuleEngine(rule); 47 | var R2 = new RuleEngine([rule]); 48 | expect(R1.rules).toEqual(R2.rules); 49 | }); 50 | it("Rules can be appended multiple times via register after creating rule engine instance", function () { 51 | var rules = [ 52 | { 53 | condition: function (R: API) { 54 | R.when(1); 55 | }, 56 | consequence: function (R: API) { 57 | R.stop(); 58 | }, 59 | }, 60 | { 61 | condition: function (R: API) { 62 | R.when(0); 63 | }, 64 | consequence: function (R: API) { 65 | R.stop(); 66 | }, 67 | }, 68 | ]; 69 | var R1 = new RuleEngine(rules); 70 | var R2 = new RuleEngine(rules[0]); 71 | var R3 = new RuleEngine(); 72 | R2.register(rules[1]); 73 | expect(R1.rules).toEqual(R2.rules); 74 | R3.register(rules); 75 | expect(R1.rules).toEqual(R3.rules); 76 | }); 77 | }); 78 | describe(".sync()", function () { 79 | it("should only push active rules into active rules array", function () { 80 | var rules = [ 81 | { 82 | condition: function (R: API) { 83 | R.when(1); 84 | }, 85 | consequence: function (R: API) { 86 | R.stop(); 87 | }, 88 | id: "one", 89 | on: true, 90 | }, 91 | { 92 | condition: function (R: API) { 93 | R.when(0); 94 | }, 95 | consequence: function (R: API) { 96 | R.stop(); 97 | }, 98 | id: "one", 99 | on: false, 100 | }, 101 | ]; 102 | var R = new RuleEngine(); 103 | R.register(rules); 104 | expect(R.activeRules).not.toEqual(R.rules); 105 | }); 106 | it("should sort the rules accroding to priority, if priority is present", function () { 107 | var rules = [ 108 | { 109 | priority: 8, 110 | index: 1, 111 | condition: function (R: API) { 112 | R.when(1); 113 | }, 114 | consequence: function (R: API) { 115 | R.stop(); 116 | }, 117 | }, 118 | { 119 | priority: 6, 120 | index: 2, 121 | condition: function (R: API) { 122 | R.when(1); 123 | }, 124 | consequence: function (R: API) { 125 | R.stop(); 126 | }, 127 | }, 128 | { 129 | priority: 9, 130 | index: 0, 131 | condition: function (R: API) { 132 | R.when(1); 133 | }, 134 | consequence: function (R: API) { 135 | R.stop(); 136 | }, 137 | }, 138 | ]; 139 | var R = new RuleEngine(); 140 | R.register(rules); 141 | expect(R.activeRules[2].index).toEqual(2); 142 | }); 143 | }); 144 | describe(".exec()", function () { 145 | it("should run consequnce when condition matches", function () { 146 | var rule = { 147 | condition: function (R: API, f: Fact) { 148 | R.when(f.transactionTotal < 500); 149 | }, 150 | consequence: function (R: API, f: Fact) { 151 | f.result = false; 152 | R.stop(); 153 | }, 154 | }; 155 | var R = new RuleEngine(rule); 156 | R.execute( 157 | { 158 | transactionTotal: 200, 159 | }, 160 | function (result) { 161 | expect(result.result).toEqual(false); 162 | } 163 | ); 164 | }); 165 | it("should chain rules and find result with next()", function () { 166 | var rule = [ 167 | { 168 | condition: function (R: API, f: Fact) { 169 | R.when(f.card == "VISA"); 170 | }, 171 | consequence: function (R: API, f: Fact) { 172 | R.stop(); 173 | f.result = "Custom Result"; 174 | }, 175 | priority: 4, 176 | }, 177 | { 178 | condition: function (R: API, f: Fact) { 179 | R.when(f.transactionTotal < 1000); 180 | }, 181 | consequence: function (R: API, f: Fact) { 182 | R.next(); 183 | }, 184 | priority: 8, 185 | }, 186 | ]; 187 | var R = new RuleEngine(rule); 188 | R.execute( 189 | { 190 | transactionTotal: 200, 191 | card: "VISA", 192 | }, 193 | function (result) { 194 | expect(result.result).toEqual("Custom Result"); 195 | } 196 | ); 197 | }); 198 | it("should provide access to rule definition properties via rule()", function () { 199 | var rule = { 200 | name: "sample rule name", 201 | id: "xyzzy", 202 | condition: function (R: API, f: Fact) { 203 | R.when(f.input === true); 204 | }, 205 | consequence: function (R: API, f: Fact) { 206 | f.result = true; 207 | f.ruleName = R.rule().name; 208 | f.ruleID = R.rule().id; 209 | R.stop(); 210 | }, 211 | }; 212 | var R = new RuleEngine(rule); 213 | R.execute( 214 | { 215 | input: true, 216 | }, 217 | function (result) { 218 | expect(result.ruleName).toEqual(rule.name); 219 | expect(result.ruleID).toEqual(rule.id); 220 | } 221 | ); 222 | }); 223 | it("should include the matched rule path", function () { 224 | var rules = [ 225 | { 226 | name: "rule A", 227 | condition: function (R: API, f: Fact) { 228 | R.when(f.x === true); 229 | }, 230 | consequence: function (R: API) { 231 | R.next(); 232 | }, 233 | }, 234 | { 235 | name: "rule B", 236 | condition: function (R: API, f: Fact) { 237 | R.when(f.y === true); 238 | }, 239 | consequence: function (R: API) { 240 | R.next(); 241 | }, 242 | }, 243 | { 244 | id: "rule C", 245 | condition: function (R: API, f: Fact) { 246 | R.when(f.x === true && f.y === false); 247 | }, 248 | consequence: function (R: API) { 249 | R.next(); 250 | }, 251 | }, 252 | { 253 | id: "rule D", 254 | condition: function (R: API, f: Fact) { 255 | R.when(f.x === false && f.y === false); 256 | }, 257 | consequence: function (R: API) { 258 | R.next(); 259 | }, 260 | }, 261 | { 262 | condition: function (R: API, f: Fact) { 263 | R.when(f.x === true && f.y === false); 264 | }, 265 | consequence: function (R: API) { 266 | R.next(); 267 | }, 268 | }, 269 | ]; 270 | var lastMatch = "index_" + (rules.length - 1).toString(); 271 | var R = new RuleEngine(rules); 272 | R.execute( 273 | { 274 | x: true, 275 | y: false, 276 | }, 277 | function (result) { 278 | expect(result.matchPath).toEqual([ 279 | rules[0].name, 280 | rules[2].id, 281 | lastMatch, 282 | ]); 283 | } 284 | ); 285 | }); 286 | 287 | it("should support fact as optional second parameter for es6 compatibility", function () { 288 | var rule = { 289 | condition: (R: API, f: Fact) => { 290 | R.when(f.transactionTotal < 500); 291 | }, 292 | consequence: (R: API, f: Fact) => { 293 | f.result = false; 294 | R.stop(); 295 | }, 296 | }; 297 | var R = new RuleEngine(rule); 298 | R.execute( 299 | { 300 | transactionTotal: 200, 301 | }, 302 | function (result) { 303 | expect(result.result).toEqual(false); 304 | } 305 | ); 306 | }); 307 | 308 | it("should work even when process.NextTick is unavailable", function () { 309 | // @ts-expect-error 310 | process.nextTick = undefined; 311 | 312 | var rule = { 313 | condition: function (R: API, f: Fact) { 314 | R.when(f.transactionTotal < 500); 315 | }, 316 | consequence: function (R: API, f: Fact) { 317 | f.result = false; 318 | R.stop(); 319 | }, 320 | }; 321 | var R = new RuleEngine(rule); 322 | R.execute( 323 | { 324 | transactionTotal: 200, 325 | }, 326 | function (result) { 327 | expect(result.result).toEqual(false); 328 | } 329 | ); 330 | }); 331 | }); 332 | describe(".findRules()", function () { 333 | var rules = [ 334 | { 335 | condition: function (R: API) { 336 | R.when(1); 337 | }, 338 | consequence: function (R: API) { 339 | R.stop(); 340 | }, 341 | id: "one", 342 | }, 343 | { 344 | condition: function (R: API) { 345 | R.when(0); 346 | }, 347 | consequence: function (R: API) { 348 | R.stop(); 349 | }, 350 | id: "two", 351 | }, 352 | ]; 353 | var R = new RuleEngine(rules); 354 | it("find selector function for rules should exact number of matches", function () { 355 | expect( 356 | R.findRules({ 357 | id: "one", 358 | }).length 359 | ).toEqual(1); 360 | }); 361 | it("find selector function for rules should give the correct match as result", function () { 362 | expect( 363 | R.findRules({ 364 | id: "one", 365 | })[0].id 366 | ).toEqual("one"); 367 | }); 368 | it("find selector function should filter off undefined entries in the query if any", function () { 369 | expect( 370 | R.findRules({ 371 | id: "one", 372 | myMistake: undefined, 373 | })[0].id 374 | ).toEqual("one"); 375 | }); 376 | it("find without condition works fine", function () { 377 | expect(R.findRules().length).toEqual(2); 378 | }); 379 | }); 380 | describe(".turn()", function () { 381 | var rules = [ 382 | { 383 | condition: function (R: API) { 384 | R.when(1); 385 | }, 386 | consequence: function (R: API) { 387 | R.stop(); 388 | }, 389 | id: "one", 390 | }, 391 | { 392 | condition: function (R: API) { 393 | R.when(0); 394 | }, 395 | consequence: function (R: API) { 396 | R.stop(); 397 | }, 398 | id: "two", 399 | on: false, 400 | }, 401 | ]; 402 | var R = new RuleEngine(rules); 403 | it("checking whether turn off rules work as expected", function () { 404 | R.turn("OFF", { 405 | id: "one", 406 | }); 407 | expect( 408 | R.findRules({ 409 | id: "one", 410 | })[0].on 411 | ).toEqual(false); 412 | }); 413 | it("checking whether turn on rules work as expected", function () { 414 | R.turn("ON", { 415 | id: "two", 416 | }); 417 | expect( 418 | R.findRules({ 419 | id: "two", 420 | })[0].on 421 | ).toEqual(true); 422 | }); 423 | }); 424 | describe(".prioritize()", function () { 425 | var rules = [ 426 | { 427 | condition: function (R: API) { 428 | R.when(1); 429 | }, 430 | consequence: function (R: API) { 431 | R.stop(); 432 | }, 433 | id: "two", 434 | priority: 1, 435 | }, 436 | { 437 | condition: function (R: API) { 438 | R.when(0); 439 | }, 440 | consequence: function (R: API) { 441 | R.stop(); 442 | }, 443 | id: "zero", 444 | priority: 8, 445 | }, 446 | { 447 | condition: function (R: API) { 448 | R.when(0); 449 | }, 450 | consequence: function (R: API) { 451 | R.stop(); 452 | }, 453 | id: "one", 454 | priority: 4, 455 | }, 456 | ]; 457 | var R = new RuleEngine(rules); 458 | it("checking whether prioritize work", function () { 459 | R.prioritize(10, { 460 | id: "one", 461 | }); 462 | expect( 463 | R.findRules({ 464 | id: "one", 465 | })[0].priority 466 | ).toEqual(10); 467 | }); 468 | it("checking whether rules reorder after prioritize", function () { 469 | R.prioritize(10, { 470 | id: "one", 471 | }); 472 | expect(R.activeRules[0].id).toEqual("one"); 473 | }); 474 | }); 475 | describe("ignoreFactChanges", function () { 476 | var rules = [ 477 | { 478 | name: "rule1", 479 | condition: function (R: API, f: Fact) { 480 | R.when(f.value1 > 5); 481 | }, 482 | consequence: function (R: API, f: Fact) { 483 | f.result = false; 484 | f.errors = f.errors || []; 485 | f.errors.push("must be less than 5"); 486 | R.next(); 487 | }, 488 | }, 489 | ]; 490 | 491 | var fact = { 492 | value1: 6, 493 | }; 494 | 495 | it("doesn't rerun when a fact changes if ignoreFactChanges is true", function (done) { 496 | var R = new RuleEngine(rules, { ignoreFactChanges: true }); 497 | 498 | R.execute(fact, function (result) { 499 | expect(result.errors).toHaveLength(1); 500 | done(); 501 | }); 502 | }); 503 | }); 504 | describe("test Parallelism", function () { 505 | var rules = [ 506 | { 507 | name: "high credibility customer - avoid checks and bypass", 508 | priority: 4, 509 | on: true, 510 | condition: function (R: API, f: Fact) { 511 | R.when(f.userCredibility && f.userCredibility > 5); 512 | }, 513 | consequence: function (R: API, f: Fact) { 514 | f.result = true; 515 | R.stop(); 516 | }, 517 | }, 518 | { 519 | name: "block guest payment above 10000", 520 | priority: 3, 521 | condition: function (R: API, f: Fact) { 522 | R.when( 523 | f.customerType && 524 | f.transactionTotal > 10000 && 525 | f.customerType == "guest" 526 | ); 527 | }, 528 | consequence: function (R: API, f: Fact) { 529 | f.result = false; 530 | R.stop(); 531 | }, 532 | }, 533 | { 534 | name: "is customer guest?", 535 | priority: 2, 536 | condition: function (R: API, f: Fact) { 537 | R.when(!f.userLoggedIn); 538 | }, 539 | consequence: function (R: API, f: Fact) { 540 | f.customerType = "guest"; 541 | // the fact has been altered above, so all rules will run again since ignoreFactChanges is not set. 542 | R.next(); 543 | }, 544 | }, 545 | { 546 | name: "block Cashcard Payment", 547 | priority: 1, 548 | condition: function (R: API, f: Fact) { 549 | R.when(f.cardType == "Cash Card"); 550 | }, 551 | consequence: function (R: API, f: Fact) { 552 | f.result = false; 553 | R.stop(); 554 | }, 555 | }, 556 | ]; 557 | 558 | var straightFact = { 559 | name: "straightFact", 560 | userCredibility: 1, 561 | userLoggedIn: true, 562 | transactionTotal: 12000, 563 | cardType: "Cash Card", 564 | }; 565 | 566 | /** example of a chaned up rule. will take two iterations. ****/ 567 | var chainedFact = { 568 | name: "chainedFact", 569 | userCredibility: 2, 570 | userLoggedIn: false, 571 | transactionTotal: 100000, 572 | cardType: "Credit Card", 573 | }; 574 | 575 | it("context switches and finishes the fact which needs least iteration first", function (done) { 576 | var R = new RuleEngine(rules); 577 | var isStraightFactFast = false; 578 | 579 | R.execute(chainedFact, function (result) { 580 | expect(isStraightFactFast).toBe(true); 581 | done(); 582 | }); 583 | 584 | R.execute(straightFact, function (result) { 585 | isStraightFactFast = true; 586 | }); 587 | }); 588 | }); 589 | }); 590 | -------------------------------------------------------------------------------- /dist/node-rules.min.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).NodeRules=t.NodeRules||{})}(this,(function(t){"use strict";var e="undefined"!=typeof global?global:"undefined"!=typeof self?self:"undefined"!=typeof window?window:{};function r(){throw new Error("setTimeout has not been defined")}function n(){throw new Error("clearTimeout has not been defined")}var o=r,i=n;function u(t){if(o===setTimeout)return setTimeout(t,0);if((o===r||!o)&&setTimeout)return o=setTimeout,setTimeout(t,0);try{return o(t,0)}catch(e){try{return o.call(null,t,0)}catch(e){return o.call(this,t,0)}}}"function"==typeof e.setTimeout&&(o=setTimeout),"function"==typeof e.clearTimeout&&(i=clearTimeout);var a,c=[],s=!1,f=-1;function l(){s&&a&&(s=!1,a.length?c=a.concat(c):f=-1,c.length&&h())}function h(){if(!s){var t=u(l);s=!0;for(var e=c.length;e;){for(a=c,c=[];++f1)for(var r=1;r-1},kt.prototype.set=function(t,e){var r=this.__data__,n=Ft(r,t);return n<0?r.push([t,e]):r[n][1]=e,this},St.prototype.clear=function(){this.__data__={hash:new zt,map:new(vt||kt),string:new zt}},St.prototype.delete=function(t){return Ut(this,t).delete(t)},St.prototype.get=function(t){return Ut(this,t).get(t)},St.prototype.has=function(t){return Ut(this,t).has(t)},St.prototype.set=function(t,e){return Ut(this,t).set(t,e),this},Et.prototype.clear=function(){this.__data__=new kt},Et.prototype.delete=function(t){return this.__data__.delete(t)},Et.prototype.get=function(t){return this.__data__.get(t)},Et.prototype.has=function(t){return this.__data__.has(t)},Et.prototype.set=function(t,e){var n=this.__data__;if(n instanceof kt){var o=n.__data__;if(!vt||o.length-1&&t%1==0&&t-1&&t%1==0&&t<=o}(t.length)&&!Kt(t)}var Jt=ht||function(){return!1};function Kt(t){var e=Qt(t)?rt.call(t):"";return e==c||e==s}function Qt(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function Xt(t){return Ht(t)?Rt(t):function(t){if(!Vt(t))return pt(t);var e=[];for(var r in Object(t))et.call(t,r)&&"constructor"!=r&&e.push(r);return e}(t)}t.exports=function(t){return $t(t,!0,!0)}}({get exports(){return S},set exports(t){S=t}},S);var E={};function R(t){return t&&t.__esModule?t:{default:t}}!function(t,e){var r=200,n="__lodash_hash_undefined__",o=1,i=2,u=9007199254740991,a="[object Arguments]",c="[object Array]",s="[object AsyncFunction]",f="[object Boolean]",l="[object Date]",h="[object Error]",p="[object Function]",_="[object GeneratorFunction]",v="[object Map]",y="[object Number]",d="[object Null]",b="[object Object]",g="[object Promise]",j="[object Proxy]",w="[object RegExp]",m="[object Set]",O="[object String]",A="[object Symbol]",T="[object Undefined]",x="[object WeakMap]",k="[object ArrayBuffer]",S="[object DataView]",E=/^\[object .+?Constructor\]$/,R=/^(?:0|[1-9]\d*)$/,P={};P["[object Float32Array]"]=P["[object Float64Array]"]=P["[object Int8Array]"]=P["[object Int16Array]"]=P["[object Int32Array]"]=P["[object Uint8Array]"]=P["[object Uint8ClampedArray]"]=P["[object Uint16Array]"]=P["[object Uint32Array]"]=!0,P[a]=P[c]=P[k]=P[f]=P[S]=P[l]=P[h]=P[p]=P[v]=P[y]=P[b]=P[w]=P[m]=P[O]=P[x]=!1;var F="object"==typeof z&&z&&z.Object===Object&&z,$="object"==typeof self&&self&&self.Object===Object&&self,M=F||$||Function("return this")(),I=e&&!e.nodeType&&e,L=I&&t&&!t.nodeType&&t,U=L&&L.exports===I,B=U&&F.process,C=function(){try{return B&&B.binding&&B.binding("util")}catch(t){}}(),D=C&&C.isTypedArray;function N(t,e){for(var r=-1,n=null==t?0:t.length;++rs))return!1;var l=a.get(t);if(l&&a.get(e))return l==e;var h=-1,p=!0,_=r&i?new kt:void 0;for(a.set(t,e),a.set(e,t);++h-1},xt.prototype.set=function(t,e){var r=this.__data__,n=Rt(r,t);return n<0?(++this.size,r.push([t,e])):r[n][1]=e,this},zt.prototype.clear=function(){this.size=0,this.__data__={hash:new Tt,map:new(pt||xt),string:new Tt}},zt.prototype.delete=function(t){var e=Bt(this,t).delete(t);return this.size-=e?1:0,e},zt.prototype.get=function(t){return Bt(this,t).get(t)},zt.prototype.has=function(t){return Bt(this,t).has(t)},zt.prototype.set=function(t,e){var r=Bt(this,t),n=r.size;return r.set(t,e),this.size+=r.size==n?0:1,this},kt.prototype.add=kt.prototype.push=function(t){return this.__data__.set(t,n),this},kt.prototype.has=function(t){return this.__data__.has(t)},St.prototype.clear=function(){this.__data__=new xt,this.size=0},St.prototype.delete=function(t){var e=this.__data__,r=e.delete(t);return this.size=e.size,r},St.prototype.get=function(t){return this.__data__.get(t)},St.prototype.has=function(t){return this.__data__.has(t)},St.prototype.set=function(t,e){var n=this.__data__;if(n instanceof xt){var o=n.__data__;if(!pt||o.length-1&&t%1==0&&t-1&&t%1==0&&t<=u}function Xt(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}function Yt(t){return null!=t&&"object"==typeof t}var Zt=D?function(t){return function(e){return t(e)}}(D):function(t){return Yt(t)&&Qt(t.length)&&!!P[Pt(t)]};function te(t){return null!=(e=t)&&Qt(e.length)&&!Kt(e)?Et(t):It(t);var e}t.exports=function(t,e){return $t(t,e)}}({get exports(){return E},set exports(t){E=t}},E),Object.defineProperty(k,"__esModule",{value:!0});var P=Object.defineProperty,F=(t,e,r)=>(((t,e,r)=>{e in t?P(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r})(t,"symbol"!=typeof e?e+"":e,r),r),$=R(S),M=R(E),I=k.RuleEngine=class{constructor(t,e){F(this,"rules",[]),F(this,"activeRules",[]),F(this,"ignoreFactChanges",!1),t&&this.register(t),e&&(this.ignoreFactChanges=e.ignoreFactChanges||!1)}init(){this.rules=[],this.activeRules=[]}register(t){Array.isArray(t)?this.rules.push(...t):null!==t&&"object"==typeof t&&this.rules.push(t),this.sync()}sync(){this.activeRules=this.rules.filter((t=>{if(void 0===t.on&&(t.on=!0),!0===t.on)return t})),this.activeRules.sort(((t,e)=>t.priority&&e.priority?e.priority-t.priority:0))}execute(t,e){const r=this;let n=!1;const o=$.default.call(void 0,t);let i=$.default.call(void 0,t),u=this.activeRules;const a=[],c=this.ignoreFactChanges;!function t(s){const f={rule:()=>u[s],when:t=>{if(t){const t=u[s].consequence;t.ruleRef=u[s].id||u[s].name||`index_${s}`,r.nextTick((()=>{a.push(t.ruleRef),t.call(o,f,o)}))}else r.nextTick((()=>{f.next()}))},restart:()=>t(0),stop:()=>(n=!0,t(0)),next:()=>{c||M.default.call(void 0,i,o)?r.nextTick((()=>t(s+1))):(i=$.default.call(void 0,o),r.nextTick((()=>{f.restart()})))}};if(u=r.activeRules,s{o.matchPath=a,e(o)}))}(0)}nextTick(t){(null==x?void 0:p)?null==x||p(t):setTimeout(t,0)}findRules(t){return void 0===t?this.rules:(Object.keys(t).forEach((e=>void 0===t[e]&&delete t[e])),this.rules.filter((e=>Object.keys(t).some((r=>t[r]===e[r])))))}turn(t,e){const r=this.findRules(e);for(let e=0,n=r.length;e