@object-ui/core 0.3.1 → 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.
- package/.turbo/turbo-build.log +4 -0
- package/dist/actions/index.d.ts +1 -1
- package/dist/actions/index.js +1 -1
- package/dist/evaluator/ExpressionCache.d.ts +101 -0
- package/dist/evaluator/ExpressionCache.js +135 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +20 -2
- package/dist/evaluator/ExpressionEvaluator.js +34 -14
- package/dist/evaluator/index.d.ts +3 -2
- package/dist/evaluator/index.js +3 -2
- package/dist/index.d.ts +10 -7
- package/dist/index.js +9 -7
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +6 -0
- package/dist/query/query-ast.d.ts +32 -0
- package/dist/query/query-ast.js +268 -0
- package/dist/registry/PluginScopeImpl.d.ts +80 -0
- package/dist/registry/PluginScopeImpl.js +243 -0
- package/dist/registry/PluginSystem.d.ts +66 -0
- package/dist/registry/PluginSystem.js +142 -0
- package/dist/registry/Registry.d.ts +73 -4
- package/dist/registry/Registry.js +112 -7
- package/dist/validation/index.d.ts +9 -0
- package/dist/validation/index.js +9 -0
- package/dist/validation/validation-engine.d.ts +70 -0
- package/dist/validation/validation-engine.js +363 -0
- package/dist/validation/validators/index.d.ts +16 -0
- package/dist/validation/validators/index.js +16 -0
- package/dist/validation/validators/object-validation-engine.d.ts +118 -0
- package/dist/validation/validators/object-validation-engine.js +538 -0
- package/package.json +13 -5
- package/src/actions/index.ts +1 -1
- package/src/evaluator/ExpressionCache.ts +192 -0
- package/src/evaluator/ExpressionEvaluator.ts +33 -14
- package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
- package/src/evaluator/index.ts +3 -2
- package/src/index.ts +10 -7
- package/src/query/__tests__/query-ast.test.ts +211 -0
- package/src/query/__tests__/window-functions.test.ts +275 -0
- package/src/query/index.ts +7 -0
- package/src/query/query-ast.ts +341 -0
- package/src/registry/PluginScopeImpl.ts +259 -0
- package/src/registry/PluginSystem.ts +161 -0
- package/src/registry/Registry.ts +125 -8
- package/src/registry/__tests__/PluginSystem.test.ts +226 -0
- package/src/registry/__tests__/Registry.test.ts +293 -0
- package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
- package/src/validation/__tests__/validation-engine.test.ts +102 -0
- package/src/validation/index.ts +10 -0
- package/src/validation/validation-engine.ts +461 -0
- package/src/validation/validators/index.ts +25 -0
- package/src/validation/validators/object-validation-engine.ts +722 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +2 -0
- package/src/adapters/index.d.ts +0 -8
- package/src/adapters/index.js +0 -10
- package/src/builder/schema-builder.d.ts +0 -294
- package/src/builder/schema-builder.js +0 -503
- package/src/index.d.ts +0 -13
- package/src/index.js +0 -16
- package/src/registry/Registry.d.ts +0 -56
- package/src/registry/Registry.js +0 -43
- package/src/types/index.d.ts +0 -19
- package/src/types/index.js +0 -8
- package/src/utils/filter-converter.d.ts +0 -57
- package/src/utils/filter-converter.js +0 -100
- package/src/validation/schema-validator.d.ts +0 -94
- package/src/validation/schema-validator.js +0 -278
|
@@ -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
|
+
}
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
* @packageDocumentation
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { ExpressionContext } from './ExpressionContext';
|
|
19
|
+
import { ExpressionContext } from './ExpressionContext.js';
|
|
20
|
+
import { ExpressionCache } from './ExpressionCache.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Options for expression evaluation
|
|
@@ -45,13 +46,17 @@ export interface EvaluationOptions {
|
|
|
45
46
|
*/
|
|
46
47
|
export class ExpressionEvaluator {
|
|
47
48
|
private context: ExpressionContext;
|
|
49
|
+
private cache: ExpressionCache;
|
|
48
50
|
|
|
49
|
-
constructor(context?: ExpressionContext | Record<string, any
|
|
51
|
+
constructor(context?: ExpressionContext | Record<string, any>, cache?: ExpressionCache) {
|
|
50
52
|
if (context instanceof ExpressionContext) {
|
|
51
53
|
this.context = context;
|
|
52
54
|
} else {
|
|
53
55
|
this.context = new ExpressionContext(context || {});
|
|
54
56
|
}
|
|
57
|
+
|
|
58
|
+
// Use provided cache or create a new one
|
|
59
|
+
this.cache = cache || new ExpressionCache();
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
/**
|
|
@@ -138,17 +143,11 @@ export class ExpressionEvaluator {
|
|
|
138
143
|
const varNames = Object.keys(contextObj);
|
|
139
144
|
const varValues = Object.values(contextObj);
|
|
140
145
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
// 1. Sanitization check (isDangerous) blocks dangerous patterns
|
|
144
|
-
// 2. Strict mode enabled ("use strict")
|
|
145
|
-
// 3. Limited scope (only contextObj variables available)
|
|
146
|
-
// 4. No access to global objects (process, window, etc.)
|
|
147
|
-
// For production use, consider: expr-eval, safe-eval, or a custom parser
|
|
148
|
-
const fn = new Function(...varNames, `"use strict"; return (${expression});`);
|
|
146
|
+
// Use cached compilation
|
|
147
|
+
const compiled = this.cache.compile(expression, varNames);
|
|
149
148
|
|
|
150
149
|
// Execute with context values
|
|
151
|
-
return fn(...varValues);
|
|
150
|
+
return compiled.fn(...varValues);
|
|
152
151
|
} catch (error) {
|
|
153
152
|
throw new Error(`Failed to evaluate expression "${expression}": ${(error as Error).message}`);
|
|
154
153
|
}
|
|
@@ -220,10 +219,30 @@ export class ExpressionEvaluator {
|
|
|
220
219
|
* Create a new evaluator with additional context data
|
|
221
220
|
*/
|
|
222
221
|
withContext(data: Record<string, any>): ExpressionEvaluator {
|
|
223
|
-
|
|
222
|
+
// Share the cache with the new evaluator for maximum efficiency
|
|
223
|
+
return new ExpressionEvaluator(this.context.createChild(data), this.cache);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get cache statistics (useful for debugging and optimization)
|
|
228
|
+
*/
|
|
229
|
+
getCacheStats() {
|
|
230
|
+
return this.cache.getStats();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Clear the expression cache
|
|
235
|
+
*/
|
|
236
|
+
clearCache(): void {
|
|
237
|
+
this.cache.clear();
|
|
224
238
|
}
|
|
225
239
|
}
|
|
226
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Shared global cache for convenience functions
|
|
243
|
+
*/
|
|
244
|
+
const globalCache = new ExpressionCache();
|
|
245
|
+
|
|
227
246
|
/**
|
|
228
247
|
* Convenience function to quickly evaluate an expression
|
|
229
248
|
*/
|
|
@@ -232,7 +251,7 @@ export function evaluateExpression(
|
|
|
232
251
|
context: Record<string, any> = {},
|
|
233
252
|
options: EvaluationOptions = {}
|
|
234
253
|
): any {
|
|
235
|
-
const evaluator = new ExpressionEvaluator(context);
|
|
254
|
+
const evaluator = new ExpressionEvaluator(context, globalCache);
|
|
236
255
|
return evaluator.evaluate(expression, options);
|
|
237
256
|
}
|
|
238
257
|
|
|
@@ -243,6 +262,6 @@ export function evaluateCondition(
|
|
|
243
262
|
condition: string | boolean | undefined,
|
|
244
263
|
context: Record<string, any> = {}
|
|
245
264
|
): boolean {
|
|
246
|
-
const evaluator = new ExpressionEvaluator(context);
|
|
265
|
+
const evaluator = new ExpressionEvaluator(context, globalCache);
|
|
247
266
|
return evaluator.evaluateCondition(condition);
|
|
248
267
|
}
|
|
@@ -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
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
+
import { ExpressionCache } from '../ExpressionCache';
|
|
11
|
+
|
|
12
|
+
describe('ExpressionCache', () => {
|
|
13
|
+
let cache: ExpressionCache;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
cache = new ExpressionCache();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should compile and cache an expression', () => {
|
|
20
|
+
const expr = 'data.amount > 1000';
|
|
21
|
+
const varNames = ['data'];
|
|
22
|
+
|
|
23
|
+
const compiled = cache.compile(expr, varNames);
|
|
24
|
+
|
|
25
|
+
expect(compiled).toBeDefined();
|
|
26
|
+
expect(compiled.expression).toBe(expr);
|
|
27
|
+
expect(compiled.varNames).toEqual(varNames);
|
|
28
|
+
expect(compiled.hitCount).toBe(1);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should return cached expression on second call', () => {
|
|
32
|
+
const expr = 'data.amount > 1000';
|
|
33
|
+
const varNames = ['data'];
|
|
34
|
+
|
|
35
|
+
const first = cache.compile(expr, varNames);
|
|
36
|
+
const second = cache.compile(expr, varNames);
|
|
37
|
+
|
|
38
|
+
expect(first).toBe(second);
|
|
39
|
+
expect(second.hitCount).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should execute compiled expression correctly', () => {
|
|
43
|
+
const expr = 'data.amount > 1000';
|
|
44
|
+
const varNames = ['data'];
|
|
45
|
+
|
|
46
|
+
const compiled = cache.compile(expr, varNames);
|
|
47
|
+
const result = compiled.fn({ amount: 1500 });
|
|
48
|
+
|
|
49
|
+
expect(result).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should handle multiple expressions', () => {
|
|
53
|
+
const expr1 = 'data.amount > 1000';
|
|
54
|
+
const expr2 = 'data.name === "John"';
|
|
55
|
+
const varNames = ['data'];
|
|
56
|
+
|
|
57
|
+
const compiled1 = cache.compile(expr1, varNames);
|
|
58
|
+
const compiled2 = cache.compile(expr2, varNames);
|
|
59
|
+
|
|
60
|
+
expect(compiled1).not.toBe(compiled2);
|
|
61
|
+
expect(compiled1.fn({ amount: 1500 })).toBe(true);
|
|
62
|
+
expect(compiled2.fn({ name: 'John' })).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should differentiate between different variable contexts', () => {
|
|
66
|
+
const expr = 'x + y';
|
|
67
|
+
|
|
68
|
+
const compiled1 = cache.compile(expr, ['x', 'y']);
|
|
69
|
+
const compiled2 = cache.compile(expr, ['x', 'y', 'z']);
|
|
70
|
+
|
|
71
|
+
// Different variable contexts should create different cache entries
|
|
72
|
+
expect(compiled1).not.toBe(compiled2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should provide cache statistics', () => {
|
|
76
|
+
cache.compile('data.amount > 1000', ['data']);
|
|
77
|
+
cache.compile('data.amount > 1000', ['data']); // Second call, increment hit
|
|
78
|
+
cache.compile('data.name === "John"', ['data']);
|
|
79
|
+
|
|
80
|
+
const stats = cache.getStats();
|
|
81
|
+
|
|
82
|
+
expect(stats.size).toBe(2);
|
|
83
|
+
expect(stats.totalHits).toBe(3);
|
|
84
|
+
expect(stats.entries).toHaveLength(2);
|
|
85
|
+
expect(stats.entries[0].hitCount).toBe(2); // Most frequently used
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should evict LRU when cache is full', () => {
|
|
89
|
+
const smallCache = new ExpressionCache(3);
|
|
90
|
+
|
|
91
|
+
smallCache.compile('expr1', ['x']);
|
|
92
|
+
smallCache.compile('expr2', ['x']);
|
|
93
|
+
smallCache.compile('expr3', ['x']);
|
|
94
|
+
|
|
95
|
+
// Access expr1 multiple times to increase hit count
|
|
96
|
+
smallCache.compile('expr1', ['x']);
|
|
97
|
+
smallCache.compile('expr1', ['x']);
|
|
98
|
+
|
|
99
|
+
// Add a 4th expression, should evict least used (expr2 or expr3)
|
|
100
|
+
smallCache.compile('expr4', ['x']);
|
|
101
|
+
|
|
102
|
+
const stats = smallCache.getStats();
|
|
103
|
+
expect(stats.size).toBe(3);
|
|
104
|
+
|
|
105
|
+
// expr1 should still be cached (highest hit count)
|
|
106
|
+
expect(smallCache.has('expr1', ['x'])).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should clear cache', () => {
|
|
110
|
+
cache.compile('data.amount > 1000', ['data']);
|
|
111
|
+
cache.compile('data.name === "John"', ['data']);
|
|
112
|
+
|
|
113
|
+
expect(cache.getStats().size).toBe(2);
|
|
114
|
+
|
|
115
|
+
cache.clear();
|
|
116
|
+
|
|
117
|
+
expect(cache.getStats().size).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should handle complex expressions', () => {
|
|
121
|
+
const expr = 'data.items.filter(item => item.price > 100).length';
|
|
122
|
+
const varNames = ['data'];
|
|
123
|
+
|
|
124
|
+
const compiled = cache.compile(expr, varNames);
|
|
125
|
+
const result = compiled.fn({
|
|
126
|
+
items: [
|
|
127
|
+
{ price: 50 },
|
|
128
|
+
{ price: 150 },
|
|
129
|
+
{ price: 200 },
|
|
130
|
+
],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result).toBe(2);
|
|
134
|
+
});
|
|
135
|
+
});
|
package/src/evaluator/index.ts
CHANGED
|
@@ -6,5 +6,6 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export * from './ExpressionContext';
|
|
10
|
-
export * from './ExpressionEvaluator';
|
|
9
|
+
export * from './ExpressionContext.js';
|
|
10
|
+
export * from './ExpressionEvaluator.js';
|
|
11
|
+
export * from './ExpressionCache.js';
|
package/src/index.ts
CHANGED
|
@@ -6,13 +6,16 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
export * from './registry/Registry';
|
|
11
|
-
export * from './
|
|
12
|
-
export * from './
|
|
13
|
-
export * from './
|
|
14
|
-
export * from './
|
|
15
|
-
export * from './
|
|
9
|
+
export type { SchemaNode, ComponentRendererProps } from './types/index.js';
|
|
10
|
+
export * from './registry/Registry.js';
|
|
11
|
+
export * from './registry/PluginSystem.js';
|
|
12
|
+
export * from './registry/PluginScopeImpl.js';
|
|
13
|
+
export * from './validation/index.js';
|
|
14
|
+
export * from './builder/schema-builder.js';
|
|
15
|
+
export * from './utils/filter-converter.js';
|
|
16
|
+
export * from './evaluator/index.js';
|
|
17
|
+
export * from './actions/index.js';
|
|
18
|
+
export * from './query/index.js';
|
|
16
19
|
// export * from './data-scope'; // TODO
|
|
17
20
|
// export * from './validators'; // TODO
|
|
18
21
|
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/core - Query AST Builder Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { QueryASTBuilder } from '../query-ast';
|
|
7
|
+
import type { QuerySchema } from '@object-ui/types';
|
|
8
|
+
|
|
9
|
+
describe('QueryASTBuilder', () => {
|
|
10
|
+
const builder = new QueryASTBuilder();
|
|
11
|
+
|
|
12
|
+
describe('Basic Query Building', () => {
|
|
13
|
+
it('should build simple SELECT query', () => {
|
|
14
|
+
const query: QuerySchema = {
|
|
15
|
+
object: 'users',
|
|
16
|
+
fields: ['id', 'name', 'email'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const ast = builder.build(query);
|
|
20
|
+
|
|
21
|
+
expect(ast.select.type).toBe('select');
|
|
22
|
+
expect(ast.select.fields).toHaveLength(3);
|
|
23
|
+
expect(ast.from.table).toBe('users');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should build SELECT * when no fields specified', () => {
|
|
27
|
+
const query: QuerySchema = {
|
|
28
|
+
object: 'users',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ast = builder.build(query);
|
|
32
|
+
|
|
33
|
+
expect(ast.select.fields).toHaveLength(1);
|
|
34
|
+
expect(ast.select.fields[0]).toMatchObject({
|
|
35
|
+
type: 'field',
|
|
36
|
+
name: '*',
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should build query with WHERE clause', () => {
|
|
41
|
+
const query: QuerySchema = {
|
|
42
|
+
object: 'users',
|
|
43
|
+
fields: ['id', 'name'],
|
|
44
|
+
filter: {
|
|
45
|
+
conditions: [
|
|
46
|
+
{
|
|
47
|
+
field: 'status',
|
|
48
|
+
operator: 'equals',
|
|
49
|
+
value: 'active',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const ast = builder.build(query);
|
|
56
|
+
|
|
57
|
+
expect(ast.where).toBeDefined();
|
|
58
|
+
expect(ast.where?.type).toBe('where');
|
|
59
|
+
expect(ast.where?.condition.type).toBe('operator');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should build query with ORDER BY', () => {
|
|
63
|
+
const query: QuerySchema = {
|
|
64
|
+
object: 'users',
|
|
65
|
+
fields: ['id', 'name'],
|
|
66
|
+
sort: [
|
|
67
|
+
{ field: 'created_at', order: 'desc' },
|
|
68
|
+
{ field: 'name', order: 'asc' },
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const ast = builder.build(query);
|
|
73
|
+
|
|
74
|
+
expect(ast.order_by).toBeDefined();
|
|
75
|
+
expect(ast.order_by?.fields).toHaveLength(2);
|
|
76
|
+
expect(ast.order_by?.fields[0].direction).toBe('desc');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should build query with LIMIT and OFFSET', () => {
|
|
80
|
+
const query: QuerySchema = {
|
|
81
|
+
object: 'users',
|
|
82
|
+
fields: ['id', 'name'],
|
|
83
|
+
limit: 10,
|
|
84
|
+
offset: 20,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const ast = builder.build(query);
|
|
88
|
+
|
|
89
|
+
expect(ast.limit).toBeDefined();
|
|
90
|
+
expect(ast.limit?.value).toBe(10);
|
|
91
|
+
expect(ast.offset).toBeDefined();
|
|
92
|
+
expect(ast.offset?.value).toBe(20);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('Advanced Query Building', () => {
|
|
97
|
+
it('should build query with JOIN', () => {
|
|
98
|
+
const query: QuerySchema = {
|
|
99
|
+
object: 'users',
|
|
100
|
+
fields: ['id', 'name', 'orders.total'],
|
|
101
|
+
joins: [
|
|
102
|
+
{
|
|
103
|
+
type: 'left',
|
|
104
|
+
object: 'orders',
|
|
105
|
+
on: {
|
|
106
|
+
local_field: 'id',
|
|
107
|
+
foreign_field: 'user_id',
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const ast = builder.build(query);
|
|
114
|
+
|
|
115
|
+
expect(ast.joins).toBeDefined();
|
|
116
|
+
expect(ast.joins).toHaveLength(1);
|
|
117
|
+
expect(ast.joins?.[0].join_type).toBe('left');
|
|
118
|
+
expect(ast.joins?.[0].table).toBe('orders');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should build query with aggregations', () => {
|
|
122
|
+
const query: QuerySchema = {
|
|
123
|
+
object: 'orders',
|
|
124
|
+
aggregations: [
|
|
125
|
+
{
|
|
126
|
+
function: 'count',
|
|
127
|
+
alias: 'total_count',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
function: 'sum',
|
|
131
|
+
field: 'amount',
|
|
132
|
+
alias: 'total_amount',
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const ast = builder.build(query);
|
|
138
|
+
|
|
139
|
+
expect(ast.select.fields).toHaveLength(2);
|
|
140
|
+
expect(ast.select.fields[0]).toMatchObject({
|
|
141
|
+
type: 'aggregate',
|
|
142
|
+
function: 'count',
|
|
143
|
+
alias: 'total_count',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should build query with GROUP BY', () => {
|
|
148
|
+
const query: QuerySchema = {
|
|
149
|
+
object: 'orders',
|
|
150
|
+
fields: ['user_id'],
|
|
151
|
+
group_by: ['user_id'],
|
|
152
|
+
aggregations: [
|
|
153
|
+
{
|
|
154
|
+
function: 'count',
|
|
155
|
+
alias: 'order_count',
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const ast = builder.build(query);
|
|
161
|
+
|
|
162
|
+
expect(ast.group_by).toBeDefined();
|
|
163
|
+
expect(ast.group_by?.fields).toHaveLength(1);
|
|
164
|
+
expect(ast.group_by?.fields[0]).toMatchObject({
|
|
165
|
+
type: 'field',
|
|
166
|
+
name: 'user_id',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('Complex Filters', () => {
|
|
172
|
+
it('should build query with nested AND/OR filters', () => {
|
|
173
|
+
const query: QuerySchema = {
|
|
174
|
+
object: 'users',
|
|
175
|
+
filter: {
|
|
176
|
+
operator: 'and',
|
|
177
|
+
conditions: [
|
|
178
|
+
{
|
|
179
|
+
field: 'status',
|
|
180
|
+
operator: 'equals',
|
|
181
|
+
value: 'active',
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
groups: [
|
|
185
|
+
{
|
|
186
|
+
operator: 'or',
|
|
187
|
+
conditions: [
|
|
188
|
+
{
|
|
189
|
+
field: 'role',
|
|
190
|
+
operator: 'equals',
|
|
191
|
+
value: 'admin',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
field: 'role',
|
|
195
|
+
operator: 'equals',
|
|
196
|
+
value: 'moderator',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const ast = builder.build(query);
|
|
205
|
+
|
|
206
|
+
expect(ast.where).toBeDefined();
|
|
207
|
+
expect(ast.where?.condition.operator).toBe('and');
|
|
208
|
+
expect(ast.where?.condition.operands.length).toBeGreaterThan(0);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|