@object-ui/core 0.3.0 → 0.5.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 (90) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +8 -0
  3. package/dist/actions/ActionRunner.d.ts +40 -0
  4. package/dist/actions/ActionRunner.js +160 -0
  5. package/dist/actions/index.d.ts +8 -0
  6. package/dist/actions/index.js +8 -0
  7. package/dist/adapters/index.d.ts +7 -0
  8. package/dist/adapters/index.js +10 -0
  9. package/dist/builder/schema-builder.d.ts +7 -0
  10. package/dist/builder/schema-builder.js +4 -6
  11. package/dist/evaluator/ExpressionCache.d.ts +101 -0
  12. package/dist/evaluator/ExpressionCache.js +135 -0
  13. package/dist/evaluator/ExpressionContext.d.ts +51 -0
  14. package/dist/evaluator/ExpressionContext.js +110 -0
  15. package/dist/evaluator/ExpressionEvaluator.d.ts +117 -0
  16. package/dist/evaluator/ExpressionEvaluator.js +220 -0
  17. package/dist/evaluator/index.d.ts +10 -0
  18. package/dist/evaluator/index.js +10 -0
  19. package/dist/index.d.ts +17 -4
  20. package/dist/index.js +16 -5
  21. package/dist/query/index.d.ts +6 -0
  22. package/dist/query/index.js +6 -0
  23. package/dist/query/query-ast.d.ts +32 -0
  24. package/dist/query/query-ast.js +268 -0
  25. package/dist/registry/PluginScopeImpl.d.ts +80 -0
  26. package/dist/registry/PluginScopeImpl.js +243 -0
  27. package/dist/registry/PluginSystem.d.ts +66 -0
  28. package/dist/registry/PluginSystem.js +142 -0
  29. package/dist/registry/Registry.d.ts +80 -4
  30. package/dist/registry/Registry.js +119 -7
  31. package/dist/types/index.d.ts +7 -0
  32. package/dist/types/index.js +7 -0
  33. package/dist/utils/filter-converter.d.ts +57 -0
  34. package/dist/utils/filter-converter.js +100 -0
  35. package/dist/validation/index.d.ts +9 -0
  36. package/dist/validation/index.js +9 -0
  37. package/dist/validation/schema-validator.d.ts +7 -0
  38. package/dist/validation/schema-validator.js +4 -6
  39. package/dist/validation/validation-engine.d.ts +70 -0
  40. package/dist/validation/validation-engine.js +363 -0
  41. package/dist/validation/validators/index.d.ts +16 -0
  42. package/dist/validation/validators/index.js +16 -0
  43. package/dist/validation/validators/object-validation-engine.d.ts +118 -0
  44. package/dist/validation/validators/object-validation-engine.js +538 -0
  45. package/package.json +26 -7
  46. package/src/actions/ActionRunner.ts +195 -0
  47. package/src/actions/index.ts +9 -0
  48. package/src/adapters/README.md +180 -0
  49. package/src/adapters/index.ts +10 -0
  50. package/src/builder/schema-builder.ts +8 -0
  51. package/src/evaluator/ExpressionCache.ts +192 -0
  52. package/src/evaluator/ExpressionContext.ts +118 -0
  53. package/src/evaluator/ExpressionEvaluator.ts +267 -0
  54. package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
  55. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
  56. package/src/evaluator/index.ts +11 -0
  57. package/src/index.test.ts +8 -0
  58. package/src/index.ts +18 -5
  59. package/src/query/__tests__/query-ast.test.ts +211 -0
  60. package/src/query/__tests__/window-functions.test.ts +275 -0
  61. package/src/query/index.ts +7 -0
  62. package/src/query/query-ast.ts +341 -0
  63. package/src/registry/PluginScopeImpl.ts +259 -0
  64. package/src/registry/PluginSystem.ts +161 -0
  65. package/src/registry/Registry.ts +133 -8
  66. package/src/registry/__tests__/PluginSystem.test.ts +226 -0
  67. package/src/registry/__tests__/Registry.test.ts +293 -0
  68. package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
  69. package/src/types/index.ts +8 -0
  70. package/src/utils/__tests__/filter-converter.test.ts +118 -0
  71. package/src/utils/filter-converter.ts +133 -0
  72. package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
  73. package/src/validation/__tests__/validation-engine.test.ts +102 -0
  74. package/src/validation/index.ts +10 -0
  75. package/src/validation/schema-validator.ts +8 -0
  76. package/src/validation/validation-engine.ts +461 -0
  77. package/src/validation/validators/index.ts +25 -0
  78. package/src/validation/validators/object-validation-engine.ts +722 -0
  79. package/tsconfig.tsbuildinfo +1 -1
  80. package/vitest.config.ts +2 -0
  81. package/src/builder/schema-builder.d.ts +0 -287
  82. package/src/builder/schema-builder.js +0 -505
  83. package/src/index.d.ts +0 -4
  84. package/src/index.js +0 -7
  85. package/src/registry/Registry.d.ts +0 -49
  86. package/src/registry/Registry.js +0 -36
  87. package/src/types/index.d.ts +0 -12
  88. package/src/types/index.js +0 -1
  89. package/src/validation/schema-validator.d.ts +0 -87
  90. package/src/validation/schema-validator.js +0 -280
