json-rules-engine-simplified 0.1.17 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +578 -578
- package/dist/json-rules-engine-simplified.js +1 -1
- package/dist/json-rules-engine-simplified.js.map +1 -1
- package/package.json +101 -101
- package/CHANGELOG.md +0 -162
package/README.md
CHANGED
@@ -1,579 +1,579 @@
|
|
1
|
-
[](https://travis-ci.org/RxNT/json-rules-engine-simplified)
|
2
|
-
[](https://coveralls.io/github/RxNT/json-rules-engine-simplified?branch=master)
|
3
|
-
[](https://badge.fury.io/js/json-rules-engine-simplified)
|
4
|
-
# json-rules-engine-simplified
|
5
|
-
A simple rules engine expressed in JSON
|
6
|
-
|
7
|
-
The primary goal of this project is to be
|
8
|
-
an alternative of [json-rules-engine](https://github.com/CacheControl/json-rules-engine) for [react-jsonschema-form-conditionals](https://github.com/RxNT/react-jsonschema-form-conditionals),
|
9
|
-
as such it has similar interface and configuration, but simplified predicate language, similar to SQL.
|
10
|
-
|
11
|
-
## Features
|
12
|
-
|
13
|
-
- Optional schema and rules validation
|
14
|
-
- Basic boolean operations (`and` `or` and `not`) that allow to have any arbitrary complexity
|
15
|
-
- Rules expressed in simple, easy to read JSON
|
16
|
-
- Declarative conditional logic with [predicates](https://github.com/landau/predicate)
|
17
|
-
- Relevant conditional logic support
|
18
|
-
- Support of nested structures with [selectn](https://github.com/wilmoore/selectn.js)
|
19
|
-
including composite arrays
|
20
|
-
- Secure - no use of eval()
|
21
|
-
|
22
|
-
## Installation
|
23
|
-
|
24
|
-
Install `json-rules-engine-simplified` by running:
|
25
|
-
|
26
|
-
```bash
|
27
|
-
npm install --s json-rules-engine-simplified
|
28
|
-
```
|
29
|
-
|
30
|
-
## Usage
|
31
|
-
|
32
|
-
The simplest example of using `json-rules-engine-simplified`
|
33
|
-
|
34
|
-
```jsx
|
35
|
-
import Engine from 'json-rules-engine-simplified'
|
36
|
-
|
37
|
-
let rules = [{
|
38
|
-
conditions: {
|
39
|
-
firstName: "empty"
|
40
|
-
},
|
41
|
-
event: {
|
42
|
-
type: "remove",
|
43
|
-
params: {
|
44
|
-
field: "password"
|
45
|
-
},
|
46
|
-
}
|
47
|
-
}];
|
48
|
-
|
49
|
-
/**
|
50
|
-
* Setup a new engine
|
51
|
-
*/
|
52
|
-
let engine = new Engine(rules);
|
53
|
-
|
54
|
-
let formData = {
|
55
|
-
lastName: "Smit"
|
56
|
-
}
|
57
|
-
|
58
|
-
// Run the engine to evaluate
|
59
|
-
engine
|
60
|
-
.run(formData)
|
61
|
-
.then(events => { // run() returns remove event
|
62
|
-
events.map(event => console.log(event.type));
|
63
|
-
})
|
64
|
-
|
65
|
-
```
|
66
|
-
|
67
|
-
Rules engine expects to know all the rules in advance, it effectively drops builder pattern, but keeps the interface.
|
68
|
-
|
69
|
-
### Appending rule to existing engine
|
70
|
-
|
71
|
-
You don't have to specify all rules at the construction time, you can add rules in different time in process.
|
72
|
-
In order to add new rules to the `Engine` use `addRule` function.
|
73
|
-
|
74
|
-
For example, following declarations are the same
|
75
|
-
```js
|
76
|
-
import Engine from 'json-rules-engine-simplified';
|
77
|
-
|
78
|
-
let engineA = new Engine();
|
79
|
-
|
80
|
-
let rule = {
|
81
|
-
conditions: {
|
82
|
-
firstName: "empty"
|
83
|
-
},
|
84
|
-
event: {
|
85
|
-
type: "remove",
|
86
|
-
params: {
|
87
|
-
field: "password"
|
88
|
-
},
|
89
|
-
}
|
90
|
-
};
|
91
|
-
|
92
|
-
engineA.addRule(rule);
|
93
|
-
|
94
|
-
let engineB = new Engine(rule);
|
95
|
-
|
96
|
-
```
|
97
|
-
|
98
|
-
In this case `engineA` and `engineB` will give the same results.
|
99
|
-
|
100
|
-
## Validation
|
101
|
-
|
102
|
-
In order to prevent most common errors, `Engine` does initial validation on the schema, during construction.
|
103
|
-
Validation is done automatically if you specify `schema` during construction.
|
104
|
-
|
105
|
-
```js
|
106
|
-
let rules = [{
|
107
|
-
conditions: {
|
108
|
-
firstName: "empty"
|
109
|
-
},
|
110
|
-
event: {
|
111
|
-
type: "remove",
|
112
|
-
params: { field: "password" },
|
113
|
-
}
|
114
|
-
}];
|
115
|
-
|
116
|
-
let schema = {
|
117
|
-
properties: {
|
118
|
-
firstName: { type: "string" },
|
119
|
-
lastName: { type: "string" }
|
120
|
-
}
|
121
|
-
}
|
122
|
-
|
123
|
-
let engine = new Engine(rules, schema);
|
124
|
-
```
|
125
|
-
### Types of errors
|
126
|
-
|
127
|
-
- Conditions field validation (conditions use fields that are not part of the schema)
|
128
|
-
- Predicate validation (used predicates are not part of the
|
129
|
-
[predicates](https://github.com/landau/predicate) library and most likely wrong)
|
130
|
-
|
131
|
-
Validation is done only during development, validation is disabled by default in `production`.
|
132
|
-
|
133
|
-
WARNING!!! Currently validation does not support nested structures, so be extra careful, when using those.
|
134
|
-
|
135
|
-
## Conditional logic
|
136
|
-
|
137
|
-
Conditional logic is based on public [predicate](https://github.com/landau/predicate) library
|
138
|
-
with boolean logic extension.
|
139
|
-
|
140
|
-
[Predicate](https://github.com/landau/predicate) library has a lot of predicates that we found more, than sufficient for our use cases.
|
141
|
-
|
142
|
-
To showcase conditional logic, we'll be using simple `registration` schema
|
143
|
-
|
144
|
-
```js
|
145
|
-
let schema = {
|
146
|
-
definitions: {
|
147
|
-
hobby: {
|
148
|
-
type: "object",
|
149
|
-
properties: {
|
150
|
-
name: { type: "string" },
|
151
|
-
durationInMonth: { type: "integer" },
|
152
|
-
}
|
153
|
-
}
|
154
|
-
},
|
155
|
-
title: "A registration form",
|
156
|
-
description: "A simple form example.",
|
157
|
-
type: "object",
|
158
|
-
required: [
|
159
|
-
"firstName",
|
160
|
-
"lastName"
|
161
|
-
],
|
162
|
-
properties: {
|
163
|
-
firstName: {
|
164
|
-
type: "string",
|
165
|
-
title: "First name"
|
166
|
-
},
|
167
|
-
lastName: {
|
168
|
-
type: "string",
|
169
|
-
title: "Last name"
|
170
|
-
},
|
171
|
-
age: {
|
172
|
-
type: "integer",
|
173
|
-
title: "Age",
|
174
|
-
},
|
175
|
-
bio: {
|
176
|
-
type: "string",
|
177
|
-
title: "Bio",
|
178
|
-
},
|
179
|
-
country: {
|
180
|
-
type: "string",
|
181
|
-
title: "Country"
|
182
|
-
},
|
183
|
-
state: {
|
184
|
-
type: "string",
|
185
|
-
title: "State"
|
186
|
-
},
|
187
|
-
zip: {
|
188
|
-
type: "string",
|
189
|
-
title: "ZIP"
|
190
|
-
},
|
191
|
-
password: {
|
192
|
-
type: "string",
|
193
|
-
title: "Password",
|
194
|
-
minLength: 3
|
195
|
-
},
|
196
|
-
telephone: {
|
197
|
-
type: "string",
|
198
|
-
title: "Telephone",
|
199
|
-
minLength: 10
|
200
|
-
},
|
201
|
-
work: { "$ref": "#/definitions/hobby" },
|
202
|
-
hobbies: {
|
203
|
-
type: "array",
|
204
|
-
items: { "$ref": "#/definitions/hobby" }
|
205
|
-
}
|
206
|
-
}
|
207
|
-
}
|
208
|
-
```
|
209
|
-
Assuming action part is taken from [react-jsonschema-form-conditionals](https://github.com/RxNT/react-jsonschema-form-conditionals)
|
210
|
-
|
211
|
-
### Single line conditionals
|
212
|
-
|
213
|
-
Let's say we want to `remove` `password` , when `firstName` is missing, we can expressed it like this:
|
214
|
-
|
215
|
-
```js
|
216
|
-
let rules = [{
|
217
|
-
conditions: {
|
218
|
-
firstName: "empty"
|
219
|
-
},
|
220
|
-
event: {
|
221
|
-
type: "remove",
|
222
|
-
params: {
|
223
|
-
field: "password"
|
224
|
-
}
|
225
|
-
}
|
226
|
-
}]
|
227
|
-
```
|
228
|
-
|
229
|
-
This translates into -
|
230
|
-
when `firstName` is `empty`, trigger `remove` `event`.
|
231
|
-
|
232
|
-
`Empty` keyword is [equal in predicate library](https://landau.github.io/predicate/#equal) and required
|
233
|
-
event will be performed only when `predicate.empty(registration.firstName)` is `true`.
|
234
|
-
|
235
|
-
### Conditionals with arguments
|
236
|
-
|
237
|
-
Let's say we need to `require` `zip`, when `age` is `less` than `16`,
|
238
|
-
because the service we are using is legal only after `16` in some countries
|
239
|
-
|
240
|
-
```js
|
241
|
-
let rules = [{
|
242
|
-
conditions: {
|
243
|
-
age: { less : 16 }
|
244
|
-
},
|
245
|
-
event: {
|
246
|
-
type: "require",
|
247
|
-
params: {
|
248
|
-
field: "zip"
|
249
|
-
}
|
250
|
-
}
|
251
|
-
}]
|
252
|
-
```
|
253
|
-
|
254
|
-
This translates into - when `age` is `less` than `16`, `require` zip.
|
255
|
-
|
256
|
-
[Less](https://landau.github.io/predicate/#less) keyword is [less in predicate](https://landau.github.io/predicate/#less) and required
|
257
|
-
event will be returned only when `predicate.empty(registration.age, 5)` is `true`.
|
258
|
-
|
259
|
-
### Boolean operations on a single field
|
260
|
-
|
261
|
-
#### AND
|
262
|
-
|
263
|
-
For the field AND is a default behavior.
|
264
|
-
|
265
|
-
Looking at previous rule, we decide that we want to change the rule and `require` `zip`,
|
266
|
-
when `age` is between `16` and `70`, so it would be available
|
267
|
-
only to people older, than `16` and younger than `70`.
|
268
|
-
|
269
|
-
```js
|
270
|
-
let rules = [{
|
271
|
-
conditions: {
|
272
|
-
age: {
|
273
|
-
greater: 16,
|
274
|
-
less : 70,
|
275
|
-
}
|
276
|
-
},
|
277
|
-
event: {
|
278
|
-
type: "require",
|
279
|
-
params: {
|
280
|
-
field: "zip"
|
281
|
-
}
|
282
|
-
}
|
283
|
-
}]
|
284
|
-
```
|
285
|
-
|
286
|
-
By default action will be applied only when both field conditions are true.
|
287
|
-
In this case, when age is `greater` than `16` and `less` than `70`.
|
288
|
-
|
289
|
-
#### NOT
|
290
|
-
|
291
|
-
Let's say we want to change the logic to opposite, and trigger event only when
|
292
|
-
`age` is `less`er then `16` or `greater` than `70`,
|
293
|
-
|
294
|
-
```js
|
295
|
-
let rules = [{
|
296
|
-
conditions: {
|
297
|
-
age: {
|
298
|
-
not: {
|
299
|
-
greater: 16,
|
300
|
-
less : 70,
|
301
|
-
}
|
302
|
-
}
|
303
|
-
},
|
304
|
-
event: {
|
305
|
-
type: "require",
|
306
|
-
params: {
|
307
|
-
field: "zip"
|
308
|
-
}
|
309
|
-
}
|
310
|
-
}]
|
311
|
-
```
|
312
|
-
|
313
|
-
This does it, since the final result will be opposite of the previous condition.
|
314
|
-
|
315
|
-
#### OR
|
316
|
-
|
317
|
-
The previous example works, but it's a bit hard to understand, luckily we can express it differently
|
318
|
-
with `or` conditional.
|
319
|
-
|
320
|
-
```js
|
321
|
-
let rules = [{
|
322
|
-
conditions: { age: {
|
323
|
-
or: [
|
324
|
-
{ lessEq : 5 },
|
325
|
-
{ greaterEq: 70 }
|
326
|
-
]
|
327
|
-
}
|
328
|
-
},
|
329
|
-
event: {
|
330
|
-
type: "require",
|
331
|
-
params: {
|
332
|
-
field: "zip"
|
333
|
-
}
|
334
|
-
}
|
335
|
-
}]
|
336
|
-
```
|
337
|
-
|
338
|
-
The result is the same as `NOT`, but easier to grasp.
|
339
|
-
|
340
|
-
### Boolean operations on multi fields
|
341
|
-
|
342
|
-
To support cases, when action depends on more, than one field meeting criteria we introduced
|
343
|
-
multi fields boolean operations.
|
344
|
-
|
345
|
-
#### Default AND operation
|
346
|
-
|
347
|
-
Let's say, when `age` is less than 70 and `country` is `USA` we want to `require` `bio`.
|
348
|
-
|
349
|
-
```js
|
350
|
-
let rules = [{
|
351
|
-
conditions: {
|
352
|
-
age: { less : 70 },
|
353
|
-
country: { is: "USA" }
|
354
|
-
},
|
355
|
-
event: {
|
356
|
-
type: "require",
|
357
|
-
params: { fields: [ "bio" ]}
|
358
|
-
}
|
359
|
-
}]
|
360
|
-
```
|
361
|
-
|
362
|
-
This is the way we can express this. By default each field is treated as a
|
363
|
-
separate condition and all conditions must be meet.
|
364
|
-
|
365
|
-
#### OR
|
366
|
-
|
367
|
-
In addition to previous rule we need `bio`, if `state` is `NY`.
|
368
|
-
|
369
|
-
```js
|
370
|
-
let rules = [{
|
371
|
-
conditions: {
|
372
|
-
or: [
|
373
|
-
{
|
374
|
-
age: { less : 70 },
|
375
|
-
country: { is: "USA" }
|
376
|
-
},
|
377
|
-
{
|
378
|
-
state: { is: "NY"}
|
379
|
-
}
|
380
|
-
]
|
381
|
-
},
|
382
|
-
event: {
|
383
|
-
type: "require",
|
384
|
-
params: { fields: [ "bio" ]}
|
385
|
-
}
|
386
|
-
}]
|
387
|
-
```
|
388
|
-
|
389
|
-
#### NOT
|
390
|
-
|
391
|
-
When we don't require `bio` we need `zip` code.
|
392
|
-
|
393
|
-
```js
|
394
|
-
let rules = [{
|
395
|
-
conditions: {
|
396
|
-
not: {
|
397
|
-
or: [
|
398
|
-
{
|
399
|
-
age: { less : 70 },
|
400
|
-
country: { is: "USA" }
|
401
|
-
},
|
402
|
-
{
|
403
|
-
state: { is: "NY"}
|
404
|
-
}
|
405
|
-
]
|
406
|
-
}
|
407
|
-
},
|
408
|
-
event: {
|
409
|
-
type: "require",
|
410
|
-
params: { fields: [ "zip" ]}
|
411
|
-
}
|
412
|
-
}]
|
413
|
-
```
|
414
|
-
|
415
|
-
### Nested object queries
|
416
|
-
|
417
|
-
Rules engine supports querying inside nested objects, with [selectn](https://github.com/wilmoore/selectn.js),
|
418
|
-
any data query that works in [selectn](https://github.com/wilmoore/selectn.js), will work in here
|
419
|
-
|
420
|
-
Let's say we need to require `state`, when `work` has a `name` `congressman`, this is how we can do this:
|
421
|
-
|
422
|
-
```js
|
423
|
-
let rules = [{
|
424
|
-
conditions: {
|
425
|
-
"work.name": { is: "congressman" }
|
426
|
-
},
|
427
|
-
event: {
|
428
|
-
type: "require",
|
429
|
-
params: { fields: [ "state" ]}
|
430
|
-
}
|
431
|
-
}]
|
432
|
-
```
|
433
|
-
|
434
|
-
### Nested arrays object queries
|
435
|
-
|
436
|
-
Sometimes we need to make changes to the form if some nested condition is true.
|
437
|
-
|
438
|
-
For example if one of the `hobbies` is `baseball`, we need to make `state` `required`.
|
439
|
-
This can be expressed like this:
|
440
|
-
|
441
|
-
```js
|
442
|
-
let rules = [{
|
443
|
-
conditions: {
|
444
|
-
hobbies: {
|
445
|
-
name: { is: "baseball" },
|
446
|
-
}
|
447
|
-
},
|
448
|
-
event: {
|
449
|
-
type: "require",
|
450
|
-
params: { fields: [ "state" ]}
|
451
|
-
}
|
452
|
-
}]
|
453
|
-
```
|
454
|
-
|
455
|
-
Rules engine will go through all the elements in the array and trigger `require` if `any` of the elements meet the criteria.
|
456
|
-
|
457
|
-
### Extending available predicates
|
458
|
-
|
459
|
-
If for some reason the list of [predicates](https://github.com/landau/predicate) is insufficient for your needs, you can extend them pretty easy,
|
460
|
-
by specifying additional predicates in global import object.
|
461
|
-
|
462
|
-
For example, if we want to add `range` predicate, that would verify, that integer value is in range, we can do it like this:
|
463
|
-
```js
|
464
|
-
import predicate from "predicate";
|
465
|
-
import Engine from "json-rules-engine-simplified";
|
466
|
-
|
467
|
-
predicate.range = predicate.curry((val, range) => {
|
468
|
-
return predicate.num(val) &&
|
469
|
-
predicate.array(range) &&
|
470
|
-
predicate.equal(range.length, 2) &&
|
471
|
-
predicate.num(range[0]) &&
|
472
|
-
predicate.num(range[1]) &&
|
473
|
-
predicate.greaterEq(val, range[0]) &&
|
474
|
-
predicate.lessEq(val, range[1]);
|
475
|
-
});
|
476
|
-
|
477
|
-
let engine = new Engine([{
|
478
|
-
conditions: { age: { range: [ 20, 40 ] } },
|
479
|
-
event: "hit"
|
480
|
-
}]);
|
481
|
-
```
|
482
|
-
|
483
|
-
Validation will automatically catch new extension and work as expected.
|
484
|
-
|
485
|
-
## Logic on nested objects
|
486
|
-
|
487
|
-
Support of nested structures with [selectn](https://github.com/wilmoore/selectn.js), so basically any query you can define in selectn you can use here.
|
488
|
-
|
489
|
-
For example if in previous example, age would be a part of person object, we could work with it like this:
|
490
|
-
```js
|
491
|
-
let rules = [ { conditions: { "person.age": { range: [ 20, 40 ] } } } ];
|
492
|
-
```
|
493
|
-
|
494
|
-
Also in order to support systems where keys with "." not allowed (for example if you would like to store data in mongo), you can use `$` to separate references:
|
495
|
-
|
496
|
-
For example, this is the same condition, but instead of `.` it uses `$`:
|
497
|
-
```js
|
498
|
-
let rules = [ { conditions: { "person$age": { range: [ 20, 40 ] } } } ];
|
499
|
-
```
|
500
|
-
|
501
|
-
## Relevant conditional logic
|
502
|
-
|
503
|
-
Sometimes you would want to validate `formData` fields one against the other.
|
504
|
-
You can do this simply by appending `$` to the beginning of reference.
|
505
|
-
|
506
|
-
For example, you want to trigger event only when `a` is less then `b`, when you don't know ahead `a` or `b` values
|
507
|
-
|
508
|
-
```js
|
509
|
-
let schema = {
|
510
|
-
type: "object",
|
511
|
-
properties: {
|
512
|
-
a: { type: "number" },
|
513
|
-
b: { type: "number" }
|
514
|
-
}
|
515
|
-
}
|
516
|
-
|
517
|
-
let rules = [{
|
518
|
-
conditions: {
|
519
|
-
a: { less: "$b" }
|
520
|
-
},
|
521
|
-
event: "some"
|
522
|
-
}]
|
523
|
-
|
524
|
-
let engine = new Engine(schema, rules);
|
525
|
-
```
|
526
|
-
This is how you do it, in run time `$b` will be replaces with field `b` value.
|
527
|
-
|
528
|
-
Relevant fields work on nested objects as well as on any field condition.
|
529
|
-
|
530
|
-
## Events
|
531
|
-
|
532
|
-
Framework does not put any restrictions on event object, that will be triggered, in case conditions are meet
|
533
|
-
|
534
|
-
For example, `event` can be a string:
|
535
|
-
```js
|
536
|
-
let rules = [{
|
537
|
-
conditions: { ... },
|
538
|
-
event: "require"
|
539
|
-
}]
|
540
|
-
```
|
541
|
-
Or number
|
542
|
-
```js
|
543
|
-
let rules = [{
|
544
|
-
conditions: { ... },
|
545
|
-
event: 4
|
546
|
-
}]
|
547
|
-
```
|
548
|
-
|
549
|
-
Or an `object`
|
550
|
-
```js
|
551
|
-
let rules = [{
|
552
|
-
conditions: { ... },
|
553
|
-
event: {
|
554
|
-
type: "require",
|
555
|
-
params: { fields: [ "state" ]}
|
556
|
-
}
|
557
|
-
}]
|
558
|
-
```
|
559
|
-
|
560
|
-
You can even return an array of events, each of which will be added to final array of results
|
561
|
-
```js
|
562
|
-
let rules = [{
|
563
|
-
conditions: { ... },
|
564
|
-
event: [
|
565
|
-
{
|
566
|
-
type: "require",
|
567
|
-
params: { field: "state"}
|
568
|
-
},
|
569
|
-
{
|
570
|
-
type: "remove",
|
571
|
-
params: { fields: "fake" }
|
572
|
-
},
|
573
|
-
]
|
574
|
-
}]
|
575
|
-
```
|
576
|
-
|
577
|
-
## License
|
578
|
-
|
1
|
+
[](https://travis-ci.org/RxNT/json-rules-engine-simplified)
|
2
|
+
[](https://coveralls.io/github/RxNT/json-rules-engine-simplified?branch=master)
|
3
|
+
[](https://badge.fury.io/js/json-rules-engine-simplified)
|
4
|
+
# json-rules-engine-simplified
|
5
|
+
A simple rules engine expressed in JSON
|
6
|
+
|
7
|
+
The primary goal of this project is to be
|
8
|
+
an alternative of [json-rules-engine](https://github.com/CacheControl/json-rules-engine) for [react-jsonschema-form-conditionals](https://github.com/RxNT/react-jsonschema-form-conditionals),
|
9
|
+
as such it has similar interface and configuration, but simplified predicate language, similar to SQL.
|
10
|
+
|
11
|
+
## Features
|
12
|
+
|
13
|
+
- Optional schema and rules validation
|
14
|
+
- Basic boolean operations (`and` `or` and `not`) that allow to have any arbitrary complexity
|
15
|
+
- Rules expressed in simple, easy to read JSON
|
16
|
+
- Declarative conditional logic with [predicates](https://github.com/landau/predicate)
|
17
|
+
- Relevant conditional logic support
|
18
|
+
- Support of nested structures with [selectn](https://github.com/wilmoore/selectn.js)
|
19
|
+
including composite arrays
|
20
|
+
- Secure - no use of eval()
|
21
|
+
|
22
|
+
## Installation
|
23
|
+
|
24
|
+
Install `json-rules-engine-simplified` by running:
|
25
|
+
|
26
|
+
```bash
|
27
|
+
npm install --s json-rules-engine-simplified
|
28
|
+
```
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
The simplest example of using `json-rules-engine-simplified`
|
33
|
+
|
34
|
+
```jsx
|
35
|
+
import Engine from 'json-rules-engine-simplified'
|
36
|
+
|
37
|
+
let rules = [{
|
38
|
+
conditions: {
|
39
|
+
firstName: "empty"
|
40
|
+
},
|
41
|
+
event: {
|
42
|
+
type: "remove",
|
43
|
+
params: {
|
44
|
+
field: "password"
|
45
|
+
},
|
46
|
+
}
|
47
|
+
}];
|
48
|
+
|
49
|
+
/**
|
50
|
+
* Setup a new engine
|
51
|
+
*/
|
52
|
+
let engine = new Engine(rules);
|
53
|
+
|
54
|
+
let formData = {
|
55
|
+
lastName: "Smit"
|
56
|
+
}
|
57
|
+
|
58
|
+
// Run the engine to evaluate
|
59
|
+
engine
|
60
|
+
.run(formData)
|
61
|
+
.then(events => { // run() returns remove event
|
62
|
+
events.map(event => console.log(event.type));
|
63
|
+
})
|
64
|
+
|
65
|
+
```
|
66
|
+
|
67
|
+
Rules engine expects to know all the rules in advance, it effectively drops builder pattern, but keeps the interface.
|
68
|
+
|
69
|
+
### Appending rule to existing engine
|
70
|
+
|
71
|
+
You don't have to specify all rules at the construction time, you can add rules in different time in process.
|
72
|
+
In order to add new rules to the `Engine` use `addRule` function.
|
73
|
+
|
74
|
+
For example, following declarations are the same
|
75
|
+
```js
|
76
|
+
import Engine from 'json-rules-engine-simplified';
|
77
|
+
|
78
|
+
let engineA = new Engine();
|
79
|
+
|
80
|
+
let rule = {
|
81
|
+
conditions: {
|
82
|
+
firstName: "empty"
|
83
|
+
},
|
84
|
+
event: {
|
85
|
+
type: "remove",
|
86
|
+
params: {
|
87
|
+
field: "password"
|
88
|
+
},
|
89
|
+
}
|
90
|
+
};
|
91
|
+
|
92
|
+
engineA.addRule(rule);
|
93
|
+
|
94
|
+
let engineB = new Engine(rule);
|
95
|
+
|
96
|
+
```
|
97
|
+
|
98
|
+
In this case `engineA` and `engineB` will give the same results.
|
99
|
+
|
100
|
+
## Validation
|
101
|
+
|
102
|
+
In order to prevent most common errors, `Engine` does initial validation on the schema, during construction.
|
103
|
+
Validation is done automatically if you specify `schema` during construction.
|
104
|
+
|
105
|
+
```js
|
106
|
+
let rules = [{
|
107
|
+
conditions: {
|
108
|
+
firstName: "empty"
|
109
|
+
},
|
110
|
+
event: {
|
111
|
+
type: "remove",
|
112
|
+
params: { field: "password" },
|
113
|
+
}
|
114
|
+
}];
|
115
|
+
|
116
|
+
let schema = {
|
117
|
+
properties: {
|
118
|
+
firstName: { type: "string" },
|
119
|
+
lastName: { type: "string" }
|
120
|
+
}
|
121
|
+
}
|
122
|
+
|
123
|
+
let engine = new Engine(rules, schema);
|
124
|
+
```
|
125
|
+
### Types of errors
|
126
|
+
|
127
|
+
- Conditions field validation (conditions use fields that are not part of the schema)
|
128
|
+
- Predicate validation (used predicates are not part of the
|
129
|
+
[predicates](https://github.com/landau/predicate) library and most likely wrong)
|
130
|
+
|
131
|
+
Validation is done only during development, validation is disabled by default in `production`.
|
132
|
+
|
133
|
+
WARNING!!! Currently validation does not support nested structures, so be extra careful, when using those.
|
134
|
+
|
135
|
+
## Conditional logic
|
136
|
+
|
137
|
+
Conditional logic is based on public [predicate](https://github.com/landau/predicate) library
|
138
|
+
with boolean logic extension.
|
139
|
+
|
140
|
+
[Predicate](https://github.com/landau/predicate) library has a lot of predicates that we found more, than sufficient for our use cases.
|
141
|
+
|
142
|
+
To showcase conditional logic, we'll be using simple `registration` schema
|
143
|
+
|
144
|
+
```js
|
145
|
+
let schema = {
|
146
|
+
definitions: {
|
147
|
+
hobby: {
|
148
|
+
type: "object",
|
149
|
+
properties: {
|
150
|
+
name: { type: "string" },
|
151
|
+
durationInMonth: { type: "integer" },
|
152
|
+
}
|
153
|
+
}
|
154
|
+
},
|
155
|
+
title: "A registration form",
|
156
|
+
description: "A simple form example.",
|
157
|
+
type: "object",
|
158
|
+
required: [
|
159
|
+
"firstName",
|
160
|
+
"lastName"
|
161
|
+
],
|
162
|
+
properties: {
|
163
|
+
firstName: {
|
164
|
+
type: "string",
|
165
|
+
title: "First name"
|
166
|
+
},
|
167
|
+
lastName: {
|
168
|
+
type: "string",
|
169
|
+
title: "Last name"
|
170
|
+
},
|
171
|
+
age: {
|
172
|
+
type: "integer",
|
173
|
+
title: "Age",
|
174
|
+
},
|
175
|
+
bio: {
|
176
|
+
type: "string",
|
177
|
+
title: "Bio",
|
178
|
+
},
|
179
|
+
country: {
|
180
|
+
type: "string",
|
181
|
+
title: "Country"
|
182
|
+
},
|
183
|
+
state: {
|
184
|
+
type: "string",
|
185
|
+
title: "State"
|
186
|
+
},
|
187
|
+
zip: {
|
188
|
+
type: "string",
|
189
|
+
title: "ZIP"
|
190
|
+
},
|
191
|
+
password: {
|
192
|
+
type: "string",
|
193
|
+
title: "Password",
|
194
|
+
minLength: 3
|
195
|
+
},
|
196
|
+
telephone: {
|
197
|
+
type: "string",
|
198
|
+
title: "Telephone",
|
199
|
+
minLength: 10
|
200
|
+
},
|
201
|
+
work: { "$ref": "#/definitions/hobby" },
|
202
|
+
hobbies: {
|
203
|
+
type: "array",
|
204
|
+
items: { "$ref": "#/definitions/hobby" }
|
205
|
+
}
|
206
|
+
}
|
207
|
+
}
|
208
|
+
```
|
209
|
+
Assuming action part is taken from [react-jsonschema-form-conditionals](https://github.com/RxNT/react-jsonschema-form-conditionals)
|
210
|
+
|
211
|
+
### Single line conditionals
|
212
|
+
|
213
|
+
Let's say we want to `remove` `password` , when `firstName` is missing, we can expressed it like this:
|
214
|
+
|
215
|
+
```js
|
216
|
+
let rules = [{
|
217
|
+
conditions: {
|
218
|
+
firstName: "empty"
|
219
|
+
},
|
220
|
+
event: {
|
221
|
+
type: "remove",
|
222
|
+
params: {
|
223
|
+
field: "password"
|
224
|
+
}
|
225
|
+
}
|
226
|
+
}]
|
227
|
+
```
|
228
|
+
|
229
|
+
This translates into -
|
230
|
+
when `firstName` is `empty`, trigger `remove` `event`.
|
231
|
+
|
232
|
+
`Empty` keyword is [equal in predicate library](https://landau.github.io/predicate/#equal) and required
|
233
|
+
event will be performed only when `predicate.empty(registration.firstName)` is `true`.
|
234
|
+
|
235
|
+
### Conditionals with arguments
|
236
|
+
|
237
|
+
Let's say we need to `require` `zip`, when `age` is `less` than `16`,
|
238
|
+
because the service we are using is legal only after `16` in some countries
|
239
|
+
|
240
|
+
```js
|
241
|
+
let rules = [{
|
242
|
+
conditions: {
|
243
|
+
age: { less : 16 }
|
244
|
+
},
|
245
|
+
event: {
|
246
|
+
type: "require",
|
247
|
+
params: {
|
248
|
+
field: "zip"
|
249
|
+
}
|
250
|
+
}
|
251
|
+
}]
|
252
|
+
```
|
253
|
+
|
254
|
+
This translates into - when `age` is `less` than `16`, `require` zip.
|
255
|
+
|
256
|
+
[Less](https://landau.github.io/predicate/#less) keyword is [less in predicate](https://landau.github.io/predicate/#less) and required
|
257
|
+
event will be returned only when `predicate.empty(registration.age, 5)` is `true`.
|
258
|
+
|
259
|
+
### Boolean operations on a single field
|
260
|
+
|
261
|
+
#### AND
|
262
|
+
|
263
|
+
For the field AND is a default behavior.
|
264
|
+
|
265
|
+
Looking at previous rule, we decide that we want to change the rule and `require` `zip`,
|
266
|
+
when `age` is between `16` and `70`, so it would be available
|
267
|
+
only to people older, than `16` and younger than `70`.
|
268
|
+
|
269
|
+
```js
|
270
|
+
let rules = [{
|
271
|
+
conditions: {
|
272
|
+
age: {
|
273
|
+
greater: 16,
|
274
|
+
less : 70,
|
275
|
+
}
|
276
|
+
},
|
277
|
+
event: {
|
278
|
+
type: "require",
|
279
|
+
params: {
|
280
|
+
field: "zip"
|
281
|
+
}
|
282
|
+
}
|
283
|
+
}]
|
284
|
+
```
|
285
|
+
|
286
|
+
By default action will be applied only when both field conditions are true.
|
287
|
+
In this case, when age is `greater` than `16` and `less` than `70`.
|
288
|
+
|
289
|
+
#### NOT
|
290
|
+
|
291
|
+
Let's say we want to change the logic to opposite, and trigger event only when
|
292
|
+
`age` is `less`er then `16` or `greater` than `70`,
|
293
|
+
|
294
|
+
```js
|
295
|
+
let rules = [{
|
296
|
+
conditions: {
|
297
|
+
age: {
|
298
|
+
not: {
|
299
|
+
greater: 16,
|
300
|
+
less : 70,
|
301
|
+
}
|
302
|
+
}
|
303
|
+
},
|
304
|
+
event: {
|
305
|
+
type: "require",
|
306
|
+
params: {
|
307
|
+
field: "zip"
|
308
|
+
}
|
309
|
+
}
|
310
|
+
}]
|
311
|
+
```
|
312
|
+
|
313
|
+
This does it, since the final result will be opposite of the previous condition.
|
314
|
+
|
315
|
+
#### OR
|
316
|
+
|
317
|
+
The previous example works, but it's a bit hard to understand, luckily we can express it differently
|
318
|
+
with `or` conditional.
|
319
|
+
|
320
|
+
```js
|
321
|
+
let rules = [{
|
322
|
+
conditions: { age: {
|
323
|
+
or: [
|
324
|
+
{ lessEq : 5 },
|
325
|
+
{ greaterEq: 70 }
|
326
|
+
]
|
327
|
+
}
|
328
|
+
},
|
329
|
+
event: {
|
330
|
+
type: "require",
|
331
|
+
params: {
|
332
|
+
field: "zip"
|
333
|
+
}
|
334
|
+
}
|
335
|
+
}]
|
336
|
+
```
|
337
|
+
|
338
|
+
The result is the same as `NOT`, but easier to grasp.
|
339
|
+
|
340
|
+
### Boolean operations on multi fields
|
341
|
+
|
342
|
+
To support cases, when action depends on more, than one field meeting criteria we introduced
|
343
|
+
multi fields boolean operations.
|
344
|
+
|
345
|
+
#### Default AND operation
|
346
|
+
|
347
|
+
Let's say, when `age` is less than 70 and `country` is `USA` we want to `require` `bio`.
|
348
|
+
|
349
|
+
```js
|
350
|
+
let rules = [{
|
351
|
+
conditions: {
|
352
|
+
age: { less : 70 },
|
353
|
+
country: { is: "USA" }
|
354
|
+
},
|
355
|
+
event: {
|
356
|
+
type: "require",
|
357
|
+
params: { fields: [ "bio" ]}
|
358
|
+
}
|
359
|
+
}]
|
360
|
+
```
|
361
|
+
|
362
|
+
This is the way we can express this. By default each field is treated as a
|
363
|
+
separate condition and all conditions must be meet.
|
364
|
+
|
365
|
+
#### OR
|
366
|
+
|
367
|
+
In addition to previous rule we need `bio`, if `state` is `NY`.
|
368
|
+
|
369
|
+
```js
|
370
|
+
let rules = [{
|
371
|
+
conditions: {
|
372
|
+
or: [
|
373
|
+
{
|
374
|
+
age: { less : 70 },
|
375
|
+
country: { is: "USA" }
|
376
|
+
},
|
377
|
+
{
|
378
|
+
state: { is: "NY"}
|
379
|
+
}
|
380
|
+
]
|
381
|
+
},
|
382
|
+
event: {
|
383
|
+
type: "require",
|
384
|
+
params: { fields: [ "bio" ]}
|
385
|
+
}
|
386
|
+
}]
|
387
|
+
```
|
388
|
+
|
389
|
+
#### NOT
|
390
|
+
|
391
|
+
When we don't require `bio` we need `zip` code.
|
392
|
+
|
393
|
+
```js
|
394
|
+
let rules = [{
|
395
|
+
conditions: {
|
396
|
+
not: {
|
397
|
+
or: [
|
398
|
+
{
|
399
|
+
age: { less : 70 },
|
400
|
+
country: { is: "USA" }
|
401
|
+
},
|
402
|
+
{
|
403
|
+
state: { is: "NY"}
|
404
|
+
}
|
405
|
+
]
|
406
|
+
}
|
407
|
+
},
|
408
|
+
event: {
|
409
|
+
type: "require",
|
410
|
+
params: { fields: [ "zip" ]}
|
411
|
+
}
|
412
|
+
}]
|
413
|
+
```
|
414
|
+
|
415
|
+
### Nested object queries
|
416
|
+
|
417
|
+
Rules engine supports querying inside nested objects, with [selectn](https://github.com/wilmoore/selectn.js),
|
418
|
+
any data query that works in [selectn](https://github.com/wilmoore/selectn.js), will work in here
|
419
|
+
|
420
|
+
Let's say we need to require `state`, when `work` has a `name` `congressman`, this is how we can do this:
|
421
|
+
|
422
|
+
```js
|
423
|
+
let rules = [{
|
424
|
+
conditions: {
|
425
|
+
"work.name": { is: "congressman" }
|
426
|
+
},
|
427
|
+
event: {
|
428
|
+
type: "require",
|
429
|
+
params: { fields: [ "state" ]}
|
430
|
+
}
|
431
|
+
}]
|
432
|
+
```
|
433
|
+
|
434
|
+
### Nested arrays object queries
|
435
|
+
|
436
|
+
Sometimes we need to make changes to the form if some nested condition is true.
|
437
|
+
|
438
|
+
For example if one of the `hobbies` is `baseball`, we need to make `state` `required`.
|
439
|
+
This can be expressed like this:
|
440
|
+
|
441
|
+
```js
|
442
|
+
let rules = [{
|
443
|
+
conditions: {
|
444
|
+
hobbies: {
|
445
|
+
name: { is: "baseball" },
|
446
|
+
}
|
447
|
+
},
|
448
|
+
event: {
|
449
|
+
type: "require",
|
450
|
+
params: { fields: [ "state" ]}
|
451
|
+
}
|
452
|
+
}]
|
453
|
+
```
|
454
|
+
|
455
|
+
Rules engine will go through all the elements in the array and trigger `require` if `any` of the elements meet the criteria.
|
456
|
+
|
457
|
+
### Extending available predicates
|
458
|
+
|
459
|
+
If for some reason the list of [predicates](https://github.com/landau/predicate) is insufficient for your needs, you can extend them pretty easy,
|
460
|
+
by specifying additional predicates in global import object.
|
461
|
+
|
462
|
+
For example, if we want to add `range` predicate, that would verify, that integer value is in range, we can do it like this:
|
463
|
+
```js
|
464
|
+
import predicate from "predicate";
|
465
|
+
import Engine from "json-rules-engine-simplified";
|
466
|
+
|
467
|
+
predicate.range = predicate.curry((val, range) => {
|
468
|
+
return predicate.num(val) &&
|
469
|
+
predicate.array(range) &&
|
470
|
+
predicate.equal(range.length, 2) &&
|
471
|
+
predicate.num(range[0]) &&
|
472
|
+
predicate.num(range[1]) &&
|
473
|
+
predicate.greaterEq(val, range[0]) &&
|
474
|
+
predicate.lessEq(val, range[1]);
|
475
|
+
});
|
476
|
+
|
477
|
+
let engine = new Engine([{
|
478
|
+
conditions: { age: { range: [ 20, 40 ] } },
|
479
|
+
event: "hit"
|
480
|
+
}]);
|
481
|
+
```
|
482
|
+
|
483
|
+
Validation will automatically catch new extension and work as expected.
|
484
|
+
|
485
|
+
## Logic on nested objects
|
486
|
+
|
487
|
+
Support of nested structures with [selectn](https://github.com/wilmoore/selectn.js), so basically any query you can define in selectn you can use here.
|
488
|
+
|
489
|
+
For example if in previous example, age would be a part of person object, we could work with it like this:
|
490
|
+
```js
|
491
|
+
let rules = [ { conditions: { "person.age": { range: [ 20, 40 ] } } } ];
|
492
|
+
```
|
493
|
+
|
494
|
+
Also in order to support systems where keys with "." not allowed (for example if you would like to store data in mongo), you can use `$` to separate references:
|
495
|
+
|
496
|
+
For example, this is the same condition, but instead of `.` it uses `$`:
|
497
|
+
```js
|
498
|
+
let rules = [ { conditions: { "person$age": { range: [ 20, 40 ] } } } ];
|
499
|
+
```
|
500
|
+
|
501
|
+
## Relevant conditional logic
|
502
|
+
|
503
|
+
Sometimes you would want to validate `formData` fields one against the other.
|
504
|
+
You can do this simply by appending `$` to the beginning of reference.
|
505
|
+
|
506
|
+
For example, you want to trigger event only when `a` is less then `b`, when you don't know ahead `a` or `b` values
|
507
|
+
|
508
|
+
```js
|
509
|
+
let schema = {
|
510
|
+
type: "object",
|
511
|
+
properties: {
|
512
|
+
a: { type: "number" },
|
513
|
+
b: { type: "number" }
|
514
|
+
}
|
515
|
+
}
|
516
|
+
|
517
|
+
let rules = [{
|
518
|
+
conditions: {
|
519
|
+
a: { less: "$b" }
|
520
|
+
},
|
521
|
+
event: "some"
|
522
|
+
}]
|
523
|
+
|
524
|
+
let engine = new Engine(schema, rules);
|
525
|
+
```
|
526
|
+
This is how you do it, in run time `$b` will be replaces with field `b` value.
|
527
|
+
|
528
|
+
Relevant fields work on nested objects as well as on any field condition.
|
529
|
+
|
530
|
+
## Events
|
531
|
+
|
532
|
+
Framework does not put any restrictions on event object, that will be triggered, in case conditions are meet
|
533
|
+
|
534
|
+
For example, `event` can be a string:
|
535
|
+
```js
|
536
|
+
let rules = [{
|
537
|
+
conditions: { ... },
|
538
|
+
event: "require"
|
539
|
+
}]
|
540
|
+
```
|
541
|
+
Or number
|
542
|
+
```js
|
543
|
+
let rules = [{
|
544
|
+
conditions: { ... },
|
545
|
+
event: 4
|
546
|
+
}]
|
547
|
+
```
|
548
|
+
|
549
|
+
Or an `object`
|
550
|
+
```js
|
551
|
+
let rules = [{
|
552
|
+
conditions: { ... },
|
553
|
+
event: {
|
554
|
+
type: "require",
|
555
|
+
params: { fields: [ "state" ]}
|
556
|
+
}
|
557
|
+
}]
|
558
|
+
```
|
559
|
+
|
560
|
+
You can even return an array of events, each of which will be added to final array of results
|
561
|
+
```js
|
562
|
+
let rules = [{
|
563
|
+
conditions: { ... },
|
564
|
+
event: [
|
565
|
+
{
|
566
|
+
type: "require",
|
567
|
+
params: { field: "state"}
|
568
|
+
},
|
569
|
+
{
|
570
|
+
type: "remove",
|
571
|
+
params: { fields: "fake" }
|
572
|
+
},
|
573
|
+
]
|
574
|
+
}]
|
575
|
+
```
|
576
|
+
|
577
|
+
## License
|
578
|
+
|
579
579
|
The project is licensed under the Apache Licence 2.0.
|