@object-ui/core 3.3.0 → 3.3.2

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +20 -1
  3. package/dist/actions/ActionRunner.d.ts +9 -0
  4. package/dist/actions/ActionRunner.js +41 -4
  5. package/dist/adapters/ValueDataSource.js +3 -1
  6. package/dist/registry/Registry.d.ts +47 -0
  7. package/dist/registry/Registry.js +92 -0
  8. package/dist/utils/filter-converter.js +25 -5
  9. package/package.json +32 -8
  10. package/.turbo/turbo-build.log +0 -4
  11. package/src/__benchmarks__/core.bench.ts +0 -64
  12. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  13. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  14. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  15. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  16. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  17. package/src/actions/ActionEngine.ts +0 -268
  18. package/src/actions/ActionRunner.ts +0 -717
  19. package/src/actions/TransactionManager.ts +0 -521
  20. package/src/actions/UndoManager.ts +0 -215
  21. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  22. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  23. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  24. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  25. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  26. package/src/actions/index.ts +0 -12
  27. package/src/adapters/ApiDataSource.ts +0 -376
  28. package/src/adapters/README.md +0 -180
  29. package/src/adapters/ValueDataSource.ts +0 -459
  30. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  31. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -571
  32. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  33. package/src/adapters/index.ts +0 -15
  34. package/src/adapters/resolveDataSource.ts +0 -79
  35. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  36. package/src/builder/schema-builder.ts +0 -584
  37. package/src/data-scope/DataScopeManager.ts +0 -269
  38. package/src/data-scope/ViewDataProvider.ts +0 -282
  39. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  40. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  41. package/src/data-scope/index.ts +0 -24
  42. package/src/errors/__tests__/errors.test.ts +0 -292
  43. package/src/errors/index.ts +0 -269
  44. package/src/evaluator/ExpressionCache.ts +0 -206
  45. package/src/evaluator/ExpressionContext.ts +0 -118
  46. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  47. package/src/evaluator/FormulaFunctions.ts +0 -398
  48. package/src/evaluator/SafeExpressionParser.ts +0 -893
  49. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  50. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  51. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -558
  52. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  53. package/src/evaluator/index.ts +0 -13
  54. package/src/index.ts +0 -38
  55. package/src/protocols/DndProtocol.ts +0 -168
  56. package/src/protocols/KeyboardProtocol.ts +0 -181
  57. package/src/protocols/NotificationProtocol.ts +0 -150
  58. package/src/protocols/ResponsiveProtocol.ts +0 -210
  59. package/src/protocols/SharingProtocol.ts +0 -185
  60. package/src/protocols/index.ts +0 -13
  61. package/src/query/__tests__/query-ast.test.ts +0 -211
  62. package/src/query/__tests__/window-functions.test.ts +0 -275
  63. package/src/query/index.ts +0 -7
  64. package/src/query/query-ast.ts +0 -341
  65. package/src/registry/PluginScopeImpl.ts +0 -259
  66. package/src/registry/PluginSystem.ts +0 -206
  67. package/src/registry/Registry.ts +0 -219
  68. package/src/registry/WidgetRegistry.ts +0 -316
  69. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  70. package/src/registry/__tests__/Registry.test.ts +0 -293
  71. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  72. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  73. package/src/theme/ThemeEngine.ts +0 -530
  74. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  75. package/src/theme/index.ts +0 -24
  76. package/src/types/index.ts +0 -21
  77. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  78. package/src/utils/__tests__/debug.test.ts +0 -134
  79. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  80. package/src/utils/__tests__/extract-records.test.ts +0 -50
  81. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  82. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  83. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  84. package/src/utils/debug-collector.ts +0 -100
  85. package/src/utils/debug.ts +0 -148
  86. package/src/utils/expand-fields.ts +0 -76
  87. package/src/utils/extract-records.ts +0 -33
  88. package/src/utils/filter-converter.ts +0 -133
  89. package/src/utils/merge-views-into-objects.ts +0 -36
  90. package/src/utils/normalize-quick-filter.ts +0 -78
  91. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  92. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  93. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  94. package/src/validation/index.ts +0 -10
  95. package/src/validation/schema-validator.ts +0 -344
  96. package/src/validation/validation-engine.ts +0 -528
  97. package/src/validation/validators/index.ts +0 -25
  98. package/src/validation/validators/object-validation-engine.ts +0 -722
  99. package/tsconfig.json +0 -15
  100. package/tsconfig.tsbuildinfo +0 -1
  101. package/vitest.config.ts +0 -2
