summaryrefslogtreecommitdiffstats
path: root/server/monitor-conditions
diff options
context:
space:
mode:
Diffstat (limited to 'server/monitor-conditions')
-rw-r--r--server/monitor-conditions/evaluator.js71
-rw-r--r--server/monitor-conditions/expression.js111
-rw-r--r--server/monitor-conditions/operators.js318
-rw-r--r--server/monitor-conditions/variables.js31
4 files changed, 531 insertions, 0 deletions
diff --git a/server/monitor-conditions/evaluator.js b/server/monitor-conditions/evaluator.js
new file mode 100644
index 0000000..3860a33
--- /dev/null
+++ b/server/monitor-conditions/evaluator.js
@@ -0,0 +1,71 @@
+const { ConditionExpressionGroup, ConditionExpression, LOGICAL } = require("./expression");
+const { operatorMap } = require("./operators");
+
+/**
+ * @param {ConditionExpression} expression Expression to evaluate
+ * @param {object} context Context to evaluate against; These are values for variables in the expression
+ * @returns {boolean} Whether the expression evaluates true or false
+ * @throws {Error}
+ */
+function evaluateExpression(expression, context) {
+ /**
+ * @type {import("./operators").ConditionOperator|null}
+ */
+ const operator = operatorMap.get(expression.operator) || null;
+ if (operator === null) {
+ throw new Error("Unexpected expression operator ID '" + expression.operator + "'. Expected one of [" + operatorMap.keys().join(",") + "]");
+ }
+
+ if (!Object.prototype.hasOwnProperty.call(context, expression.variable)) {
+ throw new Error("Variable missing in context: " + expression.variable);
+ }
+
+ return operator.test(context[expression.variable], expression.value);
+}
+
+/**
+ * @param {ConditionExpressionGroup} group Group of expressions to evaluate
+ * @param {object} context Context to evaluate against; These are values for variables in the expression
+ * @returns {boolean} Whether the group evaluates true or false
+ * @throws {Error}
+ */
+function evaluateExpressionGroup(group, context) {
+ if (!group.children.length) {
+ throw new Error("ConditionExpressionGroup must contain at least one child.");
+ }
+
+ let result = null;
+
+ for (const child of group.children) {
+ let childResult;
+
+ if (child instanceof ConditionExpression) {
+ childResult = evaluateExpression(child, context);
+ } else if (child instanceof ConditionExpressionGroup) {
+ childResult = evaluateExpressionGroup(child, context);
+ } else {
+ throw new Error("Invalid child type in ConditionExpressionGroup. Expected ConditionExpression or ConditionExpressionGroup");
+ }
+
+ if (result === null) {
+ result = childResult; // Initialize result with the first child's result
+ } else if (child.andOr === LOGICAL.OR) {
+ result = result || childResult;
+ } else if (child.andOr === LOGICAL.AND) {
+ result = result && childResult;
+ } else {
+ throw new Error("Invalid logical operator in child of ConditionExpressionGroup. Expected 'and' or 'or'. Got '" + group.andOr + "'");
+ }
+ }
+
+ if (result === null) {
+ throw new Error("ConditionExpressionGroup did not result in a boolean.");
+ }
+
+ return result;
+}
+
+module.exports = {
+ evaluateExpression,
+ evaluateExpressionGroup,
+};
diff --git a/server/monitor-conditions/expression.js b/server/monitor-conditions/expression.js
new file mode 100644
index 0000000..1e70369
--- /dev/null
+++ b/server/monitor-conditions/expression.js
@@ -0,0 +1,111 @@
+/**
+ * @readonly
+ * @enum {string}
+ */
+const LOGICAL = {
+ AND: "and",
+ OR: "or",
+};
+
+/**
+ * Recursively processes an array of raw condition objects and populates the given parent group with
+ * corresponding ConditionExpression or ConditionExpressionGroup instances.
+ * @param {Array} conditions Array of raw condition objects, where each object represents either a group or an expression.
+ * @param {ConditionExpressionGroup} parentGroup The parent group to which the instantiated ConditionExpression or ConditionExpressionGroup objects will be added.
+ * @returns {void}
+ */
+function processMonitorConditions(conditions, parentGroup) {
+ conditions.forEach(condition => {
+ const andOr = condition.andOr === LOGICAL.OR ? LOGICAL.OR : LOGICAL.AND;
+
+ if (condition.type === "group") {
+ const group = new ConditionExpressionGroup([], andOr);
+
+ // Recursively process the group's children
+ processMonitorConditions(condition.children, group);
+
+ parentGroup.children.push(group);
+ } else if (condition.type === "expression") {
+ const expression = new ConditionExpression(condition.variable, condition.operator, condition.value, andOr);
+ parentGroup.children.push(expression);
+ }
+ });
+}
+
+class ConditionExpressionGroup {
+ /**
+ * @type {ConditionExpressionGroup[]|ConditionExpression[]} Groups and/or expressions to test
+ */
+ children = [];
+
+ /**
+ * @type {LOGICAL} Connects group result with previous group/expression results
+ */
+ andOr;
+
+ /**
+ * @param {ConditionExpressionGroup[]|ConditionExpression[]} children Groups and/or expressions to test
+ * @param {LOGICAL} andOr Connects group result with previous group/expression results
+ */
+ constructor(children = [], andOr = LOGICAL.AND) {
+ this.children = children;
+ this.andOr = andOr;
+ }
+
+ /**
+ * @param {Monitor} monitor Monitor instance
+ * @returns {ConditionExpressionGroup|null} A ConditionExpressionGroup with the Monitor's conditions
+ */
+ static fromMonitor(monitor) {
+ const conditions = JSON.parse(monitor.conditions);
+ if (conditions.length === 0) {
+ return null;
+ }
+
+ const root = new ConditionExpressionGroup();
+ processMonitorConditions(conditions, root);
+
+ return root;
+ }
+}
+
+class ConditionExpression {
+ /**
+ * @type {string} ID of variable
+ */
+ variable;
+
+ /**
+ * @type {string} ID of operator
+ */
+ operator;
+
+ /**
+ * @type {string} Value to test with the operator
+ */
+ value;
+
+ /**
+ * @type {LOGICAL} Connects expression result with previous group/expression results
+ */
+ andOr;
+
+ /**
+ * @param {string} variable ID of variable to test against
+ * @param {string} operator ID of operator to test the variable with
+ * @param {string} value Value to test with the operator
+ * @param {LOGICAL} andOr Connects expression result with previous group/expression results
+ */
+ constructor(variable, operator, value, andOr = LOGICAL.AND) {
+ this.variable = variable;
+ this.operator = operator;
+ this.value = value;
+ this.andOr = andOr;
+ }
+}
+
+module.exports = {
+ LOGICAL,
+ ConditionExpressionGroup,
+ ConditionExpression,
+};
diff --git a/server/monitor-conditions/operators.js b/server/monitor-conditions/operators.js
new file mode 100644
index 0000000..d900dff
--- /dev/null
+++ b/server/monitor-conditions/operators.js
@@ -0,0 +1,318 @@
+class ConditionOperator {
+ id = undefined;
+ caption = undefined;
+
+ /**
+ * @type {mixed} variable
+ * @type {mixed} value
+ */
+ test(variable, value) {
+ throw new Error("You need to override test()");
+ }
+}
+
+const OP_STR_EQUALS = "equals";
+
+const OP_STR_NOT_EQUALS = "not_equals";
+
+const OP_CONTAINS = "contains";
+
+const OP_NOT_CONTAINS = "not_contains";
+
+const OP_STARTS_WITH = "starts_with";
+
+const OP_NOT_STARTS_WITH = "not_starts_with";
+
+const OP_ENDS_WITH = "ends_with";
+
+const OP_NOT_ENDS_WITH = "not_ends_with";
+
+const OP_NUM_EQUALS = "num_equals";
+
+const OP_NUM_NOT_EQUALS = "num_not_equals";
+
+const OP_LT = "lt";
+
+const OP_GT = "gt";
+
+const OP_LTE = "lte";
+
+const OP_GTE = "gte";
+
+/**
+ * Asserts a variable is equal to a value.
+ */
+class StringEqualsOperator extends ConditionOperator {
+ id = OP_STR_EQUALS;
+ caption = "equals";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable === value;
+ }
+}
+
+/**
+ * Asserts a variable is not equal to a value.
+ */
+class StringNotEqualsOperator extends ConditionOperator {
+ id = OP_STR_NOT_EQUALS;
+ caption = "not equals";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable !== value;
+ }
+}
+
+/**
+ * Asserts a variable contains a value.
+ * Handles both Array and String variable types.
+ */
+class ContainsOperator extends ConditionOperator {
+ id = OP_CONTAINS;
+ caption = "contains";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ if (Array.isArray(variable)) {
+ return variable.includes(value);
+ }
+
+ return variable.indexOf(value) !== -1;
+ }
+}
+
+/**
+ * Asserts a variable does not contain a value.
+ * Handles both Array and String variable types.
+ */
+class NotContainsOperator extends ConditionOperator {
+ id = OP_NOT_CONTAINS;
+ caption = "not contains";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ if (Array.isArray(variable)) {
+ return !variable.includes(value);
+ }
+
+ return variable.indexOf(value) === -1;
+ }
+}
+
+/**
+ * Asserts a variable starts with a value.
+ */
+class StartsWithOperator extends ConditionOperator {
+ id = OP_STARTS_WITH;
+ caption = "starts with";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable.startsWith(value);
+ }
+}
+
+/**
+ * Asserts a variable does not start with a value.
+ */
+class NotStartsWithOperator extends ConditionOperator {
+ id = OP_NOT_STARTS_WITH;
+ caption = "not starts with";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return !variable.startsWith(value);
+ }
+}
+
+/**
+ * Asserts a variable ends with a value.
+ */
+class EndsWithOperator extends ConditionOperator {
+ id = OP_ENDS_WITH;
+ caption = "ends with";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable.endsWith(value);
+ }
+}
+
+/**
+ * Asserts a variable does not end with a value.
+ */
+class NotEndsWithOperator extends ConditionOperator {
+ id = OP_NOT_ENDS_WITH;
+ caption = "not ends with";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return !variable.endsWith(value);
+ }
+}
+
+/**
+ * Asserts a numeric variable is equal to a value.
+ */
+class NumberEqualsOperator extends ConditionOperator {
+ id = OP_NUM_EQUALS;
+ caption = "equals";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable === Number(value);
+ }
+}
+
+/**
+ * Asserts a numeric variable is not equal to a value.
+ */
+class NumberNotEqualsOperator extends ConditionOperator {
+ id = OP_NUM_NOT_EQUALS;
+ caption = "not equals";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable !== Number(value);
+ }
+}
+
+/**
+ * Asserts a variable is less than a value.
+ */
+class LessThanOperator extends ConditionOperator {
+ id = OP_LT;
+ caption = "less than";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable < Number(value);
+ }
+}
+
+/**
+ * Asserts a variable is greater than a value.
+ */
+class GreaterThanOperator extends ConditionOperator {
+ id = OP_GT;
+ caption = "greater than";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable > Number(value);
+ }
+}
+
+/**
+ * Asserts a variable is less than or equal to a value.
+ */
+class LessThanOrEqualToOperator extends ConditionOperator {
+ id = OP_LTE;
+ caption = "less than or equal to";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable <= Number(value);
+ }
+}
+
+/**
+ * Asserts a variable is greater than or equal to a value.
+ */
+class GreaterThanOrEqualToOperator extends ConditionOperator {
+ id = OP_GTE;
+ caption = "greater than or equal to";
+
+ /**
+ * @inheritdoc
+ */
+ test(variable, value) {
+ return variable >= Number(value);
+ }
+}
+
+const operatorMap = new Map([
+ [ OP_STR_EQUALS, new StringEqualsOperator ],
+ [ OP_STR_NOT_EQUALS, new StringNotEqualsOperator ],
+ [ OP_CONTAINS, new ContainsOperator ],
+ [ OP_NOT_CONTAINS, new NotContainsOperator ],
+ [ OP_STARTS_WITH, new StartsWithOperator ],
+ [ OP_NOT_STARTS_WITH, new NotStartsWithOperator ],
+ [ OP_ENDS_WITH, new EndsWithOperator ],
+ [ OP_NOT_ENDS_WITH, new NotEndsWithOperator ],
+ [ OP_NUM_EQUALS, new NumberEqualsOperator ],
+ [ OP_NUM_NOT_EQUALS, new NumberNotEqualsOperator ],
+ [ OP_LT, new LessThanOperator ],
+ [ OP_GT, new GreaterThanOperator ],
+ [ OP_LTE, new LessThanOrEqualToOperator ],
+ [ OP_GTE, new GreaterThanOrEqualToOperator ],
+]);
+
+const defaultStringOperators = [
+ operatorMap.get(OP_STR_EQUALS),
+ operatorMap.get(OP_STR_NOT_EQUALS),
+ operatorMap.get(OP_CONTAINS),
+ operatorMap.get(OP_NOT_CONTAINS),
+ operatorMap.get(OP_STARTS_WITH),
+ operatorMap.get(OP_NOT_STARTS_WITH),
+ operatorMap.get(OP_ENDS_WITH),
+ operatorMap.get(OP_NOT_ENDS_WITH)
+];
+
+const defaultNumberOperators = [
+ operatorMap.get(OP_NUM_EQUALS),
+ operatorMap.get(OP_NUM_NOT_EQUALS),
+ operatorMap.get(OP_LT),
+ operatorMap.get(OP_GT),
+ operatorMap.get(OP_LTE),
+ operatorMap.get(OP_GTE)
+];
+
+module.exports = {
+ OP_STR_EQUALS,
+ OP_STR_NOT_EQUALS,
+ OP_CONTAINS,
+ OP_NOT_CONTAINS,
+ OP_STARTS_WITH,
+ OP_NOT_STARTS_WITH,
+ OP_ENDS_WITH,
+ OP_NOT_ENDS_WITH,
+ OP_NUM_EQUALS,
+ OP_NUM_NOT_EQUALS,
+ OP_LT,
+ OP_GT,
+ OP_LTE,
+ OP_GTE,
+ operatorMap,
+ defaultStringOperators,
+ defaultNumberOperators,
+ ConditionOperator,
+};
diff --git a/server/monitor-conditions/variables.js b/server/monitor-conditions/variables.js
new file mode 100644
index 0000000..af98d2f
--- /dev/null
+++ b/server/monitor-conditions/variables.js
@@ -0,0 +1,31 @@
+/**
+ * Represents a variable used in a condition and the set of operators that can be applied to this variable.
+ *
+ * A `ConditionVariable` holds the ID of the variable and a list of operators that define how this variable can be evaluated
+ * in conditions. For example, if the variable is a request body or a specific field in a request, the operators can include
+ * operations such as equality checks, comparisons, or other custom evaluations.
+ */
+class ConditionVariable {
+ /**
+ * @type {string}
+ */
+ id;
+
+ /**
+ * @type {import("./operators").ConditionOperator[]}
+ */
+ operators = {};
+
+ /**
+ * @param {string} id ID of variable
+ * @param {import("./operators").ConditionOperator[]} operators Operators the condition supports
+ */
+ constructor(id, operators = []) {
+ this.id = id;
+ this.operators = operators;
+ }
+}
+
+module.exports = {
+ ConditionVariable,
+};