@object-ui/core 3.1.5 → 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/dist/adapters/ValueDataSource.d.ts +5 -1
- package/dist/adapters/ValueDataSource.js +27 -0
- package/dist/errors/index.js +2 -3
- package/dist/evaluator/ExpressionCache.d.ts +9 -10
- package/dist/evaluator/ExpressionCache.js +29 -8
- package/dist/evaluator/SafeExpressionParser.d.ts +131 -0
- package/dist/evaluator/SafeExpressionParser.js +851 -0
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/protocols/DndProtocol.js +2 -14
- package/dist/protocols/KeyboardProtocol.js +1 -4
- package/dist/protocols/NotificationProtocol.js +3 -13
- package/dist/utils/debug.js +2 -1
- package/package.json +6 -6
- package/src/__tests__/protocols/DndProtocol.test.ts +4 -4
- package/src/__tests__/protocols/KeyboardProtocol.test.ts +2 -2
- package/src/__tests__/protocols/NotificationProtocol.test.ts +3 -3
- package/src/adapters/ValueDataSource.ts +21 -0
- package/src/adapters/__tests__/ValueDataSource.test.ts +99 -0
- package/src/errors/index.ts +4 -5
- package/src/evaluator/ExpressionCache.ts +24 -10
- package/src/evaluator/SafeExpressionParser.ts +893 -0
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +427 -0
- package/src/evaluator/index.ts +1 -0
- package/src/protocols/DndProtocol.ts +2 -18
- package/src/protocols/KeyboardProtocol.ts +1 -5
- package/src/protocols/NotificationProtocol.ts +3 -12
- package/src/utils/debug.ts +2 -1
- package/tsconfig.tsbuildinfo +1 -1
package/.turbo/turbo-build.log
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -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 */
|
package/dist/errors/index.js
CHANGED
|
@@ -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) &&
|
|
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 =
|
|
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
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|