@object-ui/core 3.3.0 → 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.
Files changed (99) hide show
  1. package/CHANGELOG.md +6 -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/utils/filter-converter.js +25 -5
  7. package/package.json +32 -8
  8. package/.turbo/turbo-build.log +0 -4
  9. package/src/__benchmarks__/core.bench.ts +0 -64
  10. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  11. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  12. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  13. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  14. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  15. package/src/actions/ActionEngine.ts +0 -268
  16. package/src/actions/ActionRunner.ts +0 -717
  17. package/src/actions/TransactionManager.ts +0 -521
  18. package/src/actions/UndoManager.ts +0 -215
  19. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  20. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  21. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  22. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  23. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  24. package/src/actions/index.ts +0 -12
  25. package/src/adapters/ApiDataSource.ts +0 -376
  26. package/src/adapters/README.md +0 -180
  27. package/src/adapters/ValueDataSource.ts +0 -459
  28. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  29. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -571
  30. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  31. package/src/adapters/index.ts +0 -15
  32. package/src/adapters/resolveDataSource.ts +0 -79
  33. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  34. package/src/builder/schema-builder.ts +0 -584
  35. package/src/data-scope/DataScopeManager.ts +0 -269
  36. package/src/data-scope/ViewDataProvider.ts +0 -282
  37. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  38. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  39. package/src/data-scope/index.ts +0 -24
  40. package/src/errors/__tests__/errors.test.ts +0 -292
  41. package/src/errors/index.ts +0 -269
  42. package/src/evaluator/ExpressionCache.ts +0 -206
  43. package/src/evaluator/ExpressionContext.ts +0 -118
  44. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  45. package/src/evaluator/FormulaFunctions.ts +0 -398
  46. package/src/evaluator/SafeExpressionParser.ts +0 -893
  47. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  48. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  49. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -558
  50. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  51. package/src/evaluator/index.ts +0 -13
  52. package/src/index.ts +0 -38
  53. package/src/protocols/DndProtocol.ts +0 -168
  54. package/src/protocols/KeyboardProtocol.ts +0 -181
  55. package/src/protocols/NotificationProtocol.ts +0 -150
  56. package/src/protocols/ResponsiveProtocol.ts +0 -210
  57. package/src/protocols/SharingProtocol.ts +0 -185
  58. package/src/protocols/index.ts +0 -13
  59. package/src/query/__tests__/query-ast.test.ts +0 -211
  60. package/src/query/__tests__/window-functions.test.ts +0 -275
  61. package/src/query/index.ts +0 -7
  62. package/src/query/query-ast.ts +0 -341
  63. package/src/registry/PluginScopeImpl.ts +0 -259
  64. package/src/registry/PluginSystem.ts +0 -206
  65. package/src/registry/Registry.ts +0 -219
  66. package/src/registry/WidgetRegistry.ts +0 -316
  67. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  68. package/src/registry/__tests__/Registry.test.ts +0 -293
  69. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  70. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  71. package/src/theme/ThemeEngine.ts +0 -530
  72. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  73. package/src/theme/index.ts +0 -24
  74. package/src/types/index.ts +0 -21
  75. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  76. package/src/utils/__tests__/debug.test.ts +0 -134
  77. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  78. package/src/utils/__tests__/extract-records.test.ts +0 -50
  79. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  80. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  81. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  82. package/src/utils/debug-collector.ts +0 -100
  83. package/src/utils/debug.ts +0 -148
  84. package/src/utils/expand-fields.ts +0 -76
  85. package/src/utils/extract-records.ts +0 -33
  86. package/src/utils/filter-converter.ts +0 -133
  87. package/src/utils/merge-views-into-objects.ts +0 -36
  88. package/src/utils/normalize-quick-filter.ts +0 -78
  89. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  90. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  91. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  92. package/src/validation/index.ts +0 -10
  93. package/src/validation/schema-validator.ts +0 -344
  94. package/src/validation/validation-engine.ts +0 -528
  95. package/src/validation/validators/index.ts +0 -25
  96. package/src/validation/validators/object-validation-engine.ts +0 -722
  97. package/tsconfig.json +0 -15
  98. package/tsconfig.tsbuildinfo +0 -1
  99. 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
- });