mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-08-09 08:38:37 +08:00
Monitor Conditions (#5048)
This commit is contained in:
71
server/monitor-conditions/evaluator.js
Normal file
71
server/monitor-conditions/evaluator.js
Normal file
@@ -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,
|
||||
};
|
111
server/monitor-conditions/expression.js
Normal file
111
server/monitor-conditions/expression.js
Normal file
@@ -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,
|
||||
};
|
318
server/monitor-conditions/operators.js
Normal file
318
server/monitor-conditions/operators.js
Normal file
@@ -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,
|
||||
};
|
31
server/monitor-conditions/variables.js
Normal file
31
server/monitor-conditions/variables.js
Normal file
@@ -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,
|
||||
};
|
Reference in New Issue
Block a user