@object-ui/core 0.5.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +11 -0
- package/dist/actions/ActionRunner.d.ts +228 -4
- package/dist/actions/ActionRunner.js +397 -45
- package/dist/actions/TransactionManager.d.ts +193 -0
- package/dist/actions/TransactionManager.js +410 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/adapters/ApiDataSource.d.ts +69 -0
- package/dist/adapters/ApiDataSource.js +293 -0
- package/dist/adapters/ValueDataSource.d.ts +55 -0
- package/dist/adapters/ValueDataSource.js +287 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +5 -2
- package/dist/adapters/resolveDataSource.d.ts +40 -0
- package/dist/adapters/resolveDataSource.js +59 -0
- package/dist/data-scope/DataScopeManager.d.ts +127 -0
- package/dist/data-scope/DataScopeManager.js +229 -0
- package/dist/data-scope/index.d.ts +10 -0
- package/dist/data-scope/index.js +10 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +11 -1
- package/dist/evaluator/ExpressionEvaluator.js +32 -8
- package/dist/evaluator/FormulaFunctions.d.ts +58 -0
- package/dist/evaluator/FormulaFunctions.js +350 -0
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -2
- package/dist/query/query-ast.d.ts +2 -2
- package/dist/query/query-ast.js +3 -3
- package/dist/registry/Registry.d.ts +10 -0
- package/dist/registry/Registry.js +2 -1
- package/dist/registry/WidgetRegistry.d.ts +120 -0
- package/dist/registry/WidgetRegistry.js +275 -0
- package/dist/theme/ThemeEngine.d.ts +82 -0
- package/dist/theme/ThemeEngine.js +400 -0
- package/dist/theme/index.d.ts +8 -0
- package/dist/theme/index.js +8 -0
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/validation-engine.d.ts +19 -1
- package/dist/validation/validation-engine.js +67 -2
- package/dist/validation/validators/index.d.ts +1 -1
- package/dist/validation/validators/index.js +1 -1
- package/dist/validation/validators/object-validation-engine.d.ts +2 -2
- package/dist/validation/validators/object-validation-engine.js +1 -1
- package/package.json +4 -3
- package/src/actions/ActionRunner.ts +577 -55
- package/src/actions/TransactionManager.ts +521 -0
- package/src/actions/__tests__/ActionRunner.params.test.ts +134 -0
- package/src/actions/__tests__/ActionRunner.test.ts +711 -0
- package/src/actions/__tests__/TransactionManager.test.ts +447 -0
- package/src/actions/index.ts +1 -0
- package/src/adapters/ApiDataSource.ts +349 -0
- package/src/adapters/ValueDataSource.ts +332 -0
- package/src/adapters/__tests__/ApiDataSource.test.ts +418 -0
- package/src/adapters/__tests__/ValueDataSource.test.ts +325 -0
- package/src/adapters/__tests__/resolveDataSource.test.ts +144 -0
- package/src/adapters/index.ts +6 -1
- package/src/adapters/resolveDataSource.ts +79 -0
- package/src/builder/__tests__/schema-builder.test.ts +235 -0
- package/src/data-scope/DataScopeManager.ts +269 -0
- package/src/data-scope/__tests__/DataScopeManager.test.ts +211 -0
- package/src/data-scope/index.ts +16 -0
- package/src/evaluator/ExpressionEvaluator.ts +34 -8
- package/src/evaluator/FormulaFunctions.ts +398 -0
- package/src/evaluator/__tests__/ExpressionContext.test.ts +110 -0
- package/src/evaluator/__tests__/FormulaFunctions.test.ts +447 -0
- package/src/evaluator/index.ts +1 -0
- package/src/index.ts +4 -3
- package/src/query/__tests__/window-functions.test.ts +1 -1
- package/src/query/query-ast.ts +3 -3
- package/src/registry/Registry.ts +12 -1
- package/src/registry/WidgetRegistry.ts +316 -0
- package/src/registry/__tests__/WidgetRegistry.test.ts +321 -0
- package/src/theme/ThemeEngine.ts +452 -0
- package/src/theme/__tests__/ThemeEngine.test.ts +606 -0
- package/src/theme/index.ts +22 -0
- package/src/validation/__tests__/object-validation-engine.test.ts +1 -1
- package/src/validation/__tests__/schema-validator.test.ts +118 -0
- package/src/validation/index.ts +1 -1
- package/src/validation/validation-engine.ts +61 -2
- package/src/validation/validators/index.ts +1 -1
- package/src/validation/validators/object-validation-engine.ts +2 -2
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,447 @@
|
|
|
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 { FormulaFunctions } from '../FormulaFunctions';
|
|
11
|
+
import { ExpressionEvaluator, evaluateExpression as evalExprFn } from '../ExpressionEvaluator';
|
|
12
|
+
|
|
13
|
+
describe('FormulaFunctions', () => {
|
|
14
|
+
describe('Registry', () => {
|
|
15
|
+
it('should register and retrieve custom functions', () => {
|
|
16
|
+
const formulas = new FormulaFunctions();
|
|
17
|
+
formulas.register('DOUBLE', (x: number) => x * 2);
|
|
18
|
+
expect(formulas.has('DOUBLE')).toBe(true);
|
|
19
|
+
expect(formulas.get('DOUBLE')!(5)).toBe(10);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should be case-insensitive', () => {
|
|
23
|
+
const formulas = new FormulaFunctions();
|
|
24
|
+
expect(formulas.has('sum')).toBe(true);
|
|
25
|
+
expect(formulas.has('SUM')).toBe(true);
|
|
26
|
+
expect(formulas.has('Sum')).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should list all registered function names', () => {
|
|
30
|
+
const formulas = new FormulaFunctions();
|
|
31
|
+
const names = formulas.getNames();
|
|
32
|
+
expect(names).toContain('SUM');
|
|
33
|
+
expect(names).toContain('AVG');
|
|
34
|
+
expect(names).toContain('IF');
|
|
35
|
+
expect(names).toContain('TODAY');
|
|
36
|
+
expect(names).toContain('CONCAT');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should export functions as an object', () => {
|
|
40
|
+
const formulas = new FormulaFunctions();
|
|
41
|
+
const obj = formulas.toObject();
|
|
42
|
+
expect(typeof obj.SUM).toBe('function');
|
|
43
|
+
expect(typeof obj.IF).toBe('function');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('Aggregation Functions', () => {
|
|
48
|
+
it('SUM should sum numeric values', () => {
|
|
49
|
+
const formulas = new FormulaFunctions();
|
|
50
|
+
const SUM = formulas.get('SUM')!;
|
|
51
|
+
expect(SUM(1, 2, 3)).toBe(6);
|
|
52
|
+
expect(SUM(10, 20)).toBe(30);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('SUM should handle arrays', () => {
|
|
56
|
+
const formulas = new FormulaFunctions();
|
|
57
|
+
const SUM = formulas.get('SUM')!;
|
|
58
|
+
expect(SUM([1, 2, 3])).toBe(6);
|
|
59
|
+
expect(SUM([10], [20])).toBe(30);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('SUM should handle empty args', () => {
|
|
63
|
+
const formulas = new FormulaFunctions();
|
|
64
|
+
const SUM = formulas.get('SUM')!;
|
|
65
|
+
expect(SUM()).toBe(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('AVG should calculate average', () => {
|
|
69
|
+
const formulas = new FormulaFunctions();
|
|
70
|
+
const AVG = formulas.get('AVG')!;
|
|
71
|
+
expect(AVG(10, 20, 30)).toBe(20);
|
|
72
|
+
expect(AVG([2, 4, 6])).toBe(4);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('AVG should return 0 for empty args', () => {
|
|
76
|
+
const formulas = new FormulaFunctions();
|
|
77
|
+
const AVG = formulas.get('AVG')!;
|
|
78
|
+
expect(AVG()).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('COUNT should count non-null values', () => {
|
|
82
|
+
const formulas = new FormulaFunctions();
|
|
83
|
+
const COUNT = formulas.get('COUNT')!;
|
|
84
|
+
expect(COUNT(1, 2, 3)).toBe(3);
|
|
85
|
+
expect(COUNT(1, null, undefined, 4)).toBe(2);
|
|
86
|
+
expect(COUNT([1, null, 3])).toBe(2);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('MIN should return minimum value', () => {
|
|
90
|
+
const formulas = new FormulaFunctions();
|
|
91
|
+
const MIN = formulas.get('MIN')!;
|
|
92
|
+
expect(MIN(5, 3, 8, 1)).toBe(1);
|
|
93
|
+
expect(MIN([10, 2, 7])).toBe(2);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('MIN should return 0 for empty args', () => {
|
|
97
|
+
const formulas = new FormulaFunctions();
|
|
98
|
+
const MIN = formulas.get('MIN')!;
|
|
99
|
+
expect(MIN()).toBe(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('MAX should return maximum value', () => {
|
|
103
|
+
const formulas = new FormulaFunctions();
|
|
104
|
+
const MAX = formulas.get('MAX')!;
|
|
105
|
+
expect(MAX(5, 3, 8, 1)).toBe(8);
|
|
106
|
+
expect(MAX([10, 2, 7])).toBe(10);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('MAX should return 0 for empty args', () => {
|
|
110
|
+
const formulas = new FormulaFunctions();
|
|
111
|
+
const MAX = formulas.get('MAX')!;
|
|
112
|
+
expect(MAX()).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('Date Functions', () => {
|
|
117
|
+
it('TODAY should return current date string', () => {
|
|
118
|
+
const formulas = new FormulaFunctions();
|
|
119
|
+
const TODAY = formulas.get('TODAY')!;
|
|
120
|
+
const result = TODAY();
|
|
121
|
+
// Should be YYYY-MM-DD format
|
|
122
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('NOW should return ISO timestamp', () => {
|
|
126
|
+
const formulas = new FormulaFunctions();
|
|
127
|
+
const NOW = formulas.get('NOW')!;
|
|
128
|
+
const result = NOW();
|
|
129
|
+
// Should be a valid ISO 8601 date
|
|
130
|
+
expect(new Date(result).toISOString()).toBe(result);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('DATEADD should add days', () => {
|
|
134
|
+
const formulas = new FormulaFunctions();
|
|
135
|
+
const DATEADD = formulas.get('DATEADD')!;
|
|
136
|
+
const result = DATEADD('2025-01-01T00:00:00.000Z', 5, 'days');
|
|
137
|
+
expect(new Date(result).getUTCDate()).toBe(6);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('DATEADD should add months', () => {
|
|
141
|
+
const formulas = new FormulaFunctions();
|
|
142
|
+
const DATEADD = formulas.get('DATEADD')!;
|
|
143
|
+
const result = DATEADD('2025-01-15T00:00:00.000Z', 2, 'months');
|
|
144
|
+
expect(new Date(result).getUTCMonth()).toBe(2); // March (0-indexed)
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('DATEADD should add years', () => {
|
|
148
|
+
const formulas = new FormulaFunctions();
|
|
149
|
+
const DATEADD = formulas.get('DATEADD')!;
|
|
150
|
+
const result = DATEADD('2025-06-15T00:00:00.000Z', 3, 'years');
|
|
151
|
+
expect(new Date(result).getUTCFullYear()).toBe(2028);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('DATEADD should throw for invalid date', () => {
|
|
155
|
+
const formulas = new FormulaFunctions();
|
|
156
|
+
const DATEADD = formulas.get('DATEADD')!;
|
|
157
|
+
expect(() => DATEADD('invalid', 1, 'days')).toThrow('Invalid date');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('DATEADD should throw for unsupported unit', () => {
|
|
161
|
+
const formulas = new FormulaFunctions();
|
|
162
|
+
const DATEADD = formulas.get('DATEADD')!;
|
|
163
|
+
expect(() => DATEADD('2025-01-01', 1, 'weeks')).toThrow('Unsupported unit');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('DATEDIFF should calculate day difference', () => {
|
|
167
|
+
const formulas = new FormulaFunctions();
|
|
168
|
+
const DATEDIFF = formulas.get('DATEDIFF')!;
|
|
169
|
+
expect(DATEDIFF('2025-01-01', '2025-01-11', 'days')).toBe(10);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('DATEDIFF should calculate month difference', () => {
|
|
173
|
+
const formulas = new FormulaFunctions();
|
|
174
|
+
const DATEDIFF = formulas.get('DATEDIFF')!;
|
|
175
|
+
expect(DATEDIFF('2025-01-15', '2025-04-15', 'months')).toBe(3);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('DATEDIFF should calculate year difference', () => {
|
|
179
|
+
const formulas = new FormulaFunctions();
|
|
180
|
+
const DATEDIFF = formulas.get('DATEDIFF')!;
|
|
181
|
+
expect(DATEDIFF('2020-06-01', '2025-06-01', 'years')).toBe(5);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('DATEDIFF should throw for invalid date', () => {
|
|
185
|
+
const formulas = new FormulaFunctions();
|
|
186
|
+
const DATEDIFF = formulas.get('DATEDIFF')!;
|
|
187
|
+
expect(() => DATEDIFF('invalid', '2025-01-01', 'days')).toThrow('Invalid date');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('Logic Functions', () => {
|
|
192
|
+
it('IF should return trueValue when condition is truthy', () => {
|
|
193
|
+
const formulas = new FormulaFunctions();
|
|
194
|
+
const IF = formulas.get('IF')!;
|
|
195
|
+
expect(IF(true, 'yes', 'no')).toBe('yes');
|
|
196
|
+
expect(IF(1, 'yes', 'no')).toBe('yes');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('IF should return falseValue when condition is falsy', () => {
|
|
200
|
+
const formulas = new FormulaFunctions();
|
|
201
|
+
const IF = formulas.get('IF')!;
|
|
202
|
+
expect(IF(false, 'yes', 'no')).toBe('no');
|
|
203
|
+
expect(IF(0, 'yes', 'no')).toBe('no');
|
|
204
|
+
expect(IF(null, 'yes', 'no')).toBe('no');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('AND should return true only when all args are truthy', () => {
|
|
208
|
+
const formulas = new FormulaFunctions();
|
|
209
|
+
const AND = formulas.get('AND')!;
|
|
210
|
+
expect(AND(true, true, true)).toBe(true);
|
|
211
|
+
expect(AND(true, false, true)).toBe(false);
|
|
212
|
+
expect(AND(1, 'hello', true)).toBe(true);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('OR should return true when any arg is truthy', () => {
|
|
216
|
+
const formulas = new FormulaFunctions();
|
|
217
|
+
const OR = formulas.get('OR')!;
|
|
218
|
+
expect(OR(false, false, true)).toBe(true);
|
|
219
|
+
expect(OR(false, false, false)).toBe(false);
|
|
220
|
+
expect(OR(0, '', null)).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('NOT should negate a value', () => {
|
|
224
|
+
const formulas = new FormulaFunctions();
|
|
225
|
+
const NOT = formulas.get('NOT')!;
|
|
226
|
+
expect(NOT(true)).toBe(false);
|
|
227
|
+
expect(NOT(false)).toBe(true);
|
|
228
|
+
expect(NOT(0)).toBe(true);
|
|
229
|
+
expect(NOT(1)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('SWITCH should match cases and return result', () => {
|
|
233
|
+
const formulas = new FormulaFunctions();
|
|
234
|
+
const SWITCH = formulas.get('SWITCH')!;
|
|
235
|
+
expect(SWITCH('a', 'a', 1, 'b', 2, 'c', 3)).toBe(1);
|
|
236
|
+
expect(SWITCH('b', 'a', 1, 'b', 2, 'c', 3)).toBe(2);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('SWITCH should return default when no match', () => {
|
|
240
|
+
const formulas = new FormulaFunctions();
|
|
241
|
+
const SWITCH = formulas.get('SWITCH')!;
|
|
242
|
+
expect(SWITCH('x', 'a', 1, 'b', 2, 'default')).toBe('default');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('SWITCH should return undefined when no match and no default', () => {
|
|
246
|
+
const formulas = new FormulaFunctions();
|
|
247
|
+
const SWITCH = formulas.get('SWITCH')!;
|
|
248
|
+
expect(SWITCH('x', 'a', 1, 'b', 2)).toBeUndefined();
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('String Functions', () => {
|
|
253
|
+
it('CONCAT should join strings', () => {
|
|
254
|
+
const formulas = new FormulaFunctions();
|
|
255
|
+
const CONCAT = formulas.get('CONCAT')!;
|
|
256
|
+
expect(CONCAT('Hello', ' ', 'World')).toBe('Hello World');
|
|
257
|
+
expect(CONCAT('a', 'b', 'c')).toBe('abc');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('CONCAT should handle null/undefined', () => {
|
|
261
|
+
const formulas = new FormulaFunctions();
|
|
262
|
+
const CONCAT = formulas.get('CONCAT')!;
|
|
263
|
+
expect(CONCAT('Hello', null, 'World')).toBe('HelloWorld');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('LEFT should return leftmost characters', () => {
|
|
267
|
+
const formulas = new FormulaFunctions();
|
|
268
|
+
const LEFT = formulas.get('LEFT')!;
|
|
269
|
+
expect(LEFT('Hello World', 5)).toBe('Hello');
|
|
270
|
+
expect(LEFT('Hi', 10)).toBe('Hi');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('RIGHT should return rightmost characters', () => {
|
|
274
|
+
const formulas = new FormulaFunctions();
|
|
275
|
+
const RIGHT = formulas.get('RIGHT')!;
|
|
276
|
+
expect(RIGHT('Hello World', 5)).toBe('World');
|
|
277
|
+
expect(RIGHT('Hi', 10)).toBe('Hi');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('TRIM should remove whitespace', () => {
|
|
281
|
+
const formulas = new FormulaFunctions();
|
|
282
|
+
const TRIM = formulas.get('TRIM')!;
|
|
283
|
+
expect(TRIM(' hello ')).toBe('hello');
|
|
284
|
+
expect(TRIM(' test ')).toBe('test');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('UPPER should convert to uppercase', () => {
|
|
288
|
+
const formulas = new FormulaFunctions();
|
|
289
|
+
const UPPER = formulas.get('UPPER')!;
|
|
290
|
+
expect(UPPER('hello')).toBe('HELLO');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('LOWER should convert to lowercase', () => {
|
|
294
|
+
const formulas = new FormulaFunctions();
|
|
295
|
+
const LOWER = formulas.get('LOWER')!;
|
|
296
|
+
expect(LOWER('HELLO')).toBe('hello');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('String Search Functions', () => {
|
|
301
|
+
const formulas = new FormulaFunctions();
|
|
302
|
+
|
|
303
|
+
it('should find substring position with FIND', () => {
|
|
304
|
+
const FIND = formulas.get('FIND')!;
|
|
305
|
+
expect(FIND('world', 'hello world')).toBe(6);
|
|
306
|
+
expect(FIND('xyz', 'hello world')).toBe(-1);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should find substring from start position', () => {
|
|
310
|
+
const FIND = formulas.get('FIND')!;
|
|
311
|
+
expect(FIND('l', 'hello world', 4)).toBe(9);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('should replace all occurrences with REPLACE', () => {
|
|
315
|
+
const REPLACE = formulas.get('REPLACE')!;
|
|
316
|
+
expect(REPLACE('hello world', 'o', '0')).toBe('hell0 w0rld');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should extract substring with SUBSTRING', () => {
|
|
320
|
+
const SUBSTRING = formulas.get('SUBSTRING')!;
|
|
321
|
+
expect(SUBSTRING('hello world', 6)).toBe('world');
|
|
322
|
+
expect(SUBSTRING('hello world', 0, 5)).toBe('hello');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should test regex pattern with REGEX', () => {
|
|
326
|
+
const REGEX = formulas.get('REGEX')!;
|
|
327
|
+
expect(REGEX('hello123', '\\d+')).toBe(true);
|
|
328
|
+
expect(REGEX('hello', '\\d+')).toBe(false);
|
|
329
|
+
expect(REGEX('Hello', '^hello$', 'i')).toBe(true);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('should get string length with LEN', () => {
|
|
333
|
+
const LEN = formulas.get('LEN')!;
|
|
334
|
+
expect(LEN('hello')).toBe(5);
|
|
335
|
+
expect(LEN('')).toBe(0);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('Statistical Functions', () => {
|
|
340
|
+
const formulas = new FormulaFunctions();
|
|
341
|
+
|
|
342
|
+
it('should calculate MEDIAN for odd count', () => {
|
|
343
|
+
const MEDIAN = formulas.get('MEDIAN')!;
|
|
344
|
+
expect(MEDIAN(3, 1, 2)).toBe(2);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should calculate MEDIAN for even count', () => {
|
|
348
|
+
const MEDIAN = formulas.get('MEDIAN')!;
|
|
349
|
+
expect(MEDIAN(1, 2, 3, 4)).toBe(2.5);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should return 0 for empty MEDIAN', () => {
|
|
353
|
+
const MEDIAN = formulas.get('MEDIAN')!;
|
|
354
|
+
expect(MEDIAN()).toBe(0);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it('should calculate STDEV', () => {
|
|
358
|
+
const STDEV = formulas.get('STDEV')!;
|
|
359
|
+
const result = STDEV(2, 4, 4, 4, 5, 5, 7, 9);
|
|
360
|
+
// Sample standard deviation (Bessel's correction, divides by n-1)
|
|
361
|
+
expect(result).toBeCloseTo(2.138, 2);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should return 0 for STDEV with less than 2 values', () => {
|
|
365
|
+
const STDEV = formulas.get('STDEV')!;
|
|
366
|
+
expect(STDEV(5)).toBe(0);
|
|
367
|
+
expect(STDEV()).toBe(0);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('should calculate VARIANCE', () => {
|
|
371
|
+
const VARIANCE = formulas.get('VARIANCE')!;
|
|
372
|
+
const result = VARIANCE(2, 4, 4, 4, 5, 5, 7, 9);
|
|
373
|
+
// Sample variance (Bessel's correction, divides by n-1)
|
|
374
|
+
expect(result).toBeCloseTo(4.571, 2);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('should calculate PERCENTILE', () => {
|
|
378
|
+
const PERCENTILE = formulas.get('PERCENTILE')!;
|
|
379
|
+
expect(PERCENTILE(0, 1, 2, 3, 4, 5)).toBe(1);
|
|
380
|
+
expect(PERCENTILE(100, 1, 2, 3, 4, 5)).toBe(5);
|
|
381
|
+
expect(PERCENTILE(50, 1, 2, 3, 4, 5)).toBe(3);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should handle PERCENTILE with empty values', () => {
|
|
385
|
+
const PERCENTILE = formulas.get('PERCENTILE')!;
|
|
386
|
+
expect(PERCENTILE(50)).toBe(0);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe('DATEFORMAT Function', () => {
|
|
391
|
+
const formulas = new FormulaFunctions();
|
|
392
|
+
|
|
393
|
+
it('should format date with DATEFORMAT', () => {
|
|
394
|
+
const DATEFORMAT = formulas.get('DATEFORMAT')!;
|
|
395
|
+
const result = DATEFORMAT('2026-02-10T00:00:00.000Z', 'YYYY-MM-DD');
|
|
396
|
+
expect(result).toBe('2026-02-10');
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should throw for invalid date in DATEFORMAT', () => {
|
|
400
|
+
const DATEFORMAT = formulas.get('DATEFORMAT')!;
|
|
401
|
+
expect(() => DATEFORMAT('invalid', 'YYYY')).toThrow('DATEFORMAT: Invalid date');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
describe('ExpressionEvaluator with Formula Functions', () => {
|
|
407
|
+
it('should use SUM in expressions', () => {
|
|
408
|
+
const evaluator = new ExpressionEvaluator({ values: [10, 20, 30] });
|
|
409
|
+
expect(evaluator.evaluateExpression('SUM(values)')).toBe(60);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should use AVG in expressions', () => {
|
|
413
|
+
const evaluator = new ExpressionEvaluator({ values: [10, 20, 30] });
|
|
414
|
+
expect(evaluator.evaluateExpression('AVG(values)')).toBe(20);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('should use IF in expressions', () => {
|
|
418
|
+
const evaluator = new ExpressionEvaluator({ data: { age: 25 } });
|
|
419
|
+
expect(evaluator.evaluateExpression('IF(data.age >= 18, "adult", "minor")')).toBe('adult');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should use CONCAT in template expressions', () => {
|
|
423
|
+
const evaluator = new ExpressionEvaluator({ first: 'John', last: 'Doe' });
|
|
424
|
+
expect(evaluator.evaluate('${CONCAT(first, " ", last)}')).toBe('John Doe');
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should use UPPER in expressions', () => {
|
|
428
|
+
const evaluator = new ExpressionEvaluator({ name: 'hello' });
|
|
429
|
+
expect(evaluator.evaluateExpression('UPPER(name)')).toBe('HELLO');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should support nested formula calls', () => {
|
|
433
|
+
const evaluator = new ExpressionEvaluator({ items: [10, 20, 30] });
|
|
434
|
+
expect(evaluator.evaluateExpression('IF(SUM(items) > 50, "high", "low")')).toBe('high');
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should support registering custom functions', () => {
|
|
438
|
+
const evaluator = new ExpressionEvaluator({ x: 5 });
|
|
439
|
+
evaluator.registerFunction('DOUBLE', (n: number) => n * 2);
|
|
440
|
+
expect(evaluator.evaluateExpression('DOUBLE(x)')).toBe(10);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('should use convenience function with formulas', () => {
|
|
444
|
+
expect(evalExprFn('${SUM(1, 2, 3)}')).toBe(6);
|
|
445
|
+
expect(evalExprFn('${IF(true, "yes", "no")}')).toBe('yes');
|
|
446
|
+
});
|
|
447
|
+
});
|
package/src/evaluator/index.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,12 +10,13 @@ export type { SchemaNode, ComponentRendererProps } from './types/index.js';
|
|
|
10
10
|
export * from './registry/Registry.js';
|
|
11
11
|
export * from './registry/PluginSystem.js';
|
|
12
12
|
export * from './registry/PluginScopeImpl.js';
|
|
13
|
+
export * from './registry/WidgetRegistry.js';
|
|
13
14
|
export * from './validation/index.js';
|
|
14
15
|
export * from './builder/schema-builder.js';
|
|
15
16
|
export * from './utils/filter-converter.js';
|
|
16
17
|
export * from './evaluator/index.js';
|
|
17
18
|
export * from './actions/index.js';
|
|
18
19
|
export * from './query/index.js';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export * from './adapters/index.js';
|
|
21
|
+
export * from './theme/index.js';
|
|
22
|
+
export * from './data-scope/index.js';
|
package/src/query/query-ast.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ObjectUI - Query AST Builder
|
|
3
3
|
* Phase 3.3: QuerySchema AST implementation
|
|
4
|
-
* ObjectStack Spec
|
|
4
|
+
* ObjectStack Spec v2.0.1: Window functions support
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type {
|
|
@@ -82,7 +82,7 @@ export class QueryASTBuilder {
|
|
|
82
82
|
fields.push(...query.aggregations.map(agg => this.buildAggregation(agg)));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// Add window functions (ObjectStack Spec
|
|
85
|
+
// Add window functions (ObjectStack Spec v2.0.1)
|
|
86
86
|
if (query.windows && query.windows.length > 0) {
|
|
87
87
|
fields.push(...query.windows.map(win => this.buildWindow(win)));
|
|
88
88
|
}
|
|
@@ -290,7 +290,7 @@ export class QueryASTBuilder {
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
/**
|
|
293
|
-
* Build window function node (ObjectStack Spec
|
|
293
|
+
* Build window function node (ObjectStack Spec v2.0.1)
|
|
294
294
|
*/
|
|
295
295
|
private buildWindow(config: WindowConfig): WindowNode {
|
|
296
296
|
const node: WindowNode = {
|
package/src/registry/Registry.ts
CHANGED
|
@@ -27,6 +27,16 @@ export type ComponentMeta = {
|
|
|
27
27
|
icon?: string; // Icon name or svg string
|
|
28
28
|
category?: string; // Grouping category
|
|
29
29
|
namespace?: string; // Component namespace (e.g., 'ui', 'plugin-grid', 'field')
|
|
30
|
+
/**
|
|
31
|
+
* When true, prevents the component from being registered with a non-namespaced fallback.
|
|
32
|
+
* Use this when a component should only be accessible via its full namespaced key.
|
|
33
|
+
* This avoids conflicts with other components that share the same base name.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Register as 'view:form' only, don't overwrite 'form'
|
|
37
|
+
* registry.register('form', FormView, { namespace: 'view', skipFallback: true });
|
|
38
|
+
*/
|
|
39
|
+
skipFallback?: boolean;
|
|
30
40
|
inputs?: ComponentInput[];
|
|
31
41
|
defaultProps?: Record<string, any>; // Default props when dropped
|
|
32
42
|
defaultChildren?: SchemaNode[]; // Default children when dropped
|
|
@@ -94,7 +104,8 @@ export class Registry<T = any> {
|
|
|
94
104
|
// This allows "button" to work even when registered as "ui:button"
|
|
95
105
|
// Note: If multiple namespaced components share the same short name,
|
|
96
106
|
// the last registration wins for non-namespaced lookups
|
|
97
|
-
if
|
|
107
|
+
// Skip this if skipFallback is true to avoid overwriting other components
|
|
108
|
+
if (meta?.namespace && !meta?.skipFallback) {
|
|
98
109
|
this.components.set(type, {
|
|
99
110
|
type: fullType, // Keep reference to namespaced type
|
|
100
111
|
component,
|