@object-ui/core 3.1.4 → 3.3.0

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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @object-ui/core@3.1.4 build /home/runner/work/objectui/objectui/packages/core
2
+ > @object-ui/core@3.3.0 build /home/runner/work/objectui/objectui/packages/core
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @object-ui/core
2
2
 
3
+ ## 3.3.0
4
+
5
+ ### Patch Changes
6
+
7
+ - @object-ui/types@3.3.0
8
+
9
+ ## 3.2.0
10
+
11
+ ### Patch Changes
12
+
13
+ - @object-ui/types@3.2.0
14
+
15
+ ## 3.1.5
16
+
17
+ ### Patch Changes
18
+
19
+ - @object-ui/types@3.1.5
20
+
3
21
  ## 3.1.4
4
22
 
5
23
  ### Patch Changes
@@ -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) */
@@ -217,10 +217,27 @@ export class ValueDataSource {
217
217
  writable: true,
218
218
  value: void 0
219
219
  });
220
+ Object.defineProperty(this, "mutationListeners", {
221
+ enumerable: true,
222
+ configurable: true,
223
+ writable: true,
224
+ value: new Set()
225
+ });
220
226
  // Deep clone to prevent external mutation
221
227
  this.items = JSON.parse(JSON.stringify(config.items));
222
228
  this.idField = config.idField;
223
229
  }
230
+ /** Notify all mutation subscribers */
231
+ emitMutation(event) {
232
+ for (const listener of this.mutationListeners) {
233
+ try {
234
+ listener(event);
235
+ }
236
+ catch (err) {
237
+ console.warn('ValueDataSource: mutation listener error', err);
238
+ }
239
+ }
240
+ }
224
241
  // -----------------------------------------------------------------------
225
242
  // DataSource interface
226
243
  // -----------------------------------------------------------------------
@@ -277,6 +294,7 @@ export class ValueDataSource {
277
294
  record[field] = `auto_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
278
295
  }
279
296
  this.items.push(record);
297
+ this.emitMutation({ type: 'create', resource: _resource, record: { ...record } });
280
298
  return { ...record };
281
299
  }
282
300
  async update(_resource, id, data) {
@@ -285,6 +303,7 @@ export class ValueDataSource {
285
303
  throw new Error(`ValueDataSource: Record with id "${id}" not found`);
286
304
  }
287
305
  this.items[index] = { ...this.items[index], ...data };
306
+ this.emitMutation({ type: 'update', resource: _resource, id, record: { ...this.items[index] } });
288
307
  return { ...this.items[index] };
289
308
  }
290
309
  async delete(_resource, id) {
@@ -292,6 +311,7 @@ export class ValueDataSource {
292
311
  if (index === -1)
293
312
  return false;
294
313
  this.items.splice(index, 1);
314
+ this.emitMutation({ type: 'delete', resource: _resource, id });
295
315
  return true;
296
316
  }
297
317
  async bulk(_resource, operation, data) {
@@ -370,6 +390,13 @@ export class ValueDataSource {
370
390
  });
371
391
  }
372
392
  // -----------------------------------------------------------------------
393
+ // Mutation subscription (P2 — Event Bus)
394
+ // -----------------------------------------------------------------------
395
+ onMutation(callback) {
396
+ this.mutationListeners.add(callback);
397
+ return () => { this.mutationListeners.delete(callback); };
398
+ }
399
+ // -----------------------------------------------------------------------
373
400
  // Extra utilities
374
401
  // -----------------------------------------------------------------------
375
402
  /** 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
+ }