├── .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 | [![Build Status](https://travis-ci.org/RxNT/json-rules-engine-simplified.svg?branch=master)](https://travis-ci.org/RxNT/json-rules-engine-simplified) 2 | [![Coverage Status](https://coveralls.io/repos/github/RxNT/json-rules-engine-simplified/badge.svg?branch=master)](https://coveralls.io/github/RxNT/json-rules-engine-simplified?branch=master) 3 | [![npm version](https://badge.fury.io/js/json-rules-engine-simplified.svg)](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 | }; --------------------------------------------------------------------------------