@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/actions/ActionRunner.d.ts +40 -0
  3. package/dist/actions/ActionRunner.js +160 -0
  4. package/dist/actions/index.d.ts +8 -0
  5. package/dist/actions/index.js +8 -0
  6. package/dist/adapters/index.d.ts +7 -0
  7. package/dist/adapters/index.js +10 -0
  8. package/dist/builder/schema-builder.d.ts +7 -0
  9. package/dist/builder/schema-builder.js +4 -6
  10. package/dist/evaluator/ExpressionContext.d.ts +51 -0
  11. package/dist/evaluator/ExpressionContext.js +110 -0
  12. package/dist/evaluator/ExpressionEvaluator.d.ts +99 -0
  13. package/dist/evaluator/ExpressionEvaluator.js +200 -0
  14. package/dist/evaluator/index.d.ts +9 -0
  15. package/dist/evaluator/index.js +9 -0
  16. package/dist/index.d.ts +10 -0
  17. package/dist/index.js +10 -1
  18. package/dist/registry/Registry.d.ts +7 -0
  19. package/dist/registry/Registry.js +7 -0
  20. package/dist/types/index.d.ts +7 -0
  21. package/dist/types/index.js +7 -0
  22. package/dist/utils/filter-converter.d.ts +57 -0
  23. package/dist/utils/filter-converter.js +100 -0
  24. package/dist/validation/schema-validator.d.ts +7 -0
  25. package/dist/validation/schema-validator.js +4 -6
  26. package/package.json +16 -5
  27. package/src/actions/ActionRunner.ts +195 -0
  28. package/src/actions/index.ts +9 -0
  29. package/src/adapters/README.md +180 -0
  30. package/src/adapters/index.d.ts +8 -0
  31. package/src/adapters/index.js +10 -0
  32. package/src/adapters/index.ts +10 -0
  33. package/src/builder/schema-builder.d.ts +7 -0
  34. package/src/builder/schema-builder.js +4 -6
  35. package/src/builder/schema-builder.ts +8 -0
  36. package/src/evaluator/ExpressionContext.ts +118 -0
  37. package/src/evaluator/ExpressionEvaluator.ts +248 -0
  38. package/src/evaluator/__tests__/ExpressionEvaluator.test.ts +101 -0
  39. package/src/evaluator/index.ts +10 -0
  40. package/src/index.d.ts +9 -0
  41. package/src/index.js +10 -1
  42. package/src/index.test.ts +8 -0
  43. package/src/index.ts +11 -1
  44. package/src/registry/Registry.d.ts +7 -0
  45. package/src/registry/Registry.js +7 -0
  46. package/src/registry/Registry.ts +8 -0
  47. package/src/types/index.d.ts +7 -0
  48. package/src/types/index.js +7 -0
  49. package/src/types/index.ts +8 -0
  50. package/src/utils/__tests__/filter-converter.test.ts +118 -0
  51. package/src/utils/filter-converter.d.ts +57 -0
  52. package/src/utils/filter-converter.js +100 -0
  53. package/src/utils/filter-converter.ts +133 -0
  54. package/src/validation/schema-validator.d.ts +7 -0
  55. package/src/validation/schema-validator.js +4 -6
  56. package/src/validation/schema-validator.ts +8 -0
  57. 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 = {
@@ -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", {
@@ -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;
@@ -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;
@@ -1 +1,8 @@
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 {};
@@ -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>;