@object-ui/core 0.3.1 → 2.0.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.
Files changed (118) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/dist/actions/ActionRunner.d.ts +228 -4
  4. package/dist/actions/ActionRunner.js +397 -45
  5. package/dist/actions/TransactionManager.d.ts +193 -0
  6. package/dist/actions/TransactionManager.js +410 -0
  7. package/dist/actions/index.d.ts +2 -1
  8. package/dist/actions/index.js +2 -1
  9. package/dist/adapters/ApiDataSource.d.ts +69 -0
  10. package/dist/adapters/ApiDataSource.js +293 -0
  11. package/dist/adapters/ValueDataSource.d.ts +55 -0
  12. package/dist/adapters/ValueDataSource.js +287 -0
  13. package/dist/adapters/index.d.ts +3 -0
  14. package/dist/adapters/index.js +5 -2
  15. package/dist/adapters/resolveDataSource.d.ts +40 -0
  16. package/dist/adapters/resolveDataSource.js +59 -0
  17. package/dist/data-scope/DataScopeManager.d.ts +127 -0
  18. package/dist/data-scope/DataScopeManager.js +229 -0
  19. package/dist/data-scope/index.d.ts +10 -0
  20. package/dist/data-scope/index.js +10 -0
  21. package/dist/evaluator/ExpressionCache.d.ts +101 -0
  22. package/dist/evaluator/ExpressionCache.js +135 -0
  23. package/dist/evaluator/ExpressionEvaluator.d.ts +30 -2
  24. package/dist/evaluator/ExpressionEvaluator.js +60 -16
  25. package/dist/evaluator/FormulaFunctions.d.ts +58 -0
  26. package/dist/evaluator/FormulaFunctions.js +350 -0
  27. package/dist/evaluator/index.d.ts +4 -2
  28. package/dist/evaluator/index.js +4 -2
  29. package/dist/index.d.ts +14 -7
  30. package/dist/index.js +13 -9
  31. package/dist/query/index.d.ts +6 -0
  32. package/dist/query/index.js +6 -0
  33. package/dist/query/query-ast.d.ts +32 -0
  34. package/dist/query/query-ast.js +268 -0
  35. package/dist/registry/PluginScopeImpl.d.ts +80 -0
  36. package/dist/registry/PluginScopeImpl.js +243 -0
  37. package/dist/registry/PluginSystem.d.ts +66 -0
  38. package/dist/registry/PluginSystem.js +142 -0
  39. package/dist/registry/Registry.d.ts +83 -4
  40. package/dist/registry/Registry.js +113 -7
  41. package/dist/registry/WidgetRegistry.d.ts +120 -0
  42. package/dist/registry/WidgetRegistry.js +275 -0
  43. package/dist/theme/ThemeEngine.d.ts +82 -0
  44. package/dist/theme/ThemeEngine.js +400 -0
  45. package/dist/theme/index.d.ts +8 -0
  46. package/dist/theme/index.js +8 -0
  47. package/dist/validation/index.d.ts +9 -0
  48. package/dist/validation/index.js +9 -0
  49. package/dist/validation/validation-engine.d.ts +88 -0
  50. package/dist/validation/validation-engine.js +428 -0
  51. package/dist/validation/validators/index.d.ts +16 -0
  52. package/dist/validation/validators/index.js +16 -0
  53. package/dist/validation/validators/object-validation-engine.d.ts +118 -0
  54. package/dist/validation/validators/object-validation-engine.js +538 -0
  55. package/package.json +14 -5
  56. package/src/actions/ActionRunner.ts +577 -55
  57. package/src/actions/TransactionManager.ts +521 -0
  58. package/src/actions/__tests__/ActionRunner.params.test.ts +134 -0
  59. package/src/actions/__tests__/ActionRunner.test.ts +711 -0
  60. package/src/actions/__tests__/TransactionManager.test.ts +447 -0
  61. package/src/actions/index.ts +2 -1
  62. package/src/adapters/ApiDataSource.ts +349 -0
  63. package/src/adapters/ValueDataSource.ts +332 -0
  64. package/src/adapters/__tests__/ApiDataSource.test.ts +418 -0
  65. package/src/adapters/__tests__/ValueDataSource.test.ts +325 -0
  66. package/src/adapters/__tests__/resolveDataSource.test.ts +144 -0
  67. package/src/adapters/index.ts +6 -1
  68. package/src/adapters/resolveDataSource.ts +79 -0
  69. package/src/builder/__tests__/schema-builder.test.ts +235 -0
  70. package/src/data-scope/DataScopeManager.ts +269 -0
  71. package/src/data-scope/__tests__/DataScopeManager.test.ts +211 -0
  72. package/src/data-scope/index.ts +16 -0
  73. package/src/evaluator/ExpressionCache.ts +192 -0
  74. package/src/evaluator/ExpressionEvaluator.ts +61 -16
  75. package/src/evaluator/FormulaFunctions.ts +398 -0
  76. package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
  77. package/src/evaluator/__tests__/ExpressionContext.test.ts +110 -0
  78. package/src/evaluator/__tests__/FormulaFunctions.test.ts +447 -0
  79. package/src/evaluator/index.ts +4 -2
  80. package/src/index.ts +14 -10
  81. package/src/query/__tests__/query-ast.test.ts +211 -0
  82. package/src/query/__tests__/window-functions.test.ts +275 -0
  83. package/src/query/index.ts +7 -0
  84. package/src/query/query-ast.ts +341 -0
  85. package/src/registry/PluginScopeImpl.ts +259 -0
  86. package/src/registry/PluginSystem.ts +161 -0
  87. package/src/registry/Registry.ts +136 -8
  88. package/src/registry/WidgetRegistry.ts +316 -0
  89. package/src/registry/__tests__/PluginSystem.test.ts +226 -0
  90. package/src/registry/__tests__/Registry.test.ts +293 -0
  91. package/src/registry/__tests__/WidgetRegistry.test.ts +321 -0
  92. package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
  93. package/src/theme/ThemeEngine.ts +452 -0
  94. package/src/theme/__tests__/ThemeEngine.test.ts +606 -0
  95. package/src/theme/index.ts +22 -0
  96. package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
  97. package/src/validation/__tests__/schema-validator.test.ts +118 -0
  98. package/src/validation/__tests__/validation-engine.test.ts +102 -0
  99. package/src/validation/index.ts +10 -0
  100. package/src/validation/validation-engine.ts +520 -0
  101. package/src/validation/validators/index.ts +25 -0
  102. package/src/validation/validators/object-validation-engine.ts +722 -0
  103. package/tsconfig.tsbuildinfo +1 -1
  104. package/vitest.config.ts +2 -0
  105. package/src/adapters/index.d.ts +0 -8
  106. package/src/adapters/index.js +0 -10
  107. package/src/builder/schema-builder.d.ts +0 -294
  108. package/src/builder/schema-builder.js +0 -503
  109. package/src/index.d.ts +0 -13
  110. package/src/index.js +0 -16
  111. package/src/registry/Registry.d.ts +0 -56
  112. package/src/registry/Registry.js +0 -43
  113. package/src/types/index.d.ts +0 -19
  114. package/src/types/index.js +0 -8
  115. package/src/utils/filter-converter.d.ts +0 -57
  116. package/src/utils/filter-converter.js +0 -100
  117. package/src/validation/schema-validator.d.ts +0 -94
  118. package/src/validation/schema-validator.js +0 -278