@@ -0,0 +1,195 @@
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
+ /**
10
+ * @object-ui/core - Action Runner
11
+ *
12
+ * Executes actions defined in ActionSchema and EventHandler.
13
+ */
14
+
15
+ import { ExpressionEvaluator } from '../evaluator/ExpressionEvaluator';
16
+
17
+ export interface ActionResult {
18
+ success: boolean;
19
+ data?: any;
20
+ error?: string;
21
+ reload?: boolean;
22
+ close?: boolean;
23
+ redirect?: string;
24
+ }
25
+
26
+ export interface ActionContext {
27
+ data?: Record<string, any>;
28
+ record?: any;
29
+ user?: any;
30
+ [key: string]: any;
31
+ }
32
+
33
+ export type ActionHandler = (
34
+ action: any,
35
+ context: ActionContext
36
+ ) => Promise<ActionResult> | ActionResult;
37
+
38
+ export class ActionRunner {
39
+ private handlers = new Map<string, ActionHandler>();
40
+ private evaluator: ExpressionEvaluator;
41
+ private context: ActionContext;
42
+
43
+ constructor(context: ActionContext = {}) {
44
+ this.context = context;
45
+ this.evaluator = new ExpressionEvaluator(context);
46
+ }
47
+
48
+ registerHandler(actionName: string, handler: ActionHandler): void {
49
+ this.handlers.set(actionName, handler);
50
+ }
51
+
52
+ async execute(action: any): Promise<ActionResult> {
53
+ try {
54
+ if (action.condition) {
55
+ const shouldExecute = this.evaluator.evaluateCondition(action.condition);
56
+ if (!shouldExecute) {
57
+ return { success: false, error: 'Action condition not met' };
58
+ }
59
+ }
60
+
61
+ if (action.disabled) {
62
+ const isDisabled = typeof action.disabled === 'string'
63
+ ? this.evaluator.evaluateCondition(action.disabled)
64
+ : action.disabled;
65
+
66
+ if (isDisabled) {
67
+ return { success: false, error: 'Action is disabled' };
68
+ }
69
+ }
70
+
71
+ if (action.type === 'action' || action.actionType) {
72
+ return await this.executeActionSchema(action);
73
+ } else if (action.type === 'navigation' || action.navigate) {
74
+ return await this.executeNavigation(action);
75
+ } else if (action.type === 'api' || action.api) {
76
+ return await this.executeAPI(action);
77
+ } else if (action.onClick) {
78
+ await action.onClick();
79
+ return { success: true };
80
+ }
81
+
82
+ return { success: false, error: 'Unknown action type' };
83
+ } catch (error) {
84
+ return { success: false, error: (error as Error).message };
85
+ }
86
+ }
87
+
88
+ private async executeActionSchema(action: any): Promise<ActionResult> {
89
+ const result: ActionResult = { success: true };
90
+
91
+ if (action.confirmText) {
92
+ const confirmed = await this.showConfirmation(action.confirmText);
93
+ if (!confirmed) {
94
+ return { success: false, error: 'Action cancelled by user' };
95
+ }
96
+ }
97
+
98
+ if (action.api) {
99
+ const apiResult = await this.executeAPI(action);
100
+ if (!apiResult.success) return apiResult;
101
+ result.data = apiResult.data;
102
+ }
103
+
104
+ if (action.onClick) {
105
+ await action.onClick();
106
+ }
107
+
108
+ result.reload = action.reload !== false;
109
+ result.close = action.close !== false;
110
+
111
+ if (action.redirect) {
112
+ result.redirect = this.evaluator.evaluate(action.redirect) as string;
113
+ }
114
+
115
+ return result;
116
+ }
117
+
118
+ /**
119
+ * Execute navigation action
120
+ */
121
+ private async executeNavigation(action: any): Promise<ActionResult> {
122
+ const nav = action.navigate || action;
123
+ const to = this.evaluator.evaluate(nav.to) as string;
124
+
125
+ // Validate URL to prevent javascript: or data: schemes
126
+ const isValidUrl = typeof to === 'string' && (
127
+ to.startsWith('http://') ||
128
+ to.startsWith('https://') ||
129
+ to.startsWith('/') ||
130
+ to.startsWith('./')
131
+ );
132
+
133
+ if (!isValidUrl) {
134
+ return {
135
+ success: false,
136
+ error: 'Invalid URL scheme. Only http://, https://, and relative URLs are allowed.'
137
+ };
138
+ }
139
+
140
+ if (nav.external) {
141
+ window.open(to, '_blank', 'noopener,noreferrer');
142
+ } else {
143
+ return { success: true, redirect: to };
144
+ }
145
+
146
+ return { success: true };
147
+ }
148
+
149
+ private async executeAPI(action: any): Promise<ActionResult> {
150
+ const apiConfig = action.api;
151
+
152
+ if (typeof apiConfig === 'string') {
153
+ try {
154
+ const response = await fetch(apiConfig, {
155
+ method: action.method || 'POST',
156
+ headers: { 'Content-Type': 'application/json' },
157
+ body: JSON.stringify(this.context.data || {})
158
+ });
159
+
160
+ if (!response.ok) {
161
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
162
+ }
163
+
164
+ const data = await response.json();
165
+ return { success: true, data };
166
+ } catch (error) {
167
+ return { success: false, error: (error as Error).message };
168
+ }
169
+ }
170
+
171
+ return { success: false, error: 'Complex API configuration not yet implemented' };
172
+ }
173
+
174
+ private async showConfirmation(message: string): Promise<boolean> {
175
+ const evaluatedMessage = this.evaluator.evaluate(message) as string;
176
+ return window.confirm(evaluatedMessage);
177
+ }
178
+
179
+ updateContext(newContext: Partial<ActionContext>): void {
180
+ this.context = { ...this.context, ...newContext };
181
+ this.evaluator.updateContext(newContext);
182
+ }
183
+
184
+ getContext(): ActionContext {
185
+ return this.context;
186
+ }
187
+ }
188
+
189
+ export async function executeAction(
190
+ action: any,
191
+ context: ActionContext = {}
192
+ ): Promise<ActionResult> {
193
+ const runner = new ActionRunner(context);
194
+ return await runner.execute(action);
195
+ }
@@ -0,0 +1,9 @@
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
+ export * from './ActionRunner.js';
@@ -0,0 +1,180 @@
1
+ # Data Source Adapters
2
+
3
+ This directory contains data source adapters that bridge various backend protocols with the ObjectUI DataSource interface.
4
+
5
+ ## ObjectStack Adapter
6
+
7
+ The `ObjectStackAdapter` provides seamless integration with ObjectStack Protocol servers.
8
+
9
+ ### Features
10
+
11
+ - ✅ Full CRUD operations (find, findOne, create, update, delete)
12
+ - ✅ Bulk operations (createMany, updateMany, deleteMany)
13
+ - ✅ Auto-discovery of server capabilities
14
+ - ✅ Query parameter translation (OData-style → ObjectStack)
15
+ - ✅ Proper error handling
16
+ - ✅ TypeScript types
17
+
18
+ ### Usage
19
+
20
+ ```typescript
21
+ import { createObjectStackAdapter } from '@object-ui/core';
22
+
23
+ // Create the adapter
24
+ const dataSource = createObjectStackAdapter({
25
+ baseUrl: 'https://api.example.com',
26
+ token: 'your-auth-token', // Optional
27
+ });
28
+
29
+ // Use it with ObjectUI components
30
+ const schema = {
31
+ type: 'data-table',
32
+ dataSource,
33
+ resource: 'users',
34
+ columns: [
35
+ { header: 'Name', accessorKey: 'name' },
36
+ { header: 'Email', accessorKey: 'email' },
37
+ ]
38
+ };
39
+ ```
40
+
41
+ ### Advanced Usage
42
+
43
+ ```typescript
44
+ import { ObjectStackAdapter } from '@object-ui/core';
45
+
46
+ const adapter = new ObjectStackAdapter({
47
+ baseUrl: 'https://api.example.com',
48
+ token: process.env.API_TOKEN,
49
+ fetch: customFetch // Optional: use custom fetch (e.g., Next.js fetch)
50
+ });
51
+
52
+ // Manually connect (optional, auto-connects on first request)
53
+ await adapter.connect();
54
+
55
+ // Query with filters (MongoDB-like operators)
56
+ const result = await adapter.find('tasks', {
57
+ $filter: {
58
+ status: 'active',
59
+ priority: { $gte: 2 }
60
+ },
61
+ $orderby: { createdAt: 'desc' },
62
+ $top: 20,
63
+ $skip: 0
64
+ });
65
+
66
+ // Access the underlying client for advanced operations
67
+ const client = adapter.getClient();
68
+ const metadata = await client.meta.getObject('task');
69
+ ```
70
+
71
+ ### Filter Conversion
72
+
73
+ The adapter automatically converts MongoDB-like filter operators to **ObjectStack FilterNode AST format**. This ensures compatibility with the latest ObjectStack Protocol (v0.1.2+).
74
+
75
+ #### Supported Filter Operators
76
+
77
+ | MongoDB Operator | ObjectStack Operator | Example |
78
+ |------------------|---------------------|---------|
79
+ | `$eq` or simple value | `=` | `{ status: 'active' }` → `['status', '=', 'active']` |
80
+ | `$ne` | `!=` | `{ status: { $ne: 'archived' } }` → `['status', '!=', 'archived']` |
81
+ | `$gt` | `>` | `{ age: { $gt: 18 } }` → `['age', '>', 18]` |
82
+ | `$gte` | `>=` | `{ age: { $gte: 18 } }` → `['age', '>=', 18]` |
83
+ | `$lt` | `<` | `{ age: { $lt: 65 } }` → `['age', '<', 65]` |
84
+ | `$lte` | `<=` | `{ age: { $lte: 65 } }` → `['age', '<=', 65]` |
85
+ | `$in` | `in` | `{ status: { $in: ['active', 'pending'] } }` → `['status', 'in', ['active', 'pending']]` |
86
+ | `$nin` / `$notin` | `notin` | `{ status: { $nin: ['archived'] } }` → `['status', 'notin', ['archived']]` |
87
+ | `$contains` / `$regex` | `contains` | `{ name: { $contains: 'John' } }` → `['name', 'contains', 'John']` |
88
+ | `$startswith` | `startswith` | `{ email: { $startswith: 'admin' } }` → `['email', 'startswith', 'admin']` |
89
+ | `$between` | `between` | `{ age: { $between: [18, 65] } }` → `['age', 'between', [18, 65]]` |
90
+
91
+ #### Complex Filter Examples
92
+
93
+ **Multiple conditions** are combined with `'and'`:
94
+
95
+ ```typescript
96
+ // Input
97
+ $filter: {
98
+ age: { $gte: 18, $lte: 65 },
99
+ status: 'active'
100
+ }
101
+
102
+ // Converted to AST
103
+ ['and',
104
+ ['age', '>=', 18],
105
+ ['age', '<=', 65],
106
+ ['status', '=', 'active']
107
+ ]
108
+ ```
109
+
110
+ ### Query Parameter Mapping
111
+
112
+ The adapter automatically converts ObjectUI query parameters (OData-style) to ObjectStack protocol:
113
+
114
+ | ObjectUI ($) | ObjectStack | Description |
115
+ |--------------|-------------|-------------|
116
+ | `$select` | `select` | Field selection |
117
+ | `$filter` | `filters` (AST) | Filter conditions (converted to FilterNode AST) |
118
+ | `$orderby` | `sort` | Sort order |
119
+ | `$skip` | `skip` | Pagination offset |
120
+ | `$top` | `top` | Limit records |
121
+
122
+ ### Example with Sorting
123
+
124
+ ```typescript
125
+ // OData-style
126
+ await dataSource.find('users', {
127
+ $orderby: {
128
+ createdAt: 'desc',
129
+ name: 'asc'
130
+ }
131
+ });
132
+
133
+ // Converted to ObjectStack: ['-createdAt', 'name']
134
+ ```
135
+
136
+ ## Creating Custom Adapters
137
+
138
+ To create a custom adapter, implement the `DataSource<T>` interface:
139
+
140
+ ```typescript
141
+ import type { DataSource, QueryParams, QueryResult } from '@object-ui/types';
142
+
143
+ export class MyCustomAdapter<T = any> implements DataSource<T> {
144
+ async find(resource: string, params?: QueryParams): Promise<QueryResult<T>> {
145
+ // Your implementation
146
+ }
147
+
148
+ async findOne(resource: string, id: string | number): Promise<T | null> {
149
+ // Your implementation
150
+ }
151
+
152
+ async create(resource: string, data: Partial<T>): Promise<T> {
153
+ // Your implementation
154
+ }
155
+
156
+ async update(resource: string, id: string | number, data: Partial<T>): Promise<T> {
157
+ // Your implementation
158
+ }
159
+
160
+ async delete(resource: string, id: string | number): Promise<boolean> {
161
+ // Your implementation
162
+ }
163
+
164
+ // Optional: bulk operations
165
+ async bulk?(resource: string, operation: string, data: Partial<T>[]): Promise<T[]> {
166
+ // Your implementation
167
+ }
168
+ }
169
+ ```
170
+
171
+ ## Available Adapters
172
+
173
+ - **ObjectStackAdapter** - For ObjectStack Protocol servers
174
+ - More adapters coming soon (REST, GraphQL, Supabase, Firebase, etc.)
175
+
176
+ ## Related Packages
177
+
178
+ - `@objectstack/client` - ObjectStack Client SDK
179
+ - `@objectstack/spec` - ObjectStack Protocol Specification
180
+ - `@object-ui/types` - ObjectUI Type Definitions
@@ -0,0 +1,10 @@
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
+ // export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
10
+ // Adapters have been moved to separate packages (e.g. @object-ui/data-objectstack)
@@ -1,3 +1,11 @@
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
+
1
9
  /**
2
10
  * @object-ui/core - Schema Builder
3
11
  *
@@ -0,0 +1,192 @@
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
+ /**
10
+ * @object-ui/core - Expression Cache
11
+ *
12
+ * Caches compiled expressions to avoid re-parsing on every render.
13
+ * Provides significant performance improvement for frequently evaluated expressions.
14
+ *
15
+ * @module evaluator
16
+ * @packageDocumentation
17
+ */
18
+
19
+ /**
20
+ * A compiled expression function that can be executed with context values
21
+ */
22
+ export type CompiledExpression = (...args: any[]) => any;
23
+
24
+ /**
25
+ * Expression compilation metadata
26
+ */
27
+ export interface ExpressionMetadata {
28
+ /**
29
+ * The compiled function
30
+ */
31
+ fn: CompiledExpression;
32
+
33
+ /**
34
+ * Variable names used in the expression
35
+ */
36
+ varNames: string[];
37
+
38
+ /**
39
+ * Original expression string
40
+ */
41
+ expression: string;
42
+
43
+ /**
44
+ * Timestamp when the expression was compiled
45
+ */
46
+ compiledAt: number;
47
+
48
+ /**
49
+ * Number of times this expression has been used
50
+ */
51
+ hitCount: number;
52
+ }
53
+
54
+ /**
55
+ * Cache for compiled expressions
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const cache = new ExpressionCache();
60
+ * const compiled = cache.compile('data.amount > 1000', ['data']);
61
+ * const result = compiled.fn({ amount: 1500 }); // true
62
+ * ```
63
+ */
64
+ export class ExpressionCache {
65
+ private cache = new Map<string, ExpressionMetadata>();
66
+ private maxSize: number;
67
+
68
+ /**
69
+ * Create a new expression cache
70
+ *
71
+ * @param maxSize Maximum number of expressions to cache (default: 1000)
72
+ */
73
+ constructor(maxSize = 1000) {
74
+ this.maxSize = maxSize;
75
+ }
76
+
77
+ /**
78
+ * Compile an expression or retrieve from cache
79
+ *
80
+ * @param expr The expression to compile
81
+ * @param varNames Variable names available in the context
82
+ * @returns Compiled expression metadata
83
+ */
84
+ compile(expr: string, varNames: string[]): ExpressionMetadata {
85
+ // Create a cache key that includes variable names to ensure correct scoping
86
+ const cacheKey = `${expr}::${varNames.join(',')}`;
87
+
88
+ if (this.cache.has(cacheKey)) {
89
+ const metadata = this.cache.get(cacheKey)!;
90
+ metadata.hitCount++;
91
+ return metadata;
92
+ }
93
+
94
+ // Evict least frequently used if cache is full
95
+ if (this.cache.size >= this.maxSize) {
96
+ this.evictLFU();
97
+ }
98
+
99
+ // Compile the expression
100
+ const fn = this.compileExpression(expr, varNames);
101
+
102
+ const metadata: ExpressionMetadata = {
103
+ fn,
104
+ varNames: [...varNames],
105
+ expression: expr,
106
+ compiledAt: Date.now(),
107
+ hitCount: 1,
108
+ };
109
+
110
+ this.cache.set(cacheKey, metadata);
111
+ return metadata;
112
+ }
113
+
114
+ /**
115
+ * Compile an expression into a function
116
+ */
117
+ private compileExpression(expression: string, varNames: string[]): CompiledExpression {
118
+ // SECURITY NOTE: Using Function constructor for expression evaluation.
119
+ // This is a controlled use case with:
120
+ // 1. Sanitization check (isDangerous) performed by caller
121
+ // 2. Strict mode enabled ("use strict")
122
+ // 3. Limited scope (only varNames variables available)
123
+ // 4. No access to global objects (process, window, etc.)
124
+ return new Function(...varNames, `"use strict"; return (${expression});`) as CompiledExpression;
125
+ }
126
+
127
+ /**
128
+ * Evict the least frequently used expression from cache
129
+ */
130
+ private evictLFU(): void {
131
+ let oldestKey: string | null = null;
132
+ let oldestTime = Infinity;
133
+ let lowestHits = Infinity;
134
+
135
+ // Find the entry with lowest hit count, or oldest if tied
136
+ for (const [key, metadata] of this.cache.entries()) {
137
+ if (metadata.hitCount < lowestHits ||
138
+ (metadata.hitCount === lowestHits && metadata.compiledAt < oldestTime)) {
139
+ oldestKey = key;
140
+ oldestTime = metadata.compiledAt;
141
+ lowestHits = metadata.hitCount;
142
+ }
143
+ }
144
+
145
+ if (oldestKey) {
146
+ this.cache.delete(oldestKey);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Check if an expression is cached
152
+ */
153
+ has(expr: string, varNames: string[]): boolean {
154
+ const cacheKey = `${expr}::${varNames.join(',')}`;
155
+ return this.cache.has(cacheKey);
156
+ }
157
+
158
+ /**
159
+ * Clear the cache
160
+ */
161
+ clear(): void {
162
+ this.cache.clear();
163
+ }
164
+
165
+ /**
166
+ * Get cache statistics
167
+ */
168
+ getStats(): {
169
+ size: number;
170
+ maxSize: number;
171
+ totalHits: number;
172
+ entries: Array<{ expression: string; hitCount: number }>;
173
+ } {
174
+ let totalHits = 0;
175
+ const entries: Array<{ expression: string; hitCount: number }> = [];
176
+
177
+ for (const metadata of this.cache.values()) {
178
+ totalHits += metadata.hitCount;
179
+ entries.push({
180
+ expression: metadata.expression,
181
+ hitCount: metadata.hitCount,
182
+ });
183
+ }
184
+
185
+ return {
186
+ size: this.cache.size,
187
+ maxSize: this.maxSize,
188
+ totalHits,
189
+ entries: entries.sort((a, b) => b.hitCount - a.hitCount),
190
+ };
191
+ }
192
+ }