@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.
- package/CHANGELOG.md +18 -0
- package/README.md +20 -1
- package/dist/actions/ActionRunner.d.ts +9 -0
- package/dist/actions/ActionRunner.js +41 -4
- package/dist/adapters/ValueDataSource.d.ts +5 -1
- package/dist/adapters/ValueDataSource.js +30 -1
- 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/dist/utils/filter-converter.js +25 -5
- package/package.json +33 -9
- package/.turbo/turbo-build.log +0 -4
- package/src/__benchmarks__/core.bench.ts +0 -64
- package/src/__tests__/protocols/DndProtocol.test.ts +0 -186
- package/src/__tests__/protocols/KeyboardProtocol.test.ts +0 -177
- package/src/__tests__/protocols/NotificationProtocol.test.ts +0 -142
- package/src/__tests__/protocols/ResponsiveProtocol.test.ts +0 -176
- package/src/__tests__/protocols/SharingProtocol.test.ts +0 -188
- package/src/actions/ActionEngine.ts +0 -268
- package/src/actions/ActionRunner.ts +0 -717
- package/src/actions/TransactionManager.ts +0 -521
- package/src/actions/UndoManager.ts +0 -215
- package/src/actions/__tests__/ActionEngine.test.ts +0 -206
- package/src/actions/__tests__/ActionRunner.params.test.ts +0 -134
- package/src/actions/__tests__/ActionRunner.test.ts +0 -711
- package/src/actions/__tests__/TransactionManager.test.ts +0 -447
- package/src/actions/__tests__/UndoManager.test.ts +0 -320
- package/src/actions/index.ts +0 -12
- package/src/adapters/ApiDataSource.ts +0 -376
- package/src/adapters/README.md +0 -180
- package/src/adapters/ValueDataSource.ts +0 -438
- package/src/adapters/__tests__/ApiDataSource.test.ts +0 -418
- package/src/adapters/__tests__/ValueDataSource.test.ts +0 -472
- package/src/adapters/__tests__/resolveDataSource.test.ts +0 -144
- package/src/adapters/index.ts +0 -15
- package/src/adapters/resolveDataSource.ts +0 -79
- package/src/builder/__tests__/schema-builder.test.ts +0 -235
- package/src/builder/schema-builder.ts +0 -584
- package/src/data-scope/DataScopeManager.ts +0 -269
- package/src/data-scope/ViewDataProvider.ts +0 -282
- package/src/data-scope/__tests__/DataScopeManager.test.ts +0 -211
- package/src/data-scope/__tests__/ViewDataProvider.test.ts +0 -270
- package/src/data-scope/index.ts +0 -24
- package/src/errors/__tests__/errors.test.ts +0 -292
- package/src/errors/index.ts +0 -270
- package/src/evaluator/ExpressionCache.ts +0 -192
- package/src/evaluator/ExpressionContext.ts +0 -118
- package/src/evaluator/ExpressionEvaluator.ts +0 -315
- package/src/evaluator/FormulaFunctions.ts +0 -398
- package/src/evaluator/__tests__/ExpressionCache.test.ts +0 -135
- package/src/evaluator/__tests__/ExpressionContext.test.ts +0 -110
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +0 -131
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +0 -447
- package/src/evaluator/index.ts +0 -12
- package/src/index.ts +0 -38
- package/src/protocols/DndProtocol.ts +0 -184
- package/src/protocols/KeyboardProtocol.ts +0 -185
- package/src/protocols/NotificationProtocol.ts +0 -159
- package/src/protocols/ResponsiveProtocol.ts +0 -210
- package/src/protocols/SharingProtocol.ts +0 -185
- package/src/protocols/index.ts +0 -13
- package/src/query/__tests__/query-ast.test.ts +0 -211
- package/src/query/__tests__/window-functions.test.ts +0 -275
- package/src/query/index.ts +0 -7
- package/src/query/query-ast.ts +0 -341
- package/src/registry/PluginScopeImpl.ts +0 -259
- package/src/registry/PluginSystem.ts +0 -206
- package/src/registry/Registry.ts +0 -219
- package/src/registry/WidgetRegistry.ts +0 -316
- package/src/registry/__tests__/PluginSystem.test.ts +0 -309
- package/src/registry/__tests__/Registry.test.ts +0 -293
- package/src/registry/__tests__/WidgetRegistry.test.ts +0 -321
- package/src/registry/__tests__/plugin-scope-integration.test.ts +0 -283
- package/src/theme/ThemeEngine.ts +0 -530
- package/src/theme/__tests__/ThemeEngine.test.ts +0 -668
- package/src/theme/index.ts +0 -24
- package/src/types/index.ts +0 -21
- package/src/utils/__tests__/debug-collector.test.ts +0 -102
- package/src/utils/__tests__/debug.test.ts +0 -134
- package/src/utils/__tests__/expand-fields.test.ts +0 -120
- package/src/utils/__tests__/extract-records.test.ts +0 -50
- package/src/utils/__tests__/filter-converter.test.ts +0 -118
- package/src/utils/__tests__/merge-views-into-objects.test.ts +0 -110
- package/src/utils/__tests__/normalize-quick-filter.test.ts +0 -123
- package/src/utils/debug-collector.ts +0 -100
- package/src/utils/debug.ts +0 -147
- package/src/utils/expand-fields.ts +0 -76
- package/src/utils/extract-records.ts +0 -33
- package/src/utils/filter-converter.ts +0 -133
- package/src/utils/merge-views-into-objects.ts +0 -36
- package/src/utils/normalize-quick-filter.ts +0 -78
- package/src/validation/__tests__/object-validation-engine.test.ts +0 -567
- package/src/validation/__tests__/schema-validator.test.ts +0 -118
- package/src/validation/__tests__/validation-engine.test.ts +0 -102
- package/src/validation/index.ts +0 -10
- package/src/validation/schema-validator.ts +0 -344
- package/src/validation/validation-engine.ts +0 -528
- package/src/validation/validators/index.ts +0 -25
- package/src/validation/validators/object-validation-engine.ts +0 -722
- package/tsconfig.json +0 -15
- package/tsconfig.tsbuildinfo +0 -1
- 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
|
-
|
|
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(
|
|
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
|
-
|
|
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 '
|
|
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 */
|
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
|
+
}
|