├── .eslintrc.json
├── .gitignore
├── .prettierrc
├── LICENSE
├── README.md
├── jest.config.ts
├── package.json
├── src
├── functions
│ ├── add-any-to-union.test.ts
│ ├── add-any-to-union.ts
│ ├── add-many-to-union.test.ts
│ ├── add-many-to-union.ts
│ ├── add-rule-to-union.test.ts
│ ├── add-rule-to-union.ts
│ ├── add-rules-to-union.test.ts
│ ├── add-rules-to-union.ts
│ ├── add-union-to-union.test.ts
│ ├── add-union-to-union.ts
│ ├── add-unions-to-union.test.ts
│ ├── add-unions-to-union.ts
│ ├── create-root.test.ts
│ ├── create-root.ts
│ ├── find-any-by-id.test.ts
│ ├── find-any-by-id.ts
│ ├── find-rule-by-id.test.ts
│ ├── find-rule-by-id.ts
│ ├── find-union-by-id.test.ts
│ ├── find-union-by-id.ts
│ ├── is-array-length-rule-valid.test.ts
│ ├── is-array-length-rule-valid.ts
│ ├── is-array-value-rule-valid.test.ts
│ ├── is-array-value-rule-valid.ts
│ ├── is-boolean-rule-valid.test.ts
│ ├── is-boolean-rule-valid.ts
│ ├── is-generic-comparison-rule-valid.test.ts
│ ├── is-generic-comparison-rule-valid.ts
│ ├── is-generic-type-rule-valid.test.ts
│ ├── is-generic-type-rule-valid.ts
│ ├── is-number-rule-valid.test.ts
│ ├── is-number-rule-valid.ts
│ ├── is-object-key-rule-valid.test.ts
│ ├── is-object-key-rule-valid.ts
│ ├── is-object-key-value-rule-valid.test.ts
│ ├── is-object-key-value-rule-valid.ts
│ ├── is-object-value-rule-valid.test.ts
│ ├── is-object-value-rule-valid.ts
│ ├── is-string-rule-valid.test.ts
│ ├── is-string-rule-valid.ts
│ ├── normalize.test.ts
│ ├── normalize.ts
│ ├── remove-all-by-id.test.ts
│ ├── remove-all-by-id.ts
│ ├── run.test.ts
│ ├── run.ts
│ ├── update-rule-by-id.test.ts
│ ├── update-rule-by-id.ts
│ ├── update-union-by-id.test.ts
│ ├── update-union-by-id.ts
│ ├── validate.test.ts
│ └── validate.ts
├── index.ts
├── types
│ ├── rule.ts
│ └── union.ts
├── utils
│ ├── is-object.test.ts
│ └── is-object.ts
└── validations
│ ├── rule.ts
│ └── union.ts
└── tsconfig.json
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "plugin:@typescript-eslint/recommended",
4 | "prettier"
5 | ],
6 | "parser": "@typescript-eslint/parser",
7 | "rules": {
8 | "@typescript-eslint/ban-ts-comment": "off",
9 | "@typescript-eslint/explicit-module-boundary-types": [
10 | "error",
11 | {
12 | "allowArgumentsExplicitlyTypedAsAny": true
13 | }
14 | ],
15 | "@typescript-eslint/no-explicit-any": "off",
16 | "@typescript-eslint/no-unused-vars": [
17 | "error",
18 | {
19 | "argsIgnorePattern": "^_"
20 | }
21 | ],
22 | "no-duplicate-imports": "error",
23 | "object-shorthand": "error",
24 | "sort-imports": [
25 | "error",
26 | {
27 | "allowSeparatedGroups": false,
28 | "ignoreCase": false,
29 | "ignoreDeclarationSort": false,
30 | "ignoreMemberSort": false,
31 | "memberSyntaxSortOrder": [
32 | "none",
33 | "all",
34 | "multiple",
35 | "single"
36 | ]
37 | }
38 | ],
39 | "spaced-comment": "error"
40 | }
41 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | lib-cov
2 | *.seed
3 | *.log
4 | *.csv
5 | *.dat
6 | *.out
7 | *.pid
8 | *.gz
9 | *.swp
10 |
11 | pids
12 | logs
13 | results
14 | tmp
15 |
16 | # Build
17 | public/css/main.css
18 |
19 | # Coverage reports
20 | coverage
21 |
22 | # API keys and secrets
23 | .env
24 |
25 | # Dependency directory
26 | node_modules
27 | bower_components
28 |
29 | # Editors
30 | .idea
31 | *.iml
32 |
33 | # OS metadata
34 | .DS_Store
35 | Thumbs.db
36 |
37 | # Ignore built ts files
38 | dist/**/*
39 |
40 | # ignore yarn.lock
41 | yarn.lock
42 |
43 | # misc
44 | src/test.ts
45 | lib
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "bracketSpacing": true,
3 | "printWidth": 120,
4 | "semi": true,
5 | "singleQuote": true,
6 | "tabWidth": 2,
7 | "trailingComma": "all",
8 | "useTabs": false
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Andrew Vo-Nguyen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 |
4 |
5 | [](https://github.com/andrewvo89/rules-engine-ts)
6 | [](https://github.com/andrewvo89/rules-engine-ts/issues)
7 | [](https://github.com/andrewvo89/rules-engine-ts/pulls)
8 | [](/LICENSE)
9 |
10 |
11 |
12 | ---
13 |
14 | Strongly typed rules engine for evaluating deep and complex rules.
15 |
16 | ## Table of Contents
17 |
18 | - [About](#about)
19 | - [Terminology](#terminology)
20 | - [Basic Usage](#basic-usage)
21 | - [UI Implementation Example](#ui-implementation-example)
22 | - [Installation](#installation)
23 | - [Usage](#usage)
24 | - [Rules Specification](#rule-specification)
25 | - [TypeScript Usage](#typescript-usage)
26 | - [Authors](#authors)
27 |
28 | ## About
29 |
30 | Rules Engine TS is a strongly typed rules engine for evaluating deep and complex rules. With the power of Typescript you can create type safe rules that are easy to read and maintain.
31 |
32 | ## Terminology
33 |
34 | ### Rule
35 |
36 | A rule is a single condition that can be evaluated. A rule can be of the following types:
37 |
38 | - string
39 | - number
40 | - boolean
41 | - array_value
42 | - array_length
43 | - object_key
44 | - object_value
45 | - object_key_value
46 | - generic_comparison
47 | - generic_type
48 |
49 | Depending on the `type`, certain operators are available. For example, the `string` type has the following operators:
50 |
51 | - equals_to
52 | - does_not_equal_to
53 | - contains
54 | - not_contains
55 | - starts_with
56 | - ends_with
57 |
58 | Refer to the [Rules Specification](#rules-specification) section for more information on the available properties.
59 |
60 | ### Union
61 |
62 | A union is a collection of rules and/or other unions. A union can have a connector of `and` or `or`. If the connector is `and` then all rules and unions must evaluate to true. If the connector is `or` then only one rule or union must evaluate to true.
63 |
64 | ### Root Union
65 |
66 | The root union is the top level union. It contains the same properties of a regular union but does not have a `parent_id` property.
67 |
68 | ### Parent
69 |
70 | The parent of a rule or union is the union that contains it. Any rule or union can be linked back to its parent with its `parent_id` property. The parent union should contain the rule or union in its `rules` array.
71 |
72 | ## Basic Usage
73 |
74 | The recommended way to consume `rules-engine-ts` is in a TypeScript environment. TypeScript will warn you when your rules are missing properties or if the types of your properties are incorrect. That isn't to say that `rules-engine-ts` can't be run with JavaScript. You will still get autocomplete on the available properties, but you will not get any warnings if you are missing properties or if the types of your properties are incorrect.
75 |
76 | A rules engine can be configured and run like so:
77 |
78 | ```js
79 | import { addRuleToUnion, addRulesToUnion, addUnionToUnion, createRoot, run } from 'rules-engine-ts';
80 |
81 | // Create root union
82 | const root = createRoot({ connector: 'and' });
83 |
84 | // Add a rule to the root union
85 | addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
86 |
87 | // Add a union to the root union (creates a nested ruleset)
88 | const union = addUnionToUnion(root, { connector: 'or' });
89 |
90 | // Add nested rules to the nested union
91 | addRulesToUnion(union, [
92 | { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true },
93 | { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true },
94 | ]);
95 |
96 | // Run the rules engine
97 | const pass = run(root, { age: 19, name: 'Bob' });
98 | const fail = run(root, { age: 19, name: 'Carol' });
99 |
100 | console.log(pass); // true
101 | console.log(fail); // false
102 | ```
103 |
104 | This is what the state of the Rule Engine looks like:
105 |
106 | ```json
107 | {
108 | "entity": "root_union",
109 | "id": "0d7428af-10e4-481b-84a7-056946bd4f12",
110 | "connector": "and",
111 | "rules": [
112 | {
113 | "entity": "rule",
114 | "id": "82e96b0d-886e-4a2e-bf8c-f81b02ef11ce",
115 | "parent_id": "0d7428af-10e4-481b-84a7-056946bd4f12",
116 | "type": "number",
117 | "field": "age",
118 | "operator": "greater_than",
119 | "value": 18
120 | },
121 | {
122 | "entity": "union",
123 | "id": "7c493486-409b-48df-bd66-7f4a16500c5e",
124 | "parent_id": "0d7428af-10e4-481b-84a7-056946bd4f12",
125 | "connector": "or",
126 | "rules": [
127 | {
128 | "entity": "rule",
129 | "id": "3abc4e64-d6c8-4303-9d07-b573a571f19a",
130 | "parent_id": "7c493486-409b-48df-bd66-7f4a16500c5e",
131 | "type": "string",
132 | "field": "name",
133 | "operator": "equals_to",
134 | "value": "bob",
135 | "ignore_case": true
136 | },
137 | {
138 | "entity": "rule",
139 | "id": "a3995445-55ca-49b2-8381-3d6758750413",
140 | "parent_id": "7c493486-409b-48df-bd66-7f4a16500c5e",
141 | "type": "string",
142 | "field": "name",
143 | "operator": "equals_to",
144 | "value": "alice",
145 | "ignore_case": true
146 | }
147 | ]
148 | }
149 | ]
150 | }
151 | ```
152 |
153 | ## UI Implementation Example
154 |
155 | 
156 |
157 | The rules can then be persisted into a database in JSON format:
158 |
159 | ```json
160 | {
161 | "entity": "root_union",
162 | "id": "598444ae-032c-4ae5-85da-644cf90ab920",
163 | "connector": "or",
164 | "rules": [
165 | {
166 | "entity": "rule",
167 | "id": "03fcb9b5-a3fe-4d63-97f3-dfce431c331d",
168 | "parent_id": "598444ae-032c-4ae5-85da-644cf90ab920",
169 | "type": "string",
170 | "field": "user_display_name",
171 | "operator": "equals_to",
172 | "value": "Alice",
173 | "ignore_case": true
174 | },
175 | {
176 | "id": "d60639aa-8239-40c7-9cc3-ec89f8f8c58d",
177 | "entity": "union",
178 | "connector": "and",
179 | "parent_id": "598444ae-032c-4ae5-85da-644cf90ab920",
180 | "rules": [
181 | {
182 | "entity": "rule",
183 | "id": "1821d9da-9f37-4689-a118-bf436ca37e89",
184 | "parent_id": "d60639aa-8239-40c7-9cc3-ec89f8f8c58d",
185 | "type": "string",
186 | "field": "user_display_name",
187 | "operator": "equals_to",
188 | "value": "Bob",
189 | "ignore_case": true
190 | },
191 | {
192 | "entity": "rule",
193 | "id": "c2a058ab-6005-44a4-94ae-b75736dce536",
194 | "parent_id": "d60639aa-8239-40c7-9cc3-ec89f8f8c58d",
195 | "type": "number",
196 | "field": "total_challenges",
197 | "operator": "greater_than_or_equal_to",
198 | "value": 5
199 | }
200 | ]
201 | }
202 | ]
203 | }
204 | ```
205 |
206 | At a later date, the rules can retrieved from the database and can be run by the rules engine like this:
207 |
208 | ```js
209 | import { run } from 'rules-engine-ts';
210 |
211 | const rules = getRulesFromDatabase();
212 |
213 | const pass = run(rules, { user_display_name: 'alice', total_challenges: 0 });
214 | const fail = run(rules, { user_display_name: 'bob', total_challenges: 0 });
215 |
216 | if (pass) {
217 | // do something
218 | }
219 |
220 | if (fail) {
221 | //do somehting else
222 | }
223 | ```
224 |
225 | ## Installation
226 |
227 | Install the package using your favorite package manager:
228 |
229 | ```
230 | npm install rules-engine-ts
231 | yarn add rules-engine-ts
232 | pnpm add rules-engine-ts
233 | ```
234 |
235 | ## Usage
236 |
237 | ### `createRoot(connector: 'and' | 'or'): RootUnion`
238 |
239 | Creates a root union. This is the entry point for creating a rules engine.
240 |
241 | ```js
242 | import { createRoot } from 'rules-engine-ts';
243 |
244 | const root = createRoot({ connector: 'and' });
245 | ```
246 |
247 | State of the Rules Engine:
248 |
249 | ```json
250 | {
251 | "entity": "root_union",
252 | "id": "0d7428af-10e4-481b-84a7-056946bd4f12",
253 | "connector": "and",
254 | "rules": []
255 | }
256 | ```
257 |
258 | ### `addRuleToUnion(parent: RootUnion | Union, newRule: NewRule): Rule`
259 |
260 | Adds a rule to a union or root union. The rules engine assigns a unique ID and automatically tags it with a `parent_id`. Returns the rule that was added.
261 |
262 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
263 |
264 | ```js
265 | import { addRuleToUnion, createRoot } from 'rules-engine-ts';
266 |
267 | const root = createRoot({ connector: 'and' });
268 |
269 | addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
270 | ```
271 |
272 | State of the Rules Engine:
273 |
274 | ```json
275 | {
276 | "entity": "root_union",
277 | "id": "0d7428af-10e4-481b-84a7-056946bd4f12",
278 | "connector": "and",
279 | "rules": [
280 | {
281 | "entity": "rule",
282 | "id": "82e96b0d-886e-4a2e-bf8c-f81b02ef11ce",
283 | "parent_id": "0d7428af-10e4-481b-84a7-056946bd4f12",
284 | "type": "number",
285 | "field": "age",
286 | "operator": "greater_than",
287 | "value": 18
288 | }
289 | ]
290 | }
291 | ```
292 |
293 | ### `addRulesToUnion(parent: RootUnion | Union, newRules: NewRule[]): Rule[]`
294 |
295 | Adds many rules to a union or root union. The rules engine assigns a unique ID and automatically tags it with a `parent_id`. Returns the list of rules that were added.
296 |
297 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
298 |
299 | ```js
300 | import { addRulesToUnion, createRoot } from 'rules-engine-ts';
301 |
302 | const root = createRoot({ connector: 'and' });
303 |
304 | addRulesToUnion(root, [
305 | { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true },
306 | { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true },
307 | ]);
308 | ```
309 |
310 | State of the Rules Engine:
311 |
312 | ```json
313 | {
314 | "entity": "root_union",
315 | "id": "61dadd25-22a0-4e84-abe5-92fcfd6cac9e",
316 | "connector": "and",
317 | "rules": [
318 | {
319 | "entity": "rule",
320 | "id": "50158f7e-1d87-4ca8-aaca-ef1bbb41c9c2",
321 | "parent_id": "61dadd25-22a0-4e84-abe5-92fcfd6cac9e",
322 | "type": "string",
323 | "field": "name",
324 | "operator": "equals_to",
325 | "value": "bob",
326 | "ignore_case": true
327 | },
328 | {
329 | "entity": "rule",
330 | "id": "5f6ac1d1-7ce7-40a5-a94c-5e4a47a45e28",
331 | "parent_id": "61dadd25-22a0-4e84-abe5-92fcfd6cac9e",
332 | "type": "string",
333 | "field": "name",
334 | "operator": "equals_to",
335 | "value": "alice",
336 | "ignore_case": true
337 | }
338 | ]
339 | }
340 | ```
341 |
342 | ### `addUnionToUnion(parent: RootUnion | Union, newUnion: NewUnion): Union`
343 |
344 | Adds a union to an existing union or root union. Returns the rule that was added.
345 |
346 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
347 |
348 | ```js
349 | import { addRuleToUnion, addRulesToUnion, addUnionToUnion, createRoot } from 'rules-engine-ts';
350 |
351 | const root = createRoot({ connector: 'and' });
352 |
353 | const union = addUnionToUnion(root, { connector: 'or' });
354 | addRuleToUnion(union, { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true });
355 | addRuleToUnion(union, { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true });
356 | ```
357 |
358 | State of the Rules Engine:
359 |
360 | ```json
361 | {
362 | "entity": "root_union",
363 | "id": "0d7428af-10e4-481b-84a7-056946bd4f12",
364 | "connector": "and",
365 | "rules": [
366 | {
367 | "entity": "union",
368 | "id": "7c493486-409b-48df-bd66-7f4a16500c5e",
369 | "parent_id": "0d7428af-10e4-481b-84a7-056946bd4f12",
370 | "connector": "or",
371 | "rules": [
372 | {
373 | "entity": "rule",
374 | "id": "3abc4e64-d6c8-4303-9d07-b573a571f19a",
375 | "parent_id": "7c493486-409b-48df-bd66-7f4a16500c5e",
376 | "type": "string",
377 | "field": "name",
378 | "operator": "equals_to",
379 | "value": "bob",
380 | "ignore_case": true
381 | },
382 | {
383 | "entity": "rule",
384 | "id": "a3995445-55ca-49b2-8381-3d6758750413",
385 | "parent_id": "7c493486-409b-48df-bd66-7f4a16500c5e",
386 | "type": "string",
387 | "field": "name",
388 | "operator": "equals_to",
389 | "value": "alice",
390 | "ignore_case": true
391 | }
392 | ]
393 | }
394 | ]
395 | }
396 | ```
397 |
398 | ### `addUnionsToUnion(parent: RootUnion | Union, newUnions: NewUnion[]): Union[]`
399 |
400 | Adds many unions to an existing union or root union. Returns the list of unions that were added.
401 |
402 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
403 |
404 | ```js
405 | import { addRulesToUnion, addUnionsToUnion, createRoot } from 'rules-engine-ts';
406 |
407 | const root = createRoot({ connector: 'and' });
408 |
409 | const unions = addUnionsToUnion(root, [{ connector: 'or' }, { connector: 'or' }]);
410 | addRulesToUnion(unions[0], [
411 | { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true },
412 | { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true },
413 | ]);
414 | addRulesToUnion(unions[1], [
415 | { type: 'number', field: 'age', value: 18, operator: 'equals_to' },
416 | { type: 'number', field: 'age', value: 21, operator: 'equals_to' },
417 | ]);
418 | ```
419 |
420 | State of the Rules Engine:
421 |
422 | ```json
423 | {
424 | "entity": "root_union",
425 | "id": "28a9ae06-594a-4520-8d73-2fd871804634",
426 | "connector": "and",
427 | "rules": [
428 | {
429 | "entity": "union",
430 | "id": "8e5e66dd-e86c-4f9e-acc7-7baa852fdfe8",
431 | "parent_id": "28a9ae06-594a-4520-8d73-2fd871804634",
432 | "connector": "or",
433 | "rules": [
434 | {
435 | "entity": "rule",
436 | "id": "a8ecbafd-0a1a-4e9e-bb70-8bd11a62f274",
437 | "parent_id": "8e5e66dd-e86c-4f9e-acc7-7baa852fdfe8",
438 | "type": "string",
439 | "field": "name",
440 | "operator": "equals_to",
441 | "value": "bob",
442 | "ignore_case": true
443 | },
444 | {
445 | "entity": "rule",
446 | "id": "d4fd56bf-af82-4382-a1cb-93d80cb87ef4",
447 | "parent_id": "8e5e66dd-e86c-4f9e-acc7-7baa852fdfe8",
448 | "type": "string",
449 | "field": "name",
450 | "operator": "equals_to",
451 | "value": "alice",
452 | "ignore_case": true
453 | }
454 | ]
455 | },
456 | {
457 | "entity": "union",
458 | "id": "5e5d7f00-d0f6-40d9-84b3-39600241a92f",
459 | "parent_id": "28a9ae06-594a-4520-8d73-2fd871804634",
460 | "connector": "or",
461 | "rules": [
462 | {
463 | "entity": "rule",
464 | "id": "7ea03690-d9c4-4a3e-97eb-d927cf6845e8",
465 | "parent_id": "5e5d7f00-d0f6-40d9-84b3-39600241a92f",
466 | "type": "number",
467 | "field": "age",
468 | "operator": "equals_to",
469 | "value": 18
470 | },
471 | {
472 | "entity": "rule",
473 | "id": "bb63d7da-b0dc-4d00-824c-4de09151c609",
474 | "parent_id": "5e5d7f00-d0f6-40d9-84b3-39600241a92f",
475 | "type": "number",
476 | "field": "age",
477 | "operator": "equals_to",
478 | "value": 21
479 | }
480 | ]
481 | }
482 | ]
483 | }
484 | ```
485 |
486 | ### `addAnyToUnion(parent: RootUnion | Union, newRuleOrUnion: NewRule | NewUnion): Rule | Union`
487 |
488 | Adds a rule or a union to an existing union or root union. Returns the rule or union that was added.
489 |
490 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
491 |
492 | ```js
493 | import { addAnyToUnion, createRoot } from 'rules-engine-ts';
494 |
495 | const root = createRoot({ connector: 'and' });
496 |
497 | const any = addAnyToUnion(root, { connector: 'or' });
498 | if (any.entity === 'union') {
499 | addAnyToUnion(any, { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true });
500 | addAnyToUnion(any, { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true });
501 | }
502 | ```
503 |
504 | State of the Rules Engine:
505 |
506 | ```json
507 | {
508 | "entity": "root_union",
509 | "id": "825ef3d8-3151-4367-b751-1deae8b308c1",
510 | "connector": "and",
511 | "rules": [
512 | {
513 | "entity": "union",
514 | "id": "71b5296a-5358-4399-878d-f535c9f21faf",
515 | "parent_id": "825ef3d8-3151-4367-b751-1deae8b308c1",
516 | "connector": "or",
517 | "rules": [
518 | {
519 | "entity": "rule",
520 | "id": "3cd0463f-c5e7-4dd9-98b8-9e7cf79417b5",
521 | "parent_id": "71b5296a-5358-4399-878d-f535c9f21faf",
522 | "type": "string",
523 | "field": "name",
524 | "operator": "equals_to",
525 | "value": "bob",
526 | "ignore_case": true
527 | },
528 | {
529 | "entity": "rule",
530 | "id": "f10e7cec-c737-4c2c-b137-d7ab0e26e045",
531 | "parent_id": "71b5296a-5358-4399-878d-f535c9f21faf",
532 | "type": "string",
533 | "field": "name",
534 | "operator": "equals_to",
535 | "value": "alice",
536 | "ignore_case": true
537 | }
538 | ]
539 | }
540 | ]
541 | }
542 | ```
543 |
544 | ### `addManyToUnion(parent: RootUnion | Union, newRulesOrUnions: (NewRule | NewUnion)[]): (Rule | Union)[]`
545 |
546 | Adds many rules or unions to an existing union or root union. Returns the list of rules or unions that were added.
547 |
548 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
549 |
550 | ```js
551 | import { addManyToUnion, createRoot } from 'rules-engine-ts';
552 |
553 | const root = createRoot({ connector: 'and' });
554 |
555 | addManyToUnion(root, [
556 | { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true },
557 | { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true },
558 | { connector: 'or' },
559 | ]);
560 | ```
561 |
562 | State of the Rules Engine:
563 |
564 | ```json
565 | {
566 | "entity": "root_union",
567 | "id": "26252c95-37da-47d4-b361-7ad82ae13a9b",
568 | "connector": "and",
569 | "rules": [
570 | {
571 | "entity": "rule",
572 | "id": "edbf5239-e931-480a-b231-119af2c1a1d1",
573 | "parent_id": "26252c95-37da-47d4-b361-7ad82ae13a9b",
574 | "type": "string",
575 | "field": "name",
576 | "operator": "equals_to",
577 | "value": "bob",
578 | "ignore_case": true
579 | },
580 | {
581 | "entity": "rule",
582 | "id": "1e9109fa-30cf-41a9-9e78-e8395a423d6d",
583 | "parent_id": "26252c95-37da-47d4-b361-7ad82ae13a9b",
584 | "type": "string",
585 | "field": "name",
586 | "operator": "equals_to",
587 | "value": "alice",
588 | "ignore_case": true
589 | },
590 | {
591 | "entity": "union",
592 | "id": "0bb57d03-ac07-41af-941a-6a2625bac130",
593 | "parent_id": "26252c95-37da-47d4-b361-7ad82ae13a9b",
594 | "connector": "or",
595 | "rules": []
596 | }
597 | ]
598 | }
599 | ```
600 |
601 | ### `run(union: RootUnion | Union, value: any): boolean`
602 |
603 | Evaluates a set of rules against a value. The value can be of any type (object, array, string, number, boolean, etc). Returns a boolean indicating whether the value passes the rules.
604 |
605 | ```js
606 | import { addRuleToUnion, addRulesToUnion, addUnionToUnion, createRoot, run } from 'rules-engine-ts';
607 |
608 | const root = createRoot({ connector: 'and' });
609 | addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
610 |
611 | const union = addUnionToUnion(root, { connector: 'or' });
612 | addRulesToUnion(union, [
613 | { type: 'string', field: 'name', value: 'bob', operator: 'equals_to', ignore_case: true },
614 | { type: 'string', field: 'name', value: 'alice', operator: 'equals_to', ignore_case: true },
615 | ]);
616 |
617 | const pass = run(root, { age: 19, name: 'Bob' });
618 | const fail = run(root, { age: 19, name: 'Carol' });
619 |
620 | console.log(pass); // true
621 | console.log(fail); // false
622 | ```
623 |
624 | ### `findAnyById(union: RootUnion | Union, id: string): RootUnion | Union | Rule | undefined`
625 |
626 | Finds any rule or union by id. Returns the rule or union if found, otherwise returns undefined.
627 |
628 | ```js
629 | import { addRuleToUnion, addUnionToUnion, createRoot, findAnyById } from 'rules-engine-ts';
630 |
631 | const root = createRoot({ connector: 'and' });
632 | const rule = addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
633 | const union = addUnionToUnion(root, { connector: 'or' });
634 |
635 | const foundRule = findAnyById(root, rule.id);
636 | console.log(foundRule === rule); // true
637 |
638 | const foundUnion = findAnyById(root, union.id);
639 | console.log(foundUnion === union); // true
640 | ```
641 |
642 | ### `findRuleById(union: RootUnion | Union, id: string): Rule | undefined`
643 |
644 | Finds a rule by id. Returns the rule if found, otherwise returns undefined.
645 |
646 | ```js
647 | import { addRuleToUnion, addUnionToUnion, createRoot, findRuleById } from 'rules-engine-ts';
648 |
649 | const root = createRoot({ connector: 'and' });
650 | const rule = addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
651 | const union = addUnionToUnion(root, { connector: 'or' });
652 |
653 | const foundRule = findRuleById(root, rule.id);
654 | console.log(foundRule === rule); // true
655 |
656 | const foundUnion = findRuleById(root, union.id);
657 | console.log(foundUnion); // undefined
658 | ```
659 |
660 | ### `findUnionById(union: RootUnion | Union, id: string): RootUnion | Union | undefined`
661 |
662 | Finds a union by id. Returns the union if found, otherwise returns undefined.
663 |
664 | ```js
665 | import { addRuleToUnion, addUnionToUnion, createRoot, findUnionById } from 'rules-engine-ts';
666 |
667 | const root = createRoot({ connector: 'and' });
668 | const rule = addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
669 | const union = addUnionToUnion(root, { connector: 'or' });
670 |
671 | const foundUnion = findUnionById(root, union.id);
672 | console.log(foundUnion === union); // true;
673 |
674 | const foundRule = findUnionById(root, rule.id);
675 | console.log(foundRule); // undefined;
676 | ```
677 |
678 | ### `validate(root: RootUnion): { isValid: true } | { isValid: false; reason: string }`
679 |
680 | Validates the structure of a ruleset. Returns an object with a boolean indicating whether the ruleset is valid, and a reason if the ruleset is invalid.
681 |
682 | ```js
683 | import { addRuleToUnion, createRoot, validate } from 'rules-engine-ts';
684 |
685 | const root = createRoot({ connector: 'and' });
686 | const rule = addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
687 |
688 | console.log(validate(root));
689 | // { isValid: true }
690 |
691 | rule.type = 'string';
692 |
693 | console.log(validate(root));
694 | // {
695 | // isValid: false,
696 | // reason: 'Code: invalid_union ~ Path: rules[0] ~ Message: Invalid input'
697 | // }
698 | ```
699 |
700 | ### `normalize(union: T, options?: Options): T`
701 |
702 | Normalization is a process that ensures that the ruleset is in a consistent state. It performs the following updates recursively in the following order:
703 |
704 | - Removes any rules or unions that do not conform to the type system. `options.remove_failed_validations`
705 | - Removes any unions without any rules. `options.remove_empty_unions`
706 | - Converts any union with a single rule to a rule. `options.promote_single_rule_unions`
707 | - Updates all parent ids to match the parent union `options.update_parent_ids`
708 |
709 | All these updates are turned on by default. You can disable them by passing in an options object as the second argument with the corresponding properties set to false.
710 |
711 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
712 |
713 | ```ts
714 | import { addRuleToUnion, addUnionToUnion, createRoot, normalize } from 'rules-engine-ts';
715 |
716 | import { v4 as uuidv4 } from 'uuid';
717 |
718 | const root = createRoot({ connector: 'or' });
719 |
720 | const rule1 = addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
721 | const union = addUnionToUnion(root, { connector: 'and' });
722 | const rule2 = addRuleToUnion(union, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
723 |
724 | rule1.parent_id = uuidv4();
725 | rule2.type = 'number';
726 | // @ts-expect-error
727 | union.connector = 'invalid';
728 |
729 | console.log(root); // Before normalization
730 | normalize(root, {
731 | // Normalization options (optional)
732 | promote_single_rule_unions: true,
733 | remove_empty_unions: true,
734 | remove_failed_validations: true,
735 | update_parent_ids: true,
736 | });
737 | console.log(root); // After normalization
738 | ```
739 |
740 | Before normalization:
741 |
742 | ```json
743 | {
744 | "entity": "root_union",
745 | "id": "70cf2539-b960-4831-b0f2-3b201aea550a",
746 | "connector": "or",
747 | "rules": [
748 | {
749 | "entity": "rule",
750 | "id": "4b644371-6bc2-46b1-b855-c2098df80fb3",
751 | "parent_id": "8ca677c2-b01c-4cf2-91ec-9c95b6ff7dff",
752 | "type": "string",
753 | "field": "name",
754 | "operator": "contains",
755 | "value": "bob"
756 | },
757 | {
758 | "entity": "union",
759 | "id": "c43e8705-6b4b-42b5-941c-3295c17cf5db",
760 | "parent_id": "70cf2539-b960-4831-b0f2-3b201aea550a",
761 | "connector": "invalid",
762 | "rules": [
763 | {
764 | "entity": "rule",
765 | "id": "8589e28c-a1d5-4a0b-b930-24c5931eaadb",
766 | "parent_id": "c43e8705-6b4b-42b5-941c-3295c17cf5db",
767 | "type": "number",
768 | "field": "name",
769 | "operator": "contains",
770 | "value": "alice"
771 | }
772 | ]
773 | }
774 | ]
775 | }
776 | ```
777 |
778 | After normalization:
779 |
780 | ```json
781 | {
782 | "entity": "root_union",
783 | "id": "70cf2539-b960-4831-b0f2-3b201aea550a",
784 | "connector": "or",
785 | "rules": [
786 | {
787 | "entity": "rule",
788 | "id": "4b644371-6bc2-46b1-b855-c2098df80fb3",
789 | "parent_id": "70cf2539-b960-4831-b0f2-3b201aea550a",
790 | "type": "string",
791 | "field": "name",
792 | "operator": "contains",
793 | "value": "bob"
794 | }
795 | ]
796 | }
797 | ```
798 |
799 | ### `updateRuleById(root: RootUnion, id: string, values: NewRule): Rule | undefined`
800 |
801 | Updates a rule by id. Returns the updated rule if found, otherwise returns undefined.
802 |
803 | > Note: This function mutates the input root union. Clone the union before passing it in if you want to maintain its original state.
804 |
805 | ```js
806 | import { addRuleToUnion, createRoot, updateRuleById } from 'rules-engine-ts';
807 |
808 | const root = createRoot({ connector: 'and' });
809 | const rule = addRuleToUnion(root, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
810 |
811 | console.log(root.rules[0]); // Before update
812 | updateRuleById(root, rule.id, { type: 'number', field: 'age', operator: 'less_than', value: 30 });
813 | console.log(root.rules[0]); // After update
814 | ```
815 |
816 | Before update:
817 |
818 | ```json
819 | {
820 | "entity": "rule",
821 | "id": "cc3d4bab-783a-4683-a223-8dee979b0bf0",
822 | "parent_id": "e0da0708-1fbf-4e64-887c-d7684b17dd00",
823 | "type": "number",
824 | "field": "age",
825 | "operator": "greater_than",
826 | "value": 18
827 | }
828 | ```
829 |
830 | After update:
831 |
832 | ```json
833 | {
834 | "entity": "rule",
835 | "id": "cc3d4bab-783a-4683-a223-8dee979b0bf0",
836 | "parent_id": "e0da0708-1fbf-4e64-887c-d7684b17dd00",
837 | "type": "number",
838 | "field": "age",
839 | "operator": "less_than",
840 | "value": 30
841 | }
842 | ```
843 |
844 | ### `updateUnionById(root: RootUnion, id: string, values: NewUnion): Union | RootUnion | undefined`
845 |
846 | Updates a union by id. Returns the updated union if found, otherwise returns undefined.
847 |
848 | > Note: This function mutates the input root union. Clone the union before passing it in if you want to maintain its original state.
849 |
850 | ```js
851 | import { addUnionToUnion, createRoot, updateUnionById } from 'rules-engine-ts';
852 |
853 | const root = createRoot({ connector: 'and' });
854 | const union = addUnionToUnion(root, { connector: 'and' });
855 |
856 | console.log(root.rules[0]); // Before update
857 | updateUnionById(root, union.id, { connector: 'or' });
858 | console.log(root.rules[0]); // After update
859 | ```
860 |
861 | Before update:
862 |
863 | ```json
864 | {
865 | "entity": "union",
866 | "id": "b0a289a5-f02e-4bb4-bbbf-d148d1fc570f",
867 | "parent_id": "1ac8dad7-46c0-430b-9ad1-fdb8f1fd721a",
868 | "connector": "and",
869 | "rules": []
870 | }
871 | ```
872 |
873 | After update:
874 |
875 | ```json
876 | {
877 | "entity": "union",
878 | "id": "b0a289a5-f02e-4bb4-bbbf-d148d1fc570f",
879 | "parent_id": "1ac8dad7-46c0-430b-9ad1-fdb8f1fd721a",
880 | "connector": "or",
881 | "rules": []
882 | }
883 | ```
884 |
885 | ### `removeAllById(union: T, id: string): T`
886 |
887 | Removes all rules and unions of a given id from a ruleset. Returns the updated ruleset.
888 |
889 | > Note: This function mutates the input union. Clone the union before passing it in if you want to maintain its original state.
890 |
891 | ```js
892 | import { addRuleToUnion, addUnionToUnion, createRoot, removeAllById } from 'rules-engine-ts';
893 |
894 | const root = createRoot({ connector: 'and' });
895 | const union = addUnionToUnion(root, { connector: 'or' });
896 | const rule = addRuleToUnion(union, { type: 'number', field: 'age', operator: 'greater_than', value: 18 });
897 |
898 | console.log(union.rules.length); // 1
899 | removeAllById(root, rule.id);
900 | console.log(union.rules.length); // 0
901 | ```
902 |
903 | ## Rules Specification
904 |
905 | The properties of a rule change depending on the `type` field. The `type` field acts as a discriminator to determine which properties are valid for a given rule.
906 |
907 | ### type = 'string'
908 |
909 | | Property | Value | Description |
910 | | ----------- | --------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
911 | | type | 'string' | The type of the value to be evaluated. |
912 | | field | string | The field to check. Supports nested properties, e.g. `users.admins[0].name`. |
913 | | operator | 'equals_to'
'does_not_equal_to'
'contains'
'does_not_contain'
'starts_with'
'ends_with' | The operator to use. |
914 | | value | string | The value to compare against. |
915 | | ignore_case | boolean | Whether to ignore case when comparing strings. |
916 |
917 | ### type = 'number'
918 |
919 | | Property | Value | Description |
920 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |
921 | | type | 'number' | The type of the value to be evaluated. |
922 | | field | string | The field to check. Supports nested properties, e.g. `users.admins[0].age`. |
923 | | operator | 'equals_to'
'does_not_equal_to'
'greater_than'
'greater_than_or_equal_to'
'less_than'
'less_than_or_equal_to' | The operator to use. |
924 | | value | number | The value to compare against. |
925 |
926 | ### type = 'boolean'
927 |
928 | | Property | Value | Description |
929 | | -------- | -------------------------- | --------------------------------------------------------------------------------- |
930 | | type | 'boolean' | The type of the value to be evaluated. |
931 | | field | string | The field to check. Supports nested properties, e.g. `users.admins[0].is_active`. |
932 | | operator | 'is_true'
'is_false' | The operator to use. |
933 |
934 | ### type = 'array_value'
935 |
936 | | Property | Value | Description |
937 | | -------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
938 | | type | 'array_value' | The type of the value to be evaluated. |
939 | | field | string | The field to check. Supports nested properties, e.g. `users.admins`. |
940 | | operator | 'contains'
'does_not_contain'
'contains_all' | The operator to use. |
941 | | value | any | The value to compare against. |
942 |
943 | ### type = 'array_length'
944 |
945 | | Property | Value | Description |
946 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
947 | | type | 'array_length' | The type of the value to be evaluated. |
948 | | field | string | The field to check. Supports nested properties, e.g. `users.admins`. |
949 | | operator | 'equals_to'
'does_not_equal_to'
'greater_than'
'greater_than_or_equal_to'
'less_than'
'less_than_or_equal_to' | The operator to use. |
950 | | value | number | The value to compare against. |
951 |
952 | ### type = 'object_key'
953 |
954 | | Property | Value | Description |
955 | | -------- | ----------------------------------- | ----------------------------------------------------------------------- |
956 | | type | 'object_key' | The type of the value to be evaluated. |
957 | | field | string | The field to check. Supports nested properties, e.g. `users.admins[0]`. |
958 | | operator | 'contains'
'does_not_contain' | The operator to use. |
959 | | value | string | The value to compare against. |
960 |
961 | ### type = 'object_value'
962 |
963 | | Property | Value | Description |
964 | | -------- | ----------------------------------- | -------------------------------------------------------------------- |
965 | | type | 'object_value' | The type of the value to be evaluated. |
966 | | field | string | The field to check. Supports nested properties, e.g. `users.admins`. |
967 | | operator | 'contains'
'does_not_contain' | The operator to use. |
968 | | value | any | The value to compare against. |
969 |
970 | ### type = 'object_key_value'
971 |
972 | | Property | Value | Description |
973 | | -------- | ----------------------------------- | -------------------------------------------------------------------- |
974 | | type | 'object_key_value' | The type of the value to be evaluated. |
975 | | field | string | The field to check. Supports nested properties, e.g. `users.admins`. |
976 | | operator | 'contains'
'does_not_contain' | The operator to use. |
977 | | value | { key: 'string', value: 'any' } | The value to compare against. |
978 |
979 | ### type = 'generic_comparison'
980 |
981 | | Property | Value | Description |
982 | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
983 | | type | 'generic_comparison' | The type of the value to be evaluated. |
984 | | field | string | The field to check. Supports nested properties, e.g. `users.admins[0].unknown_property`. |
985 | | operator | 'equals_to'
'does_not_equal_to'
'greater_than'
'greater_than_or_equal_to'
'less_than'
'less_than_or_equal_to' | The operator to use. |
986 |
987 | ### type = 'generic_type'
988 |
989 | | Property | Value | Description |
990 | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
991 | | type | 'generic_type' | The type of the value to be evaluated. |
992 | | field | string | The field to check. Supports nested properties, e.g. `users.admins[0].unknown_property`. |
993 | | operator | 'is_truthy'
'is_falsey'
'is_null'
'is_not_null'
'is_undefined'
'is_not_undefined'
'is_string'
'is_not_string'
'is_number'
'is_not_number'
'is_boolean'
'is_not_boolean'
'is_array'
'is_not_array'
'is_object'
'is_not_object' | The operator to use. |
994 | | value | any | The value to compare against. |
995 |
996 | ## TypeScript Usage
997 |
998 | Rules can be pre-composed using type anotations before adding it to the rules engine:
999 |
1000 | ```ts
1001 | import { NewNumberRule, NewRule, addRuleToUnion, createRoot } from 'rules-engine-ts';
1002 |
1003 | const root = createRoot({ connector: 'and' });
1004 |
1005 | const wideTypingRule: NewRule = { type: 'number', field: 'age', operator: 'greater_than', value: 18 };
1006 | const narrowTypingRule: NewNumberRule = { type: 'number', field: 'age', operator: 'less_than', value: 30 };
1007 |
1008 | const wideAfterAdding = addRuleToUnion(root, wideTypingRule);
1009 | const narrowAfterAdding = addRuleToUnion(root, narrowTypingRule);
1010 |
1011 | console.log(wideAfterAdding);
1012 | console.log(narrowAfterAdding);
1013 | ```
1014 |
1015 | ```json
1016 | {
1017 | "entity": "rule",
1018 | "id": "560f4e04-f786-4269-bbdd-704ad9793518",
1019 | "parent_id": "6fa1aaa6-cfab-4647-a30c-a58af3e0a4d4",
1020 | "type": "number",
1021 | "field": "age",
1022 | "operator": "greater_than",
1023 | "value": 18
1024 | }
1025 | ```
1026 |
1027 | ```json
1028 | {
1029 | "entity": "rule",
1030 | "id": "46a36441-3f28-4dd7-8420-b1d584527a74",
1031 | "parent_id": "6fa1aaa6-cfab-4647-a30c-a58af3e0a4d4",
1032 | "type": "number",
1033 | "field": "age",
1034 | "operator": "less_than",
1035 | "value": 30
1036 | }
1037 | ```
1038 |
1039 | Similarly, a union can also be pre-composed before adding it to the rules engine:
1040 |
1041 | ```ts
1042 | import { NewUnion, addUnionToUnion, createRoot } from 'rules-engine-ts';
1043 |
1044 | const userSelectsAnd = false;
1045 |
1046 | const root = createRoot({ connector: 'and' });
1047 | const union: NewUnion = { connector: userSelectsAnd ? 'and' : 'or' };
1048 |
1049 | const unionAfterAdding = addUnionToUnion(root, union);
1050 | console.log(unionAfterAdding);
1051 | ```
1052 |
1053 | ```json
1054 | {
1055 | "entity": "union",
1056 | "id": "d2ce2a4e-ec53-4a64-9677-e9051c634bd1",
1057 | "parent_id": "8b32fdc4-8e92-424f-9c00-1204838759e0",
1058 | "connector": "or",
1059 | "rules": []
1060 | }
1061 | ```
1062 |
1063 | ## To Do
1064 |
1065 | - [ ] Create recipe examples
1066 | - [ ] Create function to detect conflicting or redundant rules
1067 | - [ ] Create a UI builder tool
1068 |
1069 | ## Authors
1070 |
1071 | - [@andrewvo89](https://github.com/andrewvo89) - Idea & Initial work.
1072 |
1073 | See also the list of [contributors](https://github.com/andrewvo89/rules-engine-ts/contributors) who participated in this project.
1074 |
--------------------------------------------------------------------------------
/jest.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from '@jest/types';
2 |
3 | const config: Config.InitialOptions = {
4 | verbose: true,
5 | transform: {
6 | '^.+\\.tsx?$': 'ts-jest',
7 | },
8 | testPathIgnorePatterns: ['/lib/'],
9 | };
10 |
11 | export default config;
12 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rules-engine-ts",
3 | "version": "2.0.4",
4 | "repository": "https://github.com/andrewvo89/rules-engine-ts.git",
5 | "author": "Andrew Vo-Nguyen (https://andrewvo.co)",
6 | "description": "Strongly typed rules engine for evaluating deep and complex rules",
7 | "scripts": {
8 | "build": "eslint && rm -r -f lib && tsc -p tsconfig.json",
9 | "commit": "cz",
10 | "format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
11 | "lint": "eslint",
12 | "pack": "npm pack",
13 | "release:major": "yarn build && npm version major && npm publish",
14 | "release:minor": "yarn build && npm version minor && npm publish",
15 | "release:patch": "yarn build && npm version patch && npm publish",
16 | "test": "jest",
17 | "test:coverage": "yarn test --coverage"
18 | },
19 | "license": "MIT",
20 | "devDependencies": {
21 | "@types/jest": "^29.4.0",
22 | "@types/lodash.get": "^4.4.7",
23 | "@types/lodash.isplainobject": "^4.0.7",
24 | "@types/node": "^18.14.6",
25 | "@types/uuid": "^9.0.1",
26 | "@typescript-eslint/eslint-plugin": "^5.54.0",
27 | "@typescript-eslint/parser": "^5.54.0",
28 | "cz-conventional-changelog": "^3.3.0",
29 | "eslint": "^8.35.0",
30 | "eslint-config-prettier": "^8.6.0",
31 | "jest": "^29.4.3",
32 | "prettier": "^2.8.4",
33 | "ts-jest": "^29.0.5",
34 | "typescript": "^4.9.5"
35 | },
36 | "dependencies": {
37 | "lodash.get": "^4.4.2",
38 | "lodash.isplainobject": "^4.0.6",
39 | "uuid": "^9.0.0",
40 | "zod": "^3.21.2",
41 | "zod-error": "^1.5.0"
42 | },
43 | "config": {
44 | "commitizen": {
45 | "path": "./node_modules/cz-conventional-changelog"
46 | }
47 | },
48 | "main": "lib/index.js",
49 | "types": "lib/index.d.ts",
50 | "files": [
51 | "lib/**/*",
52 | "LICENSE"
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/src/functions/add-any-to-union.test.ts:
--------------------------------------------------------------------------------
1 | import { NewRule } from '../types/rule';
2 | import { NewUnion } from '../types/union';
3 | import { addAnyToUnion } from './add-any-to-union';
4 | import { createRoot } from './create-root';
5 |
6 | test('union is added to a union', () => {
7 | const root = createRoot({ connector: 'and' });
8 | const newUnion: NewUnion = {
9 | connector: 'and',
10 | };
11 | const union = addAnyToUnion(root, newUnion);
12 | expect(root.rules.length).toBe(1);
13 | expect(root.rules[0]).toBe(union);
14 | expect(union.parent_id).toBe(root.id);
15 | });
16 |
17 | test('rule is added to a union', () => {
18 | const root = createRoot({ connector: 'and' });
19 | const newRule: NewRule = {
20 | field: 'name',
21 | operator: 'contains',
22 | type: 'string',
23 | value: 'bob',
24 | };
25 | const rule = addAnyToUnion(root, newRule);
26 | expect(root.rules.length).toBe(1);
27 | expect(root.rules[0]).toBe(rule);
28 | expect(rule.parent_id).toBe(root.id);
29 | });
30 |
--------------------------------------------------------------------------------
/src/functions/add-any-to-union.ts:
--------------------------------------------------------------------------------
1 | import { NewRule, Rule } from '../types/rule';
2 | import { NewUnion, RootUnion, Union } from '../types/union';
3 |
4 | import { addRuleToUnion } from './add-rule-to-union';
5 | import { addUnionToUnion } from './add-union-to-union';
6 | import { newRuleSchema } from '../validations/rule';
7 | import { newUnionSchema } from '../validations/union';
8 |
9 | /**
10 | * Add a rule to a union.
11 | * This function will mutate the union.
12 | * @export
13 | * @param {(RootUnion | Union)} parent
14 | * @param {(NewRule | NewUnion)} newRuleOrUnion
15 | * @return {*} {(Rule | Union)}
16 | */
17 | export function addAnyToUnion(parent: RootUnion | Union, newRuleOrUnion: NewRule | NewUnion): Rule | Union {
18 | const isNewRule = (ruleOrUnion: NewRule | NewUnion): ruleOrUnion is NewRule =>
19 | newRuleSchema.safeParse(ruleOrUnion).success;
20 |
21 | if (isNewRule(newRuleOrUnion)) {
22 | return addRuleToUnion(parent, newRuleOrUnion);
23 | }
24 |
25 | return addUnionToUnion(parent, newUnionSchema.parse(newRuleOrUnion));
26 | }
27 |
--------------------------------------------------------------------------------
/src/functions/add-many-to-union.test.ts:
--------------------------------------------------------------------------------
1 | import { NewRule } from '../types/rule';
2 | import { NewUnion } from '../types/union';
3 | import { addManyToUnion } from './add-many-to-union';
4 | import { createRoot } from './create-root';
5 |
6 | test('rule and a union is added to a union', () => {
7 | const root = createRoot({ connector: 'and' });
8 | const newUnion: NewUnion = {
9 | connector: 'and',
10 | };
11 | const newRule: NewRule = {
12 | field: 'name',
13 | operator: 'contains',
14 | type: 'string',
15 | value: 'bob',
16 | };
17 | const rulesOrUnions = addManyToUnion(root, [newUnion, newRule]);
18 | expect(root.rules.length).toBe(2);
19 | rulesOrUnions.forEach((rule, index) => {
20 | expect(root.rules[index]).toBe(rule);
21 | expect(rule.parent_id).toBe(root.id);
22 | });
23 | expect(rulesOrUnions[0].entity === 'union');
24 | expect(rulesOrUnions[1].entity === 'rule');
25 | });
26 |
--------------------------------------------------------------------------------
/src/functions/add-many-to-union.ts:
--------------------------------------------------------------------------------
1 | import { NewRule, Rule } from '../types/rule';
2 | import { NewUnion, RootUnion, Union } from '../types/union';
3 |
4 | import { addAnyToUnion } from './add-any-to-union';
5 |
6 | /**
7 | * Add many rules or unions to a union.
8 | * This function will mutate the parent union.
9 | * @export
10 | * @param {(RootUnion | Union)} parent
11 | * @param {((NewRule | NewUnion)[])} newRulesOrUnions
12 | * @return {*} {((Rule | Union)[])}
13 | */
14 | export function addManyToUnion(parent: RootUnion | Union, newRulesOrUnions: (NewRule | NewUnion)[]): (Rule | Union)[] {
15 | return newRulesOrUnions.map((newRuleOrUnion) => addAnyToUnion(parent, newRuleOrUnion));
16 | }
17 |
--------------------------------------------------------------------------------
/src/functions/add-rule-to-union.test.ts:
--------------------------------------------------------------------------------
1 | import { NewRule } from '../types/rule';
2 | import { addRuleToUnion } from './add-rule-to-union';
3 | import { createRoot } from './create-root';
4 |
5 | test('rule is added to a union', () => {
6 | const root = createRoot({ connector: 'and' });
7 | const newRule: NewRule = {
8 | field: 'name',
9 | operator: 'contains',
10 | type: 'string',
11 | value: 'bob',
12 | };
13 | const rule = addRuleToUnion(root, newRule);
14 | expect(root.rules.length).toBe(1);
15 | expect(root.rules[0]).toBe(rule);
16 | expect(rule.parent_id).toBe(root.id);
17 | });
18 |
--------------------------------------------------------------------------------
/src/functions/add-rule-to-union.ts:
--------------------------------------------------------------------------------
1 | import { NewRule, Rule } from '../types/rule';
2 | import { RootUnion, Union } from '../types/union';
3 |
4 | import { ruleSchema } from '../validations/rule';
5 | import { v4 as uuidv4 } from 'uuid';
6 |
7 | /**
8 | * Add a rule to a union.
9 | * This function will mutate the parent union.
10 | * @export
11 | * @param {(RootUnion | Union)} parent
12 | * @param {NewRule} newRule
13 | * @return {*} {Rule}
14 | */
15 | export function addRuleToUnion(parent: RootUnion | Union, newRule: NewRule): Rule {
16 | const rule = ruleSchema.parse({ ...newRule, id: uuidv4(), parent_id: parent.id, entity: 'rule' });
17 | parent.rules.push(rule);
18 | return rule;
19 | }
20 |
--------------------------------------------------------------------------------
/src/functions/add-rules-to-union.test.ts:
--------------------------------------------------------------------------------
1 | import { NewRule } from '../types/rule';
2 | import { addRulesToUnion } from './add-rules-to-union';
3 | import { createRoot } from './create-root';
4 |
5 | test('rules are added to a union', () => {
6 | const root = createRoot({ connector: 'and' });
7 | const newRuleA: NewRule = {
8 | field: 'name',
9 | operator: 'contains',
10 | type: 'string',
11 | value: 'bob',
12 | };
13 | const newRuleB: NewRule = {
14 | field: 'name',
15 | operator: 'contains',
16 | type: 'string',
17 | value: 'alice',
18 | };
19 | expect(root.rules.length).toBe(0);
20 | const rules = addRulesToUnion(root, [newRuleA, newRuleB]);
21 | expect(root.rules.length).toBe(2);
22 | rules.forEach((rule, index) => {
23 | expect(root.rules[index]).toBe(rule);
24 | expect(rule.parent_id).toBe(root.id);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/functions/add-rules-to-union.ts:
--------------------------------------------------------------------------------
1 | import { NewRule, Rule } from '../types/rule';
2 | import { RootUnion, Union } from '../types/union';
3 |
4 | import { addRuleToUnion } from './add-rule-to-union';
5 |
6 | /**
7 | * Add many rules to a union.
8 | * This function will mutate the parent union.
9 | * @export
10 | * @param {(RootUnion | Union)} parent
11 | * @param {NewRule[]} newRules
12 | * @return {*} {Rule[]}
13 | */
14 | export function addRulesToUnion(parent: RootUnion | Union, newRules: NewRule[]): Rule[] {
15 | return newRules.map((newRule) => addRuleToUnion(parent, newRule));
16 | }
17 |
--------------------------------------------------------------------------------
/src/functions/add-union-to-union.test.ts:
--------------------------------------------------------------------------------
1 | import { NewUnion } from '../types/union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 |
5 | test('union is added to a union', () => {
6 | const root = createRoot({ connector: 'and' });
7 | const newUnion: NewUnion = {
8 | connector: 'and',
9 | };
10 | const union = addUnionToUnion(root, newUnion);
11 | expect(root.rules.length).toBe(1);
12 | expect(root.rules[0]).toBe(union);
13 | expect(union.parent_id).toBe(root.id);
14 | });
15 |
--------------------------------------------------------------------------------
/src/functions/add-union-to-union.ts:
--------------------------------------------------------------------------------
1 | import { NewUnion, RootUnion, Union } from '../types/union';
2 |
3 | import { unionSchema } from '../validations/union';
4 | import { v4 as uuidv4 } from 'uuid';
5 |
6 | /**
7 | * Add a new union to a union.
8 | * This function will mutate the parent union.
9 | * @export
10 | * @param {(RootUnion | Union)} parent
11 | * @param {NewUnion} newUnion
12 | * @return {*} {Union}
13 | */
14 | export function addUnionToUnion(parent: RootUnion | Union, newUnion: NewUnion): Union {
15 | const union = unionSchema.parse({ ...newUnion, id: uuidv4(), parent_id: parent.id, entity: 'union', rules: [] });
16 | parent.rules.push(union);
17 | return union;
18 | }
19 |
--------------------------------------------------------------------------------
/src/functions/add-unions-to-union.test.ts:
--------------------------------------------------------------------------------
1 | import { NewUnion } from '../types/union';
2 | import { addUnionsToUnion } from './add-unions-to-union';
3 | import { createRoot } from './create-root';
4 |
5 | test('unions are added to a union', () => {
6 | const root = createRoot({ connector: 'and' });
7 | const newUnionA: NewUnion = {
8 | connector: 'and',
9 | };
10 | const newUnionB: NewUnion = {
11 | connector: 'or',
12 | };
13 | expect(root.rules.length).toBe(0);
14 | const rules = addUnionsToUnion(root, [newUnionA, newUnionB]);
15 | expect(root.rules.length).toBe(2);
16 | rules.forEach((rule, index) => {
17 | expect(root.rules[index]).toBe(rule);
18 | expect(rule.parent_id).toBe(root.id);
19 | });
20 | });
21 |
--------------------------------------------------------------------------------
/src/functions/add-unions-to-union.ts:
--------------------------------------------------------------------------------
1 | import { NewUnion, RootUnion, Union } from '../types/union';
2 |
3 | import { addUnionToUnion } from './add-union-to-union';
4 |
5 | /**
6 | * Add many unions to a union.
7 | * This function will mutate the parent union.
8 | * @export
9 | * @param {(RootUnion | Union)} parent
10 | * @param {NewUnion[]} newUnions
11 | * @return {*} {Union[]}
12 | */
13 | export function addUnionsToUnion(parent: RootUnion | Union, newUnions: NewUnion[]): Union[] {
14 | return newUnions.map((newUnion) => addUnionToUnion(parent, newUnion));
15 | }
16 |
--------------------------------------------------------------------------------
/src/functions/create-root.test.ts:
--------------------------------------------------------------------------------
1 | import { createRoot } from './create-root';
2 | import { z } from 'zod';
3 |
4 | test('root union is created', () => {
5 | const root = createRoot({ connector: 'and' });
6 | expect(root.entity).toBe('root_union');
7 | expect(root.rules.length).toBe(0);
8 | expect(z.string().uuid().safeParse(root.id).success).toBeTruthy();
9 | expect(root.connector).toBe('and');
10 | });
11 |
--------------------------------------------------------------------------------
/src/functions/create-root.ts:
--------------------------------------------------------------------------------
1 | import { NewUnion, RootUnion } from '../types/union';
2 |
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | /**
6 | * Creates a new root union.
7 | * @export
8 | * @param {NewUnion} newUnion
9 | * @return {*} {RootUnion}
10 | */
11 | export function createRoot(newUnion: NewUnion): RootUnion {
12 | return { ...newUnion, entity: 'root_union', id: uuidv4(), rules: [] };
13 | }
14 |
--------------------------------------------------------------------------------
/src/functions/find-any-by-id.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { findAnyById } from './find-any-by-id';
5 | import { v4 as uuidv4 } from 'uuid';
6 |
7 | const root = createRoot({ connector: 'or' });
8 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
9 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
10 | const union = addUnionToUnion(root, { connector: 'and' });
11 | addRuleToUnion(union, { field: 'age', operator: 'greater_than', type: 'number', value: 18 });
12 | const rule = addRuleToUnion(union, { field: 'age', operator: 'less_than', type: 'number', value: 30 });
13 | const union2 = addUnionToUnion(union, { connector: 'and' });
14 |
15 | test('find root union', () => {
16 | const result = findAnyById(root, root.id);
17 | expect(result).toBe(root);
18 | });
19 |
20 | test('find deeply nested rule', () => {
21 | const result = findAnyById(root, rule.id);
22 | expect(result).toBe(rule);
23 | });
24 |
25 | test('find deeply nested union', () => {
26 | const result = findAnyById(root, union2.id);
27 | expect(result).toBe(union2);
28 | });
29 |
30 | test('find non existent rule', () => {
31 | const result = findAnyById(root, uuidv4());
32 | expect(result).toBeUndefined();
33 | });
34 |
--------------------------------------------------------------------------------
/src/functions/find-any-by-id.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion, Union } from '../types/union';
2 |
3 | import { Rule } from '../types/rule';
4 |
5 | /**
6 | * Find a rule or a union by id.
7 | * @export
8 | * @param {(RootUnion | Union)} union
9 | * @param {string} id
10 | * @return {*} {(RootUnion | Union | Rule | undefined)}
11 | */
12 | export function findAnyById(union: RootUnion | Union, id: string): RootUnion | Union | Rule | undefined {
13 | if (union.id === id) {
14 | return union;
15 | }
16 | return union.rules.reduce((foundUnion, ruleOrUnion) => {
17 | if (foundUnion) {
18 | return foundUnion;
19 | }
20 | if (ruleOrUnion.id === id) {
21 | return ruleOrUnion;
22 | }
23 | if (ruleOrUnion.entity === 'union') {
24 | return findAnyById(ruleOrUnion, id);
25 | }
26 | return foundUnion;
27 | }, undefined);
28 | }
29 |
--------------------------------------------------------------------------------
/src/functions/find-rule-by-id.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { findRuleById } from './find-rule-by-id';
5 | import { v4 as uuidv4 } from 'uuid';
6 |
7 | const root = createRoot({ connector: 'or' });
8 |
9 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
10 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
11 | const union = addUnionToUnion(root, { connector: 'and' });
12 |
13 | addRuleToUnion(union, { field: 'age', operator: 'greater_than', type: 'number', value: 18 });
14 | const rule = addRuleToUnion(union, { field: 'age', operator: 'less_than', type: 'number', value: 30 });
15 | const union2 = addUnionToUnion(union, { connector: 'and' });
16 |
17 | test('find root union', () => {
18 | const result = findRuleById(root, root.id);
19 | expect(result).toBeUndefined();
20 | });
21 |
22 | test('find deeply nested rule', () => {
23 | const result = findRuleById(root, rule.id);
24 | expect(result).toBe(rule);
25 | });
26 |
27 | test('find deeply nested union', () => {
28 | const result = findRuleById(root, union2.id);
29 | expect(result).toBeUndefined();
30 | });
31 |
32 | test('find non existent rule', () => {
33 | const result = findRuleById(root, uuidv4());
34 | expect(result).toBeUndefined();
35 | });
36 |
--------------------------------------------------------------------------------
/src/functions/find-rule-by-id.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion, Union } from '../types/union';
2 |
3 | import { Rule } from '../types/rule';
4 |
5 | /**
6 | * Find a rule by id.
7 | * @export
8 | * @param {(RootUnion | Union)} union
9 | * @param {string} id
10 | * @return {*} {(Rule | undefined)}
11 | */
12 | export function findRuleById(union: RootUnion | Union, id: string): Rule | undefined {
13 | return union.rules.reduce((foundRule, ruleOrUnion) => {
14 | if (foundRule) {
15 | return foundRule;
16 | }
17 | if (ruleOrUnion.entity === 'rule') {
18 | return ruleOrUnion.id === id ? ruleOrUnion : undefined;
19 | }
20 | return findRuleById(ruleOrUnion, id);
21 | }, undefined);
22 | }
23 |
--------------------------------------------------------------------------------
/src/functions/find-union-by-id.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { findUnionById } from './find-union-by-id';
5 | import { v4 as uuidv4 } from 'uuid';
6 |
7 | const root = createRoot({ connector: 'or' });
8 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
9 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
10 | const union = addUnionToUnion(root, { connector: 'and' });
11 | addRuleToUnion(union, { field: 'age', operator: 'greater_than', type: 'number', value: 18 });
12 | const rule = addRuleToUnion(union, { field: 'age', operator: 'less_than', type: 'number', value: 30 });
13 | const union2 = addUnionToUnion(union, { connector: 'and' });
14 |
15 | test('find root union', () => {
16 | const result = findUnionById(root, root.id);
17 | expect(result).toBe(root);
18 | });
19 |
20 | test('find deeply nested rule', () => {
21 | const result = findUnionById(root, rule.id);
22 | expect(result).toBeUndefined();
23 | });
24 |
25 | test('find deeply nested union', () => {
26 | const result = findUnionById(root, union2.id);
27 | expect(result).toBe(union2);
28 | });
29 |
30 | test('find non existent rule', () => {
31 | const result = findUnionById(root, uuidv4());
32 | expect(result).toBeUndefined();
33 | });
34 |
--------------------------------------------------------------------------------
/src/functions/find-union-by-id.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion, Union } from '../types/union';
2 |
3 | /**
4 | * Find a union by id.
5 | * @export
6 | * @param {(RootUnion | Union)} union
7 | * @param {string} id
8 | * @return {*} {(RootUnion | Union | undefined)}
9 | */
10 | export function findUnionById(union: RootUnion | Union, id: string): RootUnion | Union | undefined {
11 | if (union.id === id) {
12 | return union;
13 | }
14 | return union.rules.reduce((foundUnion, ruleOrUnion) => {
15 | if (foundUnion || ruleOrUnion.entity === 'rule') {
16 | return foundUnion;
17 | }
18 | if (ruleOrUnion.id === id) {
19 | return ruleOrUnion;
20 | }
21 | return findUnionById(ruleOrUnion, id);
22 | }, undefined);
23 | }
24 |
--------------------------------------------------------------------------------
/src/functions/is-array-length-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewArrayLengthRule } from '../types/rule';
2 | import { isArrayLengthRuleValid } from './is-array-length-rule-valid';
3 |
4 | const names = ['bob', 'alice'];
5 |
6 | test('array length is equal to', () => {
7 | const rule: NewArrayLengthRule = {
8 | field: 'names',
9 | operator: 'equals_to',
10 | type: 'array_length',
11 | value: 2,
12 | };
13 | const result = isArrayLengthRuleValid(rule, names);
14 | expect(result).toBeTruthy();
15 | });
16 |
17 | test('array length does not equal to', () => {
18 | const rule: NewArrayLengthRule = {
19 | field: 'names',
20 | operator: 'does_not_equal_to',
21 | type: 'array_length',
22 | value: 1,
23 | };
24 | const result = isArrayLengthRuleValid(rule, names);
25 | expect(result).toBeTruthy();
26 | });
27 |
28 | test('array length is greater than', () => {
29 | const rule: NewArrayLengthRule = {
30 | field: 'names',
31 | operator: 'greater_than',
32 | type: 'array_length',
33 | value: 1,
34 | };
35 | const result = isArrayLengthRuleValid(rule, names);
36 | expect(result).toBeTruthy();
37 | });
38 |
39 | test('array length is greater than or equal to', () => {
40 | const rule: NewArrayLengthRule = {
41 | field: 'names',
42 | operator: 'greater_than_or_equal_to',
43 | type: 'array_length',
44 | value: 2,
45 | };
46 | const result = isArrayLengthRuleValid(rule, names);
47 | expect(result).toBeTruthy();
48 | });
49 |
50 | test('array length is less than', () => {
51 | const rule: NewArrayLengthRule = {
52 | field: 'names',
53 | operator: 'less_than',
54 | type: 'array_length',
55 | value: 3,
56 | };
57 | const result = isArrayLengthRuleValid(rule, names);
58 | expect(result).toBeTruthy();
59 | });
60 |
61 | test('array length is less than or equal to', () => {
62 | const rule: NewArrayLengthRule = {
63 | field: 'names',
64 | operator: 'less_than_or_equal_to',
65 | type: 'array_length',
66 | value: 2,
67 | };
68 | const result = isArrayLengthRuleValid(rule, names);
69 | expect(result).toBeTruthy();
70 | });
71 |
72 | test('invalid operator is handled', () => {
73 | const rule: NewArrayLengthRule = {
74 | field: 'names',
75 | // @ts-expect-error
76 | operator: 'is_more_awesome_than',
77 | type: 'array_length',
78 | value: 2,
79 | };
80 | const result = isArrayLengthRuleValid(rule, names);
81 | expect(result).toBeFalsy();
82 | });
83 |
--------------------------------------------------------------------------------
/src/functions/is-array-length-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewArrayLengthRule } from '../types/rule';
2 |
3 | /**
4 | * Check if an array length rule is valid.
5 | * @export
6 | * @param {NewArrayLengthRule} rule
7 | * @param {any[]} value
8 | * @return {*} {boolean}
9 | */
10 | export function isArrayLengthRuleValid(rule: NewArrayLengthRule, value: any[]): boolean {
11 | switch (rule.operator) {
12 | case 'equals_to':
13 | return value.length === rule.value;
14 | case 'does_not_equal_to':
15 | return value.length !== rule.value;
16 | case 'greater_than':
17 | return value.length > rule.value;
18 | case 'greater_than_or_equal_to':
19 | return value.length >= rule.value;
20 | case 'less_than':
21 | return value.length < rule.value;
22 | case 'less_than_or_equal_to':
23 | return value.length <= rule.value;
24 | default:
25 | return false;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/functions/is-array-value-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewArrayValueRule } from '../types/rule';
2 | import { isArrayValueRuleValid } from './is-array-value-rule-valid';
3 |
4 | const bob = { name: 'bob' };
5 | const alice = { name: 'alice' };
6 | const carol = { name: 'carol' };
7 | const people = [bob, alice];
8 | const all_bob = [bob, bob, bob, bob];
9 |
10 | test('array contains element', () => {
11 | const rule: NewArrayValueRule = {
12 | field: 'people',
13 | operator: 'contains',
14 | type: 'array_value',
15 | value: bob,
16 | };
17 | const result = isArrayValueRuleValid(rule, people);
18 | expect(result).toBeTruthy();
19 | });
20 |
21 | test('array does not contain element', () => {
22 | const rule: NewArrayValueRule = {
23 | field: 'people',
24 | operator: 'does_not_contain',
25 | type: 'array_value',
26 | value: carol,
27 | };
28 | const result = isArrayValueRuleValid(rule, people);
29 | expect(result).toBeTruthy();
30 | });
31 |
32 | test('array contains all of an element', () => {
33 | const rule: NewArrayValueRule = {
34 | field: 'people',
35 | operator: 'contains_all',
36 | type: 'array_value',
37 | value: bob,
38 | };
39 | const result = isArrayValueRuleValid(rule, all_bob);
40 | expect(result).toBeTruthy();
41 | });
42 |
43 | test('invalid operator is handled', () => {
44 | const rule: NewArrayValueRule = {
45 | field: 'people',
46 | // @ts-expect-error
47 | operator: 'is_more_awesome_than',
48 | type: 'array_value',
49 | value: bob,
50 | };
51 | const result = isArrayValueRuleValid(rule, people);
52 | expect(result).toBeFalsy();
53 | });
54 |
--------------------------------------------------------------------------------
/src/functions/is-array-value-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewArrayValueRule } from '../types/rule';
2 |
3 | /**
4 | * Check if an array value rule is valid.
5 | * @export
6 | * @param {NewArrayValueRule} rule
7 | * @param {any[]} value
8 | * @return {*} {boolean}
9 | */
10 | export function isArrayValueRuleValid(rule: NewArrayValueRule, value: any[]): boolean {
11 | switch (rule.operator) {
12 | case 'contains':
13 | return value.includes(rule.value);
14 | case 'does_not_contain':
15 | return !value.includes(rule.value);
16 | case 'contains_all':
17 | return value.every((v) => v === rule.value);
18 | default:
19 | return false;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/functions/is-boolean-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewBooleanRule } from '../types/rule';
2 | import { isBooleanRuleValid } from './is-boolean-rule-valid';
3 |
4 | const isValid = true;
5 | const isInvalid = false;
6 |
7 | test('boolean is true', () => {
8 | const rule: NewBooleanRule = {
9 | field: 'status',
10 | operator: 'is_true',
11 | type: 'boolean',
12 | };
13 | const result = isBooleanRuleValid(rule, isValid);
14 | expect(result).toBeTruthy();
15 | });
16 |
17 | test('boolean is false', () => {
18 | const rule: NewBooleanRule = {
19 | field: 'status',
20 | operator: 'is_false',
21 | type: 'boolean',
22 | };
23 | const result = isBooleanRuleValid(rule, isInvalid);
24 | expect(result).toBeTruthy();
25 | });
26 |
27 | test('invalid operator is handled', () => {
28 | const rule: NewBooleanRule = {
29 | field: 'status',
30 | // @ts-expect-error
31 | operator: 'is_more_awesome_than',
32 | type: 'boolean',
33 | };
34 | const result = isBooleanRuleValid(rule, isValid);
35 | expect(result).toBeFalsy();
36 | });
37 |
--------------------------------------------------------------------------------
/src/functions/is-boolean-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewBooleanRule } from '../types/rule';
2 |
3 | /**
4 | * Check if a boolean rule is valid.
5 | * @export
6 | * @param {NewBooleanRule} rule
7 | * @param {boolean} value
8 | * @return {*} {boolean}
9 | */
10 | export function isBooleanRuleValid(rule: NewBooleanRule, value: boolean): boolean {
11 | switch (rule.operator) {
12 | case 'is_true':
13 | return value === true;
14 | case 'is_false':
15 | return value === false;
16 | default:
17 | return false;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/functions/is-generic-comparison-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewGenericComparisonRule } from '../types/rule';
2 | import { isGenericComparisonRuleValid } from './is-generic-comparison-rule-valid';
3 |
4 | test('value is equal to', () => {
5 | const rule: NewGenericComparisonRule = {
6 | field: 'names',
7 | operator: 'equals_to',
8 | type: 'generic_comparison',
9 | value: 2,
10 | };
11 | const result = isGenericComparisonRuleValid(rule, 2);
12 | expect(result).toBeTruthy();
13 | });
14 |
15 | test('value does not equal to', () => {
16 | const rule: NewGenericComparisonRule = {
17 | field: 'names',
18 | operator: 'does_not_equal_to',
19 | type: 'generic_comparison',
20 | value: 1,
21 | };
22 | const result = isGenericComparisonRuleValid(rule, 2);
23 | expect(result).toBeTruthy();
24 | });
25 |
26 | test('value is greater than', () => {
27 | const rule: NewGenericComparisonRule = {
28 | field: 'names',
29 | operator: 'greater_than',
30 | type: 'generic_comparison',
31 | value: 'alice',
32 | };
33 | const result = isGenericComparisonRuleValid(rule, 'bob');
34 | expect(result).toBeTruthy();
35 | });
36 |
37 | test('value is greater than or equal to', () => {
38 | const rule: NewGenericComparisonRule = {
39 | field: 'names',
40 | operator: 'greater_than_or_equal_to',
41 | type: 'generic_comparison',
42 | value: 'alice',
43 | };
44 | const result = isGenericComparisonRuleValid(rule, 'bob');
45 | expect(result).toBeTruthy();
46 | });
47 |
48 | test('value is less than', () => {
49 | const rule: NewGenericComparisonRule = {
50 | field: 'names',
51 | operator: 'less_than',
52 | type: 'generic_comparison',
53 | value: 'bob',
54 | };
55 | const result = isGenericComparisonRuleValid(rule, 'alice');
56 | expect(result).toBeTruthy();
57 | });
58 |
59 | test('value is less than or equal to', () => {
60 | const rule: NewGenericComparisonRule = {
61 | field: 'names',
62 | operator: 'less_than_or_equal_to',
63 | type: 'generic_comparison',
64 | value: 'bob',
65 | };
66 | const result = isGenericComparisonRuleValid(rule, 'alice');
67 | expect(result).toBeTruthy();
68 | });
69 |
70 | test('invalid operator is handled', () => {
71 | const rule: NewGenericComparisonRule = {
72 | field: 'names',
73 | // @ts-expect-error
74 | operator: 'is_more_awesome_than',
75 | type: 'generic_comparison',
76 | value: 1,
77 | };
78 | const result = isGenericComparisonRuleValid(rule, 1);
79 | expect(result).toBeFalsy();
80 | });
81 |
--------------------------------------------------------------------------------
/src/functions/is-generic-comparison-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewGenericComparisonRule } from '../types/rule';
2 |
3 | /**
4 | * Check if a generic comparison rule is valid.
5 | * @export
6 | * @param {NewGenericComparisonRule} rule
7 | * @param {*} value
8 | * @return {*} {boolean}
9 | */
10 | export function isGenericComparisonRuleValid(rule: NewGenericComparisonRule, value: any): boolean {
11 | switch (rule.operator) {
12 | case 'equals_to':
13 | return value === rule.value;
14 | case 'does_not_equal_to':
15 | return value !== rule.value;
16 | case 'greater_than':
17 | return value > rule.value;
18 | case 'greater_than_or_equal_to':
19 | return value >= rule.value;
20 | case 'less_than':
21 | return value < rule.value;
22 | case 'less_than_or_equal_to':
23 | return value <= rule.value;
24 | default:
25 | return false;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/functions/is-generic-type-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewGenericTypeRule } from '../types/rule';
2 | import { isGenericTypeRuleValid } from './is-generic-type-rule-valid';
3 |
4 | test('value is truth', () => {
5 | const rule: NewGenericTypeRule = {
6 | field: 'name',
7 | operator: 'is_truthy',
8 | type: 'generic_type',
9 | };
10 | const result = isGenericTypeRuleValid(rule, 'bob');
11 | expect(result).toBeTruthy();
12 | });
13 |
14 | test('value is falsey', () => {
15 | const rule: NewGenericTypeRule = {
16 | field: 'name',
17 | operator: 'is_falsey',
18 | type: 'generic_type',
19 | };
20 | const result = isGenericTypeRuleValid(rule, undefined);
21 | expect(result).toBeTruthy();
22 | });
23 |
24 | test('value is null', () => {
25 | const rule: NewGenericTypeRule = {
26 | field: 'name',
27 | operator: 'is_null',
28 | type: 'generic_type',
29 | };
30 | const result = isGenericTypeRuleValid(rule, null);
31 | expect(result).toBeTruthy();
32 | });
33 |
34 | test('value is not null', () => {
35 | const rule: NewGenericTypeRule = {
36 | field: 'name',
37 | operator: 'is_not_null',
38 | type: 'generic_type',
39 | };
40 | const result = isGenericTypeRuleValid(rule, {});
41 | expect(result).toBeTruthy();
42 | });
43 |
44 | test('value is undefined', () => {
45 | const rule: NewGenericTypeRule = {
46 | field: 'name',
47 | operator: 'is_undefined',
48 | type: 'generic_type',
49 | };
50 | const result = isGenericTypeRuleValid(rule, undefined);
51 | expect(result).toBeTruthy();
52 | });
53 |
54 | test('value is not undefined', () => {
55 | const rule: NewGenericTypeRule = {
56 | field: 'name',
57 | operator: 'is_not_undefined',
58 | type: 'generic_type',
59 | };
60 | const result = isGenericTypeRuleValid(rule, false);
61 | expect(result).toBeTruthy();
62 | });
63 |
64 | test('value is string', () => {
65 | const rule: NewGenericTypeRule = {
66 | field: 'name',
67 | operator: 'is_string',
68 | type: 'generic_type',
69 | };
70 | const result = isGenericTypeRuleValid(rule, 'alice');
71 | expect(result).toBeTruthy();
72 | });
73 |
74 | test('value is not string', () => {
75 | const rule: NewGenericTypeRule = {
76 | field: 'name',
77 | operator: 'is_not_string',
78 | type: 'generic_type',
79 | };
80 | const result = isGenericTypeRuleValid(rule, 12345);
81 | expect(result).toBeTruthy();
82 | });
83 |
84 | test('value is number', () => {
85 | const rule: NewGenericTypeRule = {
86 | field: 'name',
87 | operator: 'is_number',
88 | type: 'generic_type',
89 | };
90 | const result = isGenericTypeRuleValid(rule, 123.4);
91 | expect(result).toBeTruthy();
92 | });
93 |
94 | test('value is not number', () => {
95 | const rule: NewGenericTypeRule = {
96 | field: 'name',
97 | operator: 'is_not_number',
98 | type: 'generic_type',
99 | };
100 | const result = isGenericTypeRuleValid(rule, '12345');
101 | expect(result).toBeTruthy();
102 | });
103 |
104 | test('value is boolean', () => {
105 | const rule: NewGenericTypeRule = {
106 | field: 'name',
107 | operator: 'is_boolean',
108 | type: 'generic_type',
109 | };
110 | const result = isGenericTypeRuleValid(rule, false);
111 | expect(result).toBeTruthy();
112 | });
113 |
114 | test('value is not boolean', () => {
115 | const rule: NewGenericTypeRule = {
116 | field: 'name',
117 | operator: 'is_not_boolean',
118 | type: 'generic_type',
119 | };
120 | const result = isGenericTypeRuleValid(rule, 'false');
121 | expect(result).toBeTruthy();
122 | });
123 |
124 | test('value is array', () => {
125 | const rule: NewGenericTypeRule = {
126 | field: 'name',
127 | operator: 'is_array',
128 | type: 'generic_type',
129 | };
130 | const result = isGenericTypeRuleValid(rule, []);
131 | expect(result).toBeTruthy();
132 | });
133 |
134 | test('value is not array', () => {
135 | const rule: NewGenericTypeRule = {
136 | field: 'name',
137 | operator: 'is_not_array',
138 | type: 'generic_type',
139 | };
140 | const result = isGenericTypeRuleValid(rule, '[]');
141 | expect(result).toBeTruthy();
142 | });
143 |
144 | test('value is object', () => {
145 | const rule: NewGenericTypeRule = {
146 | field: 'name',
147 | operator: 'is_object',
148 | type: 'generic_type',
149 | };
150 | const result = isGenericTypeRuleValid(rule, { name: 'bob' });
151 | expect(result).toBeTruthy();
152 | });
153 |
154 | test('value is not array', () => {
155 | const rule: NewGenericTypeRule = {
156 | field: 'name',
157 | operator: 'is_not_object',
158 | type: 'generic_type',
159 | };
160 | const result = isGenericTypeRuleValid(rule, '{}');
161 | expect(result).toBeTruthy();
162 | });
163 |
164 | test('invalid operator is handled', () => {
165 | const rule: NewGenericTypeRule = {
166 | field: 'name',
167 | // @ts-expect-error
168 | operator: 'is_more_awesome_than',
169 | type: 'generic_type',
170 | };
171 | const result = isGenericTypeRuleValid(rule, undefined);
172 | expect(result).toBeFalsy();
173 | });
174 |
--------------------------------------------------------------------------------
/src/functions/is-generic-type-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewGenericTypeRule } from '../types/rule';
2 | import isPlainObject from 'lodash.isplainobject';
3 |
4 | /**
5 | * Check if a generic type rule is valid.
6 | * @export
7 | * @param {NewGenericTypeRule} rule
8 | * @param {*} value
9 | * @return {*} {boolean}
10 | */
11 | export function isGenericTypeRuleValid(rule: NewGenericTypeRule, value: any): boolean {
12 | switch (rule.operator) {
13 | case 'is_truthy':
14 | return !!value;
15 | case 'is_falsey':
16 | return !value;
17 | case 'is_null':
18 | return value === null;
19 | case 'is_not_null':
20 | return value !== null;
21 | case 'is_undefined':
22 | return value === undefined;
23 | case 'is_not_undefined':
24 | return value !== undefined;
25 | case 'is_string':
26 | return typeof value === 'string';
27 | case 'is_not_string':
28 | return typeof value !== 'string';
29 | case 'is_number':
30 | return typeof value === 'number';
31 | case 'is_not_number':
32 | return typeof value !== 'number';
33 | case 'is_boolean':
34 | return typeof value === 'boolean';
35 | case 'is_not_boolean':
36 | return typeof value !== 'boolean';
37 | case 'is_array':
38 | return Array.isArray(value);
39 | case 'is_not_array':
40 | return !Array.isArray(value);
41 | case 'is_object':
42 | return isPlainObject(value);
43 | case 'is_not_object':
44 | return !isPlainObject(value);
45 | default:
46 | return false;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/functions/is-number-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewNumberRule } from '../types/rule';
2 | import { isNumberRuleValid } from './is-number-rule-valid';
3 |
4 | test('number is equal to', () => {
5 | const rule: NewNumberRule = {
6 | field: 'names',
7 | operator: 'equals_to',
8 | type: 'number',
9 | value: 2,
10 | };
11 | const result = isNumberRuleValid(rule, 2);
12 | expect(result).toBeTruthy();
13 | });
14 |
15 | test('number does not equal to', () => {
16 | const rule: NewNumberRule = {
17 | field: 'names',
18 | operator: 'does_not_equal_to',
19 | type: 'number',
20 | value: 1,
21 | };
22 | const result = isNumberRuleValid(rule, 2);
23 | expect(result).toBeTruthy();
24 | });
25 |
26 | test('number is greater than', () => {
27 | const rule: NewNumberRule = {
28 | field: 'names',
29 | operator: 'greater_than',
30 | type: 'number',
31 | value: 1,
32 | };
33 | const result = isNumberRuleValid(rule, 2);
34 | expect(result).toBeTruthy();
35 | });
36 |
37 | test('number is greater than or equal to', () => {
38 | const rule: NewNumberRule = {
39 | field: 'names',
40 | operator: 'greater_than_or_equal_to',
41 | type: 'number',
42 | value: 2,
43 | };
44 | const result = isNumberRuleValid(rule, 2);
45 | expect(result).toBeTruthy();
46 | });
47 |
48 | test('number is less than', () => {
49 | const rule: NewNumberRule = {
50 | field: 'names',
51 | operator: 'less_than',
52 | type: 'number',
53 | value: 3,
54 | };
55 | const result = isNumberRuleValid(rule, 2);
56 | expect(result).toBeTruthy();
57 | });
58 |
59 | test('number is less than or equal to', () => {
60 | const rule: NewNumberRule = {
61 | field: 'names',
62 | operator: 'less_than_or_equal_to',
63 | type: 'number',
64 | value: 2,
65 | };
66 | const result = isNumberRuleValid(rule, 2);
67 | expect(result).toBeTruthy();
68 | });
69 |
70 | test('invalid operator is handled', () => {
71 | const rule: NewNumberRule = {
72 | field: 'names',
73 | // @ts-expect-error
74 | operator: 'is_more_awesome_than',
75 | type: 'number',
76 | value: 2,
77 | };
78 | const result = isNumberRuleValid(rule, 2);
79 | expect(result).toBeFalsy();
80 | });
81 |
--------------------------------------------------------------------------------
/src/functions/is-number-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewNumberRule } from '../types/rule';
2 |
3 | /**
4 | * Check if a number rule is valid.
5 | * @export
6 | * @param {NewNumberRule} rule
7 | * @param {number} value
8 | * @return {*} {boolean}
9 | */
10 | export function isNumberRuleValid(rule: NewNumberRule, value: number): boolean {
11 | switch (rule.operator) {
12 | case 'equals_to':
13 | return value === rule.value;
14 | case 'does_not_equal_to':
15 | return value !== rule.value;
16 | case 'greater_than':
17 | return value > rule.value;
18 | case 'greater_than_or_equal_to':
19 | return value >= rule.value;
20 | case 'less_than':
21 | return value < rule.value;
22 | case 'less_than_or_equal_to':
23 | return value <= rule.value;
24 | default:
25 | return false;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/functions/is-object-key-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewObjectKeyRule } from '../types/rule';
2 | import { isObjectKeyRuleValid } from './is-object-key-rule-valid';
3 |
4 | const bob = { name: 'bob' };
5 |
6 | test('object key contains element', () => {
7 | const rule: NewObjectKeyRule = {
8 | field: 'people',
9 | operator: 'contains',
10 | type: 'object_key',
11 | value: 'name',
12 | };
13 | const result = isObjectKeyRuleValid(rule, bob);
14 | expect(result).toBeTruthy();
15 | });
16 |
17 | test('object key does not contain element', () => {
18 | const rule: NewObjectKeyRule = {
19 | field: 'people',
20 | operator: 'does_not_contain',
21 | type: 'object_key',
22 | value: 'age',
23 | };
24 | const result = isObjectKeyRuleValid(rule, bob);
25 | expect(result).toBeTruthy();
26 | });
27 |
28 | test('invalid operator is handled', () => {
29 | const rule: NewObjectKeyRule = {
30 | field: 'people',
31 | // @ts-expect-error
32 | operator: 'is_more_awesome_than',
33 | type: 'object_key',
34 | value: 'height',
35 | };
36 | const result = isObjectKeyRuleValid(rule, bob);
37 | expect(result).toBeFalsy();
38 | });
39 |
--------------------------------------------------------------------------------
/src/functions/is-object-key-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewObjectKeyRule } from '../types/rule';
2 |
3 | /**
4 | * Check if an object key rule is valid.
5 | * @export
6 | * @param {NewObjectKeyRule} rule
7 | * @param {object} value
8 | * @return {*} {boolean}
9 | */
10 | export function isObjectKeyRuleValid(rule: NewObjectKeyRule, value: object): boolean {
11 | const keys = Object.keys(value);
12 | const contains = keys.includes(rule.value);
13 | switch (rule.operator) {
14 | case 'contains':
15 | return contains;
16 | case 'does_not_contain':
17 | return !contains;
18 | default:
19 | return false;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/functions/is-object-key-value-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewObjectKeyValueRule } from '../types/rule';
2 | import { isObjectKeyValueRuleValid } from './is-object-key-value-rule-valid';
3 |
4 | const bob = { name: 'bob', age: 30 };
5 |
6 | test('object key & value contains element', () => {
7 | const rule: NewObjectKeyValueRule = {
8 | field: 'people',
9 | operator: 'contains',
10 | type: 'object_key_value',
11 | value: { key: 'name', value: 'bob' },
12 | };
13 | const result = isObjectKeyValueRuleValid(rule, bob);
14 | expect(result).toBeTruthy();
15 | });
16 |
17 | test('object key & value does not contain element', () => {
18 | const rule: NewObjectKeyValueRule = {
19 | field: 'people',
20 | operator: 'does_not_contain',
21 | type: 'object_key_value',
22 | value: { key: 'name', value: 'alice' },
23 | };
24 | const result = isObjectKeyValueRuleValid(rule, bob);
25 | expect(result).toBeTruthy();
26 | });
27 |
28 | test('invalid operator is handled', () => {
29 | const rule: NewObjectKeyValueRule = {
30 | field: 'people',
31 | // @ts-expect-error
32 | operator: 'is_more_awesome_than',
33 | type: 'object_key_value',
34 | value: { key: 'name', value: 'carol' },
35 | };
36 | const result = isObjectKeyValueRuleValid(rule, bob);
37 | expect(result).toBeFalsy();
38 | });
39 |
--------------------------------------------------------------------------------
/src/functions/is-object-key-value-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewObjectKeyValueRule } from '../types/rule';
2 |
3 | /**
4 | * Check if an object key value rule is valid.
5 | * @export
6 | * @param {NewObjectKeyValueRule} rule
7 | * @param {object} value
8 | * @return {*} {boolean}
9 | */
10 | export function isObjectKeyValueRuleValid(rule: NewObjectKeyValueRule, value: object): boolean {
11 | const entries = Object.entries(value);
12 | const contains = entries.some(([key, value]) => key === rule.value.key && value === rule.value.value);
13 | switch (rule.operator) {
14 | case 'contains':
15 | return contains;
16 | case 'does_not_contain':
17 | return !contains;
18 | default:
19 | return false;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/functions/is-object-value-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewObjectValueRule } from '../types/rule';
2 | import { isObjectValueRuleValid } from './is-object-value-rule-valid';
3 |
4 | const bob = { name: 'bob' };
5 |
6 | test('object value contains element', () => {
7 | const rule: NewObjectValueRule = {
8 | field: 'people',
9 | operator: 'contains',
10 | type: 'object_value',
11 | value: 'bob',
12 | };
13 | const result = isObjectValueRuleValid(rule, bob);
14 | expect(result).toBeTruthy();
15 | });
16 |
17 | test('object value does not contain element', () => {
18 | const rule: NewObjectValueRule = {
19 | field: 'people',
20 | operator: 'does_not_contain',
21 | type: 'object_value',
22 | value: 'alice',
23 | };
24 | const result = isObjectValueRuleValid(rule, bob);
25 | expect(result).toBeTruthy();
26 | });
27 |
28 | test('invalid operator is handled', () => {
29 | const rule: NewObjectValueRule = {
30 | field: 'people',
31 | // @ts-expect-error
32 | operator: 'is_more_awesome_than',
33 | type: 'object_value',
34 | value: 'carol',
35 | };
36 | const result = isObjectValueRuleValid(rule, bob);
37 | expect(result).toBeFalsy();
38 | });
39 |
--------------------------------------------------------------------------------
/src/functions/is-object-value-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewObjectValueRule } from '../types/rule';
2 |
3 | /**
4 | * Check if an object value rule is valid.
5 | * @export
6 | * @param {NewObjectValueRule} rule
7 | * @param {object} value
8 | * @return {*} {boolean}
9 | */
10 | export function isObjectValueRuleValid(rule: NewObjectValueRule, value: object): boolean {
11 | const values = Object.values(value);
12 | const contains = values.includes(rule.value);
13 | switch (rule.operator) {
14 | case 'contains':
15 | return contains;
16 | case 'does_not_contain':
17 | return !contains;
18 | default:
19 | return false;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/functions/is-string-rule-valid.test.ts:
--------------------------------------------------------------------------------
1 | import { NewStringRule } from '../types/rule';
2 | import { isStringRuleValid } from './is-string-rule-valid';
3 |
4 | test('string equals to', () => {
5 | const rule: NewStringRule = {
6 | field: 'people',
7 | operator: 'equals_to',
8 | type: 'string',
9 | value: 'bob',
10 | };
11 | const result = isStringRuleValid(rule, 'bob');
12 | expect(result).toBeTruthy();
13 | });
14 |
15 | test('string equals to (case insensitve)', () => {
16 | const rule: NewStringRule = {
17 | field: 'people',
18 | operator: 'equals_to',
19 | type: 'string',
20 | value: 'BOB',
21 | ignore_case: true,
22 | };
23 | const result = isStringRuleValid(rule, 'BoB');
24 | expect(result).toBeTruthy();
25 | });
26 |
27 | test('string not equals to', () => {
28 | const rule: NewStringRule = {
29 | field: 'people',
30 | operator: 'does_not_equal_to',
31 | type: 'string',
32 | value: 'bob',
33 | };
34 | const result = isStringRuleValid(rule, 'alice');
35 | expect(result).toBeTruthy();
36 | });
37 |
38 | test('string contains', () => {
39 | const rule: NewStringRule = {
40 | field: 'people',
41 | operator: 'contains',
42 | type: 'string',
43 | value: 'bob',
44 | };
45 | const result = isStringRuleValid(rule, 'bobby');
46 | expect(result).toBeTruthy();
47 | });
48 |
49 | test('string does not contain', () => {
50 | const rule: NewStringRule = {
51 | field: 'people',
52 | operator: 'does_not_contain',
53 | type: 'string',
54 | value: 'alice',
55 | };
56 | const result = isStringRuleValid(rule, 'bobby');
57 | expect(result).toBeTruthy();
58 | });
59 |
60 | test('string starts with', () => {
61 | const rule: NewStringRule = {
62 | field: 'people',
63 | operator: 'starts_with',
64 | type: 'string',
65 | value: 'bob',
66 | };
67 | const result = isStringRuleValid(rule, 'bobby');
68 | expect(result).toBeTruthy();
69 | });
70 |
71 | test('string ends with', () => {
72 | const rule: NewStringRule = {
73 | field: 'people',
74 | operator: 'ends_with',
75 | type: 'string',
76 | value: 'bby',
77 | };
78 | const result = isStringRuleValid(rule, 'bobby');
79 | expect(result).toBeTruthy();
80 | });
81 |
82 | test('invalid operator is handled', () => {
83 | const rule: NewStringRule = {
84 | field: 'people',
85 | // @ts-expect-error
86 | operator: 'is_more_awesome_than',
87 | type: 'string',
88 | value: 'carol',
89 | };
90 | const result = isStringRuleValid(rule, 'carolS');
91 | expect(result).toBeFalsy();
92 | });
93 |
--------------------------------------------------------------------------------
/src/functions/is-string-rule-valid.ts:
--------------------------------------------------------------------------------
1 | import { NewStringRule } from '../types/rule';
2 |
3 | /**
4 | * Check if a string rule is valid.
5 | * @export
6 | * @param {NewStringRule} rule
7 | * @param {string} value
8 | * @return {*} {boolean}
9 | */
10 | export function isStringRuleValid(rule: NewStringRule, value: string): boolean {
11 | const caseValue = rule.ignore_case ? value.toLowerCase().trim() : value.trim();
12 | const caseRuleValue = rule.ignore_case ? rule.value.toLowerCase().trim() : rule.value.trim();
13 | switch (rule.operator) {
14 | case 'equals_to':
15 | return caseValue === caseRuleValue;
16 | case 'does_not_equal_to':
17 | return caseValue !== caseRuleValue;
18 | case 'contains':
19 | return caseValue.includes(caseRuleValue);
20 | case 'does_not_contain':
21 | return !caseValue.includes(caseRuleValue);
22 | case 'starts_with':
23 | return caseValue.startsWith(caseRuleValue);
24 | case 'ends_with':
25 | return caseValue.endsWith(caseRuleValue);
26 | default:
27 | return false;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/functions/normalize.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { normalize } from './normalize';
5 | import { v4 as uuidv4 } from 'uuid';
6 |
7 | test('normalization removes an invalid rule', () => {
8 | const root = createRoot({ connector: 'or' });
9 |
10 | const rule = addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
11 | rule.type = 'number';
12 |
13 | expect(root.rules).toHaveLength(1);
14 | normalize(root);
15 | expect(root.rules).toHaveLength(0);
16 | });
17 |
18 | test('normalization fixes the parent id of a rule', () => {
19 | const root = createRoot({ connector: 'or' });
20 |
21 | const rule = addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
22 | rule.parent_id = uuidv4();
23 |
24 | root.rules.forEach((rule) => {
25 | expect(rule.parent_id).not.toBe(root.id);
26 | });
27 | expect(root.rules[0].parent_id).not.toBe(root.id);
28 | normalize(root);
29 | root.rules.forEach((rule) => {
30 | expect(rule.parent_id).toBe(root.id);
31 | });
32 | });
33 |
34 | test('normalization removes an invalid union', () => {
35 | const root = createRoot({ connector: 'or' });
36 |
37 | const union = addUnionToUnion(root, { connector: 'and' });
38 | addRuleToUnion(union, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
39 | // @ts-expect-error
40 | union.connector = 'invalid';
41 |
42 | expect(root.rules).toHaveLength(1);
43 | normalize(root);
44 | expect(root.rules).toHaveLength(0);
45 | });
46 |
47 | test('normalization removes an union with no rules', () => {
48 | const root = createRoot({ connector: 'or' });
49 | addUnionToUnion(root, { connector: 'and' });
50 |
51 | expect(root.rules).toHaveLength(1);
52 | normalize(root);
53 | expect(root.rules).toHaveLength(0);
54 | });
55 |
56 | test('normalization promotes union with 1 rule to parent level', () => {
57 | const root = createRoot({ connector: 'or' });
58 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
59 |
60 | const union = addUnionToUnion(root, { connector: 'and' });
61 | const rule = addRuleToUnion(union, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
62 |
63 | expect(root.rules[1].entity).toBe('union');
64 | expect(root.rules[1].id).toBe(union.id);
65 |
66 | normalize(root);
67 |
68 | expect(root.rules[1].entity).toBe('rule');
69 | expect(root.rules[1].id).toBe(rule.id);
70 | });
71 |
72 | test('normalization finds nothing wrong', () => {
73 | const root = createRoot({ connector: 'or' });
74 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
75 |
76 | const union = addUnionToUnion(root, { connector: 'and' });
77 | addRuleToUnion(union, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
78 | addRuleToUnion(union, { field: 'age', operator: 'greater_than', type: 'number', value: 18 });
79 |
80 | expect(root.rules).toHaveLength(2);
81 | root.rules.forEach((rule) => {
82 | expect(rule.parent_id).toBe(root.id);
83 | });
84 |
85 | normalize(root);
86 |
87 | expect(root.rules).toHaveLength(2);
88 | root.rules.forEach((rule) => {
89 | expect(rule.parent_id).toBe(root.id);
90 | });
91 | });
92 |
93 | test('normalization has all options turn off', () => {
94 | const root = createRoot({ connector: 'or' });
95 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
96 |
97 | const union = addUnionToUnion(root, { connector: 'and' });
98 | const rule = addRuleToUnion(union, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
99 | addRuleToUnion(union, { field: 'age', operator: 'greater_than', type: 'number', value: 18 });
100 |
101 | expect(union.parent_id).toBe(root.id);
102 | expect(rule.parent_id).toBe(union.id);
103 |
104 | union.parent_id = uuidv4();
105 | rule.parent_id = uuidv4();
106 |
107 | normalize(root, {
108 | update_parent_ids: false,
109 | promote_single_rule_unions: false,
110 | remove_empty_unions: false,
111 | remove_failed_validations: false,
112 | });
113 |
114 | expect(union.parent_id).not.toBe(root.id);
115 | expect(rule.parent_id).not.toBe(union.id);
116 | });
117 |
--------------------------------------------------------------------------------
/src/functions/normalize.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion, Union } from '../types/union';
2 |
3 | import { Rule } from '../types/rule';
4 | import { ruleSchema } from '../validations/rule';
5 | import { unionSchema } from '../validations/union';
6 |
7 | type Options = {
8 | remove_failed_validations?: boolean;
9 | remove_empty_unions?: boolean;
10 | promote_single_rule_unions?: boolean;
11 | update_parent_ids?: boolean;
12 | };
13 |
14 | export function normalize(union: T, options?: Options): T {
15 | const promote_single_rule_unions = options?.promote_single_rule_unions ?? true;
16 | const remove_empty_unions = options?.remove_empty_unions ?? true;
17 | const remove_failed_validations = options?.remove_failed_validations ?? true;
18 | const update_parent_ids = options?.update_parent_ids ?? true;
19 |
20 | union.rules = union.rules.reduce<(Rule | Union)[]>((rules, ruleOrUnion) => {
21 | if (ruleOrUnion.entity === 'union') {
22 | // Validate structure of a union
23 | if (remove_failed_validations) {
24 | const validated = unionSchema.safeParse(ruleOrUnion);
25 | if (!validated.success) {
26 | return rules;
27 | }
28 | }
29 | // Normalize the union
30 | const normalized = normalize(ruleOrUnion, options);
31 | // After normalization, if no rules are left, we can skip the union
32 | if (normalized.rules.length === 0 && remove_empty_unions) {
33 | return rules;
34 | }
35 | // If there is only one rule left, we can skip the union and add the rule directly
36 | if (normalized.rules.length === 1 && promote_single_rule_unions) {
37 | rules.push({ ...normalized.rules[0], parent_id: union.id });
38 | return rules;
39 | }
40 | // Append correct parent_id
41 | if (update_parent_ids) {
42 | rules.push({ ...normalized, parent_id: union.id });
43 | return rules;
44 | }
45 | rules.push(normalized);
46 | return rules;
47 | }
48 | // Validate structure of a rule
49 | if (remove_failed_validations) {
50 | const validated = ruleSchema.safeParse(ruleOrUnion);
51 | if (!validated.success) {
52 | return rules;
53 | }
54 | }
55 | // Append correct parent_id
56 | if (update_parent_ids) {
57 | rules.push({ ...ruleOrUnion, parent_id: union.id });
58 | return rules;
59 | }
60 | rules.push(ruleOrUnion);
61 | return rules;
62 | }, []);
63 | return union;
64 | }
65 |
--------------------------------------------------------------------------------
/src/functions/remove-all-by-id.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { removeAllById } from './remove-all-by-id';
5 | import { v4 as uuidv4 } from 'uuid';
6 |
7 | test('remove many deeply nested union', () => {
8 | const root = createRoot({ connector: 'and' });
9 | const union = addUnionToUnion(root, { connector: 'and' });
10 | const deepUnion = addUnionToUnion(union, { connector: 'and' });
11 | addRuleToUnion(deepUnion, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
12 | const deeperUnion = addUnionToUnion(deepUnion, { connector: 'and' });
13 |
14 | deepUnion.rules.push(deeperUnion);
15 | deepUnion.rules.push(deeperUnion);
16 | deepUnion.rules.push(deeperUnion);
17 |
18 | expect(deepUnion.rules).toContain(deeperUnion);
19 | expect(deepUnion.rules.length).toBe(5);
20 |
21 | removeAllById(root, deeperUnion.id);
22 |
23 | expect(deepUnion.rules).not.toContain(deepUnion);
24 | expect(deepUnion.rules.length).toBe(1);
25 | });
26 |
27 | test('remove non existent id', () => {
28 | const root = createRoot({ connector: 'and' });
29 | const union = addUnionToUnion(root, { connector: 'and' });
30 |
31 | expect(root.rules).toContain(union);
32 | expect(root.rules.length).toBe(1);
33 |
34 | removeAllById(root, uuidv4());
35 |
36 | expect(root.rules).toContain(union);
37 | expect(root.rules.length).toBe(1);
38 | });
39 |
--------------------------------------------------------------------------------
/src/functions/remove-all-by-id.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion, Union } from '../types/union';
2 |
3 | import { Rule } from '../types/rule';
4 |
5 | /**
6 | * Removes all rules or unions from a union and nested unions by id.
7 | * Mutates the original union.
8 | * @export
9 | * @template T
10 | * @param {T} union
11 | * @param {string} id
12 | * @return {*} {T}
13 | */
14 | export function removeAllById(union: T, id: string): T {
15 | union.rules = union.rules.reduce<(Rule | Union)[]>((list, ruleOrUnion) => {
16 | if (ruleOrUnion.id !== id) {
17 | list.push(ruleOrUnion.entity === 'union' ? removeAllById(ruleOrUnion, id) : ruleOrUnion);
18 | }
19 | return list;
20 | }, []);
21 | return union;
22 | }
23 |
--------------------------------------------------------------------------------
/src/functions/run.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { run } from './run';
5 |
6 | const root = createRoot({ connector: 'and' });
7 |
8 | const union = addUnionToUnion(root, { connector: 'and' });
9 | const firstRule = addRuleToUnion(union, {
10 | field: 'number',
11 | operator: 'greater_than',
12 | type: 'number',
13 | value: 18,
14 | });
15 | addRuleToUnion(union, { field: 'number', operator: 'less_than', type: 'number', value: 30 });
16 | addRuleToUnion(root, { field: 'string', operator: 'contains', type: 'string', value: 'bob' });
17 | addRuleToUnion(root, { field: 'boolean', operator: 'is_true', type: 'boolean' });
18 | addRuleToUnion(root, { field: 'array', operator: 'contains', type: 'array_value', value: 'alice' });
19 | addRuleToUnion(root, { field: 'array', operator: 'equals_to', type: 'array_length', value: 1 });
20 | addRuleToUnion(root, { field: 'object', operator: 'contains', type: 'object_key', value: 'name' });
21 | addRuleToUnion(root, { field: 'object', operator: 'contains', type: 'object_value', value: 'bob' });
22 | addRuleToUnion(root, {
23 | field: 'object',
24 | operator: 'contains',
25 | type: 'object_key_value',
26 | value: { key: 'name', value: 'bob' },
27 | });
28 | addRuleToUnion(root, { field: 'generic', operator: 'equals_to', type: 'generic_comparison', value: 'bob' });
29 | addRuleToUnion(root, { field: 'generic', operator: 'is_truthy', type: 'generic_type' });
30 | const orUnion = addUnionToUnion(root, { connector: 'or' });
31 | addRuleToUnion(orUnion, { field: 'number', operator: 'less_than', type: 'number', value: 30 });
32 | addRuleToUnion(orUnion, { field: 'string', operator: 'contains', type: 'string', value: 'bob' });
33 |
34 | test('rules engine passes', () => {
35 | const result = run(root, {
36 | string: 'bob',
37 | boolean: true,
38 | number: 20,
39 | array: ['alice'],
40 | object: { name: 'bob' },
41 | generic: 'bob',
42 | });
43 | expect(result).toBeTruthy();
44 | });
45 |
46 | test('rules engine fails', () => {
47 | root.connector = 'and';
48 | const result = run(root, {
49 | string: 'bob',
50 | boolean: true,
51 | number: 20,
52 | array: ['alice'],
53 | object: { name: 'bob' },
54 | generic: 'bobby',
55 | });
56 | expect(result).toBeFalsy();
57 | });
58 |
59 | test('test invalid rule', () => {
60 | const invalidRule = { ...firstRule };
61 | // @ts-expect-error
62 | delete invalidRule.id;
63 |
64 | root.rules.splice(0, 1, invalidRule);
65 | expect(() => run(root, {})).toThrowError();
66 | root.rules.splice(0, 1, firstRule);
67 | });
68 |
69 | test('test no rules available', () => {
70 | const noRuleRoot = createRoot({ connector: 'and' });
71 | const result = run(noRuleRoot, {});
72 | expect(result).toBeTruthy();
73 | });
74 |
--------------------------------------------------------------------------------
/src/functions/run.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion, Union } from '../types/union';
2 | import { rootUnionSchema, unionSchema } from '../validations/union';
3 |
4 | import { Rule } from '../types/rule';
5 | import { generateError } from 'zod-error';
6 | import get from 'lodash.get';
7 | import { isArrayLengthRuleValid } from './is-array-length-rule-valid';
8 | import { isArrayValueRuleValid } from './is-array-value-rule-valid';
9 | import { isBooleanRuleValid } from './is-boolean-rule-valid';
10 | import { isGenericComparisonRuleValid } from './is-generic-comparison-rule-valid';
11 | import { isGenericTypeRuleValid } from './is-generic-type-rule-valid';
12 | import { isNumberRuleValid } from './is-number-rule-valid';
13 | import { isObject } from '../utils/is-object';
14 | import { isObjectKeyRuleValid } from './is-object-key-rule-valid';
15 | import { isObjectKeyValueRuleValid } from './is-object-key-value-rule-valid';
16 | import { isObjectValueRuleValid } from './is-object-value-rule-valid';
17 | import { isStringRuleValid } from './is-string-rule-valid';
18 |
19 | /**
20 | * Run the rules engine against a value.
21 | * @export
22 | * @param {(RootUnion | Union)} union
23 | * @param {*} value
24 | * @return {*} {boolean}
25 | */
26 | export function run(union: RootUnion | Union, value: any): boolean {
27 | try {
28 | const validated = rootUnionSchema.or(unionSchema).parse(union);
29 | if (validated.rules.length === 0) {
30 | return true;
31 | }
32 |
33 | const callback = (ruleOrUnion: Rule | Union) => {
34 | if (ruleOrUnion.entity === 'union') {
35 | return run(ruleOrUnion, value);
36 | }
37 | const resolved = get(value, ruleOrUnion.field);
38 |
39 | if (ruleOrUnion.type === 'string' && typeof resolved === 'string') {
40 | return isStringRuleValid(ruleOrUnion, resolved);
41 | }
42 |
43 | if (ruleOrUnion.type === 'number' && typeof resolved === 'number') {
44 | return isNumberRuleValid(ruleOrUnion, resolved);
45 | }
46 |
47 | if (ruleOrUnion.type === 'boolean' && typeof resolved === 'boolean') {
48 | return isBooleanRuleValid(ruleOrUnion, resolved);
49 | }
50 |
51 | if (Array.isArray(resolved)) {
52 | if (ruleOrUnion.type === 'array_value') {
53 | return isArrayValueRuleValid(ruleOrUnion, resolved);
54 | }
55 |
56 | if (ruleOrUnion.type === 'array_length') {
57 | return isArrayLengthRuleValid(ruleOrUnion, resolved);
58 | }
59 | }
60 |
61 | if (isObject(resolved)) {
62 | if (ruleOrUnion.type === 'object_key') {
63 | return isObjectKeyRuleValid(ruleOrUnion, resolved);
64 | }
65 |
66 | if (ruleOrUnion.type === 'object_value') {
67 | return isObjectValueRuleValid(ruleOrUnion, resolved);
68 | }
69 |
70 | if (ruleOrUnion.type === 'object_key_value') {
71 | return isObjectKeyValueRuleValid(ruleOrUnion, resolved);
72 | }
73 | }
74 |
75 | if (ruleOrUnion.type === 'generic_comparison') {
76 | return isGenericComparisonRuleValid(ruleOrUnion, resolved);
77 | }
78 |
79 | if (ruleOrUnion.type === 'generic_type') {
80 | return isGenericTypeRuleValid(ruleOrUnion, resolved);
81 | }
82 | };
83 |
84 | // If the joiner is an AND, then all rules must be true
85 | if (validated.connector === 'and') {
86 | return union.rules.every(callback);
87 | }
88 |
89 | // If the joiner is an OR, then at least one rule must be true
90 | return validated.rules.some(callback);
91 | } catch (error) {
92 | throw generateError(error);
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/functions/update-rule-by-id.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { findRuleById } from './find-rule-by-id';
5 | import { updateRuleById } from './update-rule-by-id';
6 | import { v4 as uuidv4 } from 'uuid';
7 |
8 | const root = createRoot({ connector: 'or' });
9 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
10 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
11 | const union = addUnionToUnion(root, { connector: 'and' });
12 | addRuleToUnion(union, { field: 'age', operator: 'greater_than', type: 'number', value: 18 });
13 | const rule = addRuleToUnion(union, { field: 'age', operator: 'less_than', type: 'number', value: 30 });
14 |
15 | test('update a rule that exists', () => {
16 | const foundRule = findRuleById(root, rule.id);
17 | if (!foundRule) {
18 | throw new Error('Rule not found');
19 | }
20 | if (foundRule.type !== 'number') {
21 | throw new Error('Rule type is not number');
22 | }
23 | expect(foundRule.value).toBe(30);
24 | updateRuleById(root, foundRule.id, { field: 'age', operator: 'less_than', type: 'number', value: 40 });
25 | const updatedRule = findRuleById(root, rule.id);
26 | if (!updatedRule) {
27 | throw new Error('Rule not found');
28 | }
29 | if (updatedRule.type !== 'number') {
30 | throw new Error('Rule type is not number');
31 | }
32 | expect(updatedRule.value).toBe(40);
33 | });
34 |
35 | test('update a rule that does not exist', () => {
36 | const updatedRule = updateRuleById(root, uuidv4(), {
37 | field: 'age',
38 | operator: 'less_than',
39 | type: 'number',
40 | value: 40,
41 | });
42 | expect(updatedRule).toBeUndefined();
43 | });
44 |
45 | test('update a rule that does not have a valid parent', () => {
46 | const foundRule = findRuleById(root, rule.id);
47 | if (!foundRule) {
48 | throw new Error('Rule not found');
49 | }
50 | foundRule.parent_id = uuidv4();
51 | const updatedRule = updateRuleById(root, rule.id, {
52 | field: 'age',
53 | operator: 'less_than',
54 | type: 'number',
55 | value: 40,
56 | });
57 | expect(updatedRule).toBeUndefined();
58 | });
59 |
--------------------------------------------------------------------------------
/src/functions/update-rule-by-id.ts:
--------------------------------------------------------------------------------
1 | import { NewRule, Rule } from '../types/rule';
2 |
3 | import { RootUnion } from '../types/union';
4 | import { findRuleById } from './find-rule-by-id';
5 | import { findUnionById } from './find-union-by-id';
6 |
7 | /**
8 | * Update a rule by id.
9 | * If the rule is not found, return undefined.
10 | * Mutates the root object.
11 | * @export
12 | * @param {RootUnion} root
13 | * @param {string} id
14 | * @param {NewRule} values
15 | * @return {*} {(Rule | undefined)}
16 | */
17 | export function updateRuleById(root: RootUnion, id: string, values: NewRule): Rule | undefined {
18 | const foundRule = findRuleById(root, id);
19 | if (!foundRule) {
20 | return;
21 | }
22 |
23 | // Get parent union to update rules array
24 | const parent = findUnionById(root, foundRule.parent_id);
25 | if (!parent) {
26 | return;
27 | }
28 |
29 | // Update parent rules array
30 | parent.rules = parent.rules.map((ruleOrUnion) => {
31 | if (ruleOrUnion.entity === 'rule' && ruleOrUnion.id === foundRule.id) {
32 | return { ...ruleOrUnion, ...values };
33 | }
34 | return ruleOrUnion;
35 | });
36 |
37 | return findRuleById(root, id);
38 | }
39 |
--------------------------------------------------------------------------------
/src/functions/update-union-by-id.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { findUnionById } from './find-union-by-id';
5 | import { updateUnionById } from './update-union-by-id';
6 | import { v4 as uuidv4 } from 'uuid';
7 |
8 | const root = createRoot({ connector: 'or' });
9 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'bob' });
10 | addRuleToUnion(root, { field: 'name', operator: 'contains', type: 'string', value: 'alice' });
11 | const union = addUnionToUnion(root, { connector: 'and' });
12 |
13 | test('update a union that exists', () => {
14 | const foundUnion = findUnionById(root, union.id);
15 | if (!foundUnion) {
16 | throw new Error('Union not found');
17 | }
18 | expect(foundUnion.connector).toBe('and');
19 | updateUnionById(root, foundUnion.id, { connector: 'or' });
20 | const updatedUnion = findUnionById(root, union.id);
21 | if (!updatedUnion) {
22 | throw new Error('Union not found');
23 | }
24 | expect(updatedUnion.connector).toBe('or');
25 | });
26 |
27 | test('update a root union', () => {
28 | const foundUnion = findUnionById(root, root.id);
29 | if (!foundUnion) {
30 | throw new Error('Union not found');
31 | }
32 | expect(foundUnion.connector).toBe('or');
33 | updateUnionById(root, foundUnion.id, { connector: 'and' });
34 | const updatedUnion = findUnionById(root, root.id);
35 | if (!updatedUnion) {
36 | throw new Error('Union not found');
37 | }
38 | expect(updatedUnion.connector).toBe('and');
39 | });
40 |
41 | test('update a union that does not exist', () => {
42 | const updatedUnion = updateUnionById(root, uuidv4(), { connector: 'or' });
43 | expect(updatedUnion).toBeUndefined();
44 | });
45 |
46 | test('update a union that does not have a valid parent', () => {
47 | const foundUnion = findUnionById(root, union.id);
48 | if (!foundUnion) {
49 | throw new Error('Union not found');
50 | }
51 | if (foundUnion.entity === 'root_union') {
52 | throw new Error('Union is not the correct type');
53 | }
54 | foundUnion.parent_id = uuidv4();
55 | const updatedUnion = updateUnionById(root, union.id, { connector: 'or' });
56 | expect(updatedUnion).toBeUndefined();
57 | });
58 |
--------------------------------------------------------------------------------
/src/functions/update-union-by-id.ts:
--------------------------------------------------------------------------------
1 | import { NewUnion, RootUnion, Union } from '../types/union';
2 |
3 | import { findUnionById } from './find-union-by-id';
4 |
5 | /**
6 | * Update a union by id.
7 | * If the union is not found, return undefined.
8 | * Mutates the root object.
9 | * @export
10 | * @param {RootUnion} root
11 | * @param {string} id
12 | * @param {NewUnion} values
13 | * @return {*} {(Union | RootUnion | undefined)}
14 | */
15 | export function updateUnionById(root: RootUnion, id: string, values: NewUnion): Union | RootUnion | undefined {
16 | const foundUnion = findUnionById(root, id);
17 | if (!foundUnion) {
18 | return;
19 | }
20 |
21 | // Update the root union
22 | if (foundUnion.entity === 'root_union') {
23 | foundUnion.connector = values.connector;
24 | return foundUnion;
25 | }
26 |
27 | // Get parent union to update rules array
28 | const parent = findUnionById(root, foundUnion.parent_id);
29 | if (!parent) {
30 | return;
31 | }
32 |
33 | // Update parent rules array
34 | parent.rules = parent.rules.map((ruleOrUnion) => {
35 | if (ruleOrUnion.entity === 'union' && ruleOrUnion.id === foundUnion.id) {
36 | return { ...ruleOrUnion, ...values };
37 | }
38 | return ruleOrUnion;
39 | });
40 |
41 | return findUnionById(root, id);
42 | }
43 |
--------------------------------------------------------------------------------
/src/functions/validate.test.ts:
--------------------------------------------------------------------------------
1 | import { addRuleToUnion } from './add-rule-to-union';
2 | import { addUnionToUnion } from './add-union-to-union';
3 | import { createRoot } from './create-root';
4 | import { v4 as uuidv4 } from 'uuid';
5 | import { validate } from './validate';
6 |
7 | test('rules engine passes validation', () => {
8 | const root = createRoot({ connector: 'and' });
9 | addUnionToUnion(root, { connector: 'and' });
10 | addRuleToUnion(root, { field: 'number', operator: 'greater_than', type: 'number', value: 18 });
11 |
12 | const result = validate(root);
13 | expect(result.isValid).toBeTruthy();
14 | });
15 |
16 | test('rules engine validation fails validation with invalid union', () => {
17 | const root = createRoot({ connector: 'and' });
18 |
19 | // @ts-expect-error
20 | root.rules.push({ entity: 'union', id: uuidv4(), connector: 'neither', parent_id: root.id, rules: [] });
21 |
22 | const result = validate(root);
23 | expect(result.isValid).toBeFalsy();
24 | expect(!result.isValid && result.reason).toBeTruthy();
25 | });
26 |
27 | test('rules engine validation fails validation with invalid rule', () => {
28 | const root = createRoot({ connector: 'and' });
29 |
30 | root.rules.push({
31 | entity: 'rule',
32 | id: uuidv4(),
33 | field: 'number',
34 | operator: 'greater_than',
35 | // @ts-expect-error
36 | type: 'integer',
37 | value: 18,
38 | parent_id: root.id,
39 | });
40 |
41 | const result = validate(root);
42 | expect(result.isValid).toBeFalsy();
43 | expect(!result.isValid && result.reason).toBeTruthy();
44 | });
45 |
--------------------------------------------------------------------------------
/src/functions/validate.ts:
--------------------------------------------------------------------------------
1 | import { RootUnion } from '../types/union';
2 | import { generateErrorMessage } from 'zod-error';
3 | import { rootUnionSchema } from '../validations/union';
4 |
5 | /**
6 | * Validates a root union before running it.
7 | * @export
8 | * @param {RootUnion} root
9 | * @return {*} {({ isValid: true } | { isValid: false; reason: string })}
10 | */
11 | export function validate(root: RootUnion): { isValid: true } | { isValid: false; reason: string } {
12 | const validated = rootUnionSchema.safeParse(root);
13 |
14 | if (!validated.success) {
15 | return { isValid: false, reason: generateErrorMessage(validated.error.issues) };
16 | }
17 |
18 | return { isValid: true };
19 | }
20 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './functions/add-any-to-union';
2 | export * from './functions/add-many-to-union';
3 | export * from './functions/add-rule-to-union';
4 | export * from './functions/add-rules-to-union';
5 | export * from './functions/add-union-to-union';
6 | export * from './functions/add-union-to-union';
7 | export * from './functions/add-union-to-union';
8 | export * from './functions/add-unions-to-union';
9 | export * from './functions/create-root';
10 | export * from './functions/find-any-by-id';
11 | export * from './functions/find-rule-by-id';
12 | export * from './functions/find-union-by-id';
13 | export * from './functions/normalize';
14 | export * from './functions/remove-all-by-id';
15 | export * from './functions/run';
16 | export * from './functions/update-rule-by-id';
17 | export * from './functions/update-union-by-id';
18 | export * from './functions/validate';
19 | export * from './types/rule';
20 | export * from './types/union';
21 | export * from './validations/rule';
22 | export * from './validations/union';
23 |
--------------------------------------------------------------------------------
/src/types/rule.ts:
--------------------------------------------------------------------------------
1 | import {
2 | baseArrayLengthRuleSchema,
3 | baseArrayValueRuleSchema,
4 | baseBooleanRuleSchema,
5 | baseGenericComparisonRuleSchema,
6 | baseGenericTypeRuleSchema,
7 | baseNumberRuleSchema,
8 | baseObjectKeyRuleSchema,
9 | baseObjectKeyValuePairRuleSchema,
10 | baseObjectValueRuleSchema,
11 | baseStringRuleSchema,
12 | newRuleSchema,
13 | ruleSchema,
14 | } from '../validations/rule';
15 |
16 | import { z } from 'zod';
17 |
18 | export type Rule = z.infer;
19 |
20 | export type NewStringRule = z.infer;
21 |
22 | export type NewNumberRule = z.infer;
23 |
24 | export type NewBooleanRule = z.infer;
25 |
26 | export type NewArrayValueRule = z.infer;
27 |
28 | export type NewArrayLengthRule = z.infer;
29 |
30 | export type NewObjectKeyRule = z.infer;
31 |
32 | export type NewObjectValueRule = z.infer;
33 |
34 | export type NewObjectKeyValueRule = z.infer;
35 |
36 | export type NewGenericComparisonRule = z.infer;
37 |
38 | export type NewGenericTypeRule = z.infer;
39 |
40 | export type NewRule = z.infer;
41 |
--------------------------------------------------------------------------------
/src/types/union.ts:
--------------------------------------------------------------------------------
1 | import { newUnionSchema, rootUnionSchema } from '../validations/union';
2 |
3 | import { Rule } from './rule';
4 | import { z } from 'zod';
5 |
6 | export type Union = {
7 | entity: 'union';
8 | id: string;
9 | parent_id: string;
10 | connector: 'and' | 'or';
11 | rules: (Rule | Union)[];
12 | };
13 |
14 | export type RootUnion = z.infer;
15 |
16 | export type NewUnion = z.infer;
17 |
--------------------------------------------------------------------------------
/src/utils/is-object.test.ts:
--------------------------------------------------------------------------------
1 | import { isObject } from './is-object';
2 |
3 | test('is an object', () => {
4 | expect(isObject({})).toBeTruthy();
5 | });
6 |
7 | test('is not an object', () => {
8 | expect(isObject('')).toBeFalsy();
9 | });
10 |
--------------------------------------------------------------------------------
/src/utils/is-object.ts:
--------------------------------------------------------------------------------
1 | import isPlainObject from 'lodash.isplainobject';
2 |
3 | /**
4 | * Check if value is object.
5 | * @export
6 | * @param {*} value
7 | * @return {*} {value is object}
8 | */
9 | export function isObject(value: any): value is object {
10 | return isPlainObject(value);
11 | }
12 |
--------------------------------------------------------------------------------
/src/validations/rule.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const baseRuleSchema = z.object({
4 | entity: z.literal('rule'),
5 | id: z.string(),
6 | parent_id: z.string(),
7 | });
8 |
9 | export const baseStringRuleSchema = z.object({
10 | type: z.literal('string'),
11 | field: z.string(),
12 | operator: z.union([
13 | z.literal('equals_to'),
14 | z.literal('does_not_equal_to'),
15 | z.literal('contains'),
16 | z.literal('does_not_contain'),
17 | z.literal('starts_with'),
18 | z.literal('ends_with'),
19 | ]),
20 | value: z.string(),
21 | ignore_case: z.boolean().optional(),
22 | });
23 |
24 | export const stringRuleSchema = baseRuleSchema.merge(baseStringRuleSchema);
25 |
26 | export const baseNumberRuleSchema = z.object({
27 | type: z.literal('number'),
28 | field: z.string(),
29 | operator: z.union([
30 | z.literal('equals_to'),
31 | z.literal('does_not_equal_to'),
32 | z.literal('greater_than'),
33 | z.literal('greater_than_or_equal_to'),
34 | z.literal('less_than'),
35 | z.literal('less_than_or_equal_to'),
36 | ]),
37 | value: z.number(),
38 | });
39 |
40 | export const numberRuleSchema = baseRuleSchema.merge(baseNumberRuleSchema);
41 |
42 | export const baseBooleanRuleSchema = z.object({
43 | type: z.literal('boolean'),
44 | field: z.string(),
45 | operator: z.union([z.literal('is_true'), z.literal('is_false')]),
46 | });
47 |
48 | export const booleanRuleSchema = baseRuleSchema.merge(baseBooleanRuleSchema);
49 |
50 | export const baseArrayValueRuleSchema = z.object({
51 | type: z.literal('array_value'),
52 | field: z.string(),
53 | operator: z.union([z.literal('contains'), z.literal('does_not_contain'), z.literal('contains_all')]),
54 | value: z.any(),
55 | });
56 |
57 | export const arrayValueRuleSchema = baseRuleSchema.merge(baseArrayValueRuleSchema);
58 |
59 | export const baseArrayLengthRuleSchema = z.object({
60 | type: z.literal('array_length'),
61 | field: z.string(),
62 | operator: z.union([
63 | z.literal('equals_to'),
64 | z.literal('does_not_equal_to'),
65 | z.literal('greater_than'),
66 | z.literal('greater_than_or_equal_to'),
67 | z.literal('less_than'),
68 | z.literal('less_than_or_equal_to'),
69 | ]),
70 | value: z.number(),
71 | });
72 |
73 | export const arrayLengthRuleSchema = baseRuleSchema.merge(baseArrayLengthRuleSchema);
74 |
75 | export const baseObjectKeyRuleSchema = z.object({
76 | type: z.literal('object_key'),
77 | field: z.string(),
78 | operator: z.union([z.literal('contains'), z.literal('does_not_contain')]),
79 | value: z.string(),
80 | });
81 |
82 | export const objectKeyRuleSchema = baseRuleSchema.merge(baseObjectKeyRuleSchema);
83 |
84 | export const baseObjectValueRuleSchema = z.object({
85 | type: z.literal('object_value'),
86 | field: z.string(),
87 | operator: z.union([z.literal('contains'), z.literal('does_not_contain')]),
88 | value: z.any(),
89 | });
90 |
91 | export const objectValueRuleSchema = baseRuleSchema.merge(baseObjectValueRuleSchema);
92 |
93 | export const baseObjectKeyValuePairRuleSchema = z.object({
94 | type: z.literal('object_key_value'),
95 | field: z.string(),
96 | operator: z.union([z.literal('contains'), z.literal('does_not_contain')]),
97 | value: z.object({
98 | key: z.string(),
99 | value: z.any(),
100 | }),
101 | });
102 |
103 | export const objectKeyValueRuleSchema = baseRuleSchema.merge(baseObjectKeyValuePairRuleSchema);
104 |
105 | export const baseGenericComparisonRuleSchema = z.object({
106 | type: z.literal('generic_comparison'),
107 | field: z.string(),
108 | operator: z.union([
109 | z.literal('equals_to'),
110 | z.literal('does_not_equal_to'),
111 | z.literal('greater_than'),
112 | z.literal('greater_than_or_equal_to'),
113 | z.literal('less_than'),
114 | z.literal('less_than_or_equal_to'),
115 | ]),
116 | value: z.any(),
117 | });
118 |
119 | export const genericComparisonRuleSchema = baseRuleSchema.merge(baseGenericComparisonRuleSchema);
120 |
121 | export const baseGenericTypeRuleSchema = z.object({
122 | type: z.literal('generic_type'),
123 | field: z.string(),
124 | operator: z.union([
125 | z.literal('is_truthy'),
126 | z.literal('is_falsey'),
127 | z.literal('is_null'),
128 | z.literal('is_not_null'),
129 | z.literal('is_undefined'),
130 | z.literal('is_not_undefined'),
131 | z.literal('is_string'),
132 | z.literal('is_not_string'),
133 | z.literal('is_number'),
134 | z.literal('is_not_number'),
135 | z.literal('is_boolean'),
136 | z.literal('is_not_boolean'),
137 | z.literal('is_array'),
138 | z.literal('is_not_array'),
139 | z.literal('is_object'),
140 | z.literal('is_not_object'),
141 | ]),
142 | });
143 |
144 | export const genericTypeRuleSchema = baseRuleSchema.merge(baseGenericTypeRuleSchema);
145 |
146 | export const ruleSchema = z.discriminatedUnion('type', [
147 | stringRuleSchema,
148 | numberRuleSchema,
149 | booleanRuleSchema,
150 | arrayValueRuleSchema,
151 | arrayLengthRuleSchema,
152 | objectKeyRuleSchema,
153 | objectValueRuleSchema,
154 | objectKeyValueRuleSchema,
155 | genericComparisonRuleSchema,
156 | genericTypeRuleSchema,
157 | ]);
158 |
159 | export const newRuleSchema = z.union([
160 | baseStringRuleSchema,
161 | baseNumberRuleSchema,
162 | baseBooleanRuleSchema,
163 | baseArrayValueRuleSchema,
164 | baseArrayLengthRuleSchema,
165 | baseObjectKeyRuleSchema,
166 | baseObjectValueRuleSchema,
167 | baseObjectKeyValuePairRuleSchema,
168 | baseGenericComparisonRuleSchema,
169 | baseGenericTypeRuleSchema,
170 | ]);
171 |
--------------------------------------------------------------------------------
/src/validations/union.ts:
--------------------------------------------------------------------------------
1 | import { Union } from '../types/union';
2 | import { ruleSchema } from './rule';
3 | import { z } from 'zod';
4 |
5 | export const unionSchema: z.ZodType = z.object({
6 | entity: z.literal('union'),
7 | id: z.string(),
8 | parent_id: z.string(),
9 | connector: z.union([z.literal('and'), z.literal('or')]),
10 | rules: z.array(ruleSchema.or(z.lazy(() => unionSchema))),
11 | });
12 |
13 | export const rootUnionSchema = z.object({
14 | entity: z.literal('root_union'),
15 | id: z.string(),
16 | connector: z.union([z.literal('and'), z.literal('or')]),
17 | rules: z.array(ruleSchema.or(unionSchema)),
18 | });
19 |
20 | export const newUnionSchema = rootUnionSchema.pick({
21 | connector: true,
22 | });
23 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 | /* Projects */
5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
11 | /* Language and Environment */
12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
14 | // "jsx": "preserve", /* Specify what JSX code is generated. */
15 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
24 | /* Modules */
25 | "module": "commonjs", /* Specify what module code is generated. */
26 | "rootDir": "./src", /* Specify the root folder within your source files. */
27 | "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
28 | // "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */
29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
35 | // "resolveJsonModule": true, /* Enable importing .json files. */
36 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
37 | /* JavaScript Support */
38 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
39 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
40 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
41 | /* Emit */
42 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
43 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
44 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
45 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
46 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
47 | "outDir": "./lib", /* Specify an output folder for all emitted files. */
48 | // "removeComments": true, /* Disable emitting comments. */
49 | // "noEmit": true, /* Disable emitting files from a compilation. */
50 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
51 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
52 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
53 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
55 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
56 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
57 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
58 | // "newLine": "crlf", /* Set the newline character for emitting files. */
59 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
60 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
61 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
62 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
63 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
64 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
65 | /* Interop Constraints */
66 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
67 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
68 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
69 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
70 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
71 | /* Type Checking */
72 | "strict": true, /* Enable all strict type-checking options. */
73 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
74 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
75 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
76 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
77 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
78 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
79 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
80 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
81 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
82 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
83 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
84 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
85 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
86 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
87 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
88 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
89 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
90 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
91 | /* Completeness */
92 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
93 | "skipLibCheck": true /* Skip type checking all .d.ts files. */
94 | },
95 | "exclude": [
96 | "jest.config.ts"
97 | ]
98 | }
--------------------------------------------------------------------------------