@@ -1,558 +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
- import { describe, it, expect } from 'vitest';
10
- import { ExpressionEvaluator, evaluateExpression, evaluateCondition, evaluatePlainCondition } from '../ExpressionEvaluator';
11
- import { ExpressionContext } from '../ExpressionContext';
12
- import { SafeExpressionParser } from '../SafeExpressionParser';
13
-
14
- describe('ExpressionContext', () => {
15
- it('should create context with initial data', () => {
16
- const ctx = new ExpressionContext({ name: 'John', age: 30 });
17
- expect(ctx.get('name')).toBe('John');
18
- expect(ctx.get('age')).toBe(30);
19
- });
20
-
21
- it('should support nested property access', () => {
22
- const ctx = new ExpressionContext({
23
- user: {
24
- name: 'John',
25
- profile: {
26
- email: 'john@example.com'
27
- }
28
- }
29
- });
30
-
31
- expect(ctx.get('user.name')).toBe('John');
32
- expect(ctx.get('user.profile.email')).toBe('john@example.com');
33
- });
34
-
35
- it('should support scope stacking', () => {
36
- const ctx = new ExpressionContext({ x: 10 });
37
- expect(ctx.get('x')).toBe(10);
38
-
39
- ctx.pushScope({ x: 20, y: 30 });
40
- expect(ctx.get('x')).toBe(20);
41
- expect(ctx.get('y')).toBe(30);
42
-
43
- ctx.popScope();
44
- expect(ctx.get('x')).toBe(10);
45
- expect(ctx.get('y')).toBeUndefined();
46
- });
47
- });
48
-
49
- describe('ExpressionEvaluator', () => {
50
- describe('evaluate', () => {
51
- it('should evaluate simple template expressions', () => {
52
- const evaluator = new ExpressionEvaluator({ name: 'John' });
53
- expect(evaluator.evaluate('Hello ${name}!')).toBe('Hello John!');
54
- });
55
-
56
- it('should evaluate nested property access', () => {
57
- const evaluator = new ExpressionEvaluator({
58
- data: { amount: 1500 }
59
- });
60
- expect(evaluator.evaluate('Amount: ${data.amount}')).toBe('Amount: 1500');
61
- });
62
-
63
- it('should handle non-string values', () => {
64
- const evaluator = new ExpressionEvaluator({});
65
- expect(evaluator.evaluate(true)).toBe(true);
66
- expect(evaluator.evaluate(42)).toBe(42);
67
- expect(evaluator.evaluate(null)).toBe(null);
68
- });
69
- });
70
-
71
- describe('evaluateExpression', () => {
72
- it('should evaluate comparison operators', () => {
73
- const evaluator = new ExpressionEvaluator({ data: { amount: 1500 } });
74
-
75
- expect(evaluator.evaluateExpression('data.amount > 1000')).toBe(true);
76
- expect(evaluator.evaluateExpression('data.amount < 1000')).toBe(false);
77
- });
78
-
79
- it('should block dangerous expressions', () => {
80
- const evaluator = new ExpressionEvaluator({});
81
-
82
- expect(() => evaluator.evaluateExpression('eval("malicious")')).toThrow();
83
- expect(() => evaluator.evaluateExpression('process.exit()')).toThrow();
84
- });
85
- });
86
-
87
- describe('evaluateCondition', () => {
88
- it('should return boolean for condition expressions', () => {
89
- const evaluator = new ExpressionEvaluator({ data: { age: 25 } });
90
-
91
- expect(evaluator.evaluateCondition('${data.age >= 18}')).toBe(true);
92
- expect(evaluator.evaluateCondition('${data.age < 18}')).toBe(false);
93
- });
94
-
95
- it('should handle boolean values directly', () => {
96
- const evaluator = new ExpressionEvaluator({});
97
-
98
- expect(evaluator.evaluateCondition(true)).toBe(true);
99
- expect(evaluator.evaluateCondition(false)).toBe(false);
100
- });
101
- });
102
- });
103
-
104
- describe('evaluatePlainCondition', () => {
105
- it('should evaluate a plain condition with direct field references', () => {
106
- expect(evaluatePlainCondition("status == 'overdue'", { status: 'overdue' })).toBe(true);
107
- expect(evaluatePlainCondition("status == 'overdue'", { status: 'active' })).toBe(false);
108
- });
109
-
110
- it('should evaluate numeric comparisons', () => {
111
- expect(evaluatePlainCondition('amount > 1000', { amount: 2500 })).toBe(true);
112
- expect(evaluatePlainCondition('amount > 1000', { amount: 500 })).toBe(false);
113
- });
114
-
115
- it('should evaluate compound conditions', () => {
116
- expect(evaluatePlainCondition("amount > 1000 && status === 'urgent'", { amount: 2000, status: 'urgent' })).toBe(true);
117
- expect(evaluatePlainCondition("amount > 1000 && status === 'urgent'", { amount: 2000, status: 'normal' })).toBe(false);
118
- });
119
-
120
- it('should support data.field references in template expressions', () => {
121
- expect(evaluatePlainCondition('${data.amount > 1000}', { amount: 2000 })).toBe(true);
122
- expect(evaluatePlainCondition('${data.amount > 1000}', { amount: 500 })).toBe(false);
123
- });
124
-
125
- it('should return false for invalid expressions', () => {
126
- expect(evaluatePlainCondition('!!!invalidSyntax', { status: 'ok' })).toBe(false);
127
- });
128
-
129
- it('should return false for non-boolean results', () => {
130
- expect(evaluatePlainCondition('status', { status: 'active' })).toBe(false);
131
- });
132
- });
133
-
134
- // ─── CSP Safety Tests ──────────────────────────────────────────────────────
135
- //
136
- // These tests verify that expression evaluation does NOT rely on eval() or
137
- // new Function() and therefore works under strict Content Security Policy
138
- // headers that forbid 'unsafe-eval'.
139
- //
140
- // The SafeExpressionParser is the CSP-safe backend used by ExpressionCache.
141
- // All tests below exercise it directly as well as through ExpressionEvaluator.
142
-
143
- describe('SafeExpressionParser — CSP-safe evaluation', () => {
144
- const parser = new SafeExpressionParser();
145
-
146
- describe('does not use eval() or new Function()', () => {
147
- it('evaluates comparison operators without dynamic code execution', () => {
148
- // This is the exact expression from the bug report.
149
- expect(
150
- parser.evaluate(
151
- "stage !== 'closed_won' && stage !== 'closed_lost'",
152
- { stage: 'open' }
153
- )
154
- ).toBe(true);
155
-
156
- expect(
157
- parser.evaluate(
158
- "stage !== 'closed_won' && stage !== 'closed_lost'",
159
- { stage: 'closed_won' }
160
- )
161
- ).toBe(false);
162
-
163
- expect(
164
- parser.evaluate(
165
- "stage !== 'closed_won' && stage !== 'closed_lost'",
166
- { stage: 'closed_lost' }
167
- )
168
- ).toBe(false);
169
- });
170
-
171
- it('evaluates strict equality operators (===, !==)', () => {
172
- expect(parser.evaluate("status === 'active'", { status: 'active' })).toBe(true);
173
- expect(parser.evaluate("status === 'active'", { status: 'inactive' })).toBe(false);
174
- expect(parser.evaluate("status !== 'active'", { status: 'inactive' })).toBe(true);
175
- });
176
-
177
- it('evaluates loose equality operators (==, !=)', () => {
178
- expect(parser.evaluate('count == 0', { count: 0 })).toBe(true);
179
- expect(parser.evaluate('count != 0', { count: 1 })).toBe(true);
180
- });
181
-
182
- it('evaluates relational operators (>, <, >=, <=)', () => {
183
- expect(parser.evaluate('score >= 90', { score: 95 })).toBe(true);
184
- expect(parser.evaluate('score >= 90', { score: 85 })).toBe(false);
185
- expect(parser.evaluate('score <= 90', { score: 85 })).toBe(true);
186
- expect(parser.evaluate('score > 50 && score < 100', { score: 75 })).toBe(true);
187
- });
188
-
189
- it('evaluates logical operators (&&, ||, !)', () => {
190
- expect(parser.evaluate('isAdmin && isActive', { isAdmin: true, isActive: true })).toBe(true);
191
- expect(parser.evaluate('isAdmin && isActive', { isAdmin: true, isActive: false })).toBe(false);
192
- expect(parser.evaluate('isAdmin || isGuest', { isAdmin: false, isGuest: true })).toBe(true);
193
- expect(parser.evaluate('!isSuspended', { isSuspended: false })).toBe(true);
194
- expect(parser.evaluate('!isSuspended', { isSuspended: true })).toBe(false);
195
- });
196
-
197
- it('evaluates ternary expressions', () => {
198
- expect(parser.evaluate("score >= 90 ? 'A' : 'B'", { score: 95 })).toBe('A');
199
- expect(parser.evaluate("score >= 90 ? 'A' : 'B'", { score: 75 })).toBe('B');
200
- });
201
-
202
- it('evaluates nested ternary expressions', () => {
203
- const expr = "status === 'active' ? 'success' : status === 'pending' ? 'warning' : 'default'";
204
- expect(parser.evaluate(expr, { status: 'active' })).toBe('success');
205
- expect(parser.evaluate(expr, { status: 'pending' })).toBe('warning');
206
- expect(parser.evaluate(expr, { status: 'closed' })).toBe('default');
207
- });
208
-
209
- it('evaluates arithmetic operators', () => {
210
- expect(parser.evaluate('price * quantity', { price: 10, quantity: 5 })).toBe(50);
211
- expect(parser.evaluate('total - discount', { total: 100, discount: 15 })).toBe(85);
212
- expect(parser.evaluate('total / count', { total: 60, count: 3 })).toBe(20);
213
- expect(parser.evaluate('value % 3', { value: 10 })).toBe(1);
214
- });
215
-
216
- it('evaluates parenthesized expressions', () => {
217
- expect(parser.evaluate('(a + b) * c', { a: 2, b: 3, c: 4 })).toBe(20);
218
- });
219
-
220
- it('evaluates unary operators', () => {
221
- expect(parser.evaluate('-amount', { amount: 5 })).toBe(-5);
222
- expect(parser.evaluate('+amount', { amount: '42' })).toBe(42);
223
- expect(parser.evaluate('!flag', { flag: false })).toBe(true);
224
- });
225
-
226
- it('evaluates typeof operator', () => {
227
- expect(parser.evaluate('typeof name', { name: 'Alice' })).toBe('string');
228
- expect(parser.evaluate('typeof count', { count: 42 })).toBe('number');
229
- });
230
- });
231
-
232
- describe('property and method access', () => {
233
- it('evaluates dot notation property access', () => {
234
- expect(parser.evaluate('user.name', { user: { name: 'Alice' } })).toBe('Alice');
235
- expect(parser.evaluate('user.address.city', {
236
- user: { address: { city: 'London' } }
237
- })).toBe('London');
238
- });
239
-
240
- it('evaluates bracket notation property access', () => {
241
- expect(parser.evaluate("record['status']", { record: { status: 'active' } })).toBe('active');
242
- expect(parser.evaluate('arr[0]', { arr: ['first', 'second'] })).toBe('first');
243
- });
244
-
245
- it('evaluates optional chaining (?.) gracefully', () => {
246
- expect(parser.evaluate('user?.address?.city', { user: null })).toBeUndefined();
247
- expect(parser.evaluate('user?.address?.city', { user: { address: { city: 'NYC' } } })).toBe('NYC');
248
- });
249
-
250
- it('evaluates string method calls', () => {
251
- expect(parser.evaluate('name.toUpperCase()', { name: 'hello' })).toBe('HELLO');
252
- expect(parser.evaluate('name.toLowerCase()', { name: 'WORLD' })).toBe('world');
253
- expect(parser.evaluate('name.trim()', { name: ' hi ' })).toBe('hi');
254
- expect(parser.evaluate("name.includes('ell')", { name: 'hello' })).toBe(true);
255
- });
256
-
257
- it('evaluates array methods with arrow functions', () => {
258
- const items = [
259
- { name: 'apple', price: 1.5, active: true },
260
- { name: 'banana', price: 0.75, active: false },
261
- { name: 'cherry', price: 3.0, active: true },
262
- ];
263
- expect(
264
- parser.evaluate('items.filter(i => i.active).length', { items })
265
- ).toBe(2);
266
- expect(
267
- parser.evaluate('items.map(i => i.name).join(", ")', { items })
268
- ).toBe('apple, banana, cherry');
269
- expect(
270
- parser.evaluate('items.filter(i => i.price > 1).length', { items })
271
- ).toBe(2);
272
- });
273
-
274
- it('evaluates chained array method calls', () => {
275
- const users = [
276
- { name: 'Alice', isActive: true },
277
- { name: 'Bob', isActive: false },
278
- { name: 'Carol', isActive: true },
279
- ];
280
- expect(
281
- parser.evaluate('users.filter(u => u.isActive).map(u => u.name).join(", ")', { users })
282
- ).toBe('Alice, Carol');
283
- });
284
-
285
- it('evaluates array .length property', () => {
286
- expect(parser.evaluate('items.length === 0', { items: [] })).toBe(true);
287
- expect(parser.evaluate('items.length', { items: [1, 2, 3] })).toBe(3);
288
- });
289
-
290
- it('evaluates number method calls', () => {
291
- expect(parser.evaluate('price.toFixed(2)', { price: 3.14159 })).toBe('3.14');
292
- });
293
-
294
- it('evaluates Math global functions', () => {
295
- expect(parser.evaluate('Math.round(value)', { value: 3.7 })).toBe(4);
296
- expect(parser.evaluate('Math.floor(value)', { value: 3.9 })).toBe(3);
297
- expect(parser.evaluate('Math.abs(value)', { value: -5 })).toBe(5);
298
- expect(parser.evaluate('Math.max(a, b)', { a: 10, b: 7 })).toBe(10);
299
- });
300
- });
301
-
302
- describe('literals', () => {
303
- it('evaluates boolean literals', () => {
304
- expect(parser.evaluate('true', {})).toBe(true);
305
- expect(parser.evaluate('false', {})).toBe(false);
306
- });
307
-
308
- it('evaluates null and undefined literals', () => {
309
- expect(parser.evaluate('null', {})).toBeNull();
310
- expect(parser.evaluate('undefined', {})).toBeUndefined();
311
- });
312
-
313
- it('evaluates numeric literals', () => {
314
- expect(parser.evaluate('42', {})).toBe(42);
315
- expect(parser.evaluate('3.14', {})).toBeCloseTo(3.14);
316
- expect(parser.evaluate('1e3', {})).toBe(1000);
317
- });
318
-
319
- it('evaluates string literals with single and double quotes', () => {
320
- expect(parser.evaluate("'hello'", {})).toBe('hello');
321
- expect(parser.evaluate('"world"', {})).toBe('world');
322
- });
323
-
324
- it('evaluates string escape sequences', () => {
325
- expect(parser.evaluate("'line1\\nline2'", {})).toBe('line1\nline2');
326
- expect(parser.evaluate("'tab\\there'", {})).toBe('tab\there');
327
- });
328
-
329
- it('evaluates array literals', () => {
330
- expect(parser.evaluate('[1, 2, 3]', {})).toEqual([1, 2, 3]);
331
- expect(parser.evaluate("['a', 'b']", {})).toEqual(['a', 'b']);
332
- });
333
-
334
- it('evaluates NaN and Infinity literals', () => {
335
- expect(parser.evaluate('Infinity', {})).toBe(Infinity);
336
- expect(Number.isNaN(parser.evaluate('NaN', {}) as number)).toBe(true);
337
- });
338
- });
339
-
340
- describe('nullish coalescing', () => {
341
- it('evaluates ?? operator', () => {
342
- expect(parser.evaluate('value ?? "default"', { value: null })).toBe('default');
343
- expect(parser.evaluate('value ?? "default"', { value: undefined })).toBe('default');
344
- expect(parser.evaluate('value ?? "default"', { value: 0 })).toBe(0);
345
- expect(parser.evaluate('value ?? "default"', { value: '' })).toBe('');
346
- });
347
- });
348
-
349
- describe('short-circuit evaluation', () => {
350
- it('|| does not evaluate RHS when LHS is truthy', () => {
351
- // 'missingVar' is not in context — would throw ReferenceError without short-circuit.
352
- expect(parser.evaluate('true || missingVar', {})).toBe(true);
353
- });
354
-
355
- it('&& does not evaluate RHS when LHS is falsy', () => {
356
- expect(parser.evaluate('false && missingVar', {})).toBe(false);
357
- });
358
-
359
- it('?? does not evaluate RHS when LHS is not nullish', () => {
360
- expect(parser.evaluate('"present" ?? missingVar', {})).toBe('present');
361
- expect(parser.evaluate('0 ?? missingVar', {})).toBe(0);
362
- expect(parser.evaluate('"" ?? missingVar', {})).toBe('');
363
- });
364
-
365
- it('?? DOES evaluate RHS when LHS is null/undefined', () => {
366
- expect(parser.evaluate('null ?? "fallback"', {})).toBe('fallback');
367
- expect(parser.evaluate('undefined ?? "fallback"', {})).toBe('fallback');
368
- });
369
-
370
- it('ternary true branch: does not evaluate false branch', () => {
371
- expect(parser.evaluate("true ? 'yes' : missingVar", {})).toBe('yes');
372
- });
373
-
374
- it('ternary false branch: does not evaluate true branch', () => {
375
- expect(parser.evaluate("false ? missingVar : 'no'", {})).toBe('no');
376
- });
377
-
378
- it('nested ternary short-circuits correctly', () => {
379
- const expr = "status === 'a' ? 'alpha' : status === 'b' ? 'beta' : 'other'";
380
- expect(parser.evaluate(expr, { status: 'a' })).toBe('alpha');
381
- expect(parser.evaluate(expr, { status: 'b' })).toBe('beta');
382
- expect(parser.evaluate(expr, { status: 'c' })).toBe('other');
383
- });
384
- });
385
-
386
- describe('sandbox security', () => {
387
- it('blocks constructor property access via dot notation', () => {
388
- expect(() =>
389
- parser.evaluate('name.constructor', { name: 'hello' })
390
- ).toThrow(TypeError);
391
- });
392
-
393
- it('blocks constructor property access via bracket notation', () => {
394
- expect(() =>
395
- parser.evaluate("name['constructor']", { name: 'hello' })
396
- ).toThrow(TypeError);
397
- });
398
-
399
- it('blocks __proto__ access', () => {
400
- expect(() =>
401
- parser.evaluate('obj.__proto__', { obj: {} })
402
- ).toThrow(TypeError);
403
- });
404
-
405
- it('blocks prototype access', () => {
406
- expect(() =>
407
- parser.evaluate('fn.prototype', { fn: () => {} })
408
- ).toThrow(TypeError);
409
- });
410
-
411
- it('blocks constructor method calls', () => {
412
- expect(() =>
413
- parser.evaluate("name['constructor']('return 1')()", { name: 'hello' })
414
- ).toThrow(TypeError);
415
- });
416
-
417
- it('does not expose String/Number/Boolean/Array globals (removed to prevent .constructor escape)', () => {
418
- expect(() => parser.evaluate('String', {})).toThrow(ReferenceError);
419
- expect(() => parser.evaluate('Number', {})).toThrow(ReferenceError);
420
- expect(() => parser.evaluate('Boolean', {})).toThrow(ReferenceError);
421
- expect(() => parser.evaluate('Array', {})).toThrow(ReferenceError);
422
- });
423
- });
424
-
425
- describe('error handling', () => {
426
- it('throws ReferenceError for undefined identifiers', () => {
427
- expect(() => parser.evaluate('nonExistentVar', {})).toThrow(ReferenceError);
428
- });
429
-
430
- it('returns undefined gracefully for missing property access (no throw)', () => {
431
- expect(parser.evaluate('user.missingProp', { user: {} })).toBeUndefined();
432
- expect(parser.evaluate('user.address.city', { user: {} })).toBeUndefined();
433
- });
434
-
435
- it('throws SyntaxError for unclosed parentheses', () => {
436
- // Use a valid inner expression so the error is about the missing ')' not the content.
437
- expect(() => parser.evaluate('(1 + 2', {})).toThrow(SyntaxError);
438
- });
439
-
440
- it('throws SyntaxError for unclosed array literal', () => {
441
- expect(() => parser.evaluate('[1, 2', {})).toThrow(SyntaxError);
442
- });
443
-
444
- it('throws SyntaxError for malformed numeric exponent (e.g. 1e)', () => {
445
- // '1e' has no exponent digits — the stricter parser rejects it.
446
- expect(() => parser.evaluate('1e', {})).toThrow(SyntaxError);
447
- });
448
- });
449
- });
450
-
451
- describe('ExpressionEvaluator — CSP safety integration', () => {
452
- it('evaluates the bug-report expression without CSP violation', () => {
453
- // Exact expression from the bug report that was blocked by CSP in production.
454
- const evaluator = new ExpressionEvaluator({ stage: 'open' });
455
- expect(
456
- evaluator.evaluate("${stage !== 'closed_won' && stage !== 'closed_lost'}")
457
- ).toBe(true);
458
-
459
- const evaluator2 = new ExpressionEvaluator({ stage: 'closed_won' });
460
- expect(
461
- evaluator2.evaluate("${stage !== 'closed_won' && stage !== 'closed_lost'}")
462
- ).toBe(false);
463
- });
464
-
465
- it('supports all comparison operators via the safe parser', () => {
466
- const e = new ExpressionEvaluator({ a: 10, b: 10, c: 5 });
467
- expect(e.evaluateExpression('a === b')).toBe(true);
468
- expect(e.evaluateExpression('a !== c')).toBe(true);
469
- expect(e.evaluateExpression('a > c')).toBe(true);
470
- expect(e.evaluateExpression('c < a')).toBe(true);
471
- expect(e.evaluateExpression('a >= b')).toBe(true);
472
- expect(e.evaluateExpression('c <= a')).toBe(true);
473
- });
474
-
475
- it('supports logical operators via the safe parser', () => {
476
- const e = new ExpressionEvaluator({ x: true, y: false });
477
- expect(e.evaluateExpression('x && !y')).toBe(true);
478
- expect(e.evaluateExpression('x || y')).toBe(true);
479
- expect(e.evaluateExpression('!x || !y')).toBe(true);
480
- });
481
-
482
- it('supports ternary expressions via the safe parser', () => {
483
- const e = new ExpressionEvaluator({ score: 95 });
484
- expect(e.evaluateExpression("score >= 90 ? 'A' : 'B'")).toBe('A');
485
- });
486
-
487
- it('supports property access via the safe parser', () => {
488
- const e = new ExpressionEvaluator({ user: { role: 'admin', isActive: true } });
489
- expect(e.evaluateExpression("user.role === 'admin'")).toBe(true);
490
- expect(e.evaluateExpression('user.isActive')).toBe(true);
491
- });
492
-
493
- it('supports arithmetic via the safe parser', () => {
494
- const e = new ExpressionEvaluator({ price: 10, qty: 5 });
495
- expect(e.evaluateExpression('price * qty')).toBe(50);
496
- });
497
-
498
- it('supports arrow-function array methods via the safe parser', () => {
499
- const e = new ExpressionEvaluator({
500
- items: [{ price: 50 }, { price: 150 }, { price: 200 }],
501
- });
502
- expect(e.evaluateExpression('items.filter(item => item.price > 100).length')).toBe(2);
503
- });
504
-
505
- it('supports formula functions via the safe parser', () => {
506
- const e = new ExpressionEvaluator({ values: [10, 20, 30] });
507
- expect(e.evaluateExpression('SUM(values)')).toBe(60);
508
- expect(e.evaluateExpression('AVG(values)')).toBe(20);
509
- });
510
-
511
- it('supports Math global via the safe parser', () => {
512
- const e = new ExpressionEvaluator({ value: 3.7 });
513
- expect(e.evaluateExpression('Math.round(value)')).toBe(4);
514
- });
515
-
516
- it('does not use eval() or new Function() during evaluation', () => {
517
- // Spy on both to ensure they are NEVER called (via construct OR apply).
518
- const originalEval = globalThis.eval;
519
- const originalFunction = Function;
520
- const evalCalls: string[] = [];
521
- const functionCalls: string[] = [];
522
-
523
- globalThis.eval = (...args: Parameters<typeof eval>) => {
524
- evalCalls.push(String(args[0]));
525
- return originalEval(...args);
526
- };
527
-
528
- const FunctionProxy = new Proxy(Function, {
529
- construct(target, args) {
530
- functionCalls.push(`new Function(${String(args)})`);
531
- return Reflect.construct(target, args);
532
- },
533
- apply(target, thisArg, args) {
534
- // Catches indirect calls like: Function('return 1')() or String['constructor']('...')
535
- functionCalls.push(`Function(${String(args)})`);
536
- return Reflect.apply(target, thisArg, args);
537
- },
538
- });
539
- (globalThis as any).Function = FunctionProxy;
540
-
541
- try {
542
- const e = new ExpressionEvaluator({
543
- stage: 'open',
544
- data: { amount: 1500 },
545
- items: [{ active: true }, { active: false }],
546
- });
547
- e.evaluate("${stage !== 'closed_won' && stage !== 'closed_lost'}");
548
- e.evaluateExpression('data.amount > 1000');
549
- e.evaluateExpression('items.filter(i => i.active).length');
550
-
551
- expect(evalCalls).toHaveLength(0);
552
- expect(functionCalls).toHaveLength(0);
553
- } finally {
554
- globalThis.eval = originalEval;
555
- (globalThis as any).Function = originalFunction;
556
- }
557
- });
558
- });