@@ -0,0 +1,59 @@
1
+ /**
2
+ * ObjectUI — resolveDataSource
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
+ * Factory function to create the right DataSource from a ViewData config.
9
+ */
10
+ import { ApiDataSource } from './ApiDataSource.js';
11
+ import { ValueDataSource } from './ValueDataSource.js';
12
+ /**
13
+ * Resolve a ViewData configuration into a concrete DataSource instance.
14
+ *
15
+ * - `provider: 'object'` → returns `fallback` (the context DataSource — typically ObjectStackAdapter)
16
+ * - `provider: 'api'` → returns a new `ApiDataSource`
17
+ * - `provider: 'value'` → returns a new `ValueDataSource`
18
+ *
19
+ * @param viewData - The ViewData configuration from the schema
20
+ * @param fallback - The default DataSource from context (for `provider: 'object'`)
21
+ * @param options - Additional options for adapter construction
22
+ * @returns A DataSource instance, or null if neither viewData nor fallback is available
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const ds = resolveDataSource(
27
+ * { provider: 'api', read: { url: '/api/users' } },
28
+ * contextDataSource,
29
+ * );
30
+ * const result = await ds.find('users');
31
+ * ```
32
+ */
33
+ export function resolveDataSource(viewData, fallback, options) {
34
+ if (!viewData) {
35
+ return fallback ?? null;
36
+ }
37
+ switch (viewData.provider) {
38
+ case 'object':
39
+ // Delegate to the context DataSource (ObjectStackAdapter, etc.)
40
+ return fallback ?? null;
41
+ case 'api': {
42
+ const config = {
43
+ read: viewData.read,
44
+ write: viewData.write,
45
+ fetch: options?.fetch,
46
+ defaultHeaders: options?.defaultHeaders,
47
+ };
48
+ return new ApiDataSource(config);
49
+ }
50
+ case 'value':
51
+ return new ValueDataSource({
52
+ items: (viewData.items ?? []),
53
+ idField: options?.idField,
54
+ });
55
+ default:
56
+ // Unknown provider — fall back to context
57
+ return fallback ?? null;
58
+ }
59
+ }
@@ -0,0 +1,127 @@
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
+ * @object-ui/core - DataScope Manager
10
+ *
11
+ * Runtime implementation of the DataContext interface for managing
12
+ * named data scopes. Provides row-level data access control and
13
+ * reactive data state management within the UI component tree.
14
+ *
15
+ * @module data-scope
16
+ * @packageDocumentation
17
+ */
18
+ import type { DataScope, DataContext, DataSource } from '@object-ui/types';
19
+ /**
20
+ * Row-level filter for restricting data access within a scope
21
+ */
22
+ export interface RowLevelFilter {
23
+ /** Field to filter on */
24
+ field: string;
25
+ /** Filter operator */
26
+ operator: 'eq' | 'ne' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | 'nin' | 'contains';
27
+ /** Filter value */
28
+ value: any;
29
+ }
30
+ /**
31
+ * Configuration for creating a data scope
32
+ */
33
+ export interface DataScopeConfig {
34
+ /** Data source instance */
35
+ dataSource?: DataSource;
36
+ /** Initial data */
37
+ data?: any;
38
+ /** Row-level filters to apply */
39
+ filters?: RowLevelFilter[];
40
+ /** Whether this scope is read-only */
41
+ readOnly?: boolean;
42
+ }
43
+ /**
44
+ * DataScopeManager — Runtime implementation of DataContext.
45
+ *
46
+ * Manages named data scopes for the component tree, providing:
47
+ * - Scope registration and lookup
48
+ * - Row-level security via filters
49
+ * - Data state management (data, loading, error)
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * const manager = new DataScopeManager();
54
+ * manager.registerScope('contacts', {
55
+ * dataSource: myDataSource,
56
+ * data: [],
57
+ * });
58
+ * const scope = manager.getScope('contacts');
59
+ * ```
60
+ */
61
+ export declare class DataScopeManager implements DataContext {
62
+ scopes: Record<string, DataScope>;
63
+ private filters;
64
+ private readOnlyScopes;
65
+ private listeners;
66
+ /**
67
+ * Register a data scope
68
+ */
69
+ registerScope(name: string, scope: DataScope): void;
70
+ /**
71
+ * Register a data scope with configuration
72
+ */
73
+ registerScopeWithConfig(name: string, config: DataScopeConfig): void;
74
+ /**
75
+ * Get a data scope by name
76
+ */
77
+ getScope(name: string): DataScope | undefined;
78
+ /**
79
+ * Remove a data scope
80
+ */
81
+ removeScope(name: string): void;
82
+ /**
83
+ * Check if a scope is read-only
84
+ */
85
+ isReadOnly(name: string): boolean;
86
+ /**
87
+ * Get row-level filters for a scope
88
+ */
89
+ getFilters(name: string): RowLevelFilter[];
90
+ /**
91
+ * Set row-level filters for a scope
92
+ */
93
+ setFilters(name: string, filters: RowLevelFilter[]): void;
94
+ /**
95
+ * Apply row-level filters to a dataset
96
+ */
97
+ applyFilters(name: string, data: any[]): any[];
98
+ /**
99
+ * Update data in a scope
100
+ */
101
+ updateScopeData(name: string, data: any): void;
102
+ /**
103
+ * Update loading state for a scope
104
+ */
105
+ updateScopeLoading(name: string, loading: boolean): void;
106
+ /**
107
+ * Update error state for a scope
108
+ */
109
+ updateScopeError(name: string, error: Error | string | null): void;
110
+ /**
111
+ * Subscribe to scope changes
112
+ */
113
+ onScopeChange(name: string, listener: (scope: DataScope) => void): () => void;
114
+ /**
115
+ * Get all registered scope names
116
+ */
117
+ getScopeNames(): string[];
118
+ /**
119
+ * Clear all scopes
120
+ */
121
+ clear(): void;
122
+ private notifyListeners;
123
+ }
124
+ /**
125
+ * Default DataScopeManager instance
126
+ */
127
+ export declare const defaultDataScopeManager: DataScopeManager;
@@ -0,0 +1,229 @@
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
+ * DataScopeManager — Runtime implementation of DataContext.
10
+ *
11
+ * Manages named data scopes for the component tree, providing:
12
+ * - Scope registration and lookup
13
+ * - Row-level security via filters
14
+ * - Data state management (data, loading, error)
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const manager = new DataScopeManager();
19
+ * manager.registerScope('contacts', {
20
+ * dataSource: myDataSource,
21
+ * data: [],
22
+ * });
23
+ * const scope = manager.getScope('contacts');
24
+ * ```
25
+ */
26
+ export class DataScopeManager {
27
+ constructor() {
28
+ Object.defineProperty(this, "scopes", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: {}
33
+ });
34
+ Object.defineProperty(this, "filters", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: {}
39
+ });
40
+ Object.defineProperty(this, "readOnlyScopes", {
41
+ enumerable: true,
42
+ configurable: true,
43
+ writable: true,
44
+ value: new Set()
45
+ });
46
+ Object.defineProperty(this, "listeners", {
47
+ enumerable: true,
48
+ configurable: true,
49
+ writable: true,
50
+ value: new Map()
51
+ });
52
+ }
53
+ /**
54
+ * Register a data scope
55
+ */
56
+ registerScope(name, scope) {
57
+ this.scopes[name] = scope;
58
+ this.notifyListeners(name, scope);
59
+ }
60
+ /**
61
+ * Register a data scope with configuration
62
+ */
63
+ registerScopeWithConfig(name, config) {
64
+ const scope = {
65
+ dataSource: config.dataSource,
66
+ data: config.data,
67
+ loading: false,
68
+ error: null,
69
+ };
70
+ if (config.filters) {
71
+ this.filters[name] = config.filters;
72
+ }
73
+ if (config.readOnly) {
74
+ this.readOnlyScopes.add(name);
75
+ }
76
+ this.scopes[name] = scope;
77
+ this.notifyListeners(name, scope);
78
+ }
79
+ /**
80
+ * Get a data scope by name
81
+ */
82
+ getScope(name) {
83
+ return this.scopes[name];
84
+ }
85
+ /**
86
+ * Remove a data scope
87
+ */
88
+ removeScope(name) {
89
+ delete this.scopes[name];
90
+ delete this.filters[name];
91
+ this.readOnlyScopes.delete(name);
92
+ this.listeners.delete(name);
93
+ }
94
+ /**
95
+ * Check if a scope is read-only
96
+ */
97
+ isReadOnly(name) {
98
+ return this.readOnlyScopes.has(name);
99
+ }
100
+ /**
101
+ * Get row-level filters for a scope
102
+ */
103
+ getFilters(name) {
104
+ return this.filters[name] || [];
105
+ }
106
+ /**
107
+ * Set row-level filters for a scope
108
+ */
109
+ setFilters(name, filters) {
110
+ this.filters[name] = filters;
111
+ }
112
+ /**
113
+ * Apply row-level filters to a dataset
114
+ */
115
+ applyFilters(name, data) {
116
+ const scopeFilters = this.filters[name];
117
+ if (!scopeFilters || scopeFilters.length === 0) {
118
+ return data;
119
+ }
120
+ return data.filter(row => {
121
+ return scopeFilters.every(filter => {
122
+ const fieldValue = row[filter.field];
123
+ return evaluateFilter(fieldValue, filter.operator, filter.value);
124
+ });
125
+ });
126
+ }
127
+ /**
128
+ * Update data in a scope
129
+ */
130
+ updateScopeData(name, data) {
131
+ const scope = this.scopes[name];
132
+ if (!scope)
133
+ return;
134
+ if (this.readOnlyScopes.has(name)) {
135
+ throw new Error(`Cannot update read-only scope: ${name}`);
136
+ }
137
+ scope.data = data;
138
+ this.notifyListeners(name, scope);
139
+ }
140
+ /**
141
+ * Update loading state for a scope
142
+ */
143
+ updateScopeLoading(name, loading) {
144
+ const scope = this.scopes[name];
145
+ if (!scope)
146
+ return;
147
+ scope.loading = loading;
148
+ this.notifyListeners(name, scope);
149
+ }
150
+ /**
151
+ * Update error state for a scope
152
+ */
153
+ updateScopeError(name, error) {
154
+ const scope = this.scopes[name];
155
+ if (!scope)
156
+ return;
157
+ scope.error = error;
158
+ this.notifyListeners(name, scope);
159
+ }
160
+ /**
161
+ * Subscribe to scope changes
162
+ */
163
+ onScopeChange(name, listener) {
164
+ if (!this.listeners.has(name)) {
165
+ this.listeners.set(name, []);
166
+ }
167
+ this.listeners.get(name).push(listener);
168
+ return () => {
169
+ const arr = this.listeners.get(name);
170
+ if (arr) {
171
+ const idx = arr.indexOf(listener);
172
+ if (idx >= 0)
173
+ arr.splice(idx, 1);
174
+ }
175
+ };
176
+ }
177
+ /**
178
+ * Get all registered scope names
179
+ */
180
+ getScopeNames() {
181
+ return Object.keys(this.scopes);
182
+ }
183
+ /**
184
+ * Clear all scopes
185
+ */
186
+ clear() {
187
+ this.scopes = {};
188
+ this.filters = {};
189
+ this.readOnlyScopes.clear();
190
+ this.listeners.clear();
191
+ }
192
+ notifyListeners(name, scope) {
193
+ const arr = this.listeners.get(name);
194
+ if (arr) {
195
+ arr.forEach(listener => listener(scope));
196
+ }
197
+ }
198
+ }
199
+ /**
200
+ * Evaluate a single filter condition against a field value
201
+ */
202
+ function evaluateFilter(fieldValue, operator, filterValue) {
203
+ switch (operator) {
204
+ case 'eq':
205
+ return fieldValue === filterValue;
206
+ case 'ne':
207
+ return fieldValue !== filterValue;
208
+ case 'gt':
209
+ return fieldValue > filterValue;
210
+ case 'lt':
211
+ return fieldValue < filterValue;
212
+ case 'gte':
213
+ return fieldValue >= filterValue;
214
+ case 'lte':
215
+ return fieldValue <= filterValue;
216
+ case 'in':
217
+ return Array.isArray(filterValue) && filterValue.includes(fieldValue);
218
+ case 'nin':
219
+ return Array.isArray(filterValue) && !filterValue.includes(fieldValue);
220
+ case 'contains':
221
+ return typeof fieldValue === 'string' && fieldValue.includes(String(filterValue));
222
+ default:
223
+ return true;
224
+ }
225
+ }
226
+ /**
227
+ * Default DataScopeManager instance
228
+ */
229
+ export const defaultDataScopeManager = new DataScopeManager();
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @object-ui/core - DataScope Module
3
+ *
4
+ * Runtime data scope management for row-level security and
5
+ * reactive data state within the UI component tree.
6
+ *
7
+ * @module data-scope
8
+ * @packageDocumentation
9
+ */
10
+ export { DataScopeManager, defaultDataScopeManager, type RowLevelFilter, type DataScopeConfig, } from './DataScopeManager.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @object-ui/core - DataScope Module
3
+ *
4
+ * Runtime data scope management for row-level security and
5
+ * reactive data state within the UI component tree.
6
+ *
7
+ * @module data-scope
8
+ * @packageDocumentation
9
+ */
10
+ export { DataScopeManager, defaultDataScopeManager, } from './DataScopeManager.js';
@@ -0,0 +1,101 @@
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
+ * @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
+ /**
18
+ * A compiled expression function that can be executed with context values
19
+ */
20
+ export type CompiledExpression = (...args: any[]) => any;
21
+ /**
22
+ * Expression compilation metadata
23
+ */
24
+ export interface ExpressionMetadata {
25
+ /**
26
+ * The compiled function
27
+ */
28
+ fn: CompiledExpression;
29
+ /**
30
+ * Variable names used in the expression
31
+ */
32
+ varNames: string[];
33
+ /**
34
+ * Original expression string
35
+ */
36
+ expression: string;
37
+ /**
38
+ * Timestamp when the expression was compiled
39
+ */
40
+ compiledAt: number;
41
+ /**
42
+ * Number of times this expression has been used
43
+ */
44
+ hitCount: number;
45
+ }
46
+ /**
47
+ * Cache for compiled expressions
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * const cache = new ExpressionCache();
52
+ * const compiled = cache.compile('data.amount > 1000', ['data']);
53
+ * const result = compiled.fn({ amount: 1500 }); // true
54
+ * ```
55
+ */
56
+ export declare class ExpressionCache {
57
+ private cache;
58
+ private maxSize;
59
+ /**
60
+ * Create a new expression cache
61
+ *
62
+ * @param maxSize Maximum number of expressions to cache (default: 1000)
63
+ */
64
+ constructor(maxSize?: number);
65
+ /**
66
+ * Compile an expression or retrieve from cache
67
+ *
68
+ * @param expr The expression to compile
69
+ * @param varNames Variable names available in the context
70
+ * @returns Compiled expression metadata
71
+ */
72
+ compile(expr: string, varNames: string[]): ExpressionMetadata;
73
+ /**
74
+ * Compile an expression into a function
75
+ */
76
+ private compileExpression;
77
+ /**
78
+ * Evict the least frequently used expression from cache
79
+ */
80
+ private evictLFU;
81
+ /**
82
+ * Check if an expression is cached
83
+ */
84
+ has(expr: string, varNames: string[]): boolean;
85
+ /**
86
+ * Clear the cache
87
+ */
88
+ clear(): void;
89
+ /**
90
+ * Get cache statistics
91
+ */
92
+ getStats(): {
93
+ size: number;
94
+ maxSize: number;
95
+ totalHits: number;
96
+ entries: Array<{
97
+ expression: string;
98
+ hitCount: number;
99
+ }>;
100
+ };
101
+ }
@@ -0,0 +1,135 @@
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
+ * Cache for compiled expressions
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const cache = new ExpressionCache();
14
+ * const compiled = cache.compile('data.amount > 1000', ['data']);
15
+ * const result = compiled.fn({ amount: 1500 }); // true
16
+ * ```
17
+ */
18
+ export class ExpressionCache {
19
+ /**
20
+ * Create a new expression cache
21
+ *
22
+ * @param maxSize Maximum number of expressions to cache (default: 1000)
23
+ */
24
+ constructor(maxSize = 1000) {
25
+ Object.defineProperty(this, "cache", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: new Map()
30
+ });
31
+ Object.defineProperty(this, "maxSize", {
32
+ enumerable: true,
33
+ configurable: true,
34
+ writable: true,
35
+ value: void 0
36
+ });
37
+ this.maxSize = maxSize;
38
+ }
39
+ /**
40
+ * Compile an expression or retrieve from cache
41
+ *
42
+ * @param expr The expression to compile
43
+ * @param varNames Variable names available in the context
44
+ * @returns Compiled expression metadata
45
+ */
46
+ compile(expr, varNames) {
47
+ // Create a cache key that includes variable names to ensure correct scoping
48
+ const cacheKey = `${expr}::${varNames.join(',')}`;
49
+ if (this.cache.has(cacheKey)) {
50
+ const metadata = this.cache.get(cacheKey);
51
+ metadata.hitCount++;
52
+ return metadata;
53
+ }
54
+ // Evict least frequently used if cache is full
55
+ if (this.cache.size >= this.maxSize) {
56
+ this.evictLFU();
57
+ }
58
+ // Compile the expression
59
+ const fn = this.compileExpression(expr, varNames);
60
+ const metadata = {
61
+ fn,
62
+ varNames: [...varNames],
63
+ expression: expr,
64
+ compiledAt: Date.now(),
65
+ hitCount: 1,
66
+ };
67
+ this.cache.set(cacheKey, metadata);
68
+ return metadata;
69
+ }
70
+ /**
71
+ * Compile an expression into a function
72
+ */
73
+ 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});`);
81
+ }
82
+ /**
83
+ * Evict the least frequently used expression from cache
84
+ */
85
+ evictLFU() {
86
+ let oldestKey = null;
87
+ let oldestTime = Infinity;
88
+ let lowestHits = Infinity;
89
+ // Find the entry with lowest hit count, or oldest if tied
90
+ for (const [key, metadata] of this.cache.entries()) {
91
+ if (metadata.hitCount < lowestHits ||
92
+ (metadata.hitCount === lowestHits && metadata.compiledAt < oldestTime)) {
93
+ oldestKey = key;
94
+ oldestTime = metadata.compiledAt;
95
+ lowestHits = metadata.hitCount;
96
+ }
97
+ }
98
+ if (oldestKey) {
99
+ this.cache.delete(oldestKey);
100
+ }
101
+ }
102
+ /**
103
+ * Check if an expression is cached
104
+ */
105
+ has(expr, varNames) {
106
+ const cacheKey = `${expr}::${varNames.join(',')}`;
107
+ return this.cache.has(cacheKey);
108
+ }
109
+ /**
110
+ * Clear the cache
111
+ */
112
+ clear() {
113
+ this.cache.clear();
114
+ }
115
+ /**
116
+ * Get cache statistics
117
+ */
118
+ getStats() {
119
+ let totalHits = 0;
120
+ const entries = [];
121
+ for (const metadata of this.cache.values()) {
122
+ totalHits += metadata.hitCount;
123
+ entries.push({
124
+ expression: metadata.expression,
125
+ hitCount: metadata.hitCount,
126
+ });
127
+ }
128
+ return {
129
+ size: this.cache.size,
130
+ maxSize: this.maxSize,
131
+ totalHits,
132
+ entries: entries.sort((a, b) => b.hitCount - a.hitCount),
133
+ };
134
+ }
135
+ }