@object-ui/core 3.1.5 → 3.3.1
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/CHANGELOG.md +18 -0
- package/README.md +20 -1
- package/dist/actions/ActionRunner.d.ts +9 -0
- package/dist/actions/ActionRunner.js +41 -4
- package/dist/adapters/ValueDataSource.d.ts +5 -1
- package/dist/adapters/ValueDataSource.js +30 -1
- package/dist/errors/index.js +2 -3
- package/dist/evaluator/ExpressionCache.d.ts +9 -10
- package/dist/evaluator/ExpressionCache.js +29 -8
- package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
- package/dist/evaluator/SafeExpressionParser.js +851 -0
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/protocols/DndProtocol.js +2 -14
- package/dist/protocols/KeyboardProtocol.js +1 -4
- package/dist/protocols/NotificationProtocol.js +3 -13
- package/dist/utils/debug.js +2 -1
- package/dist/utils/filter-converter.js +25 -5
- package/package.json +33 -9
- package/.turbo/turbo-build.log +0 -4
- package/src/__benchmarks__/core.bench.ts +0 -64
- package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
- package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
- package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
- package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
- package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
- package/src/actions/ActionEngine.ts +0 -268
- package/src/actions/ActionRunner.ts +0 -717
- package/src/actions/TransactionManager.ts +0 -521
- package/src/actions/UndoManager.ts +0 -215
- package/src/actions/__tests__/ActionEngine.test.ts +0 -206
- package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
- package/src/actions/__tests__/ActionRunner.test.ts +0 -711
- package/src/actions/__tests__/TransactionManager.test.ts +0 -447
- package/src/actions/__tests__/UndoManager.test.ts +0 -320
- package/src/actions/index.ts +0 -12
- package/src/adapters/ApiDataSource.ts +0 -376
- package/src/adapters/README.md +0 -180
- package/src/adapters/ValueDataSource.ts +0 -438
- package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
- package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
- package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
- package/src/adapters/index.ts +0 -15
- package/src/adapters/resolveDataSource.ts +0 -79
- package/src/builder/__tests__/schema-builder.test.ts +0 -235
- package/src/builder/schema-builder.ts +0 -584
- package/src/data-scope/DataScopeManager.ts +0 -269
- package/src/data-scope/ViewDataProvider.ts +0 -282
- package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
- package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
- package/src/data-scope/index.ts +0 -24
- package/src/errors/__tests__/errors.test.ts +0 -292
- package/src/errors/index.ts +0 -270
- package/src/evaluator/ExpressionCache.ts +0 -192
- package/src/evaluator/ExpressionContext.ts +0 -118
- package/src/evaluator/ExpressionEvaluator.ts +0 -315
- package/src/evaluator/FormulaFunctions.ts +0 -398
- package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
- package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
- package/src/evaluator/index.ts +0 -12
- package/src/index.ts +0 -38
- package/src/protocols/DndProtocol.ts +0 -184
- package/src/protocols/KeyboardProtocol.ts +0 -185
- package/src/protocols/NotificationProtocol.ts +0 -159
- package/src/protocols/ResponsiveProtocol.ts +0 -210
- package/src/protocols/SharingProtocol.ts +0 -185
- package/src/protocols/index.ts +0 -13
- package/src/query/__tests__/query-ast.test.ts +0 -211
- package/src/query/__tests__/window-functions.test.ts +0 -275
- package/src/query/index.ts +0 -7
- package/src/query/query-ast.ts +0 -341
- package/src/registry/PluginScopeImpl.ts +0 -259
- package/src/registry/PluginSystem.ts +0 -206
- package/src/registry/Registry.ts +0 -219
- package/src/registry/WidgetRegistry.ts +0 -316
- package/src/registry/__tests__/PluginSystem.test.ts +0 -309
- package/src/registry/__tests__/Registry.test.ts +0 -293
- package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
- package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
- package/src/theme/ThemeEngine.ts +0 -530
- package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
- package/src/theme/index.ts +0 -24
- package/src/types/index.ts +0 -21
- package/src/utils/__tests__/debug-collector.test.ts +0 -102
- package/src/utils/__tests__/debug.test.ts +0 -134
- package/src/utils/__tests__/expand-fields.test.ts +0 -120
- package/src/utils/__tests__/extract-records.test.ts +0 -50
- package/src/utils/__tests__/filter-converter.test.ts +0 -118
- package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
- package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
- package/src/utils/debug-collector.ts +0 -100
- package/src/utils/debug.ts +0 -147
- package/src/utils/expand-fields.ts +0 -76
- package/src/utils/extract-records.ts +0 -33
- package/src/utils/filter-converter.ts +0 -133
- package/src/utils/merge-views-into-objects.ts +0 -36
- package/src/utils/normalize-quick-filter.ts +0 -78
- package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
- package/src/validation/__tests__/schema-validator.test.ts +0 -118
- package/src/validation/__tests__/validation-engine.test.ts +0 -102
- package/src/validation/index.ts +0 -10
- package/src/validation/schema-validator.ts +0 -344
- package/src/validation/validation-engine.ts +0 -528
- package/src/validation/validators/index.ts +0 -25
- package/src/validation/validators/object-validation-engine.ts +0 -722
- package/tsconfig.json +0 -15
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -2
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @object-ui/core - Expression Cache
|
|
11
|
-
*
|
|
12
|
-
* Caches compiled expressions to avoid re-parsing on every render.
|
|
13
|
-
* Provides significant performance improvement for frequently evaluated expressions.
|
|
14
|
-
*
|
|
15
|
-
* @module evaluator
|
|
16
|
-
* @packageDocumentation
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* A compiled expression function that can be executed with context values
|
|
21
|
-
*/
|
|
22
|
-
export type CompiledExpression = (...args: any[]) => any;
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Expression compilation metadata
|
|
26
|
-
*/
|
|
27
|
-
export interface ExpressionMetadata {
|
|
28
|
-
/**
|
|
29
|
-
* The compiled function
|
|
30
|
-
*/
|
|
31
|
-
fn: CompiledExpression;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Variable names used in the expression
|
|
35
|
-
*/
|
|
36
|
-
varNames: string[];
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Original expression string
|
|
40
|
-
*/
|
|
41
|
-
expression: string;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Timestamp when the expression was compiled
|
|
45
|
-
*/
|
|
46
|
-
compiledAt: number;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Number of times this expression has been used
|
|
50
|
-
*/
|
|
51
|
-
hitCount: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Cache for compiled expressions
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* const cache = new ExpressionCache();
|
|
60
|
-
* const compiled = cache.compile('data.amount > 1000', ['data']);
|
|
61
|
-
* const result = compiled.fn({ amount: 1500 }); // true
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export class ExpressionCache {
|
|
65
|
-
private cache = new Map<string, ExpressionMetadata>();
|
|
66
|
-
private maxSize: number;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Create a new expression cache
|
|
70
|
-
*
|
|
71
|
-
* @param maxSize Maximum number of expressions to cache (default: 1000)
|
|
72
|
-
*/
|
|
73
|
-
constructor(maxSize = 1000) {
|
|
74
|
-
this.maxSize = maxSize;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Compile an expression or retrieve from cache
|
|
79
|
-
*
|
|
80
|
-
* @param expr The expression to compile
|
|
81
|
-
* @param varNames Variable names available in the context
|
|
82
|
-
* @returns Compiled expression metadata
|
|
83
|
-
*/
|
|
84
|
-
compile(expr: string, varNames: string[]): ExpressionMetadata {
|
|
85
|
-
// Create a cache key that includes variable names to ensure correct scoping
|
|
86
|
-
const cacheKey = `${expr}::${varNames.join(',')}`;
|
|
87
|
-
|
|
88
|
-
if (this.cache.has(cacheKey)) {
|
|
89
|
-
const metadata = this.cache.get(cacheKey)!;
|
|
90
|
-
metadata.hitCount++;
|
|
91
|
-
return metadata;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Evict least frequently used if cache is full
|
|
95
|
-
if (this.cache.size >= this.maxSize) {
|
|
96
|
-
this.evictLFU();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Compile the expression
|
|
100
|
-
const fn = this.compileExpression(expr, varNames);
|
|
101
|
-
|
|
102
|
-
const metadata: ExpressionMetadata = {
|
|
103
|
-
fn,
|
|
104
|
-
varNames: [...varNames],
|
|
105
|
-
expression: expr,
|
|
106
|
-
compiledAt: Date.now(),
|
|
107
|
-
hitCount: 1,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
this.cache.set(cacheKey, metadata);
|
|
111
|
-
return metadata;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Compile an expression into a function
|
|
116
|
-
*/
|
|
117
|
-
private compileExpression(expression: string, varNames: string[]): CompiledExpression {
|
|
118
|
-
// SECURITY NOTE: Using Function constructor for expression evaluation.
|
|
119
|
-
// This is a controlled use case with:
|
|
120
|
-
// 1. Sanitization check (isDangerous) performed by caller
|
|
121
|
-
// 2. Strict mode enabled ("use strict")
|
|
122
|
-
// 3. Limited scope (only varNames variables available)
|
|
123
|
-
// 4. No access to global objects (process, window, etc.)
|
|
124
|
-
return new Function(...varNames, `"use strict"; return (${expression});`) as CompiledExpression;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Evict the least frequently used expression from cache
|
|
129
|
-
*/
|
|
130
|
-
private evictLFU(): void {
|
|
131
|
-
let oldestKey: string | null = null;
|
|
132
|
-
let oldestTime = Infinity;
|
|
133
|
-
let lowestHits = Infinity;
|
|
134
|
-
|
|
135
|
-
// Find the entry with lowest hit count, or oldest if tied
|
|
136
|
-
for (const [key, metadata] of this.cache.entries()) {
|
|
137
|
-
if (metadata.hitCount < lowestHits ||
|
|
138
|
-
(metadata.hitCount === lowestHits && metadata.compiledAt < oldestTime)) {
|
|
139
|
-
oldestKey = key;
|
|
140
|
-
oldestTime = metadata.compiledAt;
|
|
141
|
-
lowestHits = metadata.hitCount;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (oldestKey) {
|
|
146
|
-
this.cache.delete(oldestKey);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Check if an expression is cached
|
|
152
|
-
*/
|
|
153
|
-
has(expr: string, varNames: string[]): boolean {
|
|
154
|
-
const cacheKey = `${expr}::${varNames.join(',')}`;
|
|
155
|
-
return this.cache.has(cacheKey);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Clear the cache
|
|
160
|
-
*/
|
|
161
|
-
clear(): void {
|
|
162
|
-
this.cache.clear();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Get cache statistics
|
|
167
|
-
*/
|
|
168
|
-
getStats(): {
|
|
169
|
-
size: number;
|
|
170
|
-
maxSize: number;
|
|
171
|
-
totalHits: number;
|
|
172
|
-
entries: Array<{ expression: string; hitCount: number }>;
|
|
173
|
-
} {
|
|
174
|
-
let totalHits = 0;
|
|
175
|
-
const entries: Array<{ expression: string; hitCount: number }> = [];
|
|
176
|
-
|
|
177
|
-
for (const metadata of this.cache.values()) {
|
|
178
|
-
totalHits += metadata.hitCount;
|
|
179
|
-
entries.push({
|
|
180
|
-
expression: metadata.expression,
|
|
181
|
-
hitCount: metadata.hitCount,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return {
|
|
186
|
-
size: this.cache.size,
|
|
187
|
-
maxSize: this.maxSize,
|
|
188
|
-
totalHits,
|
|
189
|
-
entries: entries.sort((a, b) => b.hitCount - a.hitCount),
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @object-ui/core - Expression Context
|
|
11
|
-
*
|
|
12
|
-
* Manages variable scope and data context for expression evaluation.
|
|
13
|
-
*
|
|
14
|
-
* @module evaluator
|
|
15
|
-
* @packageDocumentation
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Expression context for variable resolution
|
|
20
|
-
*/
|
|
21
|
-
export class ExpressionContext {
|
|
22
|
-
private scopes: Map<string, any>[] = [];
|
|
23
|
-
|
|
24
|
-
constructor(initialData: Record<string, any> = {}) {
|
|
25
|
-
this.scopes.push(new Map(Object.entries(initialData)));
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Push a new scope onto the context stack
|
|
30
|
-
*/
|
|
31
|
-
pushScope(data: Record<string, any>): void {
|
|
32
|
-
this.scopes.push(new Map(Object.entries(data)));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Pop the current scope from the context stack
|
|
37
|
-
*/
|
|
38
|
-
popScope(): void {
|
|
39
|
-
if (this.scopes.length > 1) {
|
|
40
|
-
this.scopes.pop();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Get a variable value from the context
|
|
46
|
-
* Searches from innermost to outermost scope
|
|
47
|
-
*/
|
|
48
|
-
get(path: string): any {
|
|
49
|
-
// Split path by dots for nested access
|
|
50
|
-
const parts = path.split('.');
|
|
51
|
-
const varName = parts[0];
|
|
52
|
-
|
|
53
|
-
// Search scopes from innermost to outermost
|
|
54
|
-
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
55
|
-
if (this.scopes[i].has(varName)) {
|
|
56
|
-
let value = this.scopes[i].get(varName);
|
|
57
|
-
|
|
58
|
-
// Navigate nested path
|
|
59
|
-
for (let j = 1; j < parts.length; j++) {
|
|
60
|
-
if (value && typeof value === 'object') {
|
|
61
|
-
value = value[parts[j]];
|
|
62
|
-
} else {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return value;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Set a variable value in the current scope
|
|
76
|
-
*/
|
|
77
|
-
set(name: string, value: any): void {
|
|
78
|
-
if (this.scopes.length > 0) {
|
|
79
|
-
this.scopes[this.scopes.length - 1].set(name, value);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Check if a variable exists in any scope
|
|
85
|
-
*/
|
|
86
|
-
has(name: string): boolean {
|
|
87
|
-
const varName = name.split('.')[0];
|
|
88
|
-
for (let i = this.scopes.length - 1; i >= 0; i--) {
|
|
89
|
-
if (this.scopes[i].has(varName)) {
|
|
90
|
-
return true;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return false;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get all variables from all scopes as a flat object
|
|
98
|
-
*/
|
|
99
|
-
toObject(): Record<string, any> {
|
|
100
|
-
const result: Record<string, any> = {};
|
|
101
|
-
// Merge from outermost to innermost (later scopes override earlier ones)
|
|
102
|
-
for (const scope of this.scopes) {
|
|
103
|
-
for (const [key, value] of scope.entries()) {
|
|
104
|
-
result[key] = value;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Create a child context with additional data
|
|
112
|
-
*/
|
|
113
|
-
createChild(data: Record<string, any> = {}): ExpressionContext {
|
|
114
|
-
const child = new ExpressionContext();
|
|
115
|
-
child.scopes = [...this.scopes, new Map(Object.entries(data))];
|
|
116
|
-
return child;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* @object-ui/core - Expression Evaluator
|
|
11
|
-
*
|
|
12
|
-
* Evaluates template string expressions like ${data.amount > 1000} for dynamic UI behavior.
|
|
13
|
-
* Supports variable substitution, comparison operators, and basic JavaScript expressions.
|
|
14
|
-
*
|
|
15
|
-
* @module evaluator
|
|
16
|
-
* @packageDocumentation
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { ExpressionContext } from './ExpressionContext.js';
|
|
20
|
-
import { ExpressionCache } from './ExpressionCache.js';
|
|
21
|
-
import { FormulaFunctions } from './FormulaFunctions.js';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Options for expression evaluation
|
|
25
|
-
*/
|
|
26
|
-
export interface EvaluationOptions {
|
|
27
|
-
/**
|
|
28
|
-
* Default value to return if evaluation fails
|
|
29
|
-
*/
|
|
30
|
-
defaultValue?: any;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Whether to throw errors on evaluation failure
|
|
34
|
-
* @default false
|
|
35
|
-
*/
|
|
36
|
-
throwOnError?: boolean;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Whether to sanitize the expression before evaluation
|
|
40
|
-
* @default true
|
|
41
|
-
*/
|
|
42
|
-
sanitize?: boolean;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Expression evaluator for dynamic UI expressions
|
|
47
|
-
*/
|
|
48
|
-
export class ExpressionEvaluator {
|
|
49
|
-
private context: ExpressionContext;
|
|
50
|
-
private cache: ExpressionCache;
|
|
51
|
-
private formulas: FormulaFunctions;
|
|
52
|
-
|
|
53
|
-
constructor(
|
|
54
|
-
context?: ExpressionContext | Record<string, any>,
|
|
55
|
-
cache?: ExpressionCache,
|
|
56
|
-
formulas?: FormulaFunctions,
|
|
57
|
-
) {
|
|
58
|
-
if (context instanceof ExpressionContext) {
|
|
59
|
-
this.context = context;
|
|
60
|
-
} else {
|
|
61
|
-
this.context = new ExpressionContext(context || {});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Use provided cache or create a new one
|
|
65
|
-
this.cache = cache || new ExpressionCache();
|
|
66
|
-
this.formulas = formulas || new FormulaFunctions();
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Evaluate a string that may contain template expressions like ${...}
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```ts
|
|
74
|
-
* const evaluator = new ExpressionEvaluator({ data: { amount: 1500 } });
|
|
75
|
-
* evaluator.evaluate('${data.amount > 1000}'); // Returns: true
|
|
76
|
-
* evaluator.evaluate('Amount is ${data.amount}'); // Returns: "Amount is 1500"
|
|
77
|
-
* ```
|
|
78
|
-
*/
|
|
79
|
-
evaluate(expression: string | boolean | number | null | undefined, options: EvaluationOptions = {}): any {
|
|
80
|
-
// Handle non-string primitives
|
|
81
|
-
if (typeof expression !== 'string') {
|
|
82
|
-
return expression;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const { defaultValue, throwOnError = false, sanitize = true } = options;
|
|
86
|
-
|
|
87
|
-
try {
|
|
88
|
-
// Check if string contains template expressions
|
|
89
|
-
const hasTemplates = expression.includes('${');
|
|
90
|
-
|
|
91
|
-
if (!hasTemplates) {
|
|
92
|
-
// No templates, return as-is
|
|
93
|
-
return expression;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Special case: if the entire string is a single template expression, return the value directly
|
|
97
|
-
const singleTemplateMatch = expression.match(/^\$\{([^}]+)\}$/);
|
|
98
|
-
if (singleTemplateMatch) {
|
|
99
|
-
return this.evaluateExpression(singleTemplateMatch[1].trim(), { sanitize });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Replace all ${...} expressions in a string with multiple parts
|
|
103
|
-
return expression.replace(/\$\{([^}]+)\}/g, (match, expr) => {
|
|
104
|
-
try {
|
|
105
|
-
const result = this.evaluateExpression(expr.trim(), { sanitize });
|
|
106
|
-
return String(result ?? '');
|
|
107
|
-
} catch (error) {
|
|
108
|
-
if (throwOnError) {
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
console.warn(`Expression evaluation failed for: ${expr}`, error);
|
|
112
|
-
return match; // Return original if evaluation fails
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (throwOnError) {
|
|
117
|
-
throw error;
|
|
118
|
-
}
|
|
119
|
-
console.warn(`Failed to evaluate expression: ${expression}`, error);
|
|
120
|
-
return defaultValue ?? expression;
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Evaluate a single expression (without ${} wrapper)
|
|
126
|
-
*
|
|
127
|
-
* @example
|
|
128
|
-
* ```ts
|
|
129
|
-
* evaluator.evaluateExpression('data.amount > 1000'); // Returns: true
|
|
130
|
-
* evaluator.evaluateExpression('data.user.name'); // Returns: "John"
|
|
131
|
-
* ```
|
|
132
|
-
*/
|
|
133
|
-
evaluateExpression(expression: string, options: { sanitize?: boolean } = {}): any {
|
|
134
|
-
const { sanitize = true } = options;
|
|
135
|
-
|
|
136
|
-
if (!expression || expression.trim() === '') {
|
|
137
|
-
return undefined;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Sanitize expression to prevent dangerous code execution
|
|
141
|
-
if (sanitize && this.isDangerous(expression)) {
|
|
142
|
-
throw new Error(`Potentially dangerous expression detected: ${expression}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
try {
|
|
146
|
-
// Create a safe evaluation function
|
|
147
|
-
const contextObj = this.context.toObject();
|
|
148
|
-
|
|
149
|
-
// Inject formula functions into the evaluation context
|
|
150
|
-
const formulaObj = this.formulas.toObject();
|
|
151
|
-
const mergedContext = { ...formulaObj, ...contextObj };
|
|
152
|
-
|
|
153
|
-
// Build safe function with context variables
|
|
154
|
-
const varNames = Object.keys(mergedContext);
|
|
155
|
-
const varValues = Object.values(mergedContext);
|
|
156
|
-
|
|
157
|
-
// Use cached compilation
|
|
158
|
-
const compiled = this.cache.compile(expression, varNames);
|
|
159
|
-
|
|
160
|
-
// Execute with context values
|
|
161
|
-
return compiled.fn(...varValues);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
throw new Error(`Failed to evaluate expression "${expression}": ${(error as Error).message}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Check if expression contains potentially dangerous code
|
|
169
|
-
*/
|
|
170
|
-
private isDangerous(expression: string): boolean {
|
|
171
|
-
const dangerousPatterns = [
|
|
172
|
-
/eval\s*\(/i,
|
|
173
|
-
/Function\s*\(/i,
|
|
174
|
-
/setTimeout\s*\(/i,
|
|
175
|
-
/setInterval\s*\(/i,
|
|
176
|
-
/import\s*\(/i,
|
|
177
|
-
/require\s*\(/i,
|
|
178
|
-
/process\./i,
|
|
179
|
-
/global\./i,
|
|
180
|
-
/window\./i,
|
|
181
|
-
/document\./i,
|
|
182
|
-
/__proto__/i,
|
|
183
|
-
/constructor\s*\(/i,
|
|
184
|
-
/prototype\./i,
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
return dangerousPatterns.some(pattern => pattern.test(expression));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Evaluate a conditional expression and return boolean
|
|
192
|
-
*
|
|
193
|
-
* @example
|
|
194
|
-
* ```ts
|
|
195
|
-
* evaluator.evaluateCondition('${data.age >= 18}'); // Returns: true/false
|
|
196
|
-
* ```
|
|
197
|
-
*/
|
|
198
|
-
evaluateCondition(condition: string | boolean | undefined, options: EvaluationOptions = {}): boolean {
|
|
199
|
-
if (typeof condition === 'boolean') {
|
|
200
|
-
return condition;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!condition) {
|
|
204
|
-
return true; // Default to visible/enabled if no condition
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const result = this.evaluate(condition, options);
|
|
208
|
-
|
|
209
|
-
// Convert result to boolean
|
|
210
|
-
return Boolean(result);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Update the context with new data
|
|
215
|
-
*/
|
|
216
|
-
updateContext(data: Record<string, any>): void {
|
|
217
|
-
Object.entries(data).forEach(([key, value]) => {
|
|
218
|
-
this.context.set(key, value);
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Get the current context
|
|
224
|
-
*/
|
|
225
|
-
getContext(): ExpressionContext {
|
|
226
|
-
return this.context;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Create a new evaluator with additional context data
|
|
231
|
-
*/
|
|
232
|
-
withContext(data: Record<string, any>): ExpressionEvaluator {
|
|
233
|
-
// Share the cache and formulas with the new evaluator for maximum efficiency
|
|
234
|
-
return new ExpressionEvaluator(this.context.createChild(data), this.cache, this.formulas);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Get cache statistics (useful for debugging and optimization)
|
|
239
|
-
*/
|
|
240
|
-
getCacheStats() {
|
|
241
|
-
return this.cache.getStats();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Clear the expression cache
|
|
246
|
-
*/
|
|
247
|
-
clearCache(): void {
|
|
248
|
-
this.cache.clear();
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Get the formula functions registry
|
|
253
|
-
*/
|
|
254
|
-
getFormulas(): FormulaFunctions {
|
|
255
|
-
return this.formulas;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Register a custom formula function
|
|
260
|
-
*/
|
|
261
|
-
registerFunction(name: string, fn: (...args: any[]) => any): void {
|
|
262
|
-
this.formulas.register(name, fn);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Shared global cache and formulas for convenience functions
|
|
268
|
-
*/
|
|
269
|
-
const globalCache = new ExpressionCache();
|
|
270
|
-
const globalFormulas = new FormulaFunctions();
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Convenience function to quickly evaluate an expression
|
|
274
|
-
*/
|
|
275
|
-
export function evaluateExpression(
|
|
276
|
-
expression: string | boolean | number | null | undefined,
|
|
277
|
-
context: Record<string, any> = {},
|
|
278
|
-
options: EvaluationOptions = {}
|
|
279
|
-
): any {
|
|
280
|
-
const evaluator = new ExpressionEvaluator(context, globalCache, globalFormulas);
|
|
281
|
-
return evaluator.evaluate(expression, options);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Convenience function to evaluate a condition
|
|
286
|
-
*/
|
|
287
|
-
export function evaluateCondition(
|
|
288
|
-
condition: string | boolean | undefined,
|
|
289
|
-
context: Record<string, any> = {}
|
|
290
|
-
): boolean {
|
|
291
|
-
const evaluator = new ExpressionEvaluator(context, globalCache, globalFormulas);
|
|
292
|
-
return evaluator.evaluateCondition(condition);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Convenience function to evaluate a plain condition string against a data record.
|
|
297
|
-
* Supports both template expressions (e.g., '${data.amount > 1000}') and
|
|
298
|
-
* plain expressions (e.g., "status == 'overdue'").
|
|
299
|
-
* Record fields are available both directly (status) and namespaced (data.status).
|
|
300
|
-
*/
|
|
301
|
-
export function evaluatePlainCondition(
|
|
302
|
-
condition: string,
|
|
303
|
-
record: Record<string, any>
|
|
304
|
-
): boolean {
|
|
305
|
-
const evaluator = new ExpressionEvaluator({ ...record, data: record }, globalCache, globalFormulas);
|
|
306
|
-
try {
|
|
307
|
-
const isTemplate = /\$\{/.test(condition);
|
|
308
|
-
const result = isTemplate
|
|
309
|
-
? evaluator.evaluate(condition, { throwOnError: true })
|
|
310
|
-
: evaluator.evaluateExpression(condition);
|
|
311
|
-
return result === true;
|
|
312
|
-
} catch {
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
}
|