Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
vamsee committed Oct 6, 2020
2 parents 3d4670a + 8b70d81 commit e47b191
Show file tree
Hide file tree
Showing 18 changed files with 1,183 additions and 29 deletions.
116 changes: 113 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## Introduction

This oe-cloud module implements the business rule functionality. The most common way to represent a rule is via **decision tables**.
We additionally allow rules to be represented hierarchially via a concept called **decision graphs and services**. Both are accessed
through different APIs.
This oe-cloud module implements the business rule functionality. The most common way to represent a rule is via **decision tables**.
We additionally allow rules to be represented hierarchially via a concept called **decision graphs, decision services and decision tree**. All are accessed
through different APIs.

This module extensively makes use of js-feel business rule engine.

Expand All @@ -19,6 +19,8 @@ oe-cloud module on its own. Alternatively, you can work with excel files for rep
they will not give you the visual experience required, and, it becomes tedious to understand the relationship between various
decision elements. However, decision graphs and services provide for a powerful and complex rule representations.

There is a way to execute a chain of decision rules via a concept called **decision tree**. Decision Tree uses the tree representation to solve the problem in which each node is either a *decision table* or *decision gate*. Based on the branch conditions, rules are executed. This can be invoked through `DecisionTree.exec` remote method.

## How are rules actually implemented?

We currently make use of js-feel javascript DMN based rule engine to execute decisions.
Expand Down Expand Up @@ -93,6 +95,114 @@ with the payload and the name of the rule
#### Example


### 3. Decision Tree

To insert a decision tree, we post JSON as follows to the **DecisionTree** model.

```
[
{
"name": "<TreeName>",
"nodes": "<Array_of_objects_comprising_of_DecisionTables_and_DecisionGates>",
"connections": "<Array_of_objects_showing_connections_between_different_nodes>"
}
]
```
> Note: If done via HTTP, we do a POST to `/api/DecisionTrees/`
To execute this, we call the `DecisionTree.exec` remote method or make a POST to `/api/DecisionTrees/exec`
with the payload and the name of the tree.

#### Example

Post below data to `/api/DecisionTrees/`

````
[
{
"name": "TestTree",
"nodes": [
{
"id": "n-rkljn4byr",
"name": "UserLocation",
"nodeType": "DECISION_TABLE",
"x": 574,
"y": 8,
"data": {},
"skipFeel": true
},
{
"id": "n-fub5yhz6f",
"name": "Decision Gate 2",
"nodeType": "DECISION_GATE",
"x": 574,
"y": 168,
"data": {},
"skipFeel": true
},
{
"id": "n-rze21y6nh",
"name": "eligibility_USA",
"nodeType": "DECISION_TABLE",
"x": 327,
"y": 425,
"data": {},
"skipFeel": true
},
{
"id": "n-rbnxnuxst",
"name": "eligibility_FR",
"nodeType": "DECISION_TABLE",
"x": 856,
"y": 432,
"data": {},
"skipFeel": true
}
],
"connections": [
{
"from": "n-rkljn4byr",
"to": "n-fub5yhz6f",
"id": "c-1ghbvmu02",
"condition": ""
},
{
"from": "n-fub5yhz6f",
"to": "n-rze21y6nh",
"id": "c-ebty0cye",
"condition": "location === \"US\""
},
{
"from": "n-fub5yhz6f",
"to": "n-rbnxnuxst",
"id": "c-appxa5hi2u",
"condition": "location == \"FR\""
}
]
}
]
````

Graphical representation of above posted model

![decision-tree-graphical-representation](./test/test-data/decision-tree-example.png)

To execute this, we call the `DecisionTree.exec` remote method or make a POST to `/api/DecisionTrees/exec`
with the payload and the name of the tree.

Below data can be used as payload to execute the decision tree and `TestTree` can be used as a name for the above posted data.

````
{
"userName":"user1",
"amount": 3000,
"type":"PERSONAL_LOAN",
"experience" : 5 ,
"monthlyIncome":1000
}
````


## Towards standardization

