@object-ui/core 0.3.0 → 0.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 +8 -0
- package/dist/actions/ActionRunner.d.ts +40 -0
- package/dist/actions/ActionRunner.js +160 -0
- package/dist/actions/index.d.ts +8 -0
- package/dist/actions/index.js +8 -0
- package/dist/adapters/index.d.ts +7 -0
- package/dist/adapters/index.js +10 -0
- package/dist/builder/schema-builder.d.ts +7 -0
- package/dist/builder/schema-builder.js +4 -6
- package/dist/evaluator/ExpressionContext.d.ts +51 -0
- package/dist/evaluator/ExpressionContext.js +110 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +99 -0
- package/dist/evaluator/ExpressionEvaluator.js +200 -0
- package/dist/evaluator/index.d.ts +9 -0
- package/dist/evaluator/index.js +9 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -1
- package/dist/registry/Registry.d.ts +7 -0
- package/dist/registry/Registry.js +7 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/index.js +7 -0
- package/dist/utils/filter-converter.d.ts +57 -0
- package/dist/utils/filter-converter.js +100 -0
- package/dist/validation/schema-validator.d.ts +7 -0
- package/dist/validation/schema-validator.js +4 -6
- package/package.json +16 -5
- package/src/actions/ActionRunner.ts +195 -0
- package/src/actions/index.ts +9 -0
- package/src/adapters/README.md +180 -0
- package/src/adapters/index.d.ts +8 -0
- package/src/adapters/index.js +10 -0
- package/src/adapters/index.ts +10 -0
- package/src/builder/schema-builder.d.ts +7 -0
- package/src/builder/schema-builder.js +4 -6
- package/src/builder/schema-builder.ts +8 -0
- package/src/evaluator/ExpressionContext.ts +118 -0
- package/src/evaluator/ExpressionEvaluator.ts +248 -0
- package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
- package/src/evaluator/index.ts +10 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +10 -1
- package/src/index.test.ts +8 -0
- package/src/index.ts +11 -1
- package/src/registry/Registry.d.ts +7 -0
- package/src/registry/Registry.js +7 -0
- package/src/registry/Registry.ts +8 -0
- package/src/types/index.d.ts +7 -0
- package/src/types/index.js +7 -0
- package/src/types/index.ts +8 -0
- package/src/utils/__tests__/filter-converter.test.ts +118 -0
- package/src/utils/filter-converter.d.ts +57 -0
- package/src/utils/filter-converter.js +100 -0
- package/src/utils/filter-converter.ts +133 -0
- package/src/validation/schema-validator.d.ts +7 -0
- package/src/validation/schema-validator.js +4 -6
- package/src/validation/schema-validator.ts +8 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
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 Evaluator
|
|
11
|
+
*
|
|
12
|
+
* Evaluates template string expressions like ${data.amount > 1000} for dynamic UI behavior.
|
|
13
|
+
* Supports variable substitution, comparison operators, and basic JavaScript expressions.
|
|
14
|
+
*
|
|
15
|
+
* @module evaluator
|
|
16
|
+
* @packageDocumentation
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { ExpressionContext } from './ExpressionContext';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for expression evaluation
|
|
23
|
+
*/
|
|
24
|
+
export interface EvaluationOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Default value to return if evaluation fails
|
|
27
|
+
*/
|
|
28
|
+
defaultValue?: any;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Whether to throw errors on evaluation failure
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
throwOnError?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Whether to sanitize the expression before evaluation
|
|
38
|
+
* @default true
|
|
39
|
+
*/
|
|
40
|
+
sanitize?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Expression evaluator for dynamic UI expressions
|
|
45
|
+
*/
|
|
46
|
+
export class ExpressionEvaluator {
|
|
47
|
+
private context: ExpressionContext;
|
|
48
|
+
|
|
49
|
+
constructor(context?: ExpressionContext | Record<string, any>) {
|
|
50
|
+
if (context instanceof ExpressionContext) {
|
|
51
|
+
this.context = context;
|
|
52
|
+
} else {
|
|
53
|
+
this.context = new ExpressionContext(context || {});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Evaluate a string that may contain template expressions like ${...}
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* const evaluator = new ExpressionEvaluator({ data: { amount: 1500 } });
|
|
63
|
+
* evaluator.evaluate('${data.amount > 1000}'); // Returns: true
|
|
64
|
+
* evaluator.evaluate('Amount is ${data.amount}'); // Returns: "Amount is 1500"
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
evaluate(expression: string | boolean | number | null | undefined, options: EvaluationOptions = {}): any {
|
|
68
|
+
// Handle non-string primitives
|
|
69
|
+
if (typeof expression !== 'string') {
|
|
70
|
+
return expression;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { defaultValue, throwOnError = false, sanitize = true } = options;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Check if string contains template expressions
|
|
77
|
+
const hasTemplates = expression.includes('${');
|
|
78
|
+
|
|
79
|
+
if (!hasTemplates) {
|
|
80
|
+
// No templates, return as-is
|
|
81
|
+
return expression;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Special case: if the entire string is a single template expression, return the value directly
|
|
85
|
+
const singleTemplateMatch = expression.match(/^\$\{([^}]+)\}$/);
|
|
86
|
+
if (singleTemplateMatch) {
|
|
87
|
+
return this.evaluateExpression(singleTemplateMatch[1].trim(), { sanitize });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Replace all ${...} expressions in a string with multiple parts
|
|
91
|
+
return expression.replace(/\$\{([^}]+)\}/g, (match, expr) => {
|
|
92
|
+
try {
|
|
93
|
+
const result = this.evaluateExpression(expr.trim(), { sanitize });
|
|
94
|
+
return String(result ?? '');
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (throwOnError) {
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
console.warn(`Expression evaluation failed for: ${expr}`, error);
|
|
100
|
+
return match; // Return original if evaluation fails
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (throwOnError) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
console.warn(`Failed to evaluate expression: ${expression}`, error);
|
|
108
|
+
return defaultValue ?? expression;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Evaluate a single expression (without ${} wrapper)
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* evaluator.evaluateExpression('data.amount > 1000'); // Returns: true
|
|
118
|
+
* evaluator.evaluateExpression('data.user.name'); // Returns: "John"
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
evaluateExpression(expression: string, options: { sanitize?: boolean } = {}): any {
|
|
122
|
+
const { sanitize = true } = options;
|
|
123
|
+
|
|
124
|
+
if (!expression || expression.trim() === '') {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Sanitize expression to prevent dangerous code execution
|
|
129
|
+
if (sanitize && this.isDangerous(expression)) {
|
|
130
|
+
throw new Error(`Potentially dangerous expression detected: ${expression}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
// Create a safe evaluation function
|
|
135
|
+
const contextObj = this.context.toObject();
|
|
136
|
+
|
|
137
|
+
// Build safe function with context variables
|
|
138
|
+
const varNames = Object.keys(contextObj);
|
|
139
|
+
const varValues = Object.values(contextObj);
|
|
140
|
+
|
|
141
|
+
// SECURITY NOTE: Using Function constructor for expression evaluation.
|
|
142
|
+
// This is a controlled use case with:
|
|
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});`);
|
|
149
|
+
|
|
150
|
+
// Execute with context values
|
|
151
|
+
return fn(...varValues);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new Error(`Failed to evaluate expression "${expression}": ${(error as Error).message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Check if expression contains potentially dangerous code
|
|
159
|
+
*/
|
|
160
|
+
private isDangerous(expression: string): boolean {
|
|
161
|
+
const dangerousPatterns = [
|
|
162
|
+
/eval\s*\(/i,
|
|
163
|
+
/Function\s*\(/i,
|
|
164
|
+
/setTimeout\s*\(/i,
|
|
165
|
+
/setInterval\s*\(/i,
|
|
166
|
+
/import\s*\(/i,
|
|
167
|
+
/require\s*\(/i,
|
|
168
|
+
/process\./i,
|
|
169
|
+
/global\./i,
|
|
170
|
+
/window\./i,
|
|
171
|
+
/document\./i,
|
|
172
|
+
/__proto__/i,
|
|
173
|
+
/constructor\s*\(/i,
|
|
174
|
+
/prototype\./i,
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
return dangerousPatterns.some(pattern => pattern.test(expression));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Evaluate a conditional expression and return boolean
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* evaluator.evaluateCondition('${data.age >= 18}'); // Returns: true/false
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
evaluateCondition(condition: string | boolean | undefined, options: EvaluationOptions = {}): boolean {
|
|
189
|
+
if (typeof condition === 'boolean') {
|
|
190
|
+
return condition;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!condition) {
|
|
194
|
+
return true; // Default to visible/enabled if no condition
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const result = this.evaluate(condition, options);
|
|
198
|
+
|
|
199
|
+
// Convert result to boolean
|
|
200
|
+
return Boolean(result);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Update the context with new data
|
|
205
|
+
*/
|
|
206
|
+
updateContext(data: Record<string, any>): void {
|
|
207
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
208
|
+
this.context.set(key, value);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Get the current context
|
|
214
|
+
*/
|
|
215
|
+
getContext(): ExpressionContext {
|
|
216
|
+
return this.context;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a new evaluator with additional context data
|
|
221
|
+
*/
|
|
222
|
+
withContext(data: Record<string, any>): ExpressionEvaluator {
|
|
223
|
+
return new ExpressionEvaluator(this.context.createChild(data));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Convenience function to quickly evaluate an expression
|
|
229
|
+
*/
|
|
230
|
+
export function evaluateExpression(
|
|
231
|
+
expression: string | boolean | number | null | undefined,
|
|
232
|
+
context: Record<string, any> = {},
|
|
233
|
+
options: EvaluationOptions = {}
|
|
234
|
+
): any {
|
|
235
|
+
const evaluator = new ExpressionEvaluator(context);
|
|
236
|
+
return evaluator.evaluate(expression, options);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convenience function to evaluate a condition
|
|
241
|
+
*/
|
|
242
|
+
export function evaluateCondition(
|
|
243
|
+
condition: string | boolean | undefined,
|
|
244
|
+
context: Record<string, any> = {}
|
|
245
|
+
): boolean {
|
|
246
|
+
const evaluator = new ExpressionEvaluator(context);
|
|
247
|
+
return evaluator.evaluateCondition(condition);
|
|
248
|
+
}
|
|
@@ -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
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { ExpressionEvaluator, evaluateExpression, evaluateCondition } from '../ExpressionEvaluator';
|
|
11
|
+
import { ExpressionContext } from '../ExpressionContext';
|
|
12
|
+
|
|
13
|
+
describe('ExpressionContext', () => {
|
|
14
|
+
it('should create context with initial data', () => {
|
|
15
|
+
const ctx = new ExpressionContext({ name: 'John', age: 30 });
|
|
16
|
+
expect(ctx.get('name')).toBe('John');
|
|
17
|
+
expect(ctx.get('age')).toBe(30);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should support nested property access', () => {
|
|
21
|
+
const ctx = new ExpressionContext({
|
|
22
|
+
user: {
|
|
23
|
+
name: 'John',
|
|
24
|
+
profile: {
|
|
25
|
+
email: 'john@example.com'
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(ctx.get('user.name')).toBe('John');
|
|
31
|
+
expect(ctx.get('user.profile.email')).toBe('john@example.com');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should support scope stacking', () => {
|
|
35
|
+
const ctx = new ExpressionContext({ x: 10 });
|
|
36
|
+
expect(ctx.get('x')).toBe(10);
|
|
37
|
+
|
|
38
|
+
ctx.pushScope({ x: 20, y: 30 });
|
|
39
|
+
expect(ctx.get('x')).toBe(20);
|
|
40
|
+
expect(ctx.get('y')).toBe(30);
|
|
41
|
+
|
|
42
|
+
ctx.popScope();
|
|
43
|
+
expect(ctx.get('x')).toBe(10);
|
|
44
|
+
expect(ctx.get('y')).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('ExpressionEvaluator', () => {
|
|
49
|
+
describe('evaluate', () => {
|
|
50
|
+
it('should evaluate simple template expressions', () => {
|
|
51
|
+
const evaluator = new ExpressionEvaluator({ name: 'John' });
|
|
52
|
+
expect(evaluator.evaluate('Hello ${name}!')).toBe('Hello John!');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should evaluate nested property access', () => {
|
|
56
|
+
const evaluator = new ExpressionEvaluator({
|
|
57
|
+
data: { amount: 1500 }
|
|
58
|
+
});
|
|
59
|
+
expect(evaluator.evaluate('Amount: ${data.amount}')).toBe('Amount: 1500');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should handle non-string values', () => {
|
|
63
|
+
const evaluator = new ExpressionEvaluator({});
|
|
64
|
+
expect(evaluator.evaluate(true)).toBe(true);
|
|
65
|
+
expect(evaluator.evaluate(42)).toBe(42);
|
|
66
|
+
expect(evaluator.evaluate(null)).toBe(null);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('evaluateExpression', () => {
|
|
71
|
+
it('should evaluate comparison operators', () => {
|
|
72
|
+
const evaluator = new ExpressionEvaluator({ data: { amount: 1500 } });
|
|
73
|
+
|
|
74
|
+
expect(evaluator.evaluateExpression('data.amount > 1000')).toBe(true);
|
|
75
|
+
expect(evaluator.evaluateExpression('data.amount < 1000')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should block dangerous expressions', () => {
|
|
79
|
+
const evaluator = new ExpressionEvaluator({});
|
|
80
|
+
|
|
81
|
+
expect(() => evaluator.evaluateExpression('eval("malicious")')).toThrow();
|
|
82
|
+
expect(() => evaluator.evaluateExpression('process.exit()')).toThrow();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('evaluateCondition', () => {
|
|
87
|
+
it('should return boolean for condition expressions', () => {
|
|
88
|
+
const evaluator = new ExpressionEvaluator({ data: { age: 25 } });
|
|
89
|
+
|
|
90
|
+
expect(evaluator.evaluateCondition('${data.age >= 18}')).toBe(true);
|
|
91
|
+
expect(evaluator.evaluateCondition('${data.age < 18}')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle boolean values directly', () => {
|
|
95
|
+
const evaluator = new ExpressionEvaluator({});
|
|
96
|
+
|
|
97
|
+
expect(evaluator.evaluateCondition(true)).toBe(true);
|
|
98
|
+
expect(evaluator.evaluateCondition(false)).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -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 * from './ExpressionContext';
|
|
10
|
+
export * from './ExpressionEvaluator';
|
package/src/index.d.ts
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
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
|
+
*/
|
|
1
8
|
export * from './types';
|
|
2
9
|
export * from './registry/Registry';
|
|
3
10
|
export * from './validation/schema-validator';
|
|
4
11
|
export * from './builder/schema-builder';
|
|
12
|
+
export * from './adapters';
|
|
13
|
+
export * from './utils/filter-converter';
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
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
|
+
*/
|
|
1
8
|
export * from './types';
|
|
2
9
|
export * from './registry/Registry';
|
|
3
10
|
export * from './validation/schema-validator';
|
|
4
11
|
export * from './builder/schema-builder';
|
|
12
|
+
export * from './utils/filter-converter';
|
|
13
|
+
export * from './evaluator';
|
|
14
|
+
export * from './actions';
|
|
5
15
|
// export * from './data-scope'; // TODO
|
|
6
|
-
// export * from './evaluator'; // TODO
|
|
7
16
|
// export * from './validators'; // TODO
|
package/src/index.test.ts
CHANGED
|
@@ -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
|
import { describe, it, expect } from 'vitest';
|
|
2
10
|
|
|
3
11
|
describe('core', () => {
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
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
|
export * from './types';
|
|
2
10
|
export * from './registry/Registry';
|
|
3
11
|
export * from './validation/schema-validator';
|
|
4
12
|
export * from './builder/schema-builder';
|
|
13
|
+
export * from './utils/filter-converter';
|
|
14
|
+
export * from './evaluator';
|
|
15
|
+
export * from './actions';
|
|
5
16
|
// export * from './data-scope'; // TODO
|
|
6
|
-
// export * from './evaluator'; // TODO
|
|
7
17
|
// export * from './validators'; // TODO
|
|
8
18
|
|
|
@@ -1,3 +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
|
+
*/
|
|
1
8
|
import type { SchemaNode } from '../types';
|
|
2
9
|
export type ComponentRenderer<T = any> = T;
|
|
3
10
|
export type ComponentInput = {
|
package/src/registry/Registry.js
CHANGED
|
@@ -1,3 +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
|
+
*/
|
|
1
8
|
export class Registry {
|
|
2
9
|
constructor() {
|
|
3
10
|
Object.defineProperty(this, "components", {
|
package/src/registry/Registry.ts
CHANGED
|
@@ -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
|
import type { SchemaNode } from '../types';
|
|
2
10
|
|
|
3
11
|
export type ComponentRenderer<T = any> = T;
|
package/src/types/index.d.ts
CHANGED
|
@@ -1,3 +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
|
+
*/
|
|
1
8
|
export interface SchemaNode {
|
|
2
9
|
type: string;
|
|
3
10
|
id?: string;
|
package/src/types/index.js
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -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
|
export interface SchemaNode {
|
|
2
10
|
type: string;
|
|
3
11
|
id?: string;
|
|
@@ -0,0 +1,118 @@
|
|
|
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, vi } from 'vitest';
|
|
10
|
+
import { convertFiltersToAST, convertOperatorToAST, type FilterNode } from '../filter-converter';
|
|
11
|
+
|
|
12
|
+
describe('Filter Converter Utilities', () => {
|
|
13
|
+
describe('convertOperatorToAST', () => {
|
|
14
|
+
it('should convert known operators', () => {
|
|
15
|
+
expect(convertOperatorToAST('$eq')).toBe('=');
|
|
16
|
+
expect(convertOperatorToAST('$ne')).toBe('!=');
|
|
17
|
+
expect(convertOperatorToAST('$gt')).toBe('>');
|
|
18
|
+
expect(convertOperatorToAST('$gte')).toBe('>=');
|
|
19
|
+
expect(convertOperatorToAST('$lt')).toBe('<');
|
|
20
|
+
expect(convertOperatorToAST('$lte')).toBe('<=');
|
|
21
|
+
expect(convertOperatorToAST('$in')).toBe('in');
|
|
22
|
+
expect(convertOperatorToAST('$nin')).toBe('notin');
|
|
23
|
+
expect(convertOperatorToAST('$notin')).toBe('notin');
|
|
24
|
+
expect(convertOperatorToAST('$contains')).toBe('contains');
|
|
25
|
+
expect(convertOperatorToAST('$startswith')).toBe('startswith');
|
|
26
|
+
expect(convertOperatorToAST('$between')).toBe('between');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should return null for unknown operators', () => {
|
|
30
|
+
expect(convertOperatorToAST('$unknown')).toBe(null);
|
|
31
|
+
expect(convertOperatorToAST('$exists')).toBe(null);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('convertFiltersToAST', () => {
|
|
36
|
+
it('should convert simple equality filter', () => {
|
|
37
|
+
const result = convertFiltersToAST({ status: 'active' });
|
|
38
|
+
expect(result).toEqual(['status', '=', 'active']);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should convert single operator filter', () => {
|
|
42
|
+
const result = convertFiltersToAST({ age: { $gte: 18 } });
|
|
43
|
+
expect(result).toEqual(['age', '>=', 18]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should convert multiple operators on same field', () => {
|
|
47
|
+
const result = convertFiltersToAST({ age: { $gte: 18, $lte: 65 } }) as FilterNode;
|
|
48
|
+
expect(result[0]).toBe('and');
|
|
49
|
+
expect(result.slice(1)).toContainEqual(['age', '>=', 18]);
|
|
50
|
+
expect(result.slice(1)).toContainEqual(['age', '<=', 65]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should convert multiple fields with and logic', () => {
|
|
54
|
+
const result = convertFiltersToAST({
|
|
55
|
+
age: { $gte: 18 },
|
|
56
|
+
status: 'active'
|
|
57
|
+
}) as FilterNode;
|
|
58
|
+
expect(result[0]).toBe('and');
|
|
59
|
+
expect(result.slice(1)).toContainEqual(['age', '>=', 18]);
|
|
60
|
+
expect(result.slice(1)).toContainEqual(['status', '=', 'active']);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should handle $in operator', () => {
|
|
64
|
+
const result = convertFiltersToAST({
|
|
65
|
+
status: { $in: ['active', 'pending'] }
|
|
66
|
+
});
|
|
67
|
+
expect(result).toEqual(['status', 'in', ['active', 'pending']]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should handle $nin operator', () => {
|
|
71
|
+
const result = convertFiltersToAST({
|
|
72
|
+
status: { $nin: ['archived'] }
|
|
73
|
+
});
|
|
74
|
+
expect(result).toEqual(['status', 'notin', ['archived']]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should warn on $regex operator and convert to contains', () => {
|
|
78
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
79
|
+
|
|
80
|
+
const result = convertFiltersToAST({
|
|
81
|
+
name: { $regex: '^John' }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual(['name', 'contains', '^John']);
|
|
85
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
86
|
+
expect.stringContaining('[ObjectUI] Warning: $regex operator is not fully supported')
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
consoleSpy.mockRestore();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw error on unknown operator', () => {
|
|
93
|
+
expect(() => {
|
|
94
|
+
convertFiltersToAST({ age: { $unknown: 18 } });
|
|
95
|
+
}).toThrow('[ObjectUI] Unknown filter operator');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should skip null and undefined values', () => {
|
|
99
|
+
const result = convertFiltersToAST({
|
|
100
|
+
name: 'John',
|
|
101
|
+
age: null,
|
|
102
|
+
email: undefined
|
|
103
|
+
});
|
|
104
|
+
expect(result).toEqual(['name', '=', 'John']);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should return original filter if empty after filtering', () => {
|
|
108
|
+
const result = convertFiltersToAST({
|
|
109
|
+
age: null,
|
|
110
|
+
email: undefined
|
|
111
|
+
});
|
|
112
|
+
expect(result).toEqual({
|
|
113
|
+
age: null,
|
|
114
|
+
email: undefined
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
* Filter Converter Utilities
|
|
10
|
+
*
|
|
11
|
+
* Shared utilities for converting MongoDB-like filter operators
|
|
12
|
+
* to ObjectStack FilterNode AST format.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* FilterNode AST type definition
|
|
16
|
+
* Represents a filter condition or a logical combination of conditions
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Simple condition
|
|
20
|
+
* ['status', '=', 'active']
|
|
21
|
+
*
|
|
22
|
+
* // Logical combination
|
|
23
|
+
* ['and', ['age', '>=', 18], ['status', '=', 'active']]
|
|
24
|
+
*/
|
|
25
|
+
export type FilterNode = [string, string, any] | [string, ...FilterNode[]];
|
|
26
|
+
/**
|
|
27
|
+
* Map MongoDB-like operators to ObjectStack filter operators.
|
|
28
|
+
*
|
|
29
|
+
* @param operator - MongoDB-style operator (e.g., '$gte', '$in')
|
|
30
|
+
* @returns ObjectStack operator or null if not recognized
|
|
31
|
+
*/
|
|
32
|
+
export declare function convertOperatorToAST(operator: string): string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Convert object-based filters to ObjectStack FilterNode AST format.
|
|
35
|
+
* Converts MongoDB-like operators to ObjectStack filter expressions.
|
|
36
|
+
*
|
|
37
|
+
* @param filter - Object-based filter with optional operators
|
|
38
|
+
* @returns FilterNode AST array
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Simple filter - converted to AST
|
|
42
|
+
* convertFiltersToAST({ status: 'active' })
|
|
43
|
+
* // => ['status', '=', 'active']
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Complex filter with operators
|
|
47
|
+
* convertFiltersToAST({ age: { $gte: 18 } })
|
|
48
|
+
* // => ['age', '>=', 18]
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // Multiple conditions
|
|
52
|
+
* convertFiltersToAST({ age: { $gte: 18, $lte: 65 }, status: 'active' })
|
|
53
|
+
* // => ['and', ['age', '>=', 18], ['age', '<=', 65], ['status', '=', 'active']]
|
|
54
|
+
*
|
|
55
|
+
* @throws {Error} If an unknown operator is encountered
|
|
56
|
+
*/
|
|
57
|
+
export declare function convertFiltersToAST(filter: Record<string, any>): FilterNode | Record<string, any>;
|