-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcalc-evaluator.ts
More file actions
71 lines (59 loc) · 2.08 KB
/
Copy pathcalc-evaluator.ts
File metadata and controls
71 lines (59 loc) · 2.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { Parser } from 'expr-eval-fork';
import type { DashboardVariables } from '../custom/model/dashboard.types.js';
const LOOKUP_VARIABLE_PATH_RE = /lookup\(\s*\$variables((?:\.[a-zA-Z_][a-zA-Z0-9_]*)+)\s*,/g;
const CALC_PARSER_OPTIONS = {
allowMemberAccess: false,
operators: {
assignment: false,
concatenate: false,
conditional: true,
comparison: true,
fndef: false,
in: false,
logical: true,
random: false,
},
} as const;
export function evaluateCalc(
calc: string,
values: Record<string, unknown>,
variables: DashboardVariables = {},
) {
const parser = createCalcParser(variables);
return parser.parse(normalizeLookupPaths(calc)).evaluate(normalizeCalcValues(values));
}
function createCalcParser(variables: DashboardVariables) {
const parser = new Parser(CALC_PARSER_OPTIONS);
parser.functions.lookup = (path: string | number, key: string | number, defaultValue = 0) => {
const map = resolveVariablePath(variables, String(path));
const value = isRecord(map) && Object.prototype.hasOwnProperty.call(map, String(key))
? map[String(key)]
: defaultValue;
return toFiniteNumber(value);
};
return parser;
}
function normalizeLookupPaths(calc: string) {
return calc.replace(LOOKUP_VARIABLE_PATH_RE, (_match, path: string) => {
return `lookup("${path.replace(/^\./, '')}",`;
});
}
function normalizeCalcValues(values: Record<string, unknown>) {
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
key,
typeof value === 'string' ? value : toFiniteNumber(value),
]));
}
function toFiniteNumber(value: unknown) {
const numberValue = typeof value === 'number' ? value : Number(value);
return Number.isFinite(numberValue) ? numberValue : 0;
}
function resolveVariablePath(variables: DashboardVariables, path: string) {
return path
.split('.')
.filter(Boolean)
.reduce<unknown>((current, segment) => isRecord(current) ? current[segment] : undefined, variables);
}
function isRecord(value: unknown): value is Record<string, any> {
return typeof value === 'object' && value !== null;
}