@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +18 -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.d.ts +5 -1
  6. package/dist/adapters/ValueDataSource.js +30 -1
  7. package/dist/errors/index.js +2 -3
  8. package/dist/evaluator/ExpressionCache.d.ts +9 -10
  9. package/dist/evaluator/ExpressionCache.js +29 -8
  10. package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
  11. package/dist/evaluator/SafeExpressionParser.js +851 -0
  12. package/dist/evaluator/index.d.ts +1 -0
  13. package/dist/evaluator/index.js +1 -0
  14. package/dist/protocols/DndProtocol.js +2 -14
  15. package/dist/protocols/KeyboardProtocol.js +1 -4
  16. package/dist/protocols/NotificationProtocol.js +3 -13
  17. package/dist/utils/debug.js +2 -1
  18. package/dist/utils/filter-converter.js +25 -5
  19. package/package.json +33 -9
  20. package/.turbo/turbo-build.log +0 -4
  21. package/src/__benchmarks__/core.bench.ts +0 -64
  22. package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
  23. package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
  24. package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
  25. package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
  26. package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
  27. package/src/actions/ActionEngine.ts +0 -268
  28. package/src/actions/ActionRunner.ts +0 -717
  29. package/src/actions/TransactionManager.ts +0 -521
  30. package/src/actions/UndoManager.ts +0 -215
  31. package/src/actions/__tests__/ActionEngine.test.ts +0 -206
  32. package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
  33. package/src/actions/__tests__/ActionRunner.test.ts +0 -711
  34. package/src/actions/__tests__/TransactionManager.test.ts +0 -447
  35. package/src/actions/__tests__/UndoManager.test.ts +0 -320
  36. package/src/actions/index.ts +0 -12
  37. package/src/adapters/ApiDataSource.ts +0 -376
  38. package/src/adapters/README.md +0 -180
  39. package/src/adapters/ValueDataSource.ts +0 -438
  40. package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
  41. package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
  42. package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
  43. package/src/adapters/index.ts +0 -15
  44. package/src/adapters/resolveDataSource.ts +0 -79
  45. package/src/builder/__tests__/schema-builder.test.ts +0 -235
  46. package/src/builder/schema-builder.ts +0 -584
  47. package/src/data-scope/DataScopeManager.ts +0 -269
  48. package/src/data-scope/ViewDataProvider.ts +0 -282
  49. package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
  50. package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
  51. package/src/data-scope/index.ts +0 -24
  52. package/src/errors/__tests__/errors.test.ts +0 -292
  53. package/src/errors/index.ts +0 -270
  54. package/src/evaluator/ExpressionCache.ts +0 -192
  55. package/src/evaluator/ExpressionContext.ts +0 -118
  56. package/src/evaluator/ExpressionEvaluator.ts +0 -315
  57. package/src/evaluator/FormulaFunctions.ts +0 -398
  58. package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
  59. package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
  60. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
  61. package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
  62. package/src/evaluator/index.ts +0 -12
  63. package/src/index.ts +0 -38
  64. package/src/protocols/DndProtocol.ts +0 -184
  65. package/src/protocols/KeyboardProtocol.ts +0 -185
  66. package/src/protocols/NotificationProtocol.ts +0 -159
  67. package/src/protocols/ResponsiveProtocol.ts +0 -210
  68. package/src/protocols/SharingProtocol.ts +0 -185
  69. package/src/protocols/index.ts +0 -13
  70. package/src/query/__tests__/query-ast.test.ts +0 -211
  71. package/src/query/__tests__/window-functions.test.ts +0 -275
  72. package/src/query/index.ts +0 -7
  73. package/src/query/query-ast.ts +0 -341
  74. package/src/registry/PluginScopeImpl.ts +0 -259
  75. package/src/registry/PluginSystem.ts +0 -206
  76. package/src/registry/Registry.ts +0 -219
  77. package/src/registry/WidgetRegistry.ts +0 -316
  78. package/src/registry/__tests__/PluginSystem.test.ts +0 -309
  79. package/src/registry/__tests__/Registry.test.ts +0 -293
  80. package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
  81. package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
  82. package/src/theme/ThemeEngine.ts +0 -530
  83. package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
  84. package/src/theme/index.ts +0 -24
  85. package/src/types/index.ts +0 -21
  86. package/src/utils/__tests__/debug-collector.test.ts +0 -102
  87. package/src/utils/__tests__/debug.test.ts +0 -134
  88. package/src/utils/__tests__/expand-fields.test.ts +0 -120
  89. package/src/utils/__tests__/extract-records.test.ts +0 -50
  90. package/src/utils/__tests__/filter-converter.test.ts +0 -118
  91. package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
  92. package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
  93. package/src/utils/debug-collector.ts +0 -100
  94. package/src/utils/debug.ts +0 -147
  95. package/src/utils/expand-fields.ts +0 -76
  96. package/src/utils/extract-records.ts +0 -33
  97. package/src/utils/filter-converter.ts +0 -133
  98. package/src/utils/merge-views-into-objects.ts +0 -36
  99. package/src/utils/normalize-quick-filter.ts +0 -78
  100. package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
  101. package/src/validation/__tests__/schema-validator.test.ts +0 -118
  102. package/src/validation/__tests__/validation-engine.test.ts +0 -102
  103. package/src/validation/index.ts +0 -10
  104. package/src/validation/schema-validator.ts +0 -344
  105. package/src/validation/validation-engine.ts +0 -528
  106. package/src/validation/validators/index.ts +0 -25
  107. package/src/validation/validators/object-validation-engine.ts +0 -722
  108. package/tsconfig.json +0 -15
  109. package/tsconfig.tsbuildinfo +0 -1
  110. package/vitest.config.ts +0 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @object-ui/core
