@object-ui/core 0.5.0 ā 3.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 +28 -0
- package/dist/__benchmarks__/core.bench.d.ts +8 -0
- package/dist/__benchmarks__/core.bench.js +53 -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/errors/index.d.ts +75 -0
- package/dist/errors/index.js +224 -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 +6 -0
- package/dist/index.js +6 -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 +9 -2
- package/dist/registry/WidgetRegistry.d.ts +120 -0
- package/dist/registry/WidgetRegistry.js +275 -0
- package/dist/theme/ThemeEngine.d.ts +105 -0
- package/dist/theme/ThemeEngine.js +469 -0
- package/dist/theme/index.d.ts +8 -0
- package/dist/theme/index.js +8 -0
- package/dist/utils/debug.d.ts +31 -0
- package/dist/utils/debug.js +62 -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 +74 -3
- 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/__benchmarks__/core.bench.ts +64 -0
- 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/errors/__tests__/errors.test.ts +292 -0
- package/src/errors/index.ts +270 -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 +6 -3
- package/src/query/__tests__/window-functions.test.ts +1 -1
- package/src/query/query-ast.ts +3 -3
- package/src/registry/Registry.ts +19 -2
- package/src/registry/WidgetRegistry.ts +316 -0
- package/src/registry/__tests__/WidgetRegistry.test.ts +321 -0
- package/src/theme/ThemeEngine.ts +530 -0
- package/src/theme/__tests__/ThemeEngine.test.ts +668 -0
- package/src/theme/index.ts +24 -0
- package/src/utils/__tests__/debug.test.ts +83 -0
- package/src/utils/debug.ts +66 -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 +70 -3
- 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,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @object-ui/core - DataScope Module
|
|
3
|
+
*
|
|
4
|
+
* Runtime data scope management for row-level security and
|
|
5
|
+
* reactive data state within the UI component tree.
|
|
6
|
+
*
|
|
7
|
+
* @module data-scope
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
DataScopeManager,
|
|
13
|
+
defaultDataScopeManager,
|
|
14
|
+
type RowLevelFilter,
|
|
15
|
+
type DataScopeConfig,
|
|
16
|
+
} from './DataScopeManager.js';
|
|
@@ -0,0 +1,292 @@
|
|
|
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 {
|
|
11
|
+
ObjectUIError,
|
|
12
|
+
SchemaError,
|
|
13
|
+
RegistryError,
|
|
14
|
+
ExpressionError,
|
|
15
|
+
PluginError,
|
|
16
|
+
FieldValidationError,
|
|
17
|
+
ERROR_CODES,
|
|
18
|
+
createError,
|
|
19
|
+
formatErrorMessage,
|
|
20
|
+
isObjectUIError,
|
|
21
|
+
isErrorCode,
|
|
22
|
+
} from '../index';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// ObjectUIError base class
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
describe('ObjectUIError', () => {
|
|
29
|
+
it('should create an error with code and message', () => {
|
|
30
|
+
const err = new ObjectUIError('something broke', 'OBJUI-001');
|
|
31
|
+
|
|
32
|
+
expect(err).toBeInstanceOf(Error);
|
|
33
|
+
expect(err).toBeInstanceOf(ObjectUIError);
|
|
34
|
+
expect(err.name).toBe('ObjectUIError');
|
|
35
|
+
expect(err.message).toBe('something broke');
|
|
36
|
+
expect(err.code).toBe('OBJUI-001');
|
|
37
|
+
expect(err.details).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should accept optional details', () => {
|
|
41
|
+
const details = { type: 'fancy-button' };
|
|
42
|
+
const err = new ObjectUIError('oops', 'OBJUI-001', details);
|
|
43
|
+
|
|
44
|
+
expect(err.details).toEqual(details);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should serialize to JSON', () => {
|
|
48
|
+
const err = new ObjectUIError('oops', 'OBJUI-001', { a: 1 });
|
|
49
|
+
const json = err.toJSON();
|
|
50
|
+
|
|
51
|
+
expect(json).toEqual(
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
name: 'ObjectUIError',
|
|
54
|
+
message: 'oops',
|
|
55
|
+
code: 'OBJUI-001',
|
|
56
|
+
details: { a: 1 },
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
expect(json.stack).toBeDefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should have a proper stack trace', () => {
|
|
63
|
+
const err = new ObjectUIError('trace me', 'OBJUI-001');
|
|
64
|
+
expect(err.stack).toBeDefined();
|
|
65
|
+
expect(err.stack).toContain('trace me');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// ERROR_CODES registry
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
describe('ERROR_CODES', () => {
|
|
74
|
+
it('should contain all 10 defined error codes', () => {
|
|
75
|
+
const expectedCodes = [
|
|
76
|
+
'OBJUI-001',
|
|
77
|
+
'OBJUI-002',
|
|
78
|
+
'OBJUI-003',
|
|
79
|
+
'OBJUI-004',
|
|
80
|
+
'OBJUI-005',
|
|
81
|
+
'OBJUI-006',
|
|
82
|
+
'OBJUI-007',
|
|
83
|
+
'OBJUI-008',
|
|
84
|
+
'OBJUI-009',
|
|
85
|
+
'OBJUI-010',
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const code of expectedCodes) {
|
|
89
|
+
expect(ERROR_CODES[code]).toBeDefined();
|
|
90
|
+
expect(ERROR_CODES[code].code).toBe(code);
|
|
91
|
+
expect(ERROR_CODES[code].message).toBeTruthy();
|
|
92
|
+
expect(ERROR_CODES[code].suggestion).toBeTruthy();
|
|
93
|
+
expect(ERROR_CODES[code].docUrl).toContain(code);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// isErrorCode type guard
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
describe('isErrorCode', () => {
|
|
103
|
+
it('should return true for known codes', () => {
|
|
104
|
+
expect(isErrorCode('OBJUI-001')).toBe(true);
|
|
105
|
+
expect(isErrorCode('OBJUI-010')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return false for unknown codes', () => {
|
|
109
|
+
expect(isErrorCode('OBJUI-999')).toBe(false);
|
|
110
|
+
expect(isErrorCode(42)).toBe(false);
|
|
111
|
+
expect(isErrorCode(null)).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// isObjectUIError type guard
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
describe('isObjectUIError', () => {
|
|
120
|
+
it('should return true for ObjectUIError instances', () => {
|
|
121
|
+
expect(isObjectUIError(new ObjectUIError('a', 'OBJUI-001'))).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should return true for subclass instances', () => {
|
|
125
|
+
expect(isObjectUIError(new SchemaError('bad schema'))).toBe(true);
|
|
126
|
+
expect(isObjectUIError(new ExpressionError('bad expr'))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should return false for plain errors', () => {
|
|
130
|
+
expect(isObjectUIError(new Error('plain'))).toBe(false);
|
|
131
|
+
expect(isObjectUIError('string')).toBe(false);
|
|
132
|
+
expect(isObjectUIError(null)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Specialized error classes
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
describe('SchemaError', () => {
|
|
141
|
+
it('should default to OBJUI-002', () => {
|
|
142
|
+
const err = new SchemaError('bad schema');
|
|
143
|
+
expect(err.name).toBe('SchemaError');
|
|
144
|
+
expect(err.code).toBe('OBJUI-002');
|
|
145
|
+
expect(err).toBeInstanceOf(ObjectUIError);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('RegistryError', () => {
|
|
150
|
+
it('should accept a custom code', () => {
|
|
151
|
+
const err = new RegistryError('not found', 'OBJUI-001');
|
|
152
|
+
expect(err.name).toBe('RegistryError');
|
|
153
|
+
expect(err.code).toBe('OBJUI-001');
|
|
154
|
+
expect(err).toBeInstanceOf(ObjectUIError);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('ExpressionError', () => {
|
|
159
|
+
it('should default to OBJUI-003', () => {
|
|
160
|
+
const err = new ExpressionError('eval failed');
|
|
161
|
+
expect(err.name).toBe('ExpressionError');
|
|
162
|
+
expect(err.code).toBe('OBJUI-003');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('PluginError', () => {
|
|
167
|
+
it('should accept a custom code', () => {
|
|
168
|
+
const err = new PluginError('missing dep', 'OBJUI-004');
|
|
169
|
+
expect(err.name).toBe('PluginError');
|
|
170
|
+
expect(err.code).toBe('OBJUI-004');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('FieldValidationError', () => {
|
|
175
|
+
it('should default to OBJUI-009', () => {
|
|
176
|
+
const err = new FieldValidationError('invalid field');
|
|
177
|
+
expect(err.name).toBe('FieldValidationError');
|
|
178
|
+
expect(err.code).toBe('OBJUI-009');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// createError factory
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
describe('createError', () => {
|
|
187
|
+
it('should create a RegistryError for OBJUI-001', () => {
|
|
188
|
+
const err = createError('OBJUI-001', { type: 'fancy-button' });
|
|
189
|
+
expect(err).toBeInstanceOf(RegistryError);
|
|
190
|
+
expect(err.message).toBe('Unknown component type: "fancy-button"');
|
|
191
|
+
expect(err.code).toBe('OBJUI-001');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should create a SchemaError for OBJUI-002', () => {
|
|
195
|
+
const err = createError('OBJUI-002', { reason: 'missing type' });
|
|
196
|
+
expect(err).toBeInstanceOf(SchemaError);
|
|
197
|
+
expect(err.message).toContain('missing type');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should create an ExpressionError for OBJUI-003', () => {
|
|
201
|
+
const err = createError('OBJUI-003', { expression: '${bad}' });
|
|
202
|
+
expect(err).toBeInstanceOf(ExpressionError);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should create a PluginError for OBJUI-004', () => {
|
|
206
|
+
const err = createError('OBJUI-004', { dependency: 'chart-lib' });
|
|
207
|
+
expect(err).toBeInstanceOf(PluginError);
|
|
208
|
+
expect(err.message).toContain('chart-lib');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should create a PluginError for OBJUI-005', () => {
|
|
212
|
+
const err = createError('OBJUI-005', { plugin: 'grid' });
|
|
213
|
+
expect(err).toBeInstanceOf(PluginError);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should create a PluginError for OBJUI-006', () => {
|
|
217
|
+
const err = createError('OBJUI-006', { plugin: 'kanban' });
|
|
218
|
+
expect(err).toBeInstanceOf(PluginError);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('should create a RegistryError for OBJUI-007', () => {
|
|
222
|
+
const err = createError('OBJUI-007', { namespace: 'old-ns' });
|
|
223
|
+
expect(err).toBeInstanceOf(RegistryError);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should create a FieldValidationError for OBJUI-009', () => {
|
|
227
|
+
const err = createError('OBJUI-009', { field: 'email' });
|
|
228
|
+
expect(err).toBeInstanceOf(FieldValidationError);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should create a base ObjectUIError for OBJUI-008', () => {
|
|
232
|
+
const err = createError('OBJUI-008', { scope: 'readOnlyScope' });
|
|
233
|
+
expect(err).toBeInstanceOf(ObjectUIError);
|
|
234
|
+
expect(err.message).toContain('readOnlyScope');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should create a base ObjectUIError for OBJUI-010', () => {
|
|
238
|
+
const err = createError('OBJUI-010', { action: 'save' });
|
|
239
|
+
expect(err).toBeInstanceOf(ObjectUIError);
|
|
240
|
+
expect(err.message).toContain('save');
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should handle unknown error codes gracefully', () => {
|
|
244
|
+
const err = createError('OBJUI-999');
|
|
245
|
+
expect(err).toBeInstanceOf(ObjectUIError);
|
|
246
|
+
expect(err.message).toContain('Unknown error code');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('should pass through details', () => {
|
|
250
|
+
const err = createError('OBJUI-001', { type: 'x' }, { extra: true });
|
|
251
|
+
expect(err.details).toEqual({ extra: true });
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
// formatErrorMessage
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
describe('formatErrorMessage', () => {
|
|
260
|
+
it('should include code and message', () => {
|
|
261
|
+
const err = new ObjectUIError('something broke', 'OBJUI-001');
|
|
262
|
+
const formatted = formatErrorMessage(err, false);
|
|
263
|
+
|
|
264
|
+
expect(formatted).toBe('[OBJUI-001] something broke');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should include suggestion and docs in dev mode', () => {
|
|
268
|
+
const err = createError('OBJUI-001', { type: 'magic-box' });
|
|
269
|
+
const formatted = formatErrorMessage(err, true);
|
|
270
|
+
|
|
271
|
+
expect(formatted).toContain('[OBJUI-001]');
|
|
272
|
+
expect(formatted).toContain('š” Suggestion:');
|
|
273
|
+
expect(formatted).toContain('š Docs:');
|
|
274
|
+
expect(formatted).toContain('https://objectui.dev/docs/errors/OBJUI-001');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should not include suggestion in production mode', () => {
|
|
278
|
+
const err = createError('OBJUI-002', { reason: 'bad' });
|
|
279
|
+
const formatted = formatErrorMessage(err, false);
|
|
280
|
+
|
|
281
|
+
expect(formatted).not.toContain('š” Suggestion:');
|
|
282
|
+
expect(formatted).not.toContain('š Docs:');
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should handle errors with unknown codes gracefully', () => {
|
|
286
|
+
const err = new ObjectUIError('custom', 'CUSTOM-001');
|
|
287
|
+
const formatted = formatErrorMessage(err, true);
|
|
288
|
+
|
|
289
|
+
// No entry in ERROR_CODES so no suggestion appended
|
|
290
|
+
expect(formatted).toBe('[CUSTOM-001] custom');
|
|
291
|
+
});
|
|
292
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
// Error Code Registry
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface ErrorCodeEntry {
|
|
14
|
+
code: string;
|
|
15
|
+
message: string;
|
|
16
|
+
suggestion: string;
|
|
17
|
+
docUrl: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BASE_DOC_URL = 'https://objectui.dev/docs/errors';
|
|
21
|
+
|
|
22
|
+
export const ERROR_CODES: Record<string, ErrorCodeEntry> = {
|
|
23
|
+
'OBJUI-001': {
|
|
24
|
+
code: 'OBJUI-001',
|
|
25
|
+
message: 'Unknown component type: "${type}"',
|
|
26
|
+
suggestion:
|
|
27
|
+
'Ensure the component is registered via registry.register() before rendering. Check for typos in the component type name.',
|
|
28
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-001`,
|
|
29
|
+
},
|
|
30
|
+
'OBJUI-002': {
|
|
31
|
+
code: 'OBJUI-002',
|
|
32
|
+
message: 'Schema validation failed: ${reason}',
|
|
33
|
+
suggestion:
|
|
34
|
+
'Verify the schema against the ObjectUI JSON spec. Run the built-in schema validator for details.',
|
|
35
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-002`,
|
|
36
|
+
},
|
|
37
|
+
'OBJUI-003': {
|
|
38
|
+
code: 'OBJUI-003',
|
|
39
|
+
message: 'Expression evaluation failed: ${expression}',
|
|
40
|
+
suggestion:
|
|
41
|
+
'Check the expression syntax and ensure all referenced variables exist in the current data scope.',
|
|
42
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-003`,
|
|
43
|
+
},
|
|
44
|
+
'OBJUI-004': {
|
|
45
|
+
code: 'OBJUI-004',
|
|
46
|
+
message: 'Plugin dependency missing: "${dependency}"',
|
|
47
|
+
suggestion:
|
|
48
|
+
'Install the required dependency and register it before loading this plugin.',
|
|
49
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-004`,
|
|
50
|
+
},
|
|
51
|
+
'OBJUI-005': {
|
|
52
|
+
code: 'OBJUI-005',
|
|
53
|
+
message: 'Plugin already loaded: "${plugin}"',
|
|
54
|
+
suggestion:
|
|
55
|
+
'A plugin with the same name is already registered. Remove the duplicate or use a unique plugin name.',
|
|
56
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-005`,
|
|
57
|
+
},
|
|
58
|
+
'OBJUI-006': {
|
|
59
|
+
code: 'OBJUI-006',
|
|
60
|
+
message: 'Plugin not found: "${plugin}"',
|
|
61
|
+
suggestion:
|
|
62
|
+
'Verify the plugin name and ensure it has been registered before access.',
|
|
63
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-006`,
|
|
64
|
+
},
|
|
65
|
+
'OBJUI-007': {
|
|
66
|
+
code: 'OBJUI-007',
|
|
67
|
+
message: 'Registry namespace deprecated: "${namespace}"',
|
|
68
|
+
suggestion:
|
|
69
|
+
'Migrate to the new namespace format. See the migration guide for details.',
|
|
70
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-007`,
|
|
71
|
+
},
|
|
72
|
+
'OBJUI-008': {
|
|
73
|
+
code: 'OBJUI-008',
|
|
74
|
+
message: 'Read-only scope violation: "${scope}"',
|
|
75
|
+
suggestion:
|
|
76
|
+
'You are trying to mutate a read-only data scope. Use a writable scope or clone the data before modifying.',
|
|
77
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-008`,
|
|
78
|
+
},
|
|
79
|
+
'OBJUI-009': {
|
|
80
|
+
code: 'OBJUI-009',
|
|
81
|
+
message: 'Invalid field configuration: "${field}"',
|
|
82
|
+
suggestion:
|
|
83
|
+
'Check the field schema for required properties (type, name). Ensure the field widget is registered.',
|
|
84
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-009`,
|
|
85
|
+
},
|
|
86
|
+
'OBJUI-010': {
|
|
87
|
+
code: 'OBJUI-010',
|
|
88
|
+
message: 'Action execution failed: "${action}"',
|
|
89
|
+
suggestion:
|
|
90
|
+
'Verify the action handler is registered and that all required parameters are provided.',
|
|
91
|
+
docUrl: `${BASE_DOC_URL}/OBJUI-010`,
|
|
92
|
+
},
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Type Guards
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Check whether a string is a known ObjectUI error code.
|
|
101
|
+
*/
|
|
102
|
+
export function isErrorCode(code: unknown): code is keyof typeof ERROR_CODES {
|
|
103
|
+
return typeof code === 'string' && code in ERROR_CODES;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Base Error Class
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Base error class for all ObjectUI errors.
|
|
112
|
+
*/
|
|
113
|
+
export class ObjectUIError extends Error {
|
|
114
|
+
constructor(
|
|
115
|
+
message: string,
|
|
116
|
+
public code: string,
|
|
117
|
+
public details?: Record<string, unknown>,
|
|
118
|
+
) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = 'ObjectUIError';
|
|
121
|
+
|
|
122
|
+
// Maintains proper stack trace for where error was thrown (only in V8)
|
|
123
|
+
if (Error.captureStackTrace) {
|
|
124
|
+
Error.captureStackTrace(this, this.constructor);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Convert error to JSON for logging / debugging.
|
|
130
|
+
*/
|
|
131
|
+
toJSON() {
|
|
132
|
+
return {
|
|
133
|
+
name: this.name,
|
|
134
|
+
message: this.message,
|
|
135
|
+
code: this.code,
|
|
136
|
+
details: this.details,
|
|
137
|
+
stack: this.stack,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Type guard to check if an error is an ObjectUIError.
|
|
144
|
+
*/
|
|
145
|
+
export function isObjectUIError(error: unknown): error is ObjectUIError {
|
|
146
|
+
return error instanceof ObjectUIError;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// Specialized Error Classes
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/** Thrown when a schema is invalid or fails validation. */
|
|
154
|
+
export class SchemaError extends ObjectUIError {
|
|
155
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
156
|
+
super(message, 'OBJUI-002', details);
|
|
157
|
+
this.name = 'SchemaError';
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Thrown when a registry operation fails. */
|
|
162
|
+
export class RegistryError extends ObjectUIError {
|
|
163
|
+
constructor(message: string, code: string, details?: Record<string, unknown>) {
|
|
164
|
+
super(message, code, details);
|
|
165
|
+
this.name = 'RegistryError';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Thrown when expression evaluation fails. */
|
|
170
|
+
export class ExpressionError extends ObjectUIError {
|
|
171
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
172
|
+
super(message, 'OBJUI-003', details);
|
|
173
|
+
this.name = 'ExpressionError';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Thrown when a plugin operation fails. */
|
|
178
|
+
export class PluginError extends ObjectUIError {
|
|
179
|
+
constructor(message: string, code: string, details?: Record<string, unknown>) {
|
|
180
|
+
super(message, code, details);
|
|
181
|
+
this.name = 'PluginError';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Thrown when validation of user input / field config fails. */
|
|
186
|
+
export class FieldValidationError extends ObjectUIError {
|
|
187
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
188
|
+
super(message, 'OBJUI-009', details);
|
|
189
|
+
this.name = 'FieldValidationError';
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Factory & Formatting Helpers
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Interpolate a template string with the given params.
|
|
199
|
+
* Template variables use the `${key}` syntax.
|
|
200
|
+
*/
|
|
201
|
+
function interpolate(
|
|
202
|
+
template: string,
|
|
203
|
+
params: Record<string, string>,
|
|
204
|
+
): string {
|
|
205
|
+
return template.replace(/\$\{(\w+)\}/g, (_match, key: string) => {
|
|
206
|
+
if (!(key in params) && typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
|
|
207
|
+
console.warn(`[ObjectUI] Missing interpolation parameter "${key}" in error message template.`);
|
|
208
|
+
}
|
|
209
|
+
return params[key] ?? `\${${key}}`;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create an `ObjectUIError` (or subclass) from a known error code.
|
|
215
|
+
*
|
|
216
|
+
* @param code - A registered error code (e.g. `"OBJUI-001"`).
|
|
217
|
+
* @param params - Values to interpolate into the message template.
|
|
218
|
+
* @param details - Optional extra details attached to the error.
|
|
219
|
+
*/
|
|
220
|
+
export function createError(
|
|
221
|
+
code: string,
|
|
222
|
+
params: Record<string, string> = {},
|
|
223
|
+
details?: Record<string, unknown>,
|
|
224
|
+
): ObjectUIError {
|
|
225
|
+
const entry = ERROR_CODES[code];
|
|
226
|
+
const message = entry
|
|
227
|
+
? interpolate(entry.message, params)
|
|
228
|
+
: `Unknown error code: ${code}`;
|
|
229
|
+
|
|
230
|
+
switch (code) {
|
|
231
|
+
case 'OBJUI-002':
|
|
232
|
+
return new SchemaError(message, details);
|
|
233
|
+
case 'OBJUI-003':
|
|
234
|
+
return new ExpressionError(message, details);
|
|
235
|
+
case 'OBJUI-004':
|
|
236
|
+
case 'OBJUI-005':
|
|
237
|
+
case 'OBJUI-006':
|
|
238
|
+
return new PluginError(message, code, details);
|
|
239
|
+
case 'OBJUI-007':
|
|
240
|
+
case 'OBJUI-001':
|
|
241
|
+
return new RegistryError(message, code, details);
|
|
242
|
+
case 'OBJUI-009':
|
|
243
|
+
return new FieldValidationError(message, details);
|
|
244
|
+
default:
|
|
245
|
+
return new ObjectUIError(message, code, details);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Format an error message with actionable fix suggestions in development mode.
|
|
251
|
+
*
|
|
252
|
+
* @param error - The `ObjectUIError` to format.
|
|
253
|
+
* @param isDev - When `true`, appends the suggestion and documentation link.
|
|
254
|
+
*/
|
|
255
|
+
export function formatErrorMessage(
|
|
256
|
+
error: ObjectUIError,
|
|
257
|
+
isDev: boolean = typeof process !== 'undefined' &&
|
|
258
|
+
process.env?.NODE_ENV !== 'production',
|
|
259
|
+
): string {
|
|
260
|
+
const entry = ERROR_CODES[error.code];
|
|
261
|
+
|
|
262
|
+
let formatted = `[${error.code}] ${error.message}`;
|
|
263
|
+
|
|
264
|
+
if (isDev && entry) {
|
|
265
|
+
formatted += `\n\nš” Suggestion: ${entry.suggestion}`;
|
|
266
|
+
formatted += `\nš Docs: ${entry.docUrl}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return formatted;
|
|
270
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { ExpressionContext } from './ExpressionContext.js';
|
|
20
20
|
import { ExpressionCache } from './ExpressionCache.js';
|
|
21
|
+
import { FormulaFunctions } from './FormulaFunctions.js';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Options for expression evaluation
|
|
@@ -47,8 +48,13 @@ export interface EvaluationOptions {
|
|
|
47
48
|
export class ExpressionEvaluator {
|
|
48
49
|
private context: ExpressionContext;
|
|
49
50
|
private cache: ExpressionCache;
|
|
51
|
+
private formulas: FormulaFunctions;
|
|
50
52
|
|
|
51
|
-
constructor(
|
|
53
|
+
constructor(
|
|
54
|
+
context?: ExpressionContext | Record<string, any>,
|
|
55
|
+
cache?: ExpressionCache,
|
|
56
|
+
formulas?: FormulaFunctions,
|
|
57
|
+
) {
|
|
52
58
|
if (context instanceof ExpressionContext) {
|
|
53
59
|
this.context = context;
|
|
54
60
|
} else {
|
|
@@ -57,6 +63,7 @@ export class ExpressionEvaluator {
|
|
|
57
63
|
|
|
58
64
|
// Use provided cache or create a new one
|
|
59
65
|
this.cache = cache || new ExpressionCache();
|
|
66
|
+
this.formulas = formulas || new FormulaFunctions();
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
/**
|
|
@@ -139,9 +146,13 @@ export class ExpressionEvaluator {
|
|
|
139
146
|
// Create a safe evaluation function
|
|
140
147
|
const contextObj = this.context.toObject();
|
|
141
148
|
|
|
149
|
+
// Inject formula functions into the evaluation context
|
|
150
|
+
const formulaObj = this.formulas.toObject();
|
|
151
|
+
const mergedContext = { ...formulaObj, ...contextObj };
|
|
152
|
+
|
|
142
153
|
// Build safe function with context variables
|
|
143
|
-
const varNames = Object.keys(
|
|
144
|
-
const varValues = Object.values(
|
|
154
|
+
const varNames = Object.keys(mergedContext);
|
|
155
|
+
const varValues = Object.values(mergedContext);
|
|
145
156
|
|
|
146
157
|
// Use cached compilation
|
|
147
158
|
const compiled = this.cache.compile(expression, varNames);
|
|
@@ -219,8 +230,8 @@ export class ExpressionEvaluator {
|
|
|
219
230
|
* Create a new evaluator with additional context data
|
|
220
231
|
*/
|
|
221
232
|
withContext(data: Record<string, any>): ExpressionEvaluator {
|
|
222
|
-
// Share the cache with the new evaluator for maximum efficiency
|
|
223
|
-
return new ExpressionEvaluator(this.context.createChild(data), this.cache);
|
|
233
|
+
// Share the cache and formulas with the new evaluator for maximum efficiency
|
|
234
|
+
return new ExpressionEvaluator(this.context.createChild(data), this.cache, this.formulas);
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
/**
|
|
@@ -236,12 +247,27 @@ export class ExpressionEvaluator {
|
|
|
236
247
|
clearCache(): void {
|
|
237
248
|
this.cache.clear();
|
|
238
249
|
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get the formula functions registry
|
|
253
|
+
*/
|
|
254
|
+
getFormulas(): FormulaFunctions {
|
|
255
|
+
return this.formulas;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Register a custom formula function
|
|
260
|
+
*/
|
|
261
|
+
registerFunction(name: string, fn: (...args: any[]) => any): void {
|
|
262
|
+
this.formulas.register(name, fn);
|
|
263
|
+
}
|
|
239
264
|
}
|
|
240
265
|
|
|
241
266
|
/**
|
|
242
|
-
* Shared global cache for convenience functions
|
|
267
|
+
* Shared global cache and formulas for convenience functions
|
|
243
268
|
*/
|
|
244
269
|
const globalCache = new ExpressionCache();
|
|
270
|
+
const globalFormulas = new FormulaFunctions();
|
|
245
271
|
|
|
246
272
|
/**
|
|
247
273
|
* Convenience function to quickly evaluate an expression
|
|
@@ -251,7 +277,7 @@ export function evaluateExpression(
|
|
|
251
277
|
context: Record<string, any> = {},
|
|
252
278
|
options: EvaluationOptions = {}
|
|
253
279
|
): any {
|
|
254
|
-
const evaluator = new ExpressionEvaluator(context, globalCache);
|
|
280
|
+
const evaluator = new ExpressionEvaluator(context, globalCache, globalFormulas);
|
|
255
281
|
return evaluator.evaluate(expression, options);
|
|
256
282
|
}
|
|
257
283
|
|
|
@@ -262,6 +288,6 @@ export function evaluateCondition(
|
|
|
262
288
|
condition: string | boolean | undefined,
|
|
263
289
|
context: Record<string, any> = {}
|
|
264
290
|
): boolean {
|
|
265
|
-
const evaluator = new ExpressionEvaluator(context, globalCache);
|
|
291
|
+
const evaluator = new ExpressionEvaluator(context, globalCache, globalFormulas);
|
|
266
292
|
return evaluator.evaluateCondition(condition);
|
|
267
293
|
}
|