├── .babelrc
├── .coveralls.yml
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .npmrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package.json
├── src
├── Engine.js
├── applicableActions.js
├── checkField.js
├── conditionsMeet.js
├── constants.js
├── index.js
├── utils.js
└── validation.js
├── test
├── .eslintrc
├── Engine.invalid.test.js
├── Engine.test.js
├── applicableActions.eventArray.test.js
├── applicableActions.multiField.test.js
├── applicableActions.nestedField.test.js
├── applicableActions.nestedFieldArray.test.js
├── checkField.test.js
├── conditionsMeet.nestedField.test.js
├── conditionsMeet.test.js
├── conditionsMeet.toRelCondition.test.js
├── documentationExamples.test.js
├── issues
│ ├── 10.test.js
│ ├── 12.test.js
│ ├── 14.test.js
│ ├── 15.test.js
│ ├── 22.test.js
│ └── react-jsonschema-form-conditions
│ │ └── 59.test.js
├── predicate.test.js
├── selectn.test.js
├── utils.js
├── utils.test.js
├── validation.nestedFields.test.js
├── validation.predicates.test.js
├── validation.ref.test.js
└── validation.test.js
└── webpack.config.dist.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["react", "es2015"],
3 | "plugins": [
4 | "transform-class-properties"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | repo_token: PErHbCHp8C7DPOcd5LvqBgcnMdJzjhIYc
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | indent_style = space
2 | indent_size = 2
3 | charset = utf-8
4 | insert_final_newline = true
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "max-len": "off",
5 | "react/jsx-uses-react": 2,
6 | "react/jsx-uses-vars": 2,
7 | "react/react-in-jsx-scope": 2,
8 | "react/jsx-tag-spacing": 0,
9 | "curly": [2],
10 | "linebreak-style": [2, "unix"],
11 | "semi": [2, "always"],
12 | "comma-dangle": [0],
13 | "no-unused-vars": [2, {
14 | "vars": "all",
15 | "args": "none",
16 | "ignoreRestSiblings": true
17 | }],
18 | "no-console": [0],
19 | "object-curly-spacing": [2, "always"],
20 | "keyword-spacing": ["error"],
21 | "jest/no-disabled-tests": "warn",
22 | "jest/no-focused-tests": "error",
23 | "jest/no-identical-title": "error",
24 | "jest/valid-expect": "error"
25 | },
26 | "env": {
27 | "es6": true,
28 | "browser": true,
29 | "node": true,
30 | "jest/globals": true
31 | },
32 | "extends": "eslint:recommended",
33 | "parserOptions": {
34 | "ecmaVersion": 6,
35 | "sourceType": "module",
36 | "ecmaFeatures": {
37 | "jsx": true
38 | }
39 | },
40 | "plugins": [
41 | "react",
42 | "jest"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage
2 | npm-debug.log
3 | node_modules
4 | build
5 | dist
6 | lib
7 | yarn.lock
8 |
9 | package-lock.json
10 |
11 |
12 | # Numerous always-ignore extensions
13 | *.diff
14 | *.err
15 | *.orig
16 | *.log
17 | *.rej
18 | *.swo
19 | *.swp
20 | *.vi
21 | *~
22 | *.sass-cache
23 |
24 | # OS or Editor folders
25 | .DS_Store
26 | .cache
27 | .project
28 | .settings
29 | .tmproj
30 | nbproject
31 | Thumbs.db
32 |
33 | # NPM packages folder.
34 | node_modules/
35 |
36 | # Brunch output folder.
37 | public/
38 | .idea/
39 | json-schema-playing.iml
40 |
41 | *.iml
42 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | //registry.npmjs.org/:_authToken=${NPM_ACCESS_TOKEN}
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language:
3 | - node_js
4 | node_js:
5 | - "8"
6 | script:
7 | - npm run lint
8 | - npm run dist
9 | - npm test && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 | ## 0.1.17 (2018-04-05)
3 |
4 | * 0.1.17 ([6c0d891](https://github.com/RxNT/json-rules-engine-simplified/commit/6c0d891))
5 | * Adding Changelog ([1bc894e](https://github.com/RxNT/json-rules-engine-simplified/commit/1bc894e))
6 | * fix #23 ([6322d1b](https://github.com/RxNT/json-rules-engine-simplified/commit/6322d1b)), closes [#23](https://github.com/RxNT/json-rules-engine-simplified/issues/23)
7 | * test for #22 ([3e59ef7](https://github.com/RxNT/json-rules-engine-simplified/commit/3e59ef7)), closes [#22](https://github.com/RxNT/json-rules-engine-simplified/issues/22)
8 |
9 |
10 |
11 |
12 | ## 0.1.16 (2018-02-16)
13 |
14 | * #16 ([c271482](https://github.com/RxNT/json-rules-engine-simplified/commit/c271482)), closes [#16](https://github.com/RxNT/json-rules-engine-simplified/issues/16)
15 | * 0.1.15 ([5bbb244](https://github.com/RxNT/json-rules-engine-simplified/commit/5bbb244))
16 | * 0.1.16 ([9febb8e](https://github.com/RxNT/json-rules-engine-simplified/commit/9febb8e))
17 | * clean up ([0855e59](https://github.com/RxNT/json-rules-engine-simplified/commit/0855e59))
18 | * fix #12 ([cee44a7](https://github.com/RxNT/json-rules-engine-simplified/commit/cee44a7)), closes [#12](https://github.com/RxNT/json-rules-engine-simplified/issues/12)
19 | * fix #20 ([babbd55](https://github.com/RxNT/json-rules-engine-simplified/commit/babbd55)), closes [#20](https://github.com/RxNT/json-rules-engine-simplified/issues/20)
20 | * Fixes #18 ([5f2b777](https://github.com/RxNT/json-rules-engine-simplified/commit/5f2b777)), closes [#18](https://github.com/RxNT/json-rules-engine-simplified/issues/18)
21 |
22 |
23 |
24 |
25 | ## 0.1.14 (2017-10-30)
26 |
27 | * #15 missed validation update ([bb51457](https://github.com/RxNT/json-rules-engine-simplified/commit/bb51457)), closes [#15](https://github.com/RxNT/json-rules-engine-simplified/issues/15)
28 | * 0.1.14 ([1222800](https://github.com/RxNT/json-rules-engine-simplified/commit/1222800))
29 |
30 |
31 |
32 |
33 | ## 0.1.13 (2017-10-30)
34 |
35 | * 0.1.13 ([055c17e](https://github.com/RxNT/json-rules-engine-simplified/commit/055c17e))
36 | * fix #15 ([4501a6d](https://github.com/RxNT/json-rules-engine-simplified/commit/4501a6d)), closes [#15](https://github.com/RxNT/json-rules-engine-simplified/issues/15)
37 |
38 |
39 |
40 |
41 | ## 0.1.12 (2017-09-11)
42 |
43 | * #14 ([0b25980](https://github.com/RxNT/json-rules-engine-simplified/commit/0b25980)), closes [#14](https://github.com/RxNT/json-rules-engine-simplified/issues/14)
44 | * #14 ([cf6ffe0](https://github.com/RxNT/json-rules-engine-simplified/commit/cf6ffe0)), closes [#14](https://github.com/RxNT/json-rules-engine-simplified/issues/14)
45 | * 0.1.12 ([fec8e60](https://github.com/RxNT/json-rules-engine-simplified/commit/fec8e60))
46 | * fix #14 ([68a4438](https://github.com/RxNT/json-rules-engine-simplified/commit/68a4438)), closes [#14](https://github.com/RxNT/json-rules-engine-simplified/issues/14)
47 |
48 |
49 |
50 |
51 | ## 0.1.11 (2017-09-04)
52 |
53 | * #11 some more tests ([b026cf0](https://github.com/RxNT/json-rules-engine-simplified/commit/b026cf0)), closes [#11](https://github.com/RxNT/json-rules-engine-simplified/issues/11)
54 | * #11 some tests for failing travis ([40861ec](https://github.com/RxNT/json-rules-engine-simplified/commit/40861ec)), closes [#11](https://github.com/RxNT/json-rules-engine-simplified/issues/11)
55 | * 0.1.11 ([69cb069](https://github.com/RxNT/json-rules-engine-simplified/commit/69cb069))
56 | * fix #13 ([f561fe0](https://github.com/RxNT/json-rules-engine-simplified/commit/f561fe0)), closes [#13](https://github.com/RxNT/json-rules-engine-simplified/issues/13)
57 |
58 |
59 |
60 |
61 | ## 0.1.10 (2017-08-10)
62 |
63 | * #8 ([b48735a](https://github.com/RxNT/json-rules-engine-simplified/commit/b48735a)), closes [#8](https://github.com/RxNT/json-rules-engine-simplified/issues/8)
64 | * 0.1.10 ([09225f6](https://github.com/RxNT/json-rules-engine-simplified/commit/09225f6))
65 | * fix #8 ([cd78db5](https://github.com/RxNT/json-rules-engine-simplified/commit/cd78db5)), closes [#8](https://github.com/RxNT/json-rules-engine-simplified/issues/8)
66 | * Fixing sequence of actions during release ([c0fb150](https://github.com/RxNT/json-rules-engine-simplified/commit/c0fb150))
67 |
68 |
69 |
70 |
71 | ## 0.1.8 (2017-08-10)
72 |
73 | * 0.1.8 ([3e4cf34](https://github.com/RxNT/json-rules-engine-simplified/commit/3e4cf34))
74 | * fix #10 ([a9acaa3](https://github.com/RxNT/json-rules-engine-simplified/commit/a9acaa3)), closes [#10](https://github.com/RxNT/json-rules-engine-simplified/issues/10)
75 | * toError is none blocking in production ([3d0c582](https://github.com/RxNT/json-rules-engine-simplified/commit/3d0c582))
76 | * toError is none blocking in production ([edff07e](https://github.com/RxNT/json-rules-engine-simplified/commit/edff07e))
77 |
78 |
79 |
80 |
81 | ## 0.1.7 (2017-08-09)
82 |
83 | * #5 ([e56ea95](https://github.com/RxNT/json-rules-engine-simplified/commit/e56ea95)), closes [#5](https://github.com/RxNT/json-rules-engine-simplified/issues/5)
84 | * #5 Updating the fix ([c4514d0](https://github.com/RxNT/json-rules-engine-simplified/commit/c4514d0)), closes [#5](https://github.com/RxNT/json-rules-engine-simplified/issues/5)
85 | * 0.1.7 ([5018d35](https://github.com/RxNT/json-rules-engine-simplified/commit/5018d35))
86 |
87 |
88 |
89 |
90 | ## 0.1.6 (2017-08-09)
91 |
92 | * 0.1.6 ([15850bb](https://github.com/RxNT/json-rules-engine-simplified/commit/15850bb))
93 | * Fix, predicates run against condition arrays and the elements ([96aa6b2](https://github.com/RxNT/json-rules-engine-simplified/commit/96aa6b2))
94 | * fixing coveralls configuration ([029798e](https://github.com/RxNT/json-rules-engine-simplified/commit/029798e))
95 | * Update checkField.js ([917ae88](https://github.com/RxNT/json-rules-engine-simplified/commit/917ae88))
96 | * Want to test empty object ([10a8209](https://github.com/RxNT/json-rules-engine-simplified/commit/10a8209))
97 |
98 |
99 |
100 |
101 | ## 0.1.5 (2017-08-01)
102 |
103 | * #3 Adding appropriate error management ([235ab56](https://github.com/RxNT/json-rules-engine-simplified/commit/235ab56)), closes [#3](https://github.com/RxNT/json-rules-engine-simplified/issues/3)
104 | * #4 ([b6ac613](https://github.com/RxNT/json-rules-engine-simplified/commit/b6ac613)), closes [#4](https://github.com/RxNT/json-rules-engine-simplified/issues/4)
105 | * #4 ([b4164d2](https://github.com/RxNT/json-rules-engine-simplified/commit/b4164d2)), closes [#4](https://github.com/RxNT/json-rules-engine-simplified/issues/4)
106 | * #4 updating configs ([2b139bf](https://github.com/RxNT/json-rules-engine-simplified/commit/2b139bf)), closes [#4](https://github.com/RxNT/json-rules-engine-simplified/issues/4)
107 | * #6 Updating documentation ([6bab1e4](https://github.com/RxNT/json-rules-engine-simplified/commit/6bab1e4)), closes [#6](https://github.com/RxNT/json-rules-engine-simplified/issues/6)
108 | * 0.1.5 ([679130f](https://github.com/RxNT/json-rules-engine-simplified/commit/679130f))
109 | * Adding travis configurations ([856b5ee](https://github.com/RxNT/json-rules-engine-simplified/commit/856b5ee))
110 | * fix #4 ([7be1d2e](https://github.com/RxNT/json-rules-engine-simplified/commit/7be1d2e)), closes [#4](https://github.com/RxNT/json-rules-engine-simplified/issues/4)
111 | * fix #6 ([87a4d89](https://github.com/RxNT/json-rules-engine-simplified/commit/87a4d89)), closes [#6](https://github.com/RxNT/json-rules-engine-simplified/issues/6)
112 | * fix #7 ([02cea19](https://github.com/RxNT/json-rules-engine-simplified/commit/02cea19)), closes [#7](https://github.com/RxNT/json-rules-engine-simplified/issues/7)
113 | * some more tests ([8e33ed2](https://github.com/RxNT/json-rules-engine-simplified/commit/8e33ed2))
114 |
115 |
116 |
117 |
118 | ## 0.1.4 (2017-07-12)
119 |
120 | * #1 ([0c7cdf1](https://github.com/RxNT/json-rules-engine-simplified/commit/0c7cdf1)), closes [#1](https://github.com/RxNT/json-rules-engine-simplified/issues/1)
121 | * 0.1.4 ([079fc2a](https://github.com/RxNT/json-rules-engine-simplified/commit/079fc2a))
122 | * Aligning merge with lint ([04b7f96](https://github.com/RxNT/json-rules-engine-simplified/commit/04b7f96))
123 | * fix #1 ([4df43e8](https://github.com/RxNT/json-rules-engine-simplified/commit/4df43e8)), closes [#1](https://github.com/RxNT/json-rules-engine-simplified/issues/1)
124 | * Updating README ([f8c518a](https://github.com/RxNT/json-rules-engine-simplified/commit/f8c518a))
125 |
126 |
127 |
128 |
129 | ## 0.1.3 (2017-07-10)
130 |
131 | * 0.1.3 ([0bf731f](https://github.com/RxNT/json-rules-engine-simplified/commit/0bf731f))
132 | * Added the test scenarios for nested array ([fce6748](https://github.com/RxNT/json-rules-engine-simplified/commit/fce6748))
133 | * Fixing `not` operation ([7685be4](https://github.com/RxNT/json-rules-engine-simplified/commit/7685be4))
134 | * Fixing example with nested array ([3555909](https://github.com/RxNT/json-rules-engine-simplified/commit/3555909))
135 | * Fixing validation ([6449c59](https://github.com/RxNT/json-rules-engine-simplified/commit/6449c59))
136 | * Styling feedback ([0ac40a4](https://github.com/RxNT/json-rules-engine-simplified/commit/0ac40a4))
137 | * Updating documentation ([0ca291a](https://github.com/RxNT/json-rules-engine-simplified/commit/0ca291a))
138 |
139 |
140 |
141 |
142 | ## 0.1.2 (2017-07-08)
143 |
144 | * 0.1.2 ([ab42f48](https://github.com/RxNT/json-rules-engine-simplified/commit/ab42f48))
145 | * Updating publish to npm script ([78cfedc](https://github.com/RxNT/json-rules-engine-simplified/commit/78cfedc))
146 |
147 |
148 |
149 |
150 | ## 0.1.1 (2017-07-08)
151 |
152 | * 0.1.1 ([6cfa1d0](https://github.com/RxNT/json-rules-engine-simplified/commit/6cfa1d0))
153 | * abstracting engine ([9bba768](https://github.com/RxNT/json-rules-engine-simplified/commit/9bba768))
154 | * bumping coverage to 100% ([602ca07](https://github.com/RxNT/json-rules-engine-simplified/commit/602ca07))
155 | * Cleaning up dependencies ([a3e61df](https://github.com/RxNT/json-rules-engine-simplified/commit/a3e61df))
156 | * Disable validation of nested structures ([058ce14](https://github.com/RxNT/json-rules-engine-simplified/commit/058ce14))
157 | * Initial commit ([459d691](https://github.com/RxNT/json-rules-engine-simplified/commit/459d691))
158 | * Merging from conditionals project ([245c29c](https://github.com/RxNT/json-rules-engine-simplified/commit/245c29c))
159 | * updating validation ([7344f44](https://github.com/RxNT/json-rules-engine-simplified/commit/7344f44))
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
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 | The project is licensed under the Apache Licence 2.0.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "json-rules-engine-simplified",
3 | "version": "0.2.0",
4 | "description": "Simpl JSON rules engine",
5 | "private": false,
6 | "author": "mavarazy@gmail.com",
7 | "scripts": {
8 | "build:lib": "rimraf lib && cross-env NODE_ENV=production babel -d lib/ src/",
9 | "build:dist": "rimraf dist && cross-env NODE_ENV=production webpack --config webpack.config.dist.js --optimize-minimize",
10 | "cs-check": "prettier -l $npm_package_prettierOptions '{src,test}/**/*.js'",
11 | "cs-format": "prettier $npm_package_prettierOptions '{src,test}/**/*.js' --write",
12 | "dist": "npm run build:lib && npm run build:dist",
13 | "lint": "eslint src test",
14 | "precommit": "lint-staged",
15 | "publish-to-npm": "npm run dist && npm publish && npm version patch",
16 | "tdd": "jest --watchAll",
17 | "test": "jest --coverage"
18 | },
19 | "prettierOptions": "--jsx-bracket-same-line --trailing-comma es5 --semi",
20 | "jest": {
21 | "verbose": true,
22 | "collectCoverage": true,
23 | "collectCoverageFrom": [
24 | "src/**/*.{js,jsx}"
25 | ]
26 | },
27 | "lint-staged": {
28 | "{src,test}/**/*.js": [
29 | "npm run lint",
30 | "npm run cs-format",
31 | "git add"
32 | ]
33 | },
34 | "main": "lib/index.js",
35 | "files": [
36 | "dist",
37 | "lib"
38 | ],
39 | "engineStrict": false,
40 | "engines": {
41 | "node": ">=20.0.0",
42 | "npm": ">=10.0.0"
43 | },
44 | "dependencies": {
45 | "deep-equal": "1.0.1",
46 | "predicate": "1.2.0",
47 | "selectn": "1.1.2"
48 | },
49 | "devDependencies": {
50 | "atob": "^2.0.3",
51 | "babel-cli": "^6.26.0",
52 | "babel-core": "^6.26.0",
53 | "babel-eslint": "^7.2.3",
54 | "babel-jest": "^20.0.3",
55 | "babel-loader": "^7.1.2",
56 | "babel-plugin-transform-class-properties": "^6.24.1",
57 | "babel-polyfill": "^6.26.0",
58 | "babel-preset-env": "^1.6.0",
59 | "babel-preset-es2015": "^6.24.1",
60 | "babel-preset-react": "^6.24.1",
61 | "babel-register": "^6.26.0",
62 | "codemirror": "^5.29.0",
63 | "coveralls": "^2.13.1",
64 | "cross-env": "^5.0.5",
65 | "css-loader": "^0.28.7",
66 | "eslint": "^4.6.1",
67 | "eslint-plugin-jest": "^20.0.3",
68 | "eslint-plugin-react": "^7.3.0",
69 | "eslint-plugin-standard": "^3.0.1",
70 | "exit-hook": "^1.1.1",
71 | "express": "^4.15.4",
72 | "extract-text-webpack-plugin": "^3.0.0",
73 | "gh-pages": "^1.0.0",
74 | "has-flag": "^2.0.0",
75 | "html": "1.0.0",
76 | "husky": "^0.14.3",
77 | "jest": "^20.0.4",
78 | "jsdom": "^11.2.0",
79 | "lint-staged": "^4.1.0",
80 | "prettier": "^1.6.1",
81 | "regenerator-runtime": "^0.11.0",
82 | "rimraf": "^2.6.1",
83 | "sinon": "^3.2.1",
84 | "style-loader": "^0.18.2",
85 | "webpack": "^3.5.5"
86 | },
87 | "directories": {
88 | "test": "test"
89 | },
90 | "repository": {
91 | "type": "git",
92 | "url": "git+https://github.com/RxNT/json-rules-engine-simplified.git"
93 | },
94 | "keywords": [
95 | "rules",
96 | "engine",
97 | "rules engine"
98 | ],
99 | "license": "Apache-2.0",
100 | "homepage": "https://github.com/RxNT/json-rules-engine-simplified#readme"
101 | }
102 |
--------------------------------------------------------------------------------
/src/Engine.js:
--------------------------------------------------------------------------------
1 | import { validateConditionFields, validatePredicates } from "./validation";
2 | import applicableActions from "./applicableActions";
3 | import { isDevelopment, isObject, toArray, toError } from "./utils";
4 |
5 | const validate = schema => {
6 | let isSchemaDefined = schema !== undefined && schema !== null;
7 | if (isDevelopment() && isSchemaDefined) {
8 | if (!isObject(schema)) {
9 | toError(`Expected valid schema object, but got - ${schema}`);
10 | }
11 | return rule => {
12 | validatePredicates([rule.conditions], schema);
13 | validateConditionFields([rule.conditions], schema);
14 | };
15 | } else {
16 | return () => {};
17 | }
18 | };
19 |
20 | class Engine {
21 | constructor(rules, schema) {
22 | this.rules = [];
23 | this.validate = validate(schema);
24 |
25 | if (rules) {
26 | toArray(rules).forEach(rule => this.addRule(rule));
27 | }
28 | }
29 | addRule = rule => {
30 | this.validate(rule);
31 | this.rules.push(rule);
32 | };
33 | run = formData => Promise.resolve(applicableActions(this.rules, formData));
34 | }
35 |
36 | export default Engine;
37 |
--------------------------------------------------------------------------------
/src/applicableActions.js:
--------------------------------------------------------------------------------
1 | import { flatMap, toArray } from "./utils";
2 | import conditionsMeet from "./conditionsMeet";
3 |
4 | export default function applicableActions(rules, formData) {
5 | return flatMap(rules, ({ conditions, event }) => {
6 | if (conditionsMeet(conditions, formData)) {
7 | return toArray(event);
8 | } else {
9 | return [];
10 | }
11 | });
12 | }
13 |
--------------------------------------------------------------------------------
/src/checkField.js:
--------------------------------------------------------------------------------
1 | import predicate from "predicate";
2 | import { isObject } from "./utils";
3 |
4 | import { AND, NOT, OR } from "./constants";
5 |
6 | const doCheckField = (fieldVal, rule) => {
7 | if (isObject(rule)) {
8 | return Object.keys(rule).every(p => {
9 | let subRule = rule[p];
10 | if (p === OR || p === AND) {
11 | if (Array.isArray(subRule)) {
12 | if (p === OR) {
13 | return subRule.some(rule => doCheckField(fieldVal, rule));
14 | } else {
15 | return subRule.every(rule => doCheckField(fieldVal, rule));
16 | }
17 | } else {
18 | return false;
19 | }
20 | } else if (p === NOT) {
21 | return !doCheckField(fieldVal, subRule);
22 | } else if (predicate[p]) {
23 | return predicate[p](fieldVal, subRule);
24 | } else {
25 | return false;
26 | }
27 | });
28 | } else {
29 | return predicate[rule](fieldVal);
30 | }
31 | };
32 |
33 | export default function checkField(fieldVal, rule) {
34 | return doCheckField(fieldVal, rule);
35 | }
36 |
--------------------------------------------------------------------------------
/src/conditionsMeet.js:
--------------------------------------------------------------------------------
1 | import { isObject, toError, selectRef } from "./utils";
2 | import checkField from "./checkField";
3 | import { OR, AND, NOT } from "./constants";
4 |
5 | export function toRelCondition(refCondition, formData) {
6 | if (Array.isArray(refCondition)) {
7 | return refCondition.map(cond => toRelCondition(cond, formData));
8 | } else if (isObject(refCondition)) {
9 | return Object.keys(refCondition).reduce((agg, field) => {
10 | agg[field] = toRelCondition(refCondition[field], formData);
11 | return agg;
12 | }, {});
13 | } else if (typeof refCondition === "string" && refCondition.startsWith("$")) {
14 | return selectRef(refCondition.substr(1), formData);
15 | } else {
16 | return refCondition;
17 | }
18 | }
19 |
20 | export default function conditionsMeet(condition, formData) {
21 | if (!isObject(condition) || !isObject(formData)) {
22 | toError(
23 | `Rule ${JSON.stringify(condition)} with ${formData} can't be processed`
24 | );
25 | return false;
26 | }
27 | return Object.keys(condition).every(ref => {
28 | let refCondition = condition[ref];
29 | if (ref === OR) {
30 | return refCondition.some(rule => conditionsMeet(rule, formData));
31 | } else if (ref === AND) {
32 | return refCondition.every(rule => conditionsMeet(rule, formData));
33 | } else if (ref === NOT) {
34 | return !conditionsMeet(refCondition, formData);
35 | } else {
36 | let refVal = selectRef(ref, formData);
37 | if (Array.isArray(refVal)) {
38 | let condMeatOnce = refVal.some(
39 | val => (isObject(val) ? conditionsMeet(refCondition, val) : false)
40 | );
41 | // It's either true for an element in an array or for the whole array
42 | return (
43 | condMeatOnce ||
44 | checkField(refVal, toRelCondition(refCondition, formData))
45 | );
46 | } else {
47 | return checkField(refVal, toRelCondition(refCondition, formData));
48 | }
49 | }
50 | });
51 | }
52 |
--------------------------------------------------------------------------------
/src/constants.js:
--------------------------------------------------------------------------------
1 | export const OR = "or";
2 | export const AND = "and";
3 | export const NOT = "not";
4 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Engine from "./Engine";
2 |
3 | export default Engine;
4 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | import selectn from "selectn";
2 |
3 | export function normRef(ref) {
4 | return ref.replace(/\$/g, ".");
5 | }
6 |
7 | export function selectRef(field, formData) {
8 | let ref = normRef(field);
9 | return selectn(ref, formData);
10 | }
11 |
12 | export function isObject(obj) {
13 | return typeof obj === "object" && obj !== null;
14 | }
15 |
16 | export function isDevelopment() {
17 | return process.env.NODE_ENV !== "production";
18 | }
19 |
20 | export function toArray(event) {
21 | if (Array.isArray(event)) {
22 | return event;
23 | } else {
24 | return [event];
25 | }
26 | }
27 |
28 | export function toError(message) {
29 | if (isDevelopment()) {
30 | throw new ReferenceError(message);
31 | } else {
32 | console.error(message);
33 | }
34 | }
35 |
36 | export function isRefArray(field, schema) {
37 | return (
38 | schema.properties[field] &&
39 | schema.properties[field].type === "array" &&
40 | schema.properties[field].items &&
41 | schema.properties[field].items["$ref"]
42 | );
43 | }
44 |
45 | function fetchSchema(ref, schema) {
46 | if (ref.startsWith("#/")) {
47 | return ref
48 | .substr(2)
49 | .split("/")
50 | .reduce((schema, field) => schema[field], schema);
51 | } else {
52 | toError(
53 | "Only local references supported at this point use json-schema-deref"
54 | );
55 | return undefined;
56 | }
57 | }
58 |
59 | export function extractRefSchema(field, schema) {
60 | let { properties } = schema;
61 | if (!properties || !properties[field]) {
62 | toError(`${field} not defined in properties`);
63 | return undefined;
64 | } else if (properties[field].type === "array") {
65 | if (isRefArray(field, schema)) {
66 | return fetchSchema(properties[field].items["$ref"], schema);
67 | } else {
68 | return properties[field].items;
69 | }
70 | } else if (properties[field] && properties[field]["$ref"]) {
71 | return fetchSchema(properties[field]["$ref"], schema);
72 | } else if (properties[field] && properties[field].type === "object") {
73 | return properties[field];
74 | } else {
75 | toError(`${field} has no $ref field ref schema extraction is impossible`);
76 | return undefined;
77 | }
78 | }
79 |
80 | const concat = (x, y) => x.concat(y);
81 | export const flatMap = (xs, f) => xs.map(f).reduce(concat, []);
82 |
--------------------------------------------------------------------------------
/src/validation.js:
--------------------------------------------------------------------------------
1 | import predicate from "predicate";
2 | import {
3 | flatMap,
4 | isObject,
5 | toError,
6 | isRefArray,
7 | extractRefSchema,
8 | normRef,
9 | } from "./utils";
10 | import { OR, AND, NOT } from "./constants";
11 |
12 | const UNSUPPORTED_PREDICATES = [
13 | "and",
14 | "or",
15 | "ternary",
16 | "every",
17 | "some",
18 | "curry",
19 | "partial",
20 | "complement",
21 | "mod",
22 | ];
23 |
24 | export function predicatesFromRule(rule, schema) {
25 | if (isObject(rule)) {
26 | return flatMap(Object.keys(rule), p => {
27 | let comparable = rule[p];
28 | if (isObject(comparable) || p === NOT) {
29 | if (p === OR || p === AND) {
30 | if (Array.isArray(comparable)) {
31 | return flatMap(comparable, condition =>
32 | predicatesFromRule(condition, schema)
33 | );
34 | } else {
35 | toError(`"${p}" must be an array`);
36 | return [];
37 | }
38 | } else {
39 | let predicates = predicatesFromRule(comparable, schema);
40 | predicates.push(p);
41 | return predicates;
42 | }
43 | } else {
44 | return predicatesFromRule(p, schema);
45 | }
46 | });
47 | } else {
48 | return [rule];
49 | }
50 | }
51 |
52 | export function predicatesFromCondition(condition, schema) {
53 | return flatMap(Object.keys(condition), ref => {
54 | let refVal = condition[ref];
55 | ref = normRef(ref);
56 | if (ref === OR || ref === AND) {
57 | if (Array.isArray(refVal)) {
58 | return flatMap(refVal, c => predicatesFromCondition(c, schema));
59 | } else {
60 | toError(`${ref} with ${JSON.stringify(refVal)} must be an Array`);
61 | return [];
62 | }
63 | } else if (ref === NOT) {
64 | return predicatesFromCondition(refVal, schema);
65 | } else if (ref.indexOf(".") !== -1) {
66 | let separator = ref.indexOf(".");
67 | let schemaField = ref.substr(0, separator);
68 | let subSchema = extractRefSchema(schemaField, schema);
69 |
70 | if (subSchema) {
71 | let subSchemaField = ref.substr(separator + 1);
72 | let newCondition = { [subSchemaField]: refVal };
73 | return predicatesFromCondition(newCondition, subSchema);
74 | } else {
75 | toError(`Can't find schema for ${schemaField}`);
76 | return [];
77 | }
78 | } else if (isRefArray(ref, schema)) {
79 | let refSchema = extractRefSchema(ref, schema);
80 | return refSchema ? predicatesFromCondition(refVal, refSchema) : [];
81 | } else if (schema.properties[ref]) {
82 | return predicatesFromRule(refVal, schema);
83 | } else {
84 | toError(`Can't validate ${ref}`);
85 | return [];
86 | }
87 | });
88 | }
89 |
90 | export function listAllPredicates(conditions, schema) {
91 | let allPredicates = flatMap(conditions, condition =>
92 | predicatesFromCondition(condition, schema)
93 | );
94 | return allPredicates.filter((v, i, a) => allPredicates.indexOf(v) === i);
95 | }
96 |
97 | export function listInvalidPredicates(conditions, schema) {
98 | let refPredicates = listAllPredicates(conditions, schema);
99 | return refPredicates.filter(
100 | p => UNSUPPORTED_PREDICATES.includes(p) || predicate[p] === undefined
101 | );
102 | }
103 |
104 | export function validatePredicates(conditions, schema) {
105 | let invalidPredicates = listInvalidPredicates(conditions, schema);
106 | if (invalidPredicates.length !== 0) {
107 | toError(`Rule contains invalid predicates ${invalidPredicates}`);
108 | }
109 | }
110 |
111 | export function fieldsFromPredicates(predicate) {
112 | if (Array.isArray(predicate)) {
113 | return flatMap(predicate, fieldsFromPredicates);
114 | } else if (isObject(predicate)) {
115 | return flatMap(Object.keys(predicate), field => {
116 | let predicateValue = predicate[field];
117 | return fieldsFromPredicates(predicateValue);
118 | });
119 | } else if (typeof predicate === "string" && predicate.startsWith("$")) {
120 | return [predicate.substr(1)];
121 | } else {
122 | return [];
123 | }
124 | }
125 |
126 | export function fieldsFromCondition(condition) {
127 | return flatMap(Object.keys(condition), ref => {
128 | let refCondition = condition[ref];
129 | if (ref === OR || ref === AND) {
130 | return flatMap(refCondition, fieldsFromCondition);
131 | } else if (ref === NOT) {
132 | return fieldsFromCondition(refCondition);
133 | } else {
134 | return [normRef(ref)].concat(fieldsFromPredicates(refCondition));
135 | }
136 | });
137 | }
138 |
139 | export function listAllFields(conditions) {
140 | let allFields = flatMap(conditions, fieldsFromCondition);
141 | return allFields
142 | .filter(field => field.indexOf(".") === -1)
143 | .filter((v, i, a) => allFields.indexOf(v) === i);
144 | }
145 |
146 | export function listInvalidFields(conditions, schema) {
147 | let allFields = listAllFields(conditions);
148 | return allFields.filter(field => schema.properties[field] === undefined);
149 | }
150 |
151 | export function validateConditionFields(conditions, schema) {
152 | let invalidFields = listInvalidFields(conditions, schema);
153 | if (invalidFields.length !== 0) {
154 | toError(`Rule contains invalid fields ${invalidFields}`);
155 | }
156 | }
157 |
--------------------------------------------------------------------------------
/test/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true,
4 | },
5 | "globals": {
6 | d: true
7 | },
8 | "rules": {
9 | "no-unused-vars": [
10 | 2,
11 | {
12 | "varsIgnorePattern": "^d$"
13 | }
14 | ]
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/test/Engine.invalid.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../src/Engine";
2 | import { testInProd } from "./utils";
3 |
4 | let invalidRules = [
5 | {
6 | conditions: {
7 | age: {
8 | and: {
9 | greater: 5,
10 | less: 70,
11 | },
12 | },
13 | },
14 | event: {
15 | type: "remove",
16 | params: {
17 | fields: ["telephone"],
18 | },
19 | },
20 | },
21 | ];
22 |
23 | let schema = {
24 | properties: {
25 | age: { type: "number" },
26 | telephone: { type: "string" },
27 | },
28 | };
29 |
30 | test("ignore invalid rules if no schema provided", () => {
31 | expect(() => new Engine(invalidRules)).not.toThrow();
32 | });
33 |
34 | test("ignore empty rules with invalid schema", () => {
35 | expect(() => new Engine(invalidRules, [])).toThrow();
36 | expect(() => new Engine(invalidRules, "schema")).toThrow();
37 | });
38 |
39 | test("initialize with invalid rules", () => {
40 | expect(() => new Engine(invalidRules, schema)).toThrow();
41 | expect(
42 | testInProd(() => new Engine(invalidRules, schema))
43 | ).not.toBeUndefined();
44 | });
45 |
--------------------------------------------------------------------------------
/test/Engine.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../src/index";
2 |
3 | let rules = [
4 | {
5 | conditions: {
6 | age: {
7 | greater: 5,
8 | less: 70,
9 | },
10 | },
11 | event: {
12 | type: "remove",
13 | params: { fields: ["telephone"] },
14 | },
15 | },
16 | ];
17 |
18 | let schema = {
19 | properties: {
20 | age: { type: "number" },
21 | telephone: { type: "string" },
22 | },
23 | };
24 |
25 | let engine = new Engine(rules, schema);
26 |
27 | test("age greater 5", () => {
28 | return engine
29 | .run({ age: 10 })
30 | .then(actions =>
31 | expect(actions).toEqual([
32 | { type: "remove", params: { fields: ["telephone"] } },
33 | ])
34 | );
35 | });
36 |
37 | test("age less 5", () => {
38 | return engine.run({ age: 4 }).then(actions => expect(actions).toEqual([]));
39 | });
40 |
41 | test("age less 70 ", () => {
42 | return engine
43 | .run({ age: 69 })
44 | .then(actions =>
45 | expect(actions).toEqual([
46 | { type: "remove", params: { fields: ["telephone"] } },
47 | ])
48 | );
49 | });
50 |
51 | test("age greater 70 ", () => {
52 | return engine.run({ age: 71 }).then(actions => expect(actions).toEqual([]));
53 | });
54 |
55 | test("empty engine creation", () => {
56 | expect(new Engine()).not.toBeUndefined();
57 | expect(new Engine(undefined)).not.toBeUndefined();
58 | expect(new Engine(null)).not.toBeUndefined();
59 | expect(new Engine([])).not.toBeUndefined();
60 | expect(new Engine(rules[0])).not.toBeUndefined();
61 | });
62 |
--------------------------------------------------------------------------------
/test/applicableActions.eventArray.test.js:
--------------------------------------------------------------------------------
1 | import applicableActions from "../src/applicableActions";
2 |
3 | test("check nested fields work", function() {
4 | let rules = [
5 | {
6 | conditions: { address: "empty" },
7 | event: [{ type: "remove" }, { type: "add" }],
8 | },
9 | ];
10 | expect(applicableActions(rules, {})).toEqual([
11 | { type: "remove" },
12 | { type: "add" },
13 | ]);
14 | expect(applicableActions(rules, { address: { line: "some" } })).toEqual([]);
15 | });
16 |
17 | test("check fields of different types", function() {
18 | let rules = [
19 | {
20 | conditions: { address: "empty" },
21 | event: ["remove", 1],
22 | },
23 | ];
24 | expect(applicableActions(rules, {})).toEqual(["remove", 1]);
25 | expect(applicableActions(rules, { address: { line: "some" } })).toEqual([]);
26 | });
27 |
--------------------------------------------------------------------------------
/test/applicableActions.multiField.test.js:
--------------------------------------------------------------------------------
1 | import applicableActions from "../src/applicableActions";
2 |
3 | const ACTION = {
4 | type: "remove",
5 | params: { fields: ["password"] },
6 | };
7 |
8 | const NO_ACTION = [];
9 |
10 | test("OR works", () => {
11 | let orRules = [
12 | {
13 | conditions: {
14 | or: [{ firstName: "empty" }, { nickName: { is: "admin" } }],
15 | },
16 | event: ACTION,
17 | },
18 | ];
19 |
20 | expect(applicableActions(orRules, {})).toEqual([ACTION]);
21 | expect(
22 | applicableActions(orRules, { firstName: "Steve", nickName: "admin" })
23 | ).toEqual([ACTION]);
24 | expect(applicableActions(orRules, { firstName: "some" })).toEqual(NO_ACTION);
25 | expect(
26 | applicableActions(orRules, { firstName: "Steve", nickName: "Wonder" })
27 | ).toEqual(NO_ACTION);
28 | });
29 |
30 | test("AND works", () => {
31 | let andRules = [
32 | {
33 | conditions: {
34 | and: [
35 | { or: [{ firstName: "empty" }, { nickName: { is: "admin" } }] },
36 | { age: { is: 21 } },
37 | ],
38 | },
39 | event: ACTION,
40 | },
41 | ];
42 |
43 | expect(applicableActions(andRules, {})).toEqual(NO_ACTION);
44 | expect(applicableActions(andRules, { age: 21 })).toEqual([ACTION]);
45 | expect(applicableActions(andRules, { firstName: "some" })).toEqual(NO_ACTION);
46 | expect(
47 | applicableActions(andRules, { firstName: "Steve", nickName: "Wonder" })
48 | ).toEqual(NO_ACTION);
49 | expect(
50 | applicableActions(andRules, { firstName: "Steve", nickName: "admin" })
51 | ).toEqual(NO_ACTION);
52 | expect(
53 | applicableActions(andRules, {
54 | firstName: "Steve",
55 | nickName: "admin",
56 | age: 21,
57 | })
58 | ).toEqual([ACTION]);
59 | });
60 |
--------------------------------------------------------------------------------
/test/applicableActions.nestedField.test.js:
--------------------------------------------------------------------------------
1 | import applicableActions from "../src/applicableActions";
2 |
3 | let rules = [
4 | {
5 | conditions: {
6 | "address.line": "empty",
7 | },
8 | event: {
9 | type: "remove",
10 | },
11 | },
12 | ];
13 |
14 | test("check nested fields work", function() {
15 | expect(applicableActions(rules, {})).toEqual([{ type: "remove" }]);
16 | expect(applicableActions(rules, { address: { line: "some" } })).toEqual([]);
17 | });
18 |
--------------------------------------------------------------------------------
/test/applicableActions.nestedFieldArray.test.js:
--------------------------------------------------------------------------------
1 | import applicableActions from "../src/applicableActions";
2 |
3 | const DISPLAY_MESSAGE_SIMPLE = {
4 | type: "message",
5 | params: {
6 | validationMessage:
7 | "Get the employees working in microsoft and status in active or paid-leave",
8 | },
9 | };
10 |
11 | let rulesSimple = [
12 | {
13 | conditions: {
14 | and: [
15 | {
16 | and: [
17 | { company: { is: "microsoft" } },
18 | {
19 | or: [
20 | { status: { equal: "paid-leave" } },
21 | { status: { equal: "active" } },
22 | ],
23 | },
24 | ],
25 | },
26 | ],
27 | },
28 | event: DISPLAY_MESSAGE_SIMPLE,
29 | },
30 | ];
31 |
32 | test("check simple json work", function() {
33 | let factsSimple = {
34 | accountId: "Lincoln",
35 | company: "microsoft",
36 | status: "paid-leave",
37 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
38 | };
39 | expect(applicableActions(rulesSimple, factsSimple)).toEqual([
40 | DISPLAY_MESSAGE_SIMPLE,
41 | ]);
42 | factsSimple = {
43 | accountId: "Lincoln",
44 | company: "ibm",
45 | status: "paid-leave",
46 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
47 | };
48 | expect(applicableActions(rulesSimple, factsSimple)).toEqual([]);
49 | });
50 |
51 | const DISPLAY_MESSAGE_NESTED_SIMPLE = {
52 | type: "message",
53 | params: {
54 | validationMessage:
55 | "Get the employees working in microsoft and status in active or paid-leave",
56 | },
57 | };
58 |
59 | let rulesNestedSimple = [
60 | {
61 | conditions: {
62 | and: [
63 | { accountId: { is: "Lincoln" } },
64 | {
65 | and: [
66 | { company: { is: "microsoft" } },
67 | {
68 | or: [
69 | { "status.code.description": { equal: "paid-leave" } },
70 | { "status.code.description": { equal: "active" } },
71 | ],
72 | },
73 | ],
74 | },
75 | ],
76 | },
77 | event: DISPLAY_MESSAGE_NESTED_SIMPLE,
78 | },
79 | ];
80 |
81 | test("check simple nested json work", function() {
82 | let factsNestedSimple = {
83 | accountId: "Lincoln",
84 | company: "microsoft",
85 | status: { code: { description: "paid-leave" } },
86 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
87 | };
88 | expect(applicableActions(rulesNestedSimple, factsNestedSimple)).toEqual([
89 | DISPLAY_MESSAGE_NESTED_SIMPLE,
90 | ]);
91 | factsNestedSimple = {
92 | accountId: "Lincoln",
93 | company: "microsoft",
94 | status: { code: { description: "active" } },
95 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
96 | };
97 | expect(applicableActions(rulesNestedSimple, factsNestedSimple)).toEqual([
98 | DISPLAY_MESSAGE_NESTED_SIMPLE,
99 | ]);
100 | factsNestedSimple = {
101 | accountId: "Lincoln",
102 | company: "microsoft",
103 | status: { code: { description: "off" } },
104 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
105 | };
106 | expect(applicableActions(rulesNestedSimple, factsNestedSimple)).toEqual([]);
107 | });
108 |
109 | const DISPLAY_MESSAGE_NESTED_ARRAY = {
110 | type: "message",
111 | params: {
112 | validationMessage:
113 | "Get the employees working in microsoft and status in active or paid-leave",
114 | },
115 | };
116 |
117 | let rulesNestedArray = [
118 | {
119 | conditions: {
120 | and: [
121 | { accountId: { is: "Lincoln" } },
122 | {
123 | and: [
124 | { company: { is: "microsoft" } },
125 | {
126 | status: {
127 | or: [
128 | { code: { equal: "paid-leave" } },
129 | { code: { equal: "active" } },
130 | ],
131 | },
132 | },
133 | ],
134 | },
135 | ],
136 | },
137 | event: DISPLAY_MESSAGE_NESTED_ARRAY,
138 | },
139 | ];
140 |
141 | test("check simple nested array work", function() {
142 | let factsNestedArray = {
143 | accountId: "Lincoln",
144 | company: "microsoft",
145 | status: [{ code: "paid-leave" }, { code: "active" }],
146 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
147 | };
148 | expect(applicableActions(rulesNestedArray, factsNestedArray)).toEqual([
149 | DISPLAY_MESSAGE_NESTED_ARRAY,
150 | ]);
151 | factsNestedArray = {
152 | accountId: "Jeferryson",
153 | company: "microsoft",
154 | status: [{ code: "paid-leave" }, { code: "active" }],
155 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
156 | };
157 | expect(applicableActions(rulesNestedArray, factsNestedArray)).toEqual([]);
158 | });
159 |
160 | const DISPLAY_MESSAGE_COMPLEX_NESTED_ARRAY = {
161 | type: "message",
162 | params: {
163 | validationMessage:
164 | "Get the employees working in microsoft and status in active or paid-leave",
165 | },
166 | };
167 |
168 | let rulesComplexNestedArray = [
169 | {
170 | conditions: {
171 | Accounts: {
172 | and: [
173 | { accountId: { is: "Lincoln" } },
174 | {
175 | and: [
176 | { company: { is: "microsoft" } },
177 | {
178 | status: {
179 | code: {
180 | or: [
181 | { description: { equal: "paid-leave" } },
182 | { description: { equal: "active" } },
183 | ],
184 | },
185 | },
186 | },
187 | ],
188 | },
189 | ],
190 | },
191 | },
192 | event: DISPLAY_MESSAGE_COMPLEX_NESTED_ARRAY,
193 | },
194 | ];
195 |
196 | test("check nested complex array work", function() {
197 | let factsArrayComplexNestedArray = {
198 | Accounts: [
199 | {
200 | accountId: "Jefferson",
201 | company: "microsoft",
202 | status: [
203 | {
204 | code: [{ description: "paid-leave" }, { description: "half-day" }],
205 | },
206 | { code: [{ description: "full-day" }, { description: "Lop" }] },
207 | ],
208 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
209 | },
210 | {
211 | accountId: "Lincoln",
212 | company: "microsoft",
213 | status: [
214 | {
215 | code: [{ description: "paid-leave" }, { description: "full-day" }],
216 | },
217 | { code: [{ description: "Lop" }, { description: "active" }] },
218 | ],
219 | ptoDaysTaken: ["2016-12-25", "2016-12-21"],
220 | },
221 | ],
222 | };
223 | expect(
224 | applicableActions(rulesComplexNestedArray, factsArrayComplexNestedArray)
225 | ).toEqual([DISPLAY_MESSAGE_COMPLEX_NESTED_ARRAY]);
226 |
227 | factsArrayComplexNestedArray = {
228 | Accounts: [
229 | {
230 | accountId: "Dunken",
231 | company: "microsoft",
232 | status: [
233 | {
234 | code: [{ description: "paid-leave" }, { description: "half-day" }],
235 | },
236 | { code: [{ description: "full-day" }, { description: "Lop" }] },
237 | ],
238 | ptoDaysTaken: ["2016-12-25", "2016-12-28"],
239 | },
240 | {
241 | accountId: "Steve",
242 | company: "microsoft",
243 | status: [
244 | {
245 | code: [{ description: "paid-leave" }, { description: "full-day" }],
246 | },
247 | { code: [{ description: "Lop" }, { description: "Sick Leave" }] },
248 | ],
249 | ptoDaysTaken: ["2016-12-25", "2016-12-21"],
250 | },
251 | ],
252 | };
253 | expect(
254 | applicableActions(rulesComplexNestedArray, factsArrayComplexNestedArray)
255 | ).toEqual([]);
256 | });
257 |
--------------------------------------------------------------------------------
/test/checkField.test.js:
--------------------------------------------------------------------------------
1 | import checkField from "../src/checkField";
2 |
3 | test("single line empty checkField", () => {
4 | expect(checkField("", "empty")).toBeTruthy();
5 | expect(checkField(" ", "empty")).toBeFalsy();
6 | });
7 |
8 | test("single line NOT empty checkField", () => {
9 | expect(checkField("", { not: "empty" })).toBeFalsy();
10 | expect(checkField(" ", { not: "empty" })).toBeTruthy();
11 | });
12 |
13 | test("composite with greater", () => {
14 | expect(checkField(10, { greater: 5 })).toBeTruthy();
15 | expect(checkField(10, { greater: 15 })).toBeFalsy();
16 | });
17 | test("composite with NOT greater", () => {
18 | expect(checkField(10, { not: { greater: 5 } })).toBeFalsy();
19 | expect(checkField(10, { not: { greater: 15 } })).toBeTruthy();
20 | });
21 |
22 | test("AND in > 5 && < 12", () => {
23 | expect(checkField(10, { greater: 5 })).toBeTruthy();
24 | expect(checkField(10, { less: 12 })).toBeTruthy();
25 | expect(checkField(10, { greater: 5, less: 12 })).toBeTruthy();
26 | expect(checkField(15, { greater: 5, less: 12 })).toBeFalsy();
27 | });
28 |
29 | test("NOT with AND in ( > 5 && < 12) ", function() {
30 | expect(checkField(10, { not: { greater: 5 } })).toBeFalsy();
31 | expect(checkField(10, { not: { less: 12 } })).toBeFalsy();
32 | expect(checkField(10, { not: { greater: 5, less: 12 } })).toBeFalsy();
33 | expect(checkField(15, { not: { greater: 5, less: 12 } })).toBeTruthy();
34 | });
35 |
36 | test("OR with < 5 || > 12", () => {
37 | let rule = { or: [{ less: 5 }, { greater: 12 }] };
38 | expect(checkField(1, rule)).toBeTruthy();
39 | expect(checkField(8, rule)).toBeFalsy();
40 | expect(checkField(15, rule)).toBeTruthy();
41 | });
42 |
43 | test("or with array", () => {
44 | let rule = { or: [{ greater: 5, less: 12 }, { greater: 20, less: 30 }] };
45 | expect(checkField(1, rule)).toBeFalsy();
46 | expect(checkField(8, rule)).toBeTruthy();
47 | expect(checkField(15, rule)).toBeFalsy();
48 | expect(checkField(21, rule)).toBeTruthy();
49 | expect(checkField(31, rule)).toBeFalsy();
50 | });
51 |
52 | test("and with array", () => {
53 | let rule = { and: [{ greater: 5, less: 12 }, { greater: 10, less: 30 }] };
54 | expect(checkField(1, rule)).toBeFalsy();
55 | expect(checkField(8, rule)).toBeFalsy();
56 | expect(checkField(15, rule)).toBeFalsy();
57 | expect(checkField(21, rule)).toBeFalsy();
58 | expect(checkField(31, rule)).toBeFalsy();
59 | expect(checkField(11, rule)).toBeTruthy();
60 | });
61 |
62 | test("NOT empty checkField", () => {
63 | expect(checkField("", { not: "empty" })).toBeFalsy();
64 | expect(checkField(" ", { not: "empty" })).toBeTruthy();
65 | });
66 |
67 | test("double negation", () => {
68 | expect(checkField("", { not: { not: "empty" } })).toBeTruthy();
69 | expect(checkField(" ", { not: { not: "empty" } })).toBeFalsy();
70 | });
71 |
72 | test("invalid rule", () => {
73 | expect(checkField(1, { and: { less: 50, greater: 5 } })).toBeFalsy();
74 | expect(checkField(10, { and: { less: 50, greater: 5 } })).toBeFalsy();
75 | expect(checkField(60, { and: { less: 50, greater: 5 } })).toBeFalsy();
76 | expect(checkField(60, { "&": { less: 50, greater: 5 } })).toBeFalsy();
77 | });
78 |
--------------------------------------------------------------------------------
/test/conditionsMeet.nestedField.test.js:
--------------------------------------------------------------------------------
1 | import conditionsMeet from "../src/conditionsMeet";
2 |
3 | let obj = {
4 | medications: [
5 | { type: "A", isLiquid: false },
6 | { type: "B", isLiquid: true },
7 | { type: "C", isLiquid: false },
8 | ],
9 | };
10 |
11 | test("conditions invalid wrong type", function() {
12 | let conditions = {
13 | medications: {
14 | type: { equal: "D" },
15 | },
16 | };
17 |
18 | expect(conditionsMeet(conditions, obj)).toBeFalsy();
19 | });
20 |
21 | test("conditions invalid not liquid", function() {
22 | let conditions = {
23 | medications: {
24 | type: { equal: "A" },
25 | isLiquid: { equal: true },
26 | },
27 | };
28 |
29 | expect(conditionsMeet(conditions, obj)).toBeFalsy();
30 | });
31 |
32 | test("conditions valid just type", function() {
33 | let conditions = {
34 | medications: {
35 | type: { equal: "A" },
36 | },
37 | };
38 |
39 | expect(conditionsMeet(conditions, obj)).toBeTruthy();
40 | });
41 |
42 | test("conditions valid type and liquidity", function() {
43 | let conditions = {
44 | medications: {
45 | type: { equal: "A" },
46 | isLiquid: { equal: false },
47 | },
48 | };
49 |
50 | expect(conditionsMeet(conditions, obj)).toBeTruthy();
51 | });
52 |
--------------------------------------------------------------------------------
/test/conditionsMeet.test.js:
--------------------------------------------------------------------------------
1 | import conditionsMeet from "../src/conditionsMeet";
2 | import { testInProd } from "./utils";
3 |
4 | test("sanity checkField", function() {
5 | expect(() => conditionsMeet("empty", {})).toThrow();
6 | expect(() => conditionsMeet({}, 0)).toThrow();
7 | });
8 |
9 | test("run predicate against array and elements", () => {
10 | let condition = {
11 | options: "empty",
12 | };
13 | expect(conditionsMeet(condition, [""])).toBeTruthy();
14 | expect(conditionsMeet(condition, [])).toBeTruthy();
15 | });
16 |
17 | test("handles array of non-objects", () => {
18 | let condition = {
19 | options: {
20 | contains: "foo",
21 | },
22 | };
23 | expect(conditionsMeet(condition, { options: ["bar"] })).toBeFalsy();
24 | expect(conditionsMeet(condition, { options: [] })).toBeFalsy();
25 | expect(conditionsMeet(condition, { options: ["foo", "bar"] })).toBeTruthy();
26 | });
27 |
28 | // throws error
29 | test("handles array of numbers", () => {
30 | let condition = {
31 | options: {
32 | contains: 2,
33 | },
34 | };
35 | expect(conditionsMeet(condition, { options: [1, 2] })).toBeTruthy();
36 | expect(conditionsMeet(condition, { options: [1] })).toBeFalsy();
37 | expect(conditionsMeet(condition, { options: [] })).toBeFalsy();
38 | });
39 |
40 | test("single line", () => {
41 | let condition = {
42 | firstName: "empty",
43 | };
44 | expect(conditionsMeet(condition, {})).toBeTruthy();
45 | expect(conditionsMeet(condition, { firstName: "some" })).toBeFalsy();
46 | expect(conditionsMeet(condition, { firstName: "" })).toBeTruthy();
47 | expect(conditionsMeet(condition, { firstName: undefined })).toBeTruthy();
48 | });
49 |
50 | test("default use and", () => {
51 | let condition = {
52 | firstName: {
53 | equal: "Will",
54 | },
55 | lastName: {
56 | equal: "Smith",
57 | },
58 | };
59 | expect(conditionsMeet(condition, { firstName: "Will" })).toBeFalsy();
60 | expect(conditionsMeet(condition, { lastName: "Smith" })).toBeFalsy();
61 | expect(
62 | conditionsMeet(condition, { firstName: "Will", lastName: "Smith" })
63 | ).toBeTruthy();
64 | });
65 |
66 | test("NOT condition", () => {
67 | let condition = {
68 | not: {
69 | firstName: {
70 | equal: "Will",
71 | },
72 | },
73 | };
74 | expect(conditionsMeet(condition, { firstName: "Will" })).toBeFalsy();
75 | expect(conditionsMeet(condition, { firstName: "Smith" })).toBeTruthy();
76 | expect(
77 | conditionsMeet(condition, { firstName: "Will", lastName: "Smith" })
78 | ).toBeFalsy();
79 | });
80 |
81 | test("invalid condition", () => {
82 | expect(() => conditionsMeet("empty", {})).toThrow();
83 | expect(() => conditionsMeet({}, "empty")).toThrow();
84 | expect(testInProd(() => conditionsMeet("empty", {}))).toBeFalsy();
85 | expect(testInProd(() => conditionsMeet({}, "empty"))).toBeFalsy();
86 | });
87 |
--------------------------------------------------------------------------------
/test/conditionsMeet.toRelCondition.test.js:
--------------------------------------------------------------------------------
1 | import { toRelCondition } from "../src/conditionsMeet";
2 |
3 | test("rel simple condition", () => {
4 | expect(toRelCondition({ less: "$b" }, { b: 11 })).toEqual({ less: 11 });
5 | });
6 |
7 | test("rel complicated condition", () => {
8 | let condition = {
9 | decreasedByMoreThanPercent: {
10 | average: "$averages_monthly.cost",
11 | target: 20,
12 | },
13 | };
14 |
15 | let formData = {
16 | averages_monthly: { cost: 100 },
17 | };
18 |
19 | let expCondition = {
20 | decreasedByMoreThanPercent: {
21 | average: 100,
22 | target: 20,
23 | },
24 | };
25 |
26 | expect(toRelCondition(condition, formData)).toEqual(expCondition);
27 | });
28 |
29 | test("work with OR condition", () => {
30 | let cond = { or: [{ lessEq: "$b" }, { greaterEq: "$c" }] };
31 | let formData = { b: 16, c: 70 };
32 | let expCond = { or: [{ lessEq: 16 }, { greaterEq: 70 }] };
33 | expect(toRelCondition(cond, formData)).toEqual(expCond);
34 | });
35 |
36 | test("keep non relevant", () => {
37 | expect(toRelCondition({ range: [20, 40] }, {})).toEqual({ range: [20, 40] });
38 | });
39 |
--------------------------------------------------------------------------------
/test/documentationExamples.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../src/Engine";
2 |
3 | let EVENT = {
4 | type: "remove",
5 | params: {
6 | field: "password",
7 | },
8 | };
9 |
10 | let schema = {
11 | definitions: {
12 | hobby: {
13 | type: "object",
14 | properties: {
15 | name: { type: "string" },
16 | durationInMonth: { type: "integer" },
17 | },
18 | },
19 | },
20 | title: "A registration form",
21 | description: "A simple form example.",
22 | type: "object",
23 | required: ["firstName", "lastName"],
24 | properties: {
25 | firstName: {
26 | type: "string",
27 | title: "First name",
28 | },
29 | lastName: {
30 | type: "string",
31 | title: "Last name",
32 | },
33 | age: {
34 | type: "integer",
35 | title: "Age",
36 | },
37 | bio: {
38 | type: "string",
39 | title: "Bio",
40 | },
41 | country: {
42 | type: "string",
43 | title: "Country",
44 | },
45 | state: {
46 | type: "string",
47 | title: "State",
48 | },
49 | zip: {
50 | type: "string",
51 | title: "ZIP",
52 | },
53 | password: {
54 | type: "string",
55 | title: "Password",
56 | minLength: 3,
57 | },
58 | telephone: {
59 | type: "string",
60 | title: "Telephone",
61 | minLength: 10,
62 | },
63 | work: { $ref: "#/definitions/hobby" },
64 | hobbies: {
65 | type: "array",
66 | items: { $ref: "#/definitions/hobby" },
67 | },
68 | },
69 | };
70 |
71 | test("first example", () => {
72 | let rules = [
73 | {
74 | conditions: {
75 | firstName: "empty",
76 | },
77 | event: EVENT,
78 | },
79 | ];
80 |
81 | let engine = new Engine(rules, schema);
82 | expect.assertions(5);
83 |
84 | return Promise.all([
85 | engine.run({}).then(res => expect(res).toEqual([EVENT])),
86 | engine.run({ firstName: null }).then(res => expect(res).toEqual([EVENT])),
87 | engine.run({ firstName: "" }).then(res => expect(res).toEqual([EVENT])),
88 | engine.run({ firstName: " " }).then(res => expect(res).toEqual([])),
89 | engine.run({ firstName: "some" }).then(res => expect(res).toEqual([])),
90 | ]);
91 | });
92 |
93 | test("Conditionals with arguments", () => {
94 | let rules = [
95 | {
96 | conditions: {
97 | age: { less: 16 },
98 | },
99 | event: EVENT,
100 | },
101 | ];
102 |
103 | let engine = new Engine(rules, schema);
104 | expect.assertions(5);
105 |
106 | return Promise.all([
107 | engine.run({}).then(res => expect(res).toEqual([])),
108 | engine.run({ age: null }).then(res => expect(res).toEqual([EVENT])),
109 | engine.run({ age: 15 }).then(res => expect(res).toEqual([EVENT])),
110 | engine.run({ age: 16 }).then(res => expect(res).toEqual([])),
111 | engine.run({ age: 21 }).then(res => expect(res).toEqual([])),
112 | ]);
113 | });
114 |
115 | test("AND", () => {
116 | let rules = [
117 | {
118 | conditions: {
119 | age: {
120 | greater: 16,
121 | less: 70,
122 | },
123 | },
124 | event: EVENT,
125 | },
126 | ];
127 |
128 | let engine = new Engine(rules, schema);
129 | expect.assertions(4);
130 |
131 | return Promise.all([
132 | engine.run({ age: 16 }).then(res => expect(res).toEqual([])),
133 | engine.run({ age: 17 }).then(res => expect(res).toEqual([EVENT])),
134 | engine.run({ age: 69 }).then(res => expect(res).toEqual([EVENT])),
135 | engine.run({ age: 70 }).then(res => expect(res).toEqual([])),
136 | ]);
137 | });
138 |
139 | test("NOT", () => {
140 | let rules = [
141 | {
142 | conditions: {
143 | age: {
144 | not: {
145 | greater: 16,
146 | less: 70,
147 | },
148 | },
149 | },
150 | event: EVENT,
151 | },
152 | ];
153 |
154 | let engine = new Engine(rules, schema);
155 | expect.assertions(4);
156 |
157 | return Promise.all([
158 | engine.run({ age: 16 }).then(res => expect(res).toEqual([EVENT])),
159 | engine.run({ age: 17 }).then(res => expect(res).toEqual([])),
160 | engine.run({ age: 69 }).then(res => expect(res).toEqual([])),
161 | engine.run({ age: 70 }).then(res => expect(res).toEqual([EVENT])),
162 | ]);
163 | });
164 |
165 | test("OR", () => {
166 | let rules = [
167 | {
168 | conditions: {
169 | age: {
170 | or: [{ lessEq: 16 }, { greaterEq: 70 }],
171 | },
172 | },
173 | event: EVENT,
174 | },
175 | ];
176 |
177 | let engine = new Engine(rules, schema);
178 | expect.assertions(4);
179 |
180 | return Promise.all([
181 | engine.run({ age: 16 }).then(res => expect(res).toEqual([EVENT])),
182 | engine.run({ age: 17 }).then(res => expect(res).toEqual([])),
183 | engine.run({ age: 69 }).then(res => expect(res).toEqual([])),
184 | engine.run({ age: 70 }).then(res => expect(res).toEqual([EVENT])),
185 | ]);
186 | });
187 |
188 | test("multi field default AND", () => {
189 | let rules = [
190 | {
191 | conditions: {
192 | age: { less: 70 },
193 | country: { is: "USA" },
194 | },
195 | event: EVENT,
196 | },
197 | ];
198 |
199 | let engine = new Engine(rules, schema);
200 | expect.assertions(5);
201 |
202 | return Promise.all([
203 | engine
204 | .run({ age: 16, country: "China" })
205 | .then(res => expect(res).toEqual([])),
206 | engine
207 | .run({ age: 16, country: "Mexico" })
208 | .then(res => expect(res).toEqual([])),
209 | engine
210 | .run({ age: 16, country: "USA" })
211 | .then(res => expect(res).toEqual([EVENT])),
212 | engine
213 | .run({ age: 69, country: "USA" })
214 | .then(res => expect(res).toEqual([EVENT])),
215 | engine
216 | .run({ age: 70, country: "USA" })
217 | .then(res => expect(res).toEqual([])),
218 | ]);
219 | });
220 |
221 | test("multi field OR", () => {
222 | let rules = [
223 | {
224 | conditions: {
225 | or: [
226 | {
227 | age: { less: 70 },
228 | country: { is: "USA" },
229 | },
230 | {
231 | state: { is: "NY" },
232 | },
233 | ],
234 | },
235 | event: EVENT,
236 | },
237 | ];
238 |
239 | let engine = new Engine(rules, schema);
240 | expect.assertions(5);
241 |
242 | return Promise.all([
243 | engine
244 | .run({ age: 16, country: "China", state: "Beijing" })
245 | .then(res => expect(res).toEqual([])),
246 | engine
247 | .run({ age: 16, country: "China", state: "NY" })
248 | .then(res => expect(res).toEqual([EVENT])),
249 | engine
250 | .run({ age: 16, country: "USA" })
251 | .then(res => expect(res).toEqual([EVENT])),
252 | engine
253 | .run({ age: 80, state: "NY" })
254 | .then(res => expect(res).toEqual([EVENT])),
255 | engine
256 | .run({ age: 69, country: "USA" })
257 | .then(res => expect(res).toEqual([EVENT])),
258 | ]);
259 | });
260 |
261 | test("multi field NOT", () => {
262 | let rules = [
263 | {
264 | conditions: {
265 | not: {
266 | or: [
267 | {
268 | age: { less: 70 },
269 | country: { is: "USA" },
270 | },
271 | {
272 | state: { is: "NY" },
273 | },
274 | ],
275 | },
276 | },
277 | event: EVENT,
278 | },
279 | ];
280 |
281 | let engine = new Engine(rules, schema);
282 | expect.assertions(5);
283 |
284 | return Promise.all([
285 | engine
286 | .run({ age: 16, country: "China", state: "Beijing" })
287 | .then(res => expect(res).toEqual([EVENT])),
288 | engine
289 | .run({ age: 16, country: "China", state: "NY" })
290 | .then(res => expect(res).toEqual([])),
291 | engine
292 | .run({ age: 16, country: "USA" })
293 | .then(res => expect(res).toEqual([])),
294 | engine.run({ age: 80, state: "NY" }).then(res => expect(res).toEqual([])),
295 | engine
296 | .run({ age: 69, country: "USA" })
297 | .then(res => expect(res).toEqual([])),
298 | ]);
299 | });
300 |
301 | test("Nested object queries", () => {
302 | let rules = [
303 | {
304 | conditions: {
305 | "work.name": { is: "congressman" },
306 | },
307 | event: EVENT,
308 | },
309 | ];
310 |
311 | let engine = new Engine(rules, schema);
312 | expect.assertions(5);
313 |
314 | return Promise.all([
315 | engine.run({ work: {} }).then(res => expect(res).toEqual([])),
316 | engine.run({}).then(res => expect(res).toEqual([])),
317 | engine
318 | .run({ work: { name: "congressman" } })
319 | .then(res => expect(res).toEqual([EVENT])),
320 | engine
321 | .run({ work: { name: "president" } })
322 | .then(res => expect(res).toEqual([])),
323 | engine
324 | .run({ work: { name: "blacksmith" } })
325 | .then(res => expect(res).toEqual([])),
326 | ]);
327 | });
328 |
329 | test("Nested arrays object queries", () => {
330 | let rules = [
331 | {
332 | conditions: {
333 | hobbies: {
334 | name: { is: "baseball" },
335 | },
336 | },
337 | event: EVENT,
338 | },
339 | ];
340 |
341 | let engine = new Engine(rules, schema);
342 | expect.assertions(5);
343 |
344 | return Promise.all([
345 | engine.run({ hobbies: [] }).then(res => expect(res).toEqual([])),
346 | engine.run({}).then(res => expect(res).toEqual([])),
347 | engine
348 | .run({ hobbies: [{ name: "baseball" }] })
349 | .then(res => expect(res).toEqual([EVENT])),
350 | engine
351 | .run({
352 | hobbies: [
353 | { name: "reading" },
354 | { name: "jumping" },
355 | { name: "baseball" },
356 | ],
357 | })
358 | .then(res => expect(res).toEqual([EVENT])),
359 | engine
360 | .run({ hobbies: [{ name: "reading" }, { name: "jumping" }] })
361 | .then(res => expect(res).toEqual([])),
362 | ]);
363 | });
364 |
--------------------------------------------------------------------------------
/test/issues/10.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | listInvalidPredicates,
3 | validateConditionFields,
4 | validatePredicates,
5 | } from "../../src/validation";
6 |
7 | let rules = [
8 | {
9 | title: "Rule #2",
10 | description:
11 | "This hides Address, Email, Gender and the Password fields until First Name and Last Name have a value",
12 | conditions: {
13 | and: [
14 | {
15 | or: [
16 | { "registration.firstName": "empty" },
17 | { "registration.lastName": "empty" },
18 | ],
19 | },
20 | ],
21 | },
22 | event: {
23 | type: "remove",
24 | params: {
25 | field: [
26 | "address",
27 | "registration.gender",
28 | "registration.email",
29 | "registration.password",
30 | "registration.confirmPassword",
31 | ],
32 | },
33 | },
34 | },
35 | ];
36 |
37 | let schema = {
38 | type: "object",
39 | properties: {
40 | registration: {
41 | type: "object",
42 | properties: {
43 | firstName: { type: "string" },
44 | lastName: { type: "string" },
45 | gender: {
46 | type: "string",
47 | enum: ["Male", "Female"],
48 | },
49 | dob: { type: "string" },
50 | email: { type: "string" },
51 | password: { type: "string" },
52 | confirmPassword: { type: "string" },
53 | },
54 | required: [
55 | "firstName",
56 | "lastName",
57 | "email",
58 | "password",
59 | "confirmPassword",
60 | ],
61 | },
62 | address: {
63 | type: "object",
64 | properties: {
65 | streetNo: { type: "string" },
66 | street: { type: "string" },
67 | suburb: { type: "string" },
68 | postCode: { type: "string" },
69 | state: {
70 | type: "string",
71 | enum: ["SA", "WA", "NSW", "VIC", "TAS", "ACT", "QLD", "NT"],
72 | },
73 | country: {
74 | type: "string",
75 | enum: [],
76 | },
77 | propertyDescription: {
78 | type: "string",
79 | },
80 | },
81 | },
82 | },
83 | };
84 |
85 | test("#10 validation of predicates", () => {
86 | let conditions = rules.map(({ conditions }) => conditions);
87 | expect(listInvalidPredicates(conditions, schema)).toEqual([]);
88 | expect(validatePredicates(conditions, schema)).toBeUndefined();
89 | expect(validateConditionFields(conditions, schema)).toBeUndefined();
90 | });
91 |
--------------------------------------------------------------------------------
/test/issues/12.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../../src/Engine";
2 |
3 | const ADDRESS_SCHEMA = {
4 | type: "object",
5 | properties: {
6 | zip: { type: "string" },
7 | city: { type: "string" },
8 | street: { type: "string" },
9 | flat: { type: "string" },
10 | },
11 | };
12 |
13 | const SCHEMA = {
14 | definitions: {
15 | address: ADDRESS_SCHEMA,
16 | },
17 | type: "object",
18 | properties: {
19 | homeAddress: {
20 | $ref: "#/definitions/address",
21 | },
22 | workAddress: ADDRESS_SCHEMA,
23 | favFoodLocations: {
24 | type: "array",
25 | items: {
26 | $ref: "#/definitions/address",
27 | },
28 | },
29 | favoritePlaces: {
30 | type: "array",
31 | items: ADDRESS_SCHEMA,
32 | },
33 | },
34 | };
35 |
36 | let engine = new Engine([], SCHEMA);
37 |
38 | test("invalidates ref object", () => {
39 | expect(() =>
40 | engine.addRule({
41 | conditions: {
42 | "homeAddress.home": { is: "true" },
43 | },
44 | })
45 | ).toThrow();
46 | });
47 |
48 | test("invalidates embedded object", () => {
49 | expect(() =>
50 | engine.addRule({
51 | conditions: {
52 | "workAddress.home": { is: "true" },
53 | },
54 | })
55 | ).toThrow();
56 | });
57 |
58 | test("invalidates array object", () => {
59 | expect(() =>
60 | engine.addRule({
61 | conditions: {
62 | "favFoodLocations.home": { is: "true" },
63 | },
64 | })
65 | ).toThrow();
66 | });
67 |
68 | test("invalidates array with $ref object", () => {
69 | expect(() =>
70 | engine.addRule({
71 | conditions: {
72 | "favoritePlaces.home": { is: "true" },
73 | },
74 | })
75 | ).toThrow();
76 | });
77 |
78 | test("Validates ref object", () => {
79 | expect(
80 | engine.addRule({
81 | conditions: {
82 | "homeAddress.zip": { is: "true" },
83 | },
84 | })
85 | ).toBeUndefined();
86 | });
87 |
88 | test("Validates embedded object", () => {
89 | expect(
90 | engine.addRule({
91 | conditions: {
92 | "workAddress.zip": { is: "true" },
93 | },
94 | })
95 | ).toBeUndefined();
96 | });
97 |
98 | test("Validates array object", () => {
99 | expect(
100 | engine.addRule({
101 | conditions: {
102 | "favFoodLocations.zip": { is: "true" },
103 | },
104 | })
105 | ).toBeUndefined();
106 | });
107 |
108 | test("Validates array with $ref object", () => {
109 | expect(
110 | engine.addRule({
111 | conditions: {
112 | "favoritePlaces.zip": { is: "true" },
113 | },
114 | })
115 | ).toBeUndefined();
116 | });
117 |
--------------------------------------------------------------------------------
/test/issues/14.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../../src";
2 |
3 | test("simple relevant rules work", () => {
4 | let rules = [
5 | {
6 | conditions: {
7 | a: { less: "$b" },
8 | },
9 | event: {
10 | type: "match",
11 | },
12 | },
13 | ];
14 | let engine = new Engine(rules);
15 | return engine.run({ a: 10, b: 11 }).then(events => {
16 | expect(events.length).toEqual(1);
17 | expect(events[0]).toEqual({ type: "match" });
18 | });
19 | });
20 |
21 | test("complicated rules work", () => {
22 | let rules = [
23 | {
24 | conditions: {
25 | a: { or: [{ less: "$b" }] },
26 | },
27 | event: {
28 | type: "match",
29 | },
30 | },
31 | ];
32 | let engine = new Engine(rules);
33 | return engine.run({ a: 10, b: 11 }).then(events => {
34 | expect(events.length).toEqual(1);
35 | expect(events[0]).toEqual({ type: "match" });
36 | });
37 | });
38 |
39 | test("validation rel fields work", () => {
40 | let rules = [
41 | {
42 | conditions: {
43 | a: { less: "$b" },
44 | },
45 | event: "some",
46 | },
47 | ];
48 |
49 | let invSchema = {
50 | type: "object",
51 | properties: {
52 | a: { type: "object" },
53 | },
54 | };
55 |
56 | expect(() => new Engine(rules, invSchema)).toThrow();
57 |
58 | let valSchema = {
59 | type: "object",
60 | properties: {
61 | a: { type: "object" },
62 | b: { type: "number" },
63 | },
64 | };
65 |
66 | expect(() => new Engine(rules, valSchema)).not.toBeUndefined();
67 | });
68 |
--------------------------------------------------------------------------------
/test/issues/15.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../../src";
2 | import { listInvalidFields } from "../../src/validation";
3 |
4 | test("support $ single level of nesting", () => {
5 | let rules = [
6 | {
7 | conditions: {
8 | address$zip: { less: 1000 },
9 | },
10 | event: {
11 | type: "match",
12 | },
13 | },
14 | ];
15 | let engine = new Engine(rules);
16 | return engine.run({ address: { zip: 10 } }).then(events => {
17 | expect(events.length).toEqual(1);
18 | expect(events[0]).toEqual({ type: "match" });
19 | });
20 | });
21 |
22 | test("support $ double level of nesting", () => {
23 | let rules = [
24 | {
25 | conditions: {
26 | person$address$zip: { less: 1000 },
27 | },
28 | event: {
29 | type: "match",
30 | },
31 | },
32 | ];
33 | let engine = new Engine(rules);
34 | return engine.run({ person: { address: { zip: 10 } } }).then(events => {
35 | expect(events.length).toEqual(1);
36 | expect(events[0]).toEqual({ type: "match" });
37 | });
38 | });
39 |
40 | test("support $ during validation", () => {
41 | let schema = {
42 | type: "object",
43 | properties: {
44 | address: {
45 | type: "object",
46 | properties: {
47 | zip: { type: "number" },
48 | },
49 | },
50 | },
51 | };
52 | let conditions = [
53 | {
54 | address$zip: { less: 1000 },
55 | },
56 | ];
57 | expect(listInvalidFields(conditions, schema)).toEqual([]);
58 | });
59 |
--------------------------------------------------------------------------------
/test/issues/22.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../../src";
2 |
3 | let rules = [
4 | {
5 | conditions: {
6 | "arr[1].foo": { equal: true },
7 | },
8 | event: "some",
9 | },
10 | {
11 | conditions: {
12 | "arr[0].foo": { equal: true },
13 | },
14 | event: "what",
15 | },
16 | ];
17 | let engine = new Engine(rules);
18 |
19 | test("support array element reference first true", () => {
20 | return engine.run({ arr: [{ foo: true }, { foo: false }] }).then(events => {
21 | expect(events).toEqual(["what"]);
22 | });
23 | });
24 |
25 | test("support array element reference second true", () => {
26 | return engine.run({ arr: [{ foo: false }, { foo: true }] }).then(events => {
27 | expect(events).toEqual(["some"]);
28 | });
29 | });
30 |
31 | test("support array element both true", () => {
32 | return engine.run({ arr: [{ foo: true }, { foo: true }] }).then(events => {
33 | expect(events).toEqual(["some", "what"]);
34 | });
35 | });
36 |
37 | test("support array element both false", () => {
38 | return engine.run({ arr: [{ foo: false }, { foo: false }] }).then(events => {
39 | expect(events).toEqual([]);
40 | });
41 | });
42 |
--------------------------------------------------------------------------------
/test/issues/react-jsonschema-form-conditions/59.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../../../src/index";
2 |
3 | let rulesWithTwoEvents = {
4 | conditions: {
5 | hasBenefitsReference: { is: true },
6 | },
7 | event: [
8 | {
9 | type: "require",
10 | params: {
11 | field: "hasBD2Reference",
12 | },
13 | },
14 | {
15 | type: "require",
16 | params: {
17 | field: "BD2Reference",
18 | },
19 | },
20 | ],
21 | };
22 |
23 | let rulesWithSingleEvent = {
24 | conditions: {
25 | hasBenefitsReference: { is: true },
26 | },
27 | event: [
28 | {
29 | type: "require",
30 | params: {
31 | field: ["hasBD2Reference", "BD2Reference"],
32 | },
33 | },
34 | ],
35 | };
36 |
37 | const schema = {
38 | type: "object",
39 | properties: {
40 | hasBenefitsReference: {
41 | title: "Do you have a Benefits Reference Number?",
42 | type: "boolean",
43 | },
44 | benefitsReference: {
45 | title: "Benefits Reference Number",
46 | type: "string",
47 | },
48 | hasBD2Reference: {
49 | title: "Do you have a BD2 Number?",
50 | type: "boolean",
51 | },
52 | BD2Reference: {
53 | title: "BD2 Number",
54 | type: "string",
55 | },
56 | },
57 | };
58 |
59 | test("creation with two events on creation", () => {
60 | let engine = new Engine([rulesWithTwoEvents], schema);
61 |
62 | return engine.run({ hasBenefitsReference: true }).then(events => {
63 | expect(events.length).toEqual(2);
64 | expect(events).toEqual(rulesWithTwoEvents.event);
65 | });
66 | });
67 |
68 | test("creation with two events on add", () => {
69 | let engine = new Engine([], schema);
70 |
71 | engine.addRule(rulesWithTwoEvents);
72 |
73 | return engine.run({ hasBenefitsReference: true }).then(events => {
74 | expect(events.length).toEqual(2);
75 | expect(events).toEqual(rulesWithTwoEvents.event);
76 | });
77 | });
78 |
79 | test("creation with single event on creatin", () => {
80 | let engine = new Engine([rulesWithSingleEvent], schema);
81 |
82 | return engine.run({ hasBenefitsReference: true }).then(events => {
83 | expect(events.length).toEqual(1);
84 | expect(events).toEqual(rulesWithSingleEvent.event);
85 | });
86 | });
87 |
88 | test("creation with single event on add", () => {
89 | let engine = new Engine([], schema);
90 |
91 | engine.addRule(rulesWithSingleEvent);
92 |
93 | return engine.run({ hasBenefitsReference: true }).then(events => {
94 | expect(events.length).toEqual(1);
95 | expect(events).toEqual(rulesWithSingleEvent.event);
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/test/predicate.test.js:
--------------------------------------------------------------------------------
1 | import predicate from "predicate";
2 | import Engine from "../src/Engine";
3 |
4 | test("equal work with same strings", function() {
5 | expect(predicate.eq("Will", "Will")).toBeTruthy();
6 | expect(predicate.eq("Will", "1Will")).toBeFalsy();
7 | });
8 |
9 | test("work with empty", function() {
10 | expect(predicate.empty("")).toBeTruthy();
11 | expect(predicate.empty(undefined)).toBeTruthy();
12 | expect(predicate.empty(null)).toBeTruthy();
13 | });
14 |
15 | predicate.range = predicate.curry((val, range) => {
16 | return (
17 | predicate.num(val) &&
18 | predicate.array(range) &&
19 | predicate.equal(range.length, 2) &&
20 | predicate.greaterEq(val, range[0]) &&
21 | predicate.lessEq(val, range[1])
22 | );
23 | });
24 |
25 | let engine = new Engine([
26 | {
27 | conditions: { age: { range: [20, 40] } },
28 | event: "hit",
29 | },
30 | ]);
31 |
32 | test("not in range left", () => {
33 | return engine.run({ age: 10 }).then(events => expect(events).toEqual([]));
34 | });
35 |
36 | test("in range", () => {
37 | return engine
38 | .run({ age: 30 })
39 | .then(events => expect(events).toEqual(["hit"]));
40 | });
41 |
42 | test("not in range right", () => {
43 | return engine.run({ age: 50 }).then(events => expect(events).toEqual([]));
44 | });
45 |
--------------------------------------------------------------------------------
/test/selectn.test.js:
--------------------------------------------------------------------------------
1 | import selectn from "selectn";
2 |
3 | test("selectn on array", function() {
4 | let a = {
5 | medications: {
6 | type: "A",
7 | },
8 | };
9 |
10 | expect(selectn("medications.type", a)).toEqual("A");
11 |
12 | // let obj = {
13 | // medications: [
14 | // { type: "A" },
15 | // { type: "B" },
16 | // { type: "C" }
17 | // ]
18 | // };
19 | //expect(selectn("medications.type", obj)).toEqual(["A", "B", "C"]);
20 | });
21 |
--------------------------------------------------------------------------------
/test/utils.js:
--------------------------------------------------------------------------------
1 | export function testInProd(f) {
2 | process.env.NODE_ENV = "production";
3 | let res = f();
4 | process.env.NODE_ENV = "test";
5 | return res;
6 | }
7 |
--------------------------------------------------------------------------------
/test/utils.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | flatMap,
3 | isObject,
4 | isDevelopment,
5 | toError,
6 | extractRefSchema,
7 | isRefArray,
8 | toArray,
9 | selectRef,
10 | } from "../src/utils";
11 | import { testInProd } from "./utils";
12 |
13 | test("array flatmap", () => {
14 | expect(flatMap([[1, 2], [3], [4, 5]], x => x)).toEqual([1, 2, 3, 4, 5]);
15 | });
16 |
17 | test("isObject", () => {
18 | expect(isObject(undefined)).toBeFalsy();
19 | expect(isObject("")).toBeFalsy();
20 | expect(isObject(null)).toBeFalsy();
21 | expect(isObject(1)).toBeFalsy();
22 | expect(isObject({})).toBeTruthy();
23 | });
24 |
25 | test("isProduction", () => {
26 | expect(isDevelopment()).toBeTruthy();
27 | expect(testInProd(() => isDevelopment())).toBeFalsy();
28 | });
29 |
30 | test("error throws exception", () => {
31 | expect(() => toError("Yes")).toThrow();
32 | expect(testInProd(() => toError("Yes"))).toBeUndefined();
33 | });
34 |
35 | test("extract referenced schema", () => {
36 | let schema = {
37 | definitions: {
38 | medication: {
39 | type: "object",
40 | properties: {
41 | type: { type: "string" },
42 | isLiquid: { type: "boolean" },
43 | },
44 | },
45 | },
46 | type: "object",
47 | required: ["medications", "firstName", "lastName"],
48 | properties: {
49 | firstName: {
50 | type: "string",
51 | },
52 | lastName: {
53 | type: "string",
54 | },
55 | medications: {
56 | type: "array",
57 | items: { $ref: "#/definitions/medication" },
58 | },
59 | primaryMedication: {
60 | $ref: "#/definitions/medication",
61 | },
62 | externalConfig: {
63 | $ref: "http://example.com/oneschema.json",
64 | },
65 | registration: {
66 | type: "object",
67 | properties: {
68 | firstName: { type: "string" },
69 | lastName: { type: "string" },
70 | },
71 | },
72 | },
73 | };
74 |
75 | let { definitions: { medication }, properties: { registration } } = schema;
76 |
77 | expect(isRefArray("medications", schema)).toBeTruthy();
78 | expect(extractRefSchema("medications", schema)).toEqual(medication);
79 | expect(extractRefSchema("primaryMedication", schema)).toEqual(medication);
80 | expect(extractRefSchema("registration", schema)).toEqual(registration);
81 |
82 | expect(() => extractRefSchema("externalConfig", schema)).toThrow();
83 | expect(
84 | testInProd(() => extractRefSchema("externalConfig", schema))
85 | ).toBeUndefined();
86 |
87 | expect(() => extractRefSchema("lastName", schema)).toThrow();
88 | expect(
89 | testInProd(() => extractRefSchema("lastName", schema))
90 | ).toBeUndefined();
91 | });
92 |
93 | test("array transformation", () => {
94 | expect(toArray("Yes")).toEqual(["Yes"]);
95 | expect(toArray(["Yes", "No"])).toEqual(["Yes", "No"]);
96 | });
97 |
98 | test("select reference", () => {
99 | expect(selectRef("address.zip", { address: { zip: 1000 } })).toEqual(1000);
100 | expect(selectRef("address$zip", { address: { zip: 1000 } })).toEqual(1000);
101 | });
102 |
--------------------------------------------------------------------------------
/test/validation.nestedFields.test.js:
--------------------------------------------------------------------------------
1 | import Engine from "../src/Engine";
2 | import { listAllPredicates } from "../src/validation";
3 |
4 | let rules = [
5 | {
6 | conditions: {
7 | medications: {
8 | type: { is: "D" },
9 | },
10 | "primaryMedication.type": { equal: "C" },
11 | },
12 | event: {
13 | type: "remove",
14 | },
15 | },
16 | ];
17 |
18 | let schema = {
19 | definitions: {
20 | medications: {
21 | type: "object",
22 | properties: {
23 | type: { type: "string" },
24 | isLiquid: { type: "boolean" },
25 | },
26 | },
27 | },
28 | type: "object",
29 | required: ["medications", "firstName", "lastName"],
30 | properties: {
31 | firstName: {
32 | type: "string",
33 | },
34 | lastName: {
35 | type: "string",
36 | },
37 | medications: {
38 | type: "array",
39 | items: { $ref: "#/definitions/medications" },
40 | },
41 | primaryMedication: {
42 | $ref: "#/definitions/medications",
43 | },
44 | registration: {
45 | type: "object",
46 | properties: {
47 | firstName: { type: "string" },
48 | lastName: { type: "string" },
49 | },
50 | },
51 | },
52 | };
53 |
54 | test("list all predicates", () => {
55 | expect(listAllPredicates(rules.map(r => r.conditions), schema)).toEqual([
56 | "is",
57 | "equal",
58 | ]);
59 | });
60 |
61 | test("valid rules", () => {
62 | expect(new Engine(rules, schema)).not.toBeUndefined();
63 |
64 | let engine = new Engine(rules, schema);
65 | return engine
66 | .run({ primaryMedication: { type: "C" }, medications: [{ type: "D" }] })
67 | .then(event => expect(event.length).toEqual(1));
68 | });
69 |
--------------------------------------------------------------------------------
/test/validation.predicates.test.js:
--------------------------------------------------------------------------------
1 | import predicate from "predicate";
2 | import { listInvalidPredicates } from "../src/validation";
3 |
4 | let schema = {
5 | type: "object",
6 | properties: {
7 | firstName: { type: "string" },
8 | },
9 | };
10 |
11 | test("Check predicates", () => {
12 | const conditions = [{ firstName: "somePredicate" }];
13 |
14 | expect(listInvalidPredicates(conditions, schema)).toEqual(["somePredicate"]);
15 |
16 | predicate.somePredicate = function() {
17 | return false;
18 | };
19 |
20 | expect(listInvalidPredicates(conditions, schema)).toEqual([]);
21 | });
22 |
--------------------------------------------------------------------------------
/test/validation.ref.test.js:
--------------------------------------------------------------------------------
1 | import { predicatesFromCondition } from "../src/validation";
2 | import { testInProd } from "./utils";
3 |
4 | let schema = {
5 | definitions: {
6 | medication: {
7 | type: "object",
8 | properties: {
9 | type: { type: "string" },
10 | isLiquid: { type: "boolean" },
11 | },
12 | },
13 | },
14 | type: "object",
15 | required: ["medications", "firstName", "lastName"],
16 | properties: {
17 | firstName: {
18 | type: "string",
19 | },
20 | lastName: {
21 | type: "string",
22 | },
23 | medications: {
24 | type: "array",
25 | items: { $ref: "#/definitions/medication" },
26 | },
27 | primaryMedication: {
28 | $ref: "#/definitions/medication",
29 | },
30 | externalConfig: {
31 | $ref: "http://example.com/oneschema.json",
32 | },
33 | invalidArrayRef: {
34 | type: "array",
35 | items: {
36 | $ref: "http://example.com/oneschema.json",
37 | },
38 | },
39 | },
40 | };
41 |
42 | test("condition with external ref", () => {
43 | expect(() =>
44 | predicatesFromCondition({ "externalConfig.name": "empty" }, schema)
45 | ).toThrow();
46 | expect(
47 | testInProd(() =>
48 | predicatesFromCondition({ "externalConfig.name": "empty" }, schema)
49 | )
50 | ).toEqual([]);
51 | });
52 |
53 | test("array condition with external ref", () => {
54 | expect(() =>
55 | predicatesFromCondition({ "invalidArrayRef.name": "empty" }, schema)
56 | ).toThrow();
57 | expect(
58 | testInProd(() =>
59 | predicatesFromCondition({ "invalidArrayRef.name": "empty" }, schema)
60 | )
61 | ).toEqual([]);
62 | });
63 |
64 | test("condition with fake ref field", () => {
65 | expect(() =>
66 | predicatesFromCondition({ "fakeRef.name": "empty" }, schema)
67 | ).toThrow();
68 | expect(
69 | testInProd(() =>
70 | predicatesFromCondition({ "fakeRef.name": "empty" }, schema)
71 | )
72 | ).toEqual([]);
73 | });
74 |
75 | test("condition with fake field", () => {
76 | expect(() => predicatesFromCondition({ fakeRef: "empty" }, schema)).toThrow();
77 | expect(
78 | testInProd(() => predicatesFromCondition({ fakeRef: "empty" }, schema))
79 | ).toEqual([]);
80 | });
81 |
--------------------------------------------------------------------------------
/test/validation.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | listAllFields,
3 | listAllPredicates,
4 | listInvalidFields,
5 | listInvalidPredicates,
6 | predicatesFromCondition,
7 | predicatesFromRule,
8 | validatePredicates,
9 | validateConditionFields,
10 | } from "../src/validation";
11 | import { testInProd } from "./utils";
12 |
13 | function conditionsFrom(rules) {
14 | return rules.map(({ conditions }) => conditions);
15 | }
16 |
17 | let defSchema = {
18 | properties: {
19 | firstName: { type: "string" },
20 | password: { type: "string" },
21 | age: { type: "integer" },
22 | },
23 | };
24 |
25 | test("Check predicates", () => {
26 | const conditions = conditionsFrom([
27 | { conditions: { firstName: "epty" } },
28 | { conditions: { age: { greater: 10 } } },
29 | { conditions: { age: { less: 20 } } },
30 | ]);
31 |
32 | expect(listAllPredicates(conditions, defSchema)).toEqual([
33 | "epty",
34 | "greater",
35 | "less",
36 | ]);
37 | expect(listInvalidPredicates(conditions, defSchema)).toEqual(["epty"]);
38 | });
39 |
40 | test("Two field rule ", () => {
41 | const conditions = conditionsFrom([
42 | {
43 | conditions: { firstName: "empty" },
44 | event: { type: "remove" },
45 | },
46 | {
47 | conditions: { age: { greater: 10 } },
48 | event: { type: "require" },
49 | },
50 | {
51 | conditions: { age: { less: 20 } },
52 | event: { type: "hide" },
53 | },
54 | ]);
55 |
56 | let predicates = listAllPredicates(conditions, defSchema);
57 | expect(predicates).toEqual(["empty", "greater", "less"]);
58 |
59 | let fields = listAllFields(conditions);
60 | expect(fields).toEqual(["firstName", "age"]);
61 | });
62 |
63 | test("3 field rule ", () => {
64 | const conditions = conditionsFrom([
65 | {
66 | conditions: { firstName: "empty" },
67 | event: { type: "remove" },
68 | },
69 | {
70 | conditions: { age: { greater: 10 } },
71 | event: { type: "require" },
72 | },
73 | { conditions: { age: { less: 20 } } },
74 | {
75 | conditions: { firstName: "empty" },
76 | event: { type: "hide" },
77 | },
78 | ]);
79 |
80 | let predicates = listAllPredicates(conditions, defSchema);
81 | expect(predicates).toEqual(["empty", "greater", "less"]);
82 |
83 | let fields = listAllFields(conditions);
84 | expect(fields).toEqual(["firstName", "age"]);
85 | });
86 |
87 | test("invalidate predicates", () => {
88 | let invalidConditions = conditionsFrom([
89 | {
90 | event: { type: "remove" },
91 | conditions: {
92 | age: {
93 | wtf: {
94 | greater: 5,
95 | less: 70,
96 | },
97 | },
98 | },
99 | },
100 | ]);
101 |
102 | expect(listAllPredicates(invalidConditions, defSchema)).toEqual([
103 | "greater",
104 | "less",
105 | "wtf",
106 | ]);
107 | expect(listInvalidPredicates(invalidConditions, defSchema)).toEqual(["wtf"]);
108 | expect(() => validatePredicates(invalidConditions, defSchema)).toThrow();
109 | expect(() =>
110 | testInProd(validatePredicates(invalidConditions, defSchema))
111 | ).not.toBeUndefined();
112 | });
113 |
114 | test("invalid field", () => {
115 | let invalidFieldConditions = conditionsFrom([
116 | {
117 | conditions: { lastName: "empty" },
118 | event: {
119 | type: "remove",
120 | },
121 | },
122 | {
123 | conditions: {
124 | or: [{ lastName: "empty" }, { firstName: "empty" }],
125 | and: [{ otherName: "empty" }],
126 | },
127 | },
128 | ]);
129 |
130 | expect(listAllFields(invalidFieldConditions)).toEqual([
131 | "lastName",
132 | "firstName",
133 | "otherName",
134 | ]);
135 | expect(listInvalidFields(invalidFieldConditions, defSchema)).toEqual([
136 | "lastName",
137 | "otherName",
138 | ]);
139 | expect(() =>
140 | validateConditionFields(invalidFieldConditions, defSchema)
141 | ).toThrow();
142 | });
143 |
144 | test("invalid OR", () => {
145 | let invalidOrConditions = conditionsFrom([
146 | {
147 | conditions: {
148 | or: { firstName: "empty" },
149 | },
150 | event: { type: "remove" },
151 | },
152 | ]);
153 |
154 | expect(() => validatePredicates(invalidOrConditions, defSchema)).toThrow();
155 | });
156 |
157 | test("invalid field or", () => {
158 | let invalidFieldOr = conditionsFrom([
159 | {
160 | conditions: {
161 | firstName: {
162 | or: {
163 | is: 10,
164 | some: 25,
165 | },
166 | },
167 | },
168 | event: { type: "remove" },
169 | },
170 | ]);
171 |
172 | expect(() => predicatesFromRule(invalidFieldOr[0].firstName)).toThrow();
173 | expect(
174 | testInProd(() => predicatesFromRule(invalidFieldOr[0].firstName))
175 | ).toEqual([]);
176 | expect(() => validatePredicates(invalidFieldOr, defSchema)).toThrow();
177 | });
178 |
179 | test("invalid field NOT or", () => {
180 | let invalidFieldNotWithOr = conditionsFrom([
181 | {
182 | conditions: {
183 | not: {
184 | firstName: "or",
185 | },
186 | },
187 | event: { type: "remove" },
188 | },
189 | ]);
190 |
191 | expect(listInvalidPredicates(invalidFieldNotWithOr, defSchema)).toEqual([
192 | "or",
193 | ]);
194 | expect(() => validatePredicates(invalidFieldNotWithOr, defSchema)).toThrow();
195 | });
196 |
197 | test("invalid fields 1", () => {
198 | let inValidField = conditionsFrom([
199 | {
200 | conditions: {
201 | lastName: "empty",
202 | },
203 | event: {
204 | type: "remove",
205 | },
206 | },
207 | ]);
208 |
209 | expect(listAllFields(inValidField)).toEqual(["lastName"]);
210 | expect(() => validateConditionFields(inValidField, defSchema)).toThrow();
211 | });
212 |
213 | test("valid field or", () => {
214 | let validFieldOr = conditionsFrom([
215 | {
216 | conditions: {
217 | firstName: {
218 | or: [{ is: 10 }, { is: 25 }],
219 | },
220 | },
221 | event: {
222 | type: "remove",
223 | },
224 | },
225 | ]);
226 |
227 | expect(predicatesFromCondition(validFieldOr[0], defSchema)).toEqual([
228 | "is",
229 | "is",
230 | ]);
231 | expect(validateConditionFields(validFieldOr, defSchema)).toBeUndefined();
232 | });
233 |
234 | test("extract predicates from rule when with or & and", () => {
235 | expect(predicatesFromRule({ or: [{ is: 1 }, { less: 10 }] })).toEqual([
236 | "is",
237 | "less",
238 | ]);
239 | expect(predicatesFromRule({ and: [{ is: 1 }, { less: 10 }] })).toEqual([
240 | "is",
241 | "less",
242 | ]);
243 | });
244 |
245 | test("extract predicates from condition when with or & and", () => {
246 | let schema = {
247 | properties: {
248 | age: { type: "integer" },
249 | grade: { type: "integer" },
250 | },
251 | };
252 |
253 | expect(
254 | predicatesFromCondition(
255 | { or: [{ age: { is: 1 } }, { grade: { less: 10 } }] },
256 | schema
257 | )
258 | ).toEqual(["is", "less"]);
259 | expect(
260 | predicatesFromCondition(
261 | { and: [{ age: { is: 1 } }, { grade: { less: 10 } }] },
262 | schema
263 | )
264 | ).toEqual(["is", "less"]);
265 | });
266 |
267 | test("invalid or in rule", () => {
268 | expect(() => predicatesFromRule({ or: { is: 1 } })).toThrow();
269 | expect(() => predicatesFromRule({ and: { is: 1 } })).toThrow();
270 |
271 | expect(testInProd(() => predicatesFromRule({ or: { is: 1 } }))).toEqual([]);
272 | expect(testInProd(() => predicatesFromRule({ and: { is: 1 } }))).toEqual([]);
273 | });
274 |
275 | test("invalid or in condition", () => {
276 | expect(() => predicatesFromCondition({ or: {} })).toThrow();
277 | expect(() => predicatesFromCondition({ and: {} })).toThrow();
278 |
279 | expect(testInProd(() => predicatesFromCondition({ or: {} }))).toEqual([]);
280 | expect(testInProd(() => predicatesFromCondition({ and: {} }))).toEqual([]);
281 | });
282 |
--------------------------------------------------------------------------------
/webpack.config.dist.js:
--------------------------------------------------------------------------------
1 | var webpack = require("webpack");
2 | var path = require("path");
3 |
4 | module.exports = {
5 | cache: true,
6 | context: __dirname + "/src",
7 | entry: "./index.js",
8 | output: {
9 | path: path.resolve(__dirname, 'dist'),
10 | publicPath: "/dist/",
11 | filename: "json-rules-engine-simplified.js",
12 | library: "JSONSchemaForm",
13 | libraryTarget: "umd"
14 | },
15 | plugins: [
16 | new webpack.DefinePlugin({
17 | "process.env": {
18 | NODE_ENV: JSON.stringify("production")
19 | }
20 | })
21 | ],
22 | devtool: "source-map",
23 | externals: {
24 | react: {
25 | root: "React",
26 | commonjs: "react",
27 | commonjs2: "react",
28 | amd: "react"
29 | }
30 | },
31 | module: {
32 | loaders: [
33 | {
34 | test: /\.js$/,
35 | loaders: ["babel-loader"],
36 | }
37 | ]
38 | }
39 | };
--------------------------------------------------------------------------------