2
2
 
3
+ ## 3.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@3.3.1
8
+
9
+ ## 3.3.0
10
+
11
+ ### Patch Changes
12
+
13
+ - @object-ui/types@3.3.0
14
+
15
+ ## 3.2.0
16
+
17
+ ### Patch Changes
18
+
19
+ - @object-ui/types@3.2.0
20
+
3
21
  ## 3.1.5
4
22
 
5
23
  ### Patch Changes
package/README.md CHANGED
@@ -79,6 +79,25 @@ This allows the core types and logic to be used in:
79
79
 
80
80
  See [full documentation](https://objectui.org/api/core) for detailed API reference.
81
81
 
82
+ <!-- release-metadata:v3.3.0 -->
83
+
84
+ ## Compatibility
85
+
86
+ - **Node.js:** ≥ 18
87
+ - **TypeScript:** ≥ 5.0 (strict mode)
88
+ - **`@objectstack/spec`:** ^3.3.0
89
+ - **`@objectstack/client`:** ^3.3.0
90
+ - **Tailwind CSS:** ≥ 3.4 (for packages with UI)
91
+
92
+ ## Links
93
+
94
+ - 📚 [Documentation](https://www.objectui.org/docs/core)
95
+ - 📦 [npm package](https://www.npmjs.com/package/@object-ui/core)
96
+ - 📝 [Changelog](./CHANGELOG.md)
97
+ - 🐛 [Report an issue](https://github.com/objectstack-ai/objectui/issues)
98
+ - 🤝 [Contributing Guide](https://github.com/objectstack-ai/objectui/blob/main/CONTRIBUTING.md)
99
+ - 🗺️ [Roadmap](https://github.com/objectstack-ai/objectui/blob/main/ROADMAP.md)
100
+
82
101
  ## License
83
102
 
84
- MIT
103
+ MIT — see [LICENSE](./LICENSE).
@@ -178,6 +178,7 @@ export interface ActionParamDef {
178
178
  }
179
179
  export declare class ActionRunner {
180
180
  private handlers;
181
+ private scripts;
181
182
  private evaluator;
182
183
  private context;
183
184
  private confirmHandler;
@@ -209,6 +210,14 @@ export declare class ActionRunner {
209
210
  setParamCollectionHandler(handler: ParamCollectionHandler): void;
210
211
  registerHandler(actionName: string, handler: ActionHandler): void;
211
212
  unregisterHandler(actionName: string): void;
213
+ /**
214
+ * Register a named script handler. When a `script` action's
215
+ * `target`/`execute` matches the registered name, the handler runs
216
+ * instead of the expression evaluator. Lets dashboards/views wire
217
+ * symbolic action names (e.g. 'export_dashboard_pdf') to JS callbacks.
218
+ */
219
+ registerScript(scriptName: string, handler: ActionHandler): void;
220
+ unregisterScript(scriptName: string): void;
212
221
  execute(action: ActionDef): Promise<ActionResult>;
213
222
  /**
214
223
  * Execute multiple actions in sequence or parallel.
@@ -22,6 +22,12 @@ export class ActionRunner {
22
22
  writable: true,
23
23
  value: new Map()
24
24
  });
25
+ Object.defineProperty(this, "scripts", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: new Map()
30
+ });
25
31
  Object.defineProperty(this, "evaluator", {
26
32
  enumerable: true,
27
33
  configurable: true,
@@ -110,6 +116,18 @@ export class ActionRunner {
110
116
  unregisterHandler(actionName) {
111
117
  this.handlers.delete(actionName);
112
118
  }
119
+ /**
120
+ * Register a named script handler. When a `script` action's
121
+ * `target`/`execute` matches the registered name, the handler runs
122
+ * instead of the expression evaluator. Lets dashboards/views wire
123
+ * symbolic action names (e.g. 'export_dashboard_pdf') to JS callbacks.
124
+ */
125
+ registerScript(scriptName, handler) {
126
+ this.scripts.set(scriptName, handler);
127
+ }
128
+ unregisterScript(scriptName) {
129
+ this.scripts.delete(scriptName);
130
+ }
113
131
  async execute(action) {
114
132
  try {
115
133
  // Resolve the action type
@@ -143,14 +161,21 @@ export class ActionRunner {
143
161
  }
144
162
  // Param collection: if the action defines ActionParam[] to collect,
145
163
  // show a dialog to gather user input before executing.
146
- if (action.actionParams && Array.isArray(action.actionParams) && action.actionParams.length > 0) {
164
+ // Spec defines this as `params: ActionParam[]`; ActionRunner historically
165
+ // used `actionParams` to disambiguate from the static-params object that
166
+ // some custom handlers consume. Accept both — when `params` is an array,
167
+ // treat it as the input-collection definition.
168
+ const paramDefs = action.actionParams && Array.isArray(action.actionParams) ? action.actionParams
169
+ : (Array.isArray(action.params) ? action.params : undefined);
170
+ if (paramDefs && paramDefs.length > 0) {
147
171
  if (this.paramCollectionHandler) {
148
- const collected = await this.paramCollectionHandler(action.actionParams);
172
+ const collected = await this.paramCollectionHandler(paramDefs);
149
173
  if (collected === null) {
150
174
  return { success: false, error: 'Action cancelled by user (params)' };
151
175
  }
152
- // Merge collected params into action.params
153
- action.params = { ...(action.params || {}), ...collected };
176
+ // Merge collected params into action.params as a values map for downstream consumers.
177
+ // (Replace the array form with a values object once collected.)
178
+ action.params = { ...(Array.isArray(action.params) ? {} : (action.params || {})), ...collected };
154
179
  }
155
180
  }
156
181
  // Check for a registered custom handler first
@@ -293,6 +318,18 @@ export class ActionRunner {
293
318
  if (!script) {
294
319
  return { success: false, error: 'No script provided for script action' };
295
320
  }
321
+ // Named script registry wins over the expression evaluator. This lets
322
+ // dashboards/views bind a symbolic action name (e.g. 'export_dashboard_pdf')
323
+ // to a JS callback without piping the literal through ExpressionEvaluator.
324
+ const named = this.scripts.get(script);
325
+ if (named) {
326
+ try {
327
+ return await named(action, this.context);
328
+ }
329
+ catch (error) {
330
+ return { success: false, error: `Script execution failed: ${error.message}` };
331
+ }
332
+ }
296
333
  try {
297
334
  const result = this.evaluator.evaluate(`\${${script}}`);
298
335
  return { success: true, data: result };
@@ -8,7 +8,7 @@
8
8
  * A DataSource adapter for the `provider: 'value'` ViewData mode.
9
9
  * Operates entirely on an in-memory array — no network requests.
10
10
  */
11
- import type { DataSource, QueryParams, QueryResult, AggregateParams, AggregateResult } from '@object-ui/types';
11
+ import type { DataSource, MutationEvent, QueryParams, QueryResult, AggregateParams, AggregateResult } from '@object-ui/types';
12
12
  export interface ValueDataSourceConfig<T = any> {
13
13
  /** The static data array */
14
14
  items: T[];
@@ -38,7 +38,10 @@ export interface ValueDataSourceConfig<T = any> {
38
38
  export declare class ValueDataSource<T = any> implements DataSource<T> {
39
39
  private items;
40
40
  private idField;
41
+ private mutationListeners;
41
42
  constructor(config: ValueDataSourceConfig<T>);
43
+ /** Notify all mutation subscribers */
44
+ private emitMutation;
42
45
  find(_resource: string, params?: QueryParams): Promise<QueryResult<T>>;
43
46
  findOne(_resource: string, id: string | number, params?: QueryParams): Promise<T | null>;
44
47
  create(_resource: string, data: Partial<T>): Promise<T>;
@@ -49,6 +52,7 @@ export declare class ValueDataSource<T = any> implements DataSource<T> {
49
52
  getView(_objectName: string, _viewId: string): Promise<any | null>;
50
53
  getApp(_appId: string): Promise<any | null>;
51
54
  aggregate(_resource: string, params: AggregateParams): Promise<AggregateResult[]>;
55
+ onMutation(callback: (event: MutationEvent<T>) => void): () => void;
52
56
  /** Get the current number of items */
53
57
  get count(): number;
54
58
  /** Get a snapshot of all items (cloned) */
@@ -53,7 +53,9 @@ function matchesASTFilter(record, filterNode) {
53
53
  case 'in':
54
54
  return Array.isArray(target) && target.includes(value);
55
55
  case 'not in':
56
- case 'notin': // alias used by convertFiltersToAST
56
+ case 'not_in':
57
+ case 'nin': // canonical (per spec)
58
+ case 'notin': // legacy alias
57
59
  return Array.isArray(target) && !target.includes(value);
58
60
  case 'contains': {
59
61
  const lv = typeof value === 'string' ? value.toLowerCase() : '';
@@ -217,10 +219,27 @@ export class ValueDataSource {
217
219
  writable: true,
218
220
  value: void 0
219
221
  });
222
+ Object.defineProperty(this, "mutationListeners", {
223
+ enumerable: true,
224
+ configurable: true,
225
+ writable: true,
226
+ value: new Set()
227
+ });
220
228
  // Deep clone to prevent external mutation
221
229
  this.items = JSON.parse(JSON.stringify(config.items));
222
230
  this.idField = config.idField;
223
231
  }
232
+ /** Notify all mutation subscribers */
233
+ emitMutation(event) {
234
+ for (const listener of this.mutationListeners) {
235
+ try {
236
+ listener(event);
237
+ }
238
+ catch (err) {
239
+ console.warn('ValueDataSource: mutation listener error', err);
240
+ }
241
+ }
242
+ }
224
243
  // -----------------------------------------------------------------------
225
244
  // DataSource interface
226
245
  // -----------------------------------------------------------------------
@@ -277,6 +296,7 @@ export class ValueDataSource {
277
296
  record[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
278
297
  }
279
298
  this.items.push(record);
299
+ this.emitMutation({ type: 'create', resource: _resource, record: { ...record } });
280
300
  return { ...record };
281
301
  }
282
302
  async update(_resource, id, data) {
@@ -285,6 +305,7 @@ export class ValueDataSource {
285
305
  throw new Error(`ValueDataSource: Record with id "${id}" not found`);
286
306
  }
287
307
  this.items[index] = { ...this.items[index], ...data };
308
+ this.emitMutation({ type: 'update', resource: _resource, id, record: { ...this.items[index] } });
288
309
  return { ...this.items[index] };
289
310
  }
290
311
  async delete(_resource, id) {
@@ -292,6 +313,7 @@ export class ValueDataSource {
292
313
  if (index === -1)
293
314
  return false;
294
315
  this.items.splice(index, 1);
316
+ this.emitMutation({ type: 'delete', resource: _resource, id });
295
317
  return true;
296
318
  }
297
319
  async bulk(_resource, operation, data) {
@@ -370,6 +392,13 @@ export class ValueDataSource {
370
392
  });
371
393
  }
372
394
  // -----------------------------------------------------------------------
395
+ // Mutation subscription (P2 — Event Bus)
396
+ // -----------------------------------------------------------------------
397
+ onMutation(callback) {
398
+ this.mutationListeners.add(callback);
399
+ return () => { this.mutationListeners.delete(callback); };
400
+ }
401
+ // -----------------------------------------------------------------------
373
402
  // Extra utilities
374
403
  // -----------------------------------------------------------------------
375
404
  /** Get the current number of items */
@@ -170,7 +170,7 @@ export class FieldValidationError extends ObjectUIError {
170
170
  */
171
171
  function interpolate(template, params) {
172
172
  return template.replace(/\$\{(\w+)\}/g, (_match, key) => {
173
- if (!(key in params) && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
173
+ if (!(key in params) && globalThis.process?.env?.NODE_ENV !== 'production') {
174
174
  console.warn(`[ObjectUI] Missing interpolation parameter "${key}" in error message template.`);
175
175
  }
176
176
  return params[key] ?? `\${${key}}`;
@@ -212,8 +212,7 @@ export function createError(code, params = {}, details) {
212
212
  * @param error - The `ObjectUIError` to format.
213
213
  * @param isDev - When `true`, appends the suggestion and documentation link.
214
214
  */
215
- export function formatErrorMessage(error, isDev = typeof process !== 'undefined' &&
216
- process.env?.NODE_ENV !== 'production') {
215
+ export function formatErrorMessage(error, isDev = globalThis.process?.env?.NODE_ENV !== 'production') {
217
216
  const entry = ERROR_CODES[error.code];
218
217
  let formatted = `[${error.code}] ${error.message}`;
219
218
  if (isDev && entry) {
@@ -5,15 +5,6 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- /**
9
- * @object-ui/core - Expression Cache
10
- *
11
- * Caches compiled expressions to avoid re-parsing on every render.
12
- * Provides significant performance improvement for frequently evaluated expressions.
13
- *
14
- * @module evaluator
15
- * @packageDocumentation
16
- */
17
8
  /**
18
9
  * A compiled expression function that can be executed with context values
19
10
  */
@@ -71,7 +62,15 @@ export declare class ExpressionCache {
71
62
  */
72
63
  compile(expr: string, varNames: string[]): ExpressionMetadata;
73
64
  /**
74
- * Compile an expression into a function
65
+ * Compile an expression into a CSP-safe callable function.
66
+ *
67
+ * Uses `SafeExpressionParser` — a recursive-descent interpreter — instead of
68
+ * `new Function()` so that the expression engine works under strict
69
+ * Content Security Policy headers that forbid `'unsafe-eval'`.
70
+ *
71
+ * A single parser instance is created per compiled expression and reused
72
+ * across all invocations of the returned closure (`evaluate()` resets all
73
+ * internal state on every call), avoiding repeated allocations on hot paths.
75
74
  */
76
75
  private compileExpression;
77
76
  /**
@@ -5,6 +5,16 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ /**
9
+ * @object-ui/core - Expression Cache
10
+ *
11
+ * Caches compiled expressions to avoid re-parsing on every render.
12
+ * Provides significant performance improvement for frequently evaluated expressions.
13
+ *
14
+ * @module evaluator
15
+ * @packageDocumentation
16
+ */
17
+ import { SafeExpressionParser } from './SafeExpressionParser.js';
8
18
  /**
9
19
  * Cache for compiled expressions
10
20
  *
@@ -68,16 +78,27 @@ export class ExpressionCache {
68
78
  return metadata;
69
79
  }
70
80
  /**
71
- * Compile an expression into a function
81
+ * Compile an expression into a CSP-safe callable function.
82
+ *
83
+ * Uses `SafeExpressionParser` — a recursive-descent interpreter — instead of
84
+ * `new Function()` so that the expression engine works under strict
85
+ * Content Security Policy headers that forbid `'unsafe-eval'`.
86
+ *
87
+ * A single parser instance is created per compiled expression and reused
88
+ * across all invocations of the returned closure (`evaluate()` resets all
89
+ * internal state on every call), avoiding repeated allocations on hot paths.
72
90
  */
73
91
  compileExpression(expression, varNames) {
74
- // SECURITY NOTE: Using Function constructor for expression evaluation.
75
- // This is a controlled use case with:
76
- // 1. Sanitization check (isDangerous) performed by caller
77
- // 2. Strict mode enabled ("use strict")
78
- // 3. Limited scope (only varNames variables available)
79
- // 4. No access to global objects (process, window, etc.)
80
- return new Function(...varNames, `"use strict"; return (${expression});`);
92
+ // One parser per compiled expression reused across hot-path calls.
93
+ const parser = new SafeExpressionParser();
94
+ return (...args) => {
95
+ // Reconstruct the named variable context from positional arguments.
96
+ const context = {};
97
+ for (let i = 0; i < varNames.length; i++) {
98
+ context[varNames[i]] = args[i];
99
+ }
100
+ return parser.evaluate(expression, context);
101
+ };
81
102
  }
82
103
  /**
83
104
  * Evict the least frequently used expression from cache
@@ -0,0 +1,131 @@
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
+ * CSP-safe recursive-descent expression parser.
10
+ *
11
+ * Call `evaluate(expression, context)` to parse and execute an expression
12
+ * string against a data context object without any use of `eval()` or
13
+ * `new Function()`.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const parser = new SafeExpressionParser();
18
+ * parser.evaluate('data.amount > 1000', { data: { amount: 1500 } }); // true
19
+ * parser.evaluate('stage !== "closed_won" && stage !== "closed_lost"', { stage: 'open' }); // true
20
+ * parser.evaluate('items.filter(i => i.active).length', { items: [{active:true},{active:false}] }); // 1
21
+ * ```
22
+ */
23
+ export declare class SafeExpressionParser {
24
+ private source;
25
+ private pos;
26
+ private context;
27
+ /**
28
+ * Evaluation guard.
29
+ *
30
+ * When `false` the parser still advances `this.pos` through the source
31
+ * (maintaining correct position for the caller) but suppresses:
32
+ * - ReferenceErrors from undefined identifiers
33
+ * - actual function / method invocations
34
+ * - constructor calls
35
+ *
36
+ * This implements proper short-circuit semantics for `||`, `&&`, `??`, and
37
+ * the ternary operator without needing a separate AST pass.
38
+ */
39
+ private _evaluating;
40
+ /**
41
+ * Evaluate an expression string against a data context.
42
+ *
43
+ * Safe for use under strict CSP — never uses `eval()` or `new Function()`.
44
+ *
45
+ * @param expression - The expression to evaluate (without `${}` wrapper)
46
+ * @param context - Variables available to the expression
47
+ * @returns The evaluated result
48
+ * @throws {ReferenceError} When an identifier is not found in the context
49
+ * @throws {TypeError} On type mismatches (e.g., calling a non-function)
50
+ * @throws {SyntaxError} On malformed expression syntax
51
+ */
52
+ evaluate(expression: string, context: Record<string, unknown>): unknown;
53
+ private skipWhitespace;
54
+ private peek;
55
+ private consume;
56
+ /**
57
+ * Execute `fn` with `_evaluating` temporarily set to `enabled`.
58
+ * Restores the previous value even if `fn` throws.
59
+ *
60
+ * Used to implement short-circuit evaluation: when a branch should not be
61
+ * executed we call `withEvaluation(false, parseX)` which advances the source
62
+ * position without performing any side-effectful evaluations.
63
+ */
64
+ private withEvaluation;
65
+ /**
66
+ * Guard property accesses against sandbox-escape keys.
67
+ * Throws `TypeError` when the key is in `BLOCKED_PROPS`.
68
+ *
69
+ * Only string keys need checking: all blocked property names are strings,
70
+ * and `BLOCKED_PROPS.has()` with a number or symbol can never match them.
71
+ * Numeric indices (e.g. `arr[0]`) and symbol-keyed properties are therefore
72
+ * safe to access and are intentionally left unchecked.
73
+ */
74
+ private assertSafeProp;
75
+ /** Level 1 — Ternary: `cond ? trueVal : falseVal` (right-associative) */
76
+ private parseTernary;
77
+ /** Level 2 — Nullish coalescing: `a ?? b` */
78
+ private parseNullish;
79
+ /** Level 3 — Logical OR: `a || b` */
80
+ private parseOr;
81
+ /** Level 4 — Logical AND: `a && b` */
82
+ private parseAnd;
83
+ /** Level 5 — Equality and relational comparisons */
84
+ private parseEquality;
85
+ /** Level 6 — Addition / Subtraction */
86
+ private parseAddition;
87
+ /** Level 7 — Multiplication / Division / Modulo */
88
+ private parseMultiplication;
89
+ /** Level 8 — Unary operators: `!`, `-`, `+`, `typeof` */
90
+ private parseUnary;
91
+ /** Level 9 — Member access, method calls, function calls */
92
+ private parseMember;
93
+ /** Level 10 — Primary expressions: literals, identifiers, `(expr)`, `[…]` */
94
+ private parsePrimary;
95
+ private parseArrayLiteral;
96
+ private parseString;
97
+ private parseNumber;
98
+ /**
99
+ * Parse an identifier name (stops at non-word characters).
100
+ * Does NOT consume any trailing whitespace or operators.
101
+ */
102
+ private parseIdentifierName;
103
+ /** Parse an identifier and resolve keywords, `new`, arrows, calls, lookups. */
104
+ private parseIdentifierOrKeyword;
105
+ /**
106
+ * Handle `new ConstructorName(args)` expressions.
107
+ * Only safe constructors (Date, RegExp) are permitted.
108
+ */
109
+ private parseNewExpression;
110
+ /**
111
+ * Parse a single-param arrow function: `param => bodyExpression`
112
+ *
113
+ * The body is captured as a source substring (without evaluating it at
114
+ * parse time), so that the parameter is properly bound when the returned
115
+ * function is later invoked (e.g., inside `.filter()`, `.map()`, etc.).
116
+ */
117
+ private parseArrowFunction;
118
+ /**
119
+ * Scan forward from the current position to find the end of a sub-expression
120
+ * without evaluating it. Stops when a depth-0 `,`, `)`, or `]` is found.
121
+ * Correctly skips over string literals and nested brackets.
122
+ *
123
+ * @returns The index just past the last character of the sub-expression.
124
+ */
125
+ private scanExpressionEnd;
126
+ /**
127
+ * Parse a comma-separated argument list up to (but not including) the
128
+ * closing `)`.
129
+ */
130
+ private parseArgList;
131
+ }