Most of the work we have done above, while not fully standards compliant, have worked for us. But we do want to move towards
Expand Down
5 changes: 5 additions & 0 deletions common/models/decision-graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,11 @@ module.exports = function (DecisionGraph) {
accepts: [{
arg: 'inputData', type: 'object', http: { source: 'body' },
required: true, description: 'The JSON containing the graph node data to validate'
},
{
arg: 'options',
type: 'object',
http: 'optionsFromRequest'
}
],
http: {
Expand Down
8 changes: 7 additions & 1 deletion common/models/decision-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ module.exports = function (DecisionService) {
DecisionService.findOne({ where: { name: name } }, options, (err, result) => {
if (err) {
cb(err);
} else {
} else if (result) {
var decisions = result.decisions;
result['decision-graph'](options, (err, graph) => {
if (err) {
Expand Down Expand Up @@ -130,6 +130,12 @@ module.exports = function (DecisionService) {
.catch(cb);
}
});
} else {
var err1 = new Error(
'No Service found for ServiceName ' + name
);
err1.retriable = false;
cb(err1);
}
});
};
Expand Down
87 changes: 84 additions & 3 deletions common/models/decision-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ var assert = require('assert');
// var loopback = require('loopback');
var logger = require('oe-logger');
var log = logger('decision-table');
var { generateExcelBuffer } = require('../../lib/excel-helper');
var prefix = 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,';

// var getError = require('oe-cloud/lib/error-utils').getValidationError;
var delimiter = '&SP';

Expand Down Expand Up @@ -42,7 +45,7 @@ module.exports = function decisionTableFn(decisionTable) {
log.error(ctx.options, 'Error - Unable to process decision table data -', err);
next(new Error('Decision table data provided could not be parsed, please provide proper data'));
}
} else if ( 'decisionRules' in data) {
} else if ('decisionRules' in data) {
next();
} else {
next(new Error('Data being posted is incorrect. Either an excel file was expected for property - documentData, or, a parsed decision table object was expected for property - decisionRules'));
Expand Down Expand Up @@ -168,7 +171,7 @@ module.exports = function decisionTableFn(decisionTable) {
options = options || {};
cb = cb || utils.createPromiseCallback();

decisionTable.findOne({ where: { id: recordId }}, options, function dtFineOneCb(err, result) {
decisionTable.findOne({ where: { id: recordId } }, options, function dtFineOneCb(err, result) {
if (err) {
return cb(err);
}
Expand All @@ -179,6 +182,30 @@ module.exports = function decisionTableFn(decisionTable) {
});
};

const getFormattedValue = str => str.replace(/\"{2,}/g, '\"').replace(/^\"|\"$/g, '');

function parseContext(commaSepString) {
const csv = commaSepString.split('\n');
let context = {};
let i = 1;

for (; i < csv.length; i += 1) {
const arr = csv[i].split(delimiter).filter(String);
if (arr.length > 0 && arr[0] === 'RuleTable') {
break;
} else if (arr.length > 0) {
const count = arr[1].split('"').length - 1;
if (count > 0) {
arr[1] = getFormattedValue(arr[1]);
}
context[arr[0]] = arr[1];
}
}
// context = Object.keys(context).length > 0 ? JSON.stringify(context).replace(/"/g, '').replace(/\\/g, '"') : '';
// return context.length > 0 ? context : null;
return context;
}

decisionTable.remoteMethod('document', {
description: 'retrieve the excel file document of the corresponding decision (as base64 only)',
accessType: 'READ',
Expand All @@ -191,6 +218,11 @@ module.exports = function decisionTableFn(decisionTable) {
source: 'path'
},
description: 'record id of the corresponding decision or rule name'
},
{
arg: 'options',
type: 'object',
http: 'optionsFromRequest'
}
],
http: {
Expand All @@ -211,6 +243,11 @@ module.exports = function decisionTableFn(decisionTable) {
accepts: [{
arg: 'inputData', type: 'object', http: { source: 'body' },
required: true, description: 'The JSON containing the document data to parse'
},
{
arg: 'options',
type: 'object',
http: 'optionsFromRequest'
}
],
http: {
Expand Down Expand Up @@ -247,6 +284,50 @@ module.exports = function decisionTableFn(decisionTable) {
var sheet = workbook.Sheets[workbook.SheetNames[0]];
var csv = XLSX.utils.sheet_to_csv(sheet, { FS: delimiter });
var decisionRules = dTable.csv_to_decision_table(csv);
cb(null, decisionRules);
var contextObj = parseContext(csv);
cb(null, { decisionRules, contextObj });
};


// remote method declaration for getExcel
decisionTable.remoteMethod('getExcel', {
description: 'Generates an excel file response, given a json description for a decision table from the rule designer',
accessType: 'WRITE',
isStatic: true,
accepts: [
{
arg: 'dtJson',
type: 'object',
http: { source: 'body' },
required: true,
description: 'The JSON containing the decision table description from rule designer'
}
],
http: {
verb: 'POST',
path: '/getExcel'
},
returns: [
{
type: 'string',
root: true,
arg: 'body',
description: 'base64 encoded string which encodes the generated excel file'
}
]
});

decisionTable.getExcel = function (dtJson, options, cb) {
if (typeof cb === 'undefined' && typeof options === 'function') {
cb = options;
options = {};
}
try {
let buff = generateExcelBuffer(dtJson);
let base64Data = prefix + buff.toString('base64');
cb(null, base64Data);
} catch (error) {
cb(error);
}
};
};
6 changes: 5 additions & 1 deletion common/models/decision-table.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@
"documentName" : {
"type" : "string",
"description" : "Name of the file uploaded"
},
"dtDesignerData":{
"type":"object",
"description":"Decision Table JSON used by the rule-designer"
}
},
"oeValidations": [],
"validations": [],
"acls": [],
"methods": {},
"hidden": ["decisionRules", "documentData", "documentName"],
"hidden": ["documentData", "documentName"],
"mixins" : {
"VersionMixin" : true
}
Expand Down
Loading

0 comments on commit e47b191

Please sign in to comment.