fig-tree-evaluator 2.1.5 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
 
7
7
  A typical use case would be for evaluating **configuration** files, where you need to store dynamic values or arbitrary logic without allowing users to inject executable code. For example, a [form-builder app](https://github.com/openmsupply/conforma-web-app) might need to allow complex conditional logic for form element visibility based on previous responses, or for validation beyond what is available in standard validation libraries.
8
8
 
9
+ Another example would be to configure a [decision tree](https://en.wikipedia.org/wiki/Decision_tree) to implement branching logic.
10
+
9
11
  A range of built-in operators are available, from simple logic, arithmetic and string manipulation, to data fetching from local sources or remote APIs.
10
12
 
11
13
  [**Demo/Playground**](https://carlosnz.github.io/fig-tree-evaluator/)
@@ -18,31 +20,32 @@ A range of built-in operators are available, from simple logic, arithmetic and s
18
20
  - [Available options](#available-options)
19
21
  - [Operator nodes](#operator-nodes)
20
22
  - [Other common properties:](#other-common-properties)
21
- - [Operator & Property Aliases](#operator--property-aliases)
23
+ - [Operator \& Property Aliases](#operator--property-aliases)
22
24
  - [Operator reference](#operator-reference)
23
25
  - [AND](#and)
24
26
  - [OR](#or)
25
27
  - [EQUAL](#equal)
26
- - [NOT_EQUAL](#not_equal)
28
+ - [NOT\_EQUAL](#not_equal)
27
29
  - [PLUS](#plus)
28
30
  - [SUBTRACT](#subtract)
29
31
  - [MULTIPLY](#multiply)
30
32
  - [DIVIDE](#divide)
31
- - [GREATER_THAN](#greater_than)
32
- - [LESS_THAN](#less_than)
33
+ - [GREATER\_THAN](#greater_than)
34
+ - [LESS\_THAN](#less_than)
33
35
  - [COUNT](#count)
34
36
  - [CONDITIONAL](#conditional)
35
37
  - [REGEX](#regex)
36
- - [OBJECT_PROPERTIES](#object_properties)
37
- - [STRING_SUBSTITUTION](#string_substitution)
38
+ - [OBJECT\_PROPERTIES](#object_properties)
39
+ - [STRING\_SUBSTITUTION](#string_substitution)
38
40
  - [SPLIT](#split)
39
41
  - [GET](#get)
40
42
  - [POST](#post)
41
43
  - [GRAPHQL](#graphql)
42
- - [PG_SQL](#pg_sql)
43
- - [BUILD_OBJECT](#build_object)
44
+ - [PG\_SQL](#pg_sql)
45
+ - [BUILD\_OBJECT](#build_object)
46
+ - [MATCH](#match)
44
47
  - [PASSTHRU](#passthru)
45
- - [CUSTOM_FUNCTIONS](#custom_functions)
48
+ - [CUSTOM\_FUNCTIONS](#custom_functions)
46
49
  - [Alias Nodes](#alias-nodes)
47
50
  - [More examples](#more-examples)
48
51
  - [Development environment](#development-environment)
@@ -1128,6 +1131,94 @@ e.g.
1128
1131
  // => { one: 1, two: 2, Ned: 24 }
1129
1132
  ```
1130
1133
 
1134
+ ----
1135
+
1136
+ ### MATCH
1137
+
1138
+ *Return different values depending on a matching expression*
1139
+
1140
+ Aliases: `match`, `switch`
1141
+
1142
+ The "match" operator is equivalent to a "switch"/"case" in Javascript. It is similar to the ["conditional"](#conditional) operator, but can handle matching to any number of values, not just `true`/`false`. This provides a way to construct elaborate [**decision trees**](https://en.wikipedia.org/wiki/Decision_tree).
1143
+
1144
+ #### Properties
1145
+
1146
+ - `matchExpression` (or `match`)<sup>*</sup>: (string | number | boolean) -- a node that returns a value to be compared against possible cases.
1147
+ - `branches` (or `arms` or `cases`): (object) -- an object whose *keys* are compared against the `matchExpression`. The *value* of the matching key is returned.
1148
+ - `...branches` -- as an alternative to the `branches` object, matching key/values can be placed at the root of the node (see example)
1149
+
1150
+ e.g.
1151
+ ```js
1152
+ // Simple decision tree
1153
+ {
1154
+ operator: 'match',
1155
+ matchExpression: {
1156
+ operator: 'objectProperties',
1157
+ property: 'weather',
1158
+ },
1159
+ branches: {
1160
+ sunny: {
1161
+ operator: 'match',
1162
+ match: {
1163
+ operator: 'objectProperties',
1164
+ property: 'humidity',
1165
+ },
1166
+ cases: { high: 'NO', normal: 'YES' },
1167
+ },
1168
+ cloudy: 'YES',
1169
+ },
1170
+ rainy: {
1171
+ operator: 'match',
1172
+ match: {
1173
+ operator: 'objectProperties',
1174
+ property: 'wind',
1175
+ },
1176
+ branches: { strong: 'NO', weak: 'YES' },
1177
+ },
1178
+ }
1179
+ // With:
1180
+ // objects = { weather: "sunny", humidity: "high" } => "NO"
1181
+ // objects = { weather: "rainy", wind: "weak" } => "YES"
1182
+
1183
+ ```
1184
+
1185
+ This expression could also be written as (with branch/case keys at the root level)"
1186
+ ```js
1187
+ {
1188
+ operator: 'match',
1189
+ matchExpression: {
1190
+ operator: 'objectProperties',
1191
+ property: 'weather',
1192
+ },
1193
+ sunny: {
1194
+ operator: 'match',
1195
+ match: {
1196
+ operator: 'objectProperties',
1197
+ property: 'humidity',
1198
+ },
1199
+ high: 'NO',
1200
+ normal: 'YES',
1201
+ },
1202
+ cloudy: 'YES',
1203
+ rainy: {
1204
+ operator: 'match',
1205
+ match: {
1206
+ operator: 'objectProperties',
1207
+ property: 'wind',
1208
+ },
1209
+ strong: 'NO',
1210
+ weak: 'YES',
1211
+ },
1212
+ }
1213
+ ```
1214
+
1215
+ `children` array: `[matchExpression, key1, value1, key2, value2, ...]`
1216
+
1217
+ The pairs of `key`/`value`s are constructed into the `branches` object, the same way the objects are built using `children` in the ["buildObject"](#build_object) operator.
1218
+
1219
+
1220
+ For an example of a complex decision tree implementation, which includes [aliases](#alias-nodes), [fallbacks](#other-common-properties) and a range of operators, see the "match" test case file (`20_match.test.ts`).
1221
+
1131
1222
  ----
1132
1223
  ### PASSTHRU
1133
1224
 
@@ -2,8 +2,8 @@ import { OutputType, EvaluatorNode, CombinedOperatorNode, Operator, EvaluatorOut
2
2
  export declare const parseIfJson: (input: EvaluatorNode) => any;
3
3
  export declare const isOperatorNode: (input: EvaluatorNode) => boolean;
4
4
  export declare const getOperatorName: (operator: string, operatorAliases: {
5
- [key: string]: "AND" | "OR" | "EQUAL" | "NOT_EQUAL" | "PLUS" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" | "GREATER_THAN" | "LESS_THAN" | "CONDITIONAL" | "REGEX" | "OBJECT_PROPERTIES" | "STRING_SUBSTITUTION" | "SPLIT" | "COUNT" | "GET" | "POST" | "PG_SQL" | "GRAPHQL" | "BUILD_OBJECT" | "CUSTOM_FUNCTIONS" | "PASSTHRU";
6
- }) => "AND" | "OR" | "EQUAL" | "NOT_EQUAL" | "PLUS" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" | "GREATER_THAN" | "LESS_THAN" | "CONDITIONAL" | "REGEX" | "OBJECT_PROPERTIES" | "STRING_SUBSTITUTION" | "SPLIT" | "COUNT" | "GET" | "POST" | "PG_SQL" | "GRAPHQL" | "BUILD_OBJECT" | "CUSTOM_FUNCTIONS" | "PASSTHRU";
5
+ [key: string]: "AND" | "OR" | "EQUAL" | "NOT_EQUAL" | "PLUS" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" | "GREATER_THAN" | "LESS_THAN" | "CONDITIONAL" | "REGEX" | "OBJECT_PROPERTIES" | "STRING_SUBSTITUTION" | "SPLIT" | "COUNT" | "GET" | "POST" | "PG_SQL" | "GRAPHQL" | "BUILD_OBJECT" | "MATCH" | "CUSTOM_FUNCTIONS" | "PASSTHRU";
6
+ }) => "AND" | "OR" | "EQUAL" | "NOT_EQUAL" | "PLUS" | "SUBTRACT" | "MULTIPLY" | "DIVIDE" | "GREATER_THAN" | "LESS_THAN" | "CONDITIONAL" | "REGEX" | "OBJECT_PROPERTIES" | "STRING_SUBSTITUTION" | "SPLIT" | "COUNT" | "GET" | "POST" | "PG_SQL" | "GRAPHQL" | "BUILD_OBJECT" | "MATCH" | "CUSTOM_FUNCTIONS" | "PASSTHRU";
7
7
  export declare const truncateString: (string: string, length?: number) => string;
8
8
  export declare const fallbackOrError: (fallback: any, errorMessage: string, returnErrorAsString?: boolean) => any;
9
9
  export declare const mapPropertyAliases: (propertyAliases: {
package/build/helpers.js CHANGED
@@ -100,7 +100,8 @@ exports.errorMessage = errorMessage;
100
100
  const extractNumber = (input) => {
101
101
  if (typeof input !== 'string')
102
102
  return Number.isNaN(Number(input)) ? input : Number(input);
103
- const numberMatch = input.match(/(-?(\d+\.\d+))|(-?((?<!\.)\.\d+))|(-?\d+)/gm);
103
+ const pattern = new RegExp(/-?\d*\.?\d+/gm);
104
+ const numberMatch = input.match(pattern);
104
105
  if (!numberMatch)
105
106
  return 0;
106
107
  return Number(numberMatch[0]);
@@ -71,6 +71,8 @@
71
71
  "buildObject": "BUILD_OBJECT",
72
72
  "build": "BUILD_OBJECT",
73
73
  "object": "BUILD_OBJECT",
74
+ "match": "MATCH",
75
+ "switch": "MATCH",
74
76
  "customFunctions": "CUSTOM_FUNCTIONS",
75
77
  "customFunction": "CUSTOM_FUNCTIONS",
76
78
  "objectFunctions": "CUSTOM_FUNCTIONS",
@@ -3,6 +3,7 @@ export declare const evaluateArray: (nodes: EvaluatorNode[], params: FigTreeConf
3
3
  export declare const zipArraysToObject: <T>(keys: string[], values: T[]) => {
4
4
  [x: string]: T;
5
5
  };
6
+ export declare const singleArrayToObject: (elements: any[]) => any;
6
7
  export declare const simplifyObject: (item: number | string | boolean | GenericObject) => any;
7
8
  export declare const extractAndSimplify: (data: GenericObject | GenericObject[], returnProperty: string | undefined, fallback?: any) => any;
8
9
  export declare const isFullUrl: (url: string) => boolean;
@@ -12,10 +12,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.axiosRequest = exports.joinUrlParts = exports.isFullUrl = exports.extractAndSimplify = exports.simplifyObject = exports.zipArraysToObject = exports.evaluateArray = void 0;
15
+ exports.axiosRequest = exports.joinUrlParts = exports.isFullUrl = exports.extractAndSimplify = exports.simplifyObject = exports.singleArrayToObject = exports.zipArraysToObject = exports.evaluateArray = void 0;
16
16
  const axios_1 = __importDefault(require("axios"));
17
17
  const extract_1 = __importDefault(require("object-property-extractor/build/extract"));
18
18
  const evaluate_1 = require("../evaluate");
19
+ const typeCheck_1 = require("../typeCheck");
19
20
  const evaluateArray = (nodes, params) => __awaiter(void 0, void 0, void 0, function* () {
20
21
  if (!Array.isArray(nodes))
21
22
  return (yield (0, evaluate_1.evaluatorFunction)(nodes, params));
@@ -27,6 +28,24 @@ const zipArraysToObject = (keys, values) => {
27
28
  return Object.fromEntries(pairs);
28
29
  };
29
30
  exports.zipArraysToObject = zipArraysToObject;
31
+ const singleArrayToObject = (elements) => {
32
+ if (elements.length % 2 !== 0)
33
+ throw new Error('Even number of children required to make key/value pairs');
34
+ const keys = elements.filter((_, index) => index % 2 === 0);
35
+ const result = (0, typeCheck_1.typeCheck)(...keys.map((key) => ({
36
+ value: key,
37
+ expectedType: ['string', 'number', 'boolean'],
38
+ })));
39
+ if (result !== true)
40
+ throw new Error(result);
41
+ const values = elements.filter((_, index) => index % 2 === 1);
42
+ const output = {};
43
+ keys.forEach((key, index) => {
44
+ output[key] = values[index];
45
+ });
46
+ return output;
47
+ };
48
+ exports.singleArrayToObject = singleArrayToObject;
30
49
  const simplifyObject = (item) => {
31
50
  return item instanceof Object && Object.keys(item).length === 1 ? Object.values(item)[0] : item;
32
51
  };
@@ -21,7 +21,7 @@ const propertyAliases = {
21
21
  const evaluate = (expression, config) => __awaiter(void 0, void 0, void 0, function* () {
22
22
  const evaluatePair = (nodes) => __awaiter(void 0, void 0, void 0, function* () {
23
23
  const [key, value] = (yield (0, _operatorUtils_1.evaluateArray)(nodes, config));
24
- config.typeChecker({ name: 'key', value: key, expectedType: 'string' });
24
+ config.typeChecker({ name: 'key', value: key, expectedType: ['string', 'number', 'boolean'] });
25
25
  return [key, value];
26
26
  });
27
27
  config.typeChecker({ name: 'properties', value: expression.properties, expectedType: 'array' });
@@ -19,5 +19,6 @@ export * from './graphQL';
19
19
  export * from './getRequest';
20
20
  export * from './postRequest';
21
21
  export * from './buildObject';
22
+ export * from './match';
22
23
  export * from './customFunctions';
23
24
  export * from './passThru';
@@ -35,5 +35,6 @@ __exportStar(require("./graphQL"), exports);
35
35
  __exportStar(require("./getRequest"), exports);
36
36
  __exportStar(require("./postRequest"), exports);
37
37
  __exportStar(require("./buildObject"), exports);
38
+ __exportStar(require("./match"), exports);
38
39
  __exportStar(require("./customFunctions"), exports);
39
40
  __exportStar(require("./passThru"), exports);
@@ -0,0 +1,7 @@
1
+ import { BaseOperatorNode, EvaluatorNode, OperatorObject } from '../types';
2
+ declare const requiredProperties: readonly ["matchExpression"];
3
+ export type MatchNode = {
4
+ [key in typeof requiredProperties[number]]: EvaluatorNode;
5
+ } & BaseOperatorNode;
6
+ export declare const MATCH: OperatorObject;
7
+ export {};
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.MATCH = void 0;
13
+ const _operatorUtils_1 = require("./_operatorUtils");
14
+ const evaluate_1 = require("../evaluate");
15
+ const requiredProperties = ['matchExpression'];
16
+ const operatorAliases = ['match', 'switch'];
17
+ const propertyAliases = { arms: 'branches', cases: 'branches', match: 'matchExpression' };
18
+ const evaluate = (expression, config) => __awaiter(void 0, void 0, void 0, function* () {
19
+ const [matchExpression, branches] = (yield (0, _operatorUtils_1.evaluateArray)([expression.matchExpression, expression.branches], config));
20
+ config.typeChecker({
21
+ name: 'matchExpression',
22
+ value: matchExpression,
23
+ expectedType: ['string', 'number', 'boolean'],
24
+ }, {
25
+ name: 'branches',
26
+ value: branches,
27
+ expectedType: ['object', 'array', 'undefined'],
28
+ });
29
+ const branchObject = Array.isArray(branches) ? (0, _operatorUtils_1.singleArrayToObject)(branches) : branches !== null && branches !== void 0 ? branches : {};
30
+ if (matchExpression in branchObject)
31
+ return yield (0, evaluate_1.evaluatorFunction)(branchObject[matchExpression], config);
32
+ if ('fallback' in branchObject)
33
+ return yield (0, evaluate_1.evaluatorFunction)(branchObject.fallback, config);
34
+ if (matchExpression in expression)
35
+ return yield (0, evaluate_1.evaluatorFunction)(expression[matchExpression], config);
36
+ throw new Error(`No match found for ${matchExpression}`);
37
+ });
38
+ const parseChildren = (expression, config) => __awaiter(void 0, void 0, void 0, function* () {
39
+ const [matchExpression, ...elements] = expression.children;
40
+ const branches = (0, _operatorUtils_1.singleArrayToObject)(yield (0, _operatorUtils_1.evaluateArray)(elements, config));
41
+ return Object.assign(Object.assign({}, expression), { matchExpression, branches });
42
+ });
43
+ exports.MATCH = {
44
+ requiredProperties,
45
+ operatorAliases,
46
+ propertyAliases,
47
+ evaluate,
48
+ parseChildren,
49
+ };
@@ -1,4 +1,4 @@
1
- type ExpectedType = 'string' | 'boolean' | 'number' | 'array' | 'undefined' | 'null' | 'object';
1
+ export type ExpectedType = 'string' | 'boolean' | 'number' | 'array' | 'undefined' | 'null' | 'object';
2
2
  export type TypeCheckInput = {
3
3
  value: unknown;
4
4
  name?: string;
@@ -6,4 +6,3 @@ export type TypeCheckInput = {
6
6
  expectedType: ExpectedType | ExpectedType[];
7
7
  };
8
8
  export declare const typeCheck: (...args: TypeCheckInput[]) => true | string;
9
- export {};
package/build/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { BasicExtendedNode, SubtractionNode, DivisionNode, ComparatorNode, ConditionalNode, RegexNode, StringSubNode, SplitNode, ObjPropNode, APINode, PGNode, GraphQLNode, BuildObjectNode, FunctionNode, PassThruNode, PGConnection, GraphQLConnection } from './operators';
1
+ import { BasicExtendedNode, SubtractionNode, DivisionNode, ComparatorNode, ConditionalNode, RegexNode, StringSubNode, SplitNode, ObjPropNode, APINode, PGNode, GraphQLNode, BuildObjectNode, MatchNode, FunctionNode, PassThruNode, PGConnection, GraphQLConnection } from './operators';
2
2
  import { TypeCheckInput } from './typeCheck';
3
- export declare const Operators: readonly ["AND", "OR", "EQUAL", "NOT_EQUAL", "PLUS", "SUBTRACT", "MULTIPLY", "DIVIDE", "GREATER_THAN", "LESS_THAN", "CONDITIONAL", "REGEX", "OBJECT_PROPERTIES", "STRING_SUBSTITUTION", "SPLIT", "COUNT", "GET", "POST", "PG_SQL", "GRAPHQL", "BUILD_OBJECT", "CUSTOM_FUNCTIONS", "PASSTHRU"];
3
+ export declare const Operators: readonly ["AND", "OR", "EQUAL", "NOT_EQUAL", "PLUS", "SUBTRACT", "MULTIPLY", "DIVIDE", "GREATER_THAN", "LESS_THAN", "CONDITIONAL", "REGEX", "OBJECT_PROPERTIES", "STRING_SUBSTITUTION", "SPLIT", "COUNT", "GET", "POST", "PG_SQL", "GRAPHQL", "BUILD_OBJECT", "MATCH", "CUSTOM_FUNCTIONS", "PASSTHRU"];
4
4
  export type Operator = typeof Operators[number];
5
5
  export type GenericObject = {
6
6
  [key: string]: any;
@@ -40,8 +40,8 @@ export interface BaseOperatorNode {
40
40
  fallback?: any;
41
41
  [key: string]: EvaluatorNode;
42
42
  }
43
- export type CombinedOperatorNode = BaseOperatorNode & BasicExtendedNode & SubtractionNode & DivisionNode & ComparatorNode & ConditionalNode & RegexNode & StringSubNode & SplitNode & ObjPropNode & APINode & PGNode & GraphQLNode & BuildObjectNode & FunctionNode & PassThruNode;
44
- export type OperatorNodeUnion = BasicExtendedNode | SubtractionNode | DivisionNode | ComparatorNode | ConditionalNode | RegexNode | StringSubNode | SplitNode | ObjPropNode | PGNode | GraphQLNode | APINode | BuildObjectNode | FunctionNode | PassThruNode;
43
+ export type CombinedOperatorNode = BaseOperatorNode & BasicExtendedNode & SubtractionNode & DivisionNode & ComparatorNode & ConditionalNode & RegexNode & StringSubNode & SplitNode & ObjPropNode & APINode & PGNode & GraphQLNode & BuildObjectNode & MatchNode & FunctionNode & PassThruNode;
44
+ export type OperatorNodeUnion = BasicExtendedNode | SubtractionNode | DivisionNode | ComparatorNode | ConditionalNode | RegexNode | StringSubNode | SplitNode | ObjPropNode | PGNode | GraphQLNode | APINode | BuildObjectNode | MatchNode | FunctionNode | PassThruNode;
45
45
  export type EvaluatorOutput = string | boolean | number | GenericObject | null | undefined | any[];
46
46
  export type EvaluatorNode = CombinedOperatorNode | EvaluatorOutput;
47
47
  export type OperatorObject = {
package/build/types.js CHANGED
@@ -23,6 +23,7 @@ exports.Operators = [
23
23
  'PG_SQL',
24
24
  'GRAPHQL',
25
25
  'BUILD_OBJECT',
26
+ 'MATCH',
26
27
  'CUSTOM_FUNCTIONS',
27
28
  'PASSTHRU',
28
29
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fig-tree-evaluator",
3
- "version": "2.1.5",
3
+ "version": "2.2.0",
4
4
  "description": "Module to evaluate JSON-structured expression trees",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",