@object-ui/core 0.3.1 → 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.
Files changed (118) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +11 -0
  3. package/dist/actions/ActionRunner.d.ts +228 -4
  4. package/dist/actions/ActionRunner.js +397 -45
  5. package/dist/actions/TransactionManager.d.ts +193 -0
  6. package/dist/actions/TransactionManager.js +410 -0
  7. package/dist/actions/index.d.ts +2 -1
  8. package/dist/actions/index.js +2 -1
  9. package/dist/adapters/ApiDataSource.d.ts +69 -0
  10. package/dist/adapters/ApiDataSource.js +293 -0
  11. package/dist/adapters/ValueDataSource.d.ts +55 -0
  12. package/dist/adapters/ValueDataSource.js +287 -0
  13. package/dist/adapters/index.d.ts +3 -0
  14. package/dist/adapters/index.js +5 -2
  15. package/dist/adapters/resolveDataSource.d.ts +40 -0
  16. package/dist/adapters/resolveDataSource.js +59 -0
  17. package/dist/data-scope/DataScopeManager.d.ts +127 -0
  18. package/dist/data-scope/DataScopeManager.js +229 -0
  19. package/dist/data-scope/index.d.ts +10 -0
  20. package/dist/data-scope/index.js +10 -0
  21. package/dist/evaluator/ExpressionCache.d.ts +101 -0
  22. package/dist/evaluator/ExpressionCache.js +135 -0
  23. package/dist/evaluator/ExpressionEvaluator.d.ts +30 -2
  24. package/dist/evaluator/ExpressionEvaluator.js +60 -16
  25. package/dist/evaluator/FormulaFunctions.d.ts +58 -0
  26. package/dist/evaluator/FormulaFunctions.js +350 -0
  27. package/dist/evaluator/index.d.ts +4 -2
  28. package/dist/evaluator/index.js +4 -2
  29. package/dist/index.d.ts +14 -7
  30. package/dist/index.js +13 -9
  31. package/dist/query/index.d.ts +6 -0
  32. package/dist/query/index.js +6 -0
  33. package/dist/query/query-ast.d.ts +32 -0
  34. package/dist/query/query-ast.js +268 -0
  35. package/dist/registry/PluginScopeImpl.d.ts +80 -0
  36. package/dist/registry/PluginScopeImpl.js +243 -0
  37. package/dist/registry/PluginSystem.d.ts +66 -0
  38. package/dist/registry/PluginSystem.js +142 -0
  39. package/dist/registry/Registry.d.ts +83 -4
  40. package/dist/registry/Registry.js +113 -7
  41. package/dist/registry/WidgetRegistry.d.ts +120 -0
  42. package/dist/registry/WidgetRegistry.js +275 -0
  43. package/dist/theme/ThemeEngine.d.ts +82 -0
  44. package/dist/theme/ThemeEngine.js +400 -0
  45. package/dist/theme/index.d.ts +8 -0
  46. package/dist/theme/index.js +8 -0
  47. package/dist/validation/index.d.ts +9 -0
  48. package/dist/validation/index.js +9 -0
  49. package/dist/validation/validation-engine.d.ts +88 -0
  50. package/dist/validation/validation-engine.js +428 -0
  51. package/dist/validation/validators/index.d.ts +16 -0
  52. package/dist/validation/validators/index.js +16 -0
  53. package/dist/validation/validators/object-validation-engine.d.ts +118 -0
  54. package/dist/validation/validators/object-validation-engine.js +538 -0
  55. package/package.json +14 -5
  56. package/src/actions/ActionRunner.ts +577 -55
  57. package/src/actions/TransactionManager.ts +521 -0
  58. package/src/actions/__tests__/ActionRunner.params.test.ts +134 -0
  59. package/src/actions/__tests__/ActionRunner.test.ts +711 -0
  60. package/src/actions/__tests__/TransactionManager.test.ts +447 -0
  61. package/src/actions/index.ts +2 -1
  62. package/src/adapters/ApiDataSource.ts +349 -0
  63. package/src/adapters/ValueDataSource.ts +332 -0
  64. package/src/adapters/__tests__/ApiDataSource.test.ts +418 -0
  65. package/src/adapters/__tests__/ValueDataSource.test.ts +325 -0
  66. package/src/adapters/__tests__/resolveDataSource.test.ts +144 -0
  67. package/src/adapters/index.ts +6 -1
  68. package/src/adapters/resolveDataSource.ts +79 -0
  69. package/src/builder/__tests__/schema-builder.test.ts +235 -0
  70. package/src/data-scope/DataScopeManager.ts +269 -0
  71. package/src/data-scope/__tests__/DataScopeManager.test.ts +211 -0
  72. package/src/data-scope/index.ts +16 -0
  73. package/src/evaluator/ExpressionCache.ts +192 -0
  74. package/src/evaluator/ExpressionEvaluator.ts +61 -16
  75. package/src/evaluator/FormulaFunctions.ts +398 -0
  76. package/src/evaluator/__tests__/ExpressionCache.test.ts +135 -0
  77. package/src/evaluator/__tests__/ExpressionContext.test.ts +110 -0
  78. package/src/evaluator/__tests__/FormulaFunctions.test.ts +447 -0
  79. package/src/evaluator/index.ts +4 -2
  80. package/src/index.ts +14 -10
  81. package/src/query/__tests__/query-ast.test.ts +211 -0
  82. package/src/query/__tests__/window-functions.test.ts +275 -0
  83. package/src/query/index.ts +7 -0
  84. package/src/query/query-ast.ts +341 -0
  85. package/src/registry/PluginScopeImpl.ts +259 -0
  86. package/src/registry/PluginSystem.ts +161 -0
  87. package/src/registry/Registry.ts +136 -8
  88. package/src/registry/WidgetRegistry.ts +316 -0
  89. package/src/registry/__tests__/PluginSystem.test.ts +226 -0
  90. package/src/registry/__tests__/Registry.test.ts +293 -0
  91. package/src/registry/__tests__/WidgetRegistry.test.ts +321 -0
  92. package/src/registry/__tests__/plugin-scope-integration.test.ts +283 -0
  93. package/src/theme/ThemeEngine.ts +452 -0
  94. package/src/theme/__tests__/ThemeEngine.test.ts +606 -0
  95. package/src/theme/index.ts +22 -0
  96. package/src/validation/__tests__/object-validation-engine.test.ts +567 -0
  97. package/src/validation/__tests__/schema-validator.test.ts +118 -0
  98. package/src/validation/__tests__/validation-engine.test.ts +102 -0
  99. package/src/validation/index.ts +10 -0
  100. package/src/validation/validation-engine.ts +520 -0
  101. package/src/validation/validators/index.ts +25 -0
  102. package/src/validation/validators/object-validation-engine.ts +722 -0
  103. package/tsconfig.tsbuildinfo +1 -1
  104. package/vitest.config.ts +2 -0
  105. package/src/adapters/index.d.ts +0 -8
  106. package/src/adapters/index.js +0 -10
  107. package/src/builder/schema-builder.d.ts +0 -294
  108. package/src/builder/schema-builder.js +0 -503
  109. package/src/index.d.ts +0 -13
  110. package/src/index.js +0 -16
  111. package/src/registry/Registry.d.ts +0 -56
  112. package/src/registry/Registry.js +0 -43
  113. package/src/types/index.d.ts +0 -19
  114. package/src/types/index.js +0 -8
  115. package/src/utils/filter-converter.d.ts +0 -57
  116. package/src/utils/filter-converter.js +0 -100
  117. package/src/validation/schema-validator.d.ts +0 -94
  118. package/src/validation/schema-validator.js +0 -278
@@ -0,0 +1,325 @@
1
+ /**
2
+ * ObjectUI — ValueDataSource Tests
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 { ValueDataSource } from '../ValueDataSource';
11
+
12
+ const sampleData = [
13
+ { _id: '1', name: 'Alice', age: 30, role: 'admin' },
14
+ { _id: '2', name: 'Bob', age: 25, role: 'user' },
15
+ { _id: '3', name: 'Charlie', age: 35, role: 'admin' },
16
+ { _id: '4', name: 'Diana', age: 28, role: 'user' },
17
+ { _id: '5', name: 'Eve', age: 22, role: 'guest' },
18
+ ];
19
+
20
+ function createDS() {
21
+ return new ValueDataSource({ items: sampleData });
22
+ }
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // find
26
+ // ---------------------------------------------------------------------------
27
+
28
+ describe('ValueDataSource — find', () => {
29
+ it('should return all items with no params', async () => {
30
+ const ds = createDS();
31
+ const result = await ds.find('users');
32
+ expect(result.data).toHaveLength(5);
33
+ expect(result.total).toBe(5);
34
+ });
35
+
36
+ it('should filter by equality', async () => {
37
+ const ds = createDS();
38
+ const result = await ds.find('users', { $filter: { role: 'admin' } });
39
+ expect(result.data).toHaveLength(2);
40
+ expect(result.data.every((r: any) => r.role === 'admin')).toBe(true);
41
+ expect(result.total).toBe(2);
42
+ });
43
+
44
+ it('should filter with $gt operator', async () => {
45
+ const ds = createDS();
46
+ const result = await ds.find('users', { $filter: { age: { $gt: 28 } } });
47
+ expect(result.data).toHaveLength(2);
48
+ expect(result.data.map((r: any) => r.name).sort()).toEqual(['Alice', 'Charlie']);
49
+ });
50
+
51
+ it('should filter with $gte operator', async () => {
52
+ const ds = createDS();
53
+ const result = await ds.find('users', { $filter: { age: { $gte: 28 } } });
54
+ expect(result.data).toHaveLength(3);
55
+ });
56
+
57
+ it('should filter with $lt operator', async () => {
58
+ const ds = createDS();
59
+ const result = await ds.find('users', { $filter: { age: { $lt: 25 } } });
60
+ expect(result.data).toHaveLength(1);
61
+ expect(result.data[0].name).toBe('Eve');
62
+ });
63
+
64
+ it('should filter with $ne operator', async () => {
65
+ const ds = createDS();
66
+ const result = await ds.find('users', { $filter: { role: { $ne: 'admin' } } });
67
+ expect(result.data).toHaveLength(3);
68
+ });
69
+
70
+ it('should filter with $in operator', async () => {
71
+ const ds = createDS();
72
+ const result = await ds.find('users', {
73
+ $filter: { role: { $in: ['admin', 'guest'] } },
74
+ });
75
+ expect(result.data).toHaveLength(3);
76
+ });
77
+
78
+ it('should filter with $contains operator', async () => {
79
+ const ds = createDS();
80
+ const result = await ds.find('users', {
81
+ $filter: { name: { $contains: 'ali' } },
82
+ });
83
+ expect(result.data).toHaveLength(1); // Alice only ('ali' is not in 'Charlie')
84
+ });
85
+
86
+ it('should sort ascending by Record format', async () => {
87
+ const ds = createDS();
88
+ const result = await ds.find('users', { $orderby: { name: 'asc' } });
89
+ const names = result.data.map((r: any) => r.name);
90
+ expect(names).toEqual(['Alice', 'Bob', 'Charlie', 'Diana', 'Eve']);
91
+ });
92
+
93
+ it('should sort descending by Record format', async () => {
94
+ const ds = createDS();
95
+ const result = await ds.find('users', { $orderby: { age: 'desc' } });
96
+ expect(result.data[0].name).toBe('Charlie');
97
+ expect(result.data[4].name).toBe('Eve');
98
+ });
99
+
100
+ it('should sort by string array format', async () => {
101
+ const ds = createDS();
102
+ const result = await ds.find('users', { $orderby: ['-age'] });
103
+ expect(result.data[0].name).toBe('Charlie');
104
+ });
105
+
106
+ it('should sort by object array format', async () => {
107
+ const ds = createDS();
108
+ const result = await ds.find('users', {
109
+ $orderby: [{ field: 'age', order: 'asc' }],
110
+ });
111
+ expect(result.data[0].name).toBe('Eve');
112
+ expect(result.data[4].name).toBe('Charlie');
113
+ });
114
+
115
+ it('should paginate with $skip and $top', async () => {
116
+ const ds = createDS();
117
+ const result = await ds.find('users', { $skip: 2, $top: 2 });
118
+ expect(result.data).toHaveLength(2);
119
+ expect(result.total).toBe(5); // total before pagination
120
+ expect(result.hasMore).toBe(true);
121
+ });
122
+
123
+ it('should return hasMore=false when all items returned', async () => {
124
+ const ds = createDS();
125
+ const result = await ds.find('users', { $top: 10 });
126
+ expect(result.hasMore).toBe(false);
127
+ });
128
+
129
+ it('should select specific fields', async () => {
130
+ const ds = createDS();
131
+ const result = await ds.find('users', { $select: ['name', 'age'] });
132
+ const first = result.data[0] as any;
133
+ expect(first.name).toBeDefined();
134
+ expect(first.age).toBeDefined();
135
+ expect(first._id).toBeUndefined();
136
+ expect(first.role).toBeUndefined();
137
+ });
138
+
139
+ it('should search across string fields', async () => {
140
+ const ds = createDS();
141
+ const result = await ds.find('users', { $search: 'bob' });
142
+ expect(result.data).toHaveLength(1);
143
+ expect(result.data[0].name).toBe('Bob');
144
+ });
145
+
146
+ it('should combine filter + sort + pagination', async () => {
147
+ const ds = createDS();
148
+ const result = await ds.find('users', {
149
+ $filter: { role: 'user' },
150
+ $orderby: { age: 'asc' },
151
+ $top: 1,
152
+ });
153
+ expect(result.data).toHaveLength(1);
154
+ expect(result.data[0].name).toBe('Bob'); // youngest user
155
+ expect(result.total).toBe(2); // 2 users total
156
+ expect(result.hasMore).toBe(true);
157
+ });
158
+ });
159
+
160
+ // ---------------------------------------------------------------------------
161
+ // findOne
162
+ // ---------------------------------------------------------------------------
163
+
164
+ describe('ValueDataSource — findOne', () => {
165
+ it('should find a record by _id', async () => {
166
+ const ds = createDS();
167
+ const record = await ds.findOne('users', '3');
168
+ expect(record).not.toBeNull();
169
+ expect((record as any).name).toBe('Charlie');
170
+ });
171
+
172
+ it('should return null for missing id', async () => {
173
+ const ds = createDS();
174
+ const record = await ds.findOne('users', '999');
175
+ expect(record).toBeNull();
176
+ });
177
+
178
+ it('should use custom idField', async () => {
179
+ const ds = new ValueDataSource({
180
+ items: [{ code: 'A', label: 'Alpha' }],
181
+ idField: 'code',
182
+ });
183
+ const record = await ds.findOne('items', 'A');
184
+ expect(record).not.toBeNull();
185
+ expect((record as any).label).toBe('Alpha');
186
+ });
187
+
188
+ it('should select fields on findOne', async () => {
189
+ const ds = createDS();
190
+ const record = await ds.findOne('users', '1', { $select: ['name'] });
191
+ expect(record).not.toBeNull();
192
+ expect((record as any).name).toBe('Alice');
193
+ expect((record as any).age).toBeUndefined();
194
+ });
195
+ });
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // create
199
+ // ---------------------------------------------------------------------------
200
+
201
+ describe('ValueDataSource — create', () => {
202
+ it('should add a record to the collection', async () => {
203
+ const ds = createDS();
204
+ const created = await ds.create('users', { name: 'Frank', age: 40, role: 'user' });
205
+ expect((created as any).name).toBe('Frank');
206
+ expect(ds.count).toBe(6);
207
+ });
208
+
209
+ it('should auto-generate an ID if missing', async () => {
210
+ const ds = createDS();
211
+ const created = await ds.create('users', { name: 'Grace' });
212
+ expect((created as any)._id).toBeDefined();
213
+ expect(String((created as any)._id).startsWith('auto_')).toBe(true);
214
+ });
215
+
216
+ it('should preserve existing ID', async () => {
217
+ const ds = createDS();
218
+ const created = await ds.create('users', { _id: 'custom-id', name: 'Heidi' });
219
+ expect((created as any)._id).toBe('custom-id');
220
+ });
221
+ });
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // update
225
+ // ---------------------------------------------------------------------------
226
+
227
+ describe('ValueDataSource — update', () => {
228
+ it('should update an existing record', async () => {
229
+ const ds = createDS();
230
+ const updated = await ds.update('users', '1', { age: 31 });
231
+ expect((updated as any).age).toBe(31);
232
+ expect((updated as any).name).toBe('Alice');
233
+ });
234
+
235
+ it('should throw for non-existent record', async () => {
236
+ const ds = createDS();
237
+ await expect(ds.update('users', '999', { age: 50 })).rejects.toThrow(
238
+ 'Record with id "999" not found',
239
+ );
240
+ });
241
+
242
+ it('should persist updates in subsequent finds', async () => {
243
+ const ds = createDS();
244
+ await ds.update('users', '2', { name: 'Bobby' });
245
+ const result = await ds.findOne('users', '2');
246
+ expect((result as any).name).toBe('Bobby');
247
+ });
248
+ });
249
+
250
+ // ---------------------------------------------------------------------------
251
+ // delete
252
+ // ---------------------------------------------------------------------------
253
+
254
+ describe('ValueDataSource — delete', () => {
255
+ it('should remove a record', async () => {
256
+ const ds = createDS();
257
+ const ok = await ds.delete('users', '1');
258
+ expect(ok).toBe(true);
259
+ expect(ds.count).toBe(4);
260
+ });
261
+
262
+ it('should return false for non-existent record', async () => {
263
+ const ds = createDS();
264
+ const ok = await ds.delete('users', '999');
265
+ expect(ok).toBe(false);
266
+ expect(ds.count).toBe(5);
267
+ });
268
+ });
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // bulk
272
+ // ---------------------------------------------------------------------------
273
+
274
+ describe('ValueDataSource — bulk', () => {
275
+ it('should bulk create records', async () => {
276
+ const ds = createDS();
277
+ const results = await ds.bulk!('users', 'create', [
278
+ { name: 'X', age: 10 },
279
+ { name: 'Y', age: 20 },
280
+ ]);
281
+ expect(results).toHaveLength(2);
282
+ expect(ds.count).toBe(7);
283
+ });
284
+
285
+ it('should bulk delete records', async () => {
286
+ const ds = createDS();
287
+ await ds.bulk!('users', 'delete', [{ _id: '1' }, { _id: '2' }]);
288
+ expect(ds.count).toBe(3);
289
+ });
290
+ });
291
+
292
+ // ---------------------------------------------------------------------------
293
+ // getObjectSchema
294
+ // ---------------------------------------------------------------------------
295
+
296
+ describe('ValueDataSource — getObjectSchema', () => {
297
+ it('should infer schema from first item', async () => {
298
+ const ds = createDS();
299
+ const schema = await ds.getObjectSchema('users');
300
+ expect(schema.name).toBe('users');
301
+ expect(schema.fields._id).toBeDefined();
302
+ expect(schema.fields.name.type).toBe('string');
303
+ expect(schema.fields.age.type).toBe('number');
304
+ });
305
+
306
+ it('should return empty fields for empty dataset', async () => {
307
+ const ds = new ValueDataSource({ items: [] });
308
+ const schema = await ds.getObjectSchema('empty');
309
+ expect(schema.fields).toEqual({});
310
+ });
311
+ });
312
+
313
+ // ---------------------------------------------------------------------------
314
+ // Isolation
315
+ // ---------------------------------------------------------------------------
316
+
317
+ describe('ValueDataSource — isolation', () => {
318
+ it('should not mutate the original items array', async () => {
319
+ const original = [{ _id: '1', name: 'Test' }];
320
+ const ds = new ValueDataSource({ items: original });
321
+ await ds.create('x', { name: 'New' });
322
+ expect(original).toHaveLength(1); // original untouched
323
+ expect(ds.count).toBe(2);
324
+ });
325
+ });
@@ -0,0 +1,144 @@
1
+ /**
2
+ * ObjectUI — resolveDataSource Tests
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 { resolveDataSource } from '../resolveDataSource';
11
+ import { ApiDataSource } from '../ApiDataSource';
12
+ import { ValueDataSource } from '../ValueDataSource';
13
+ import type { DataSource, ViewData } from '@object-ui/types';
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Minimal mock DataSource for "object" provider fallback
17
+ // ---------------------------------------------------------------------------
18
+
19
+ const mockFallback: DataSource<any> = {
20
+ find: async () => ({ data: [], total: 0 }),
21
+ findOne: async () => null,
22
+ create: async (_r, d) => d as any,
23
+ update: async (_r, _id, d) => d as any,
24
+ delete: async () => true,
25
+ getObjectSchema: async (name) => ({ name, fields: {} }),
26
+ };
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Tests
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe('resolveDataSource', () => {
33
+ it('should return null when viewData is null and no fallback', () => {
34
+ const ds = resolveDataSource(null);
35
+ expect(ds).toBeNull();
36
+ });
37
+
38
+ it('should return fallback when viewData is null', () => {
39
+ const ds = resolveDataSource(null, mockFallback);
40
+ expect(ds).toBe(mockFallback);
41
+ });
42
+
43
+ it('should return null when viewData is undefined and no fallback', () => {
44
+ const ds = resolveDataSource(undefined);
45
+ expect(ds).toBeNull();
46
+ });
47
+
48
+ // -----------------------------------------------------------------------
49
+ // provider: 'object'
50
+ // -----------------------------------------------------------------------
51
+
52
+ it('should return fallback for provider: "object"', () => {
53
+ const viewData: ViewData = { provider: 'object', object: 'contacts' };
54
+ const ds = resolveDataSource(viewData, mockFallback);
55
+ expect(ds).toBe(mockFallback);
56
+ });
57
+
58
+ it('should return null for provider: "object" without fallback', () => {
59
+ const viewData: ViewData = { provider: 'object', object: 'contacts' };
60
+ const ds = resolveDataSource(viewData);
61
+ expect(ds).toBeNull();
62
+ });
63
+
64
+ // -----------------------------------------------------------------------
65
+ // provider: 'api'
66
+ // -----------------------------------------------------------------------
67
+
68
+ it('should create ApiDataSource for provider: "api"', () => {
69
+ const viewData: ViewData = {
70
+ provider: 'api',
71
+ read: { url: '/api/contacts' },
72
+ };
73
+ const ds = resolveDataSource(viewData);
74
+ expect(ds).toBeInstanceOf(ApiDataSource);
75
+ });
76
+
77
+ it('should create ApiDataSource with read and write configs', () => {
78
+ const viewData: ViewData = {
79
+ provider: 'api',
80
+ read: { url: '/api/contacts', method: 'GET' },
81
+ write: { url: '/api/contacts', method: 'POST' },
82
+ };
83
+ const ds = resolveDataSource(viewData);
84
+ expect(ds).toBeInstanceOf(ApiDataSource);
85
+ });
86
+
87
+ it('should pass adapter options to ApiDataSource', () => {
88
+ const viewData: ViewData = {
89
+ provider: 'api',
90
+ read: { url: '/api/contacts' },
91
+ };
92
+ const ds = resolveDataSource(viewData, null, {
93
+ defaultHeaders: { Authorization: 'Bearer test' },
94
+ });
95
+ expect(ds).toBeInstanceOf(ApiDataSource);
96
+ });
97
+
98
+ // -----------------------------------------------------------------------
99
+ // provider: 'value'
100
+ // -----------------------------------------------------------------------
101
+
102
+ it('should create ValueDataSource for provider: "value"', () => {
103
+ const viewData: ViewData = {
104
+ provider: 'value',
105
+ items: [{ id: '1', name: 'Test' }],
106
+ };
107
+ const ds = resolveDataSource(viewData);
108
+ expect(ds).toBeInstanceOf(ValueDataSource);
109
+ });
110
+
111
+ it('should handle empty items array', () => {
112
+ const viewData: ViewData = {
113
+ provider: 'value',
114
+ items: [],
115
+ };
116
+ const ds = resolveDataSource(viewData);
117
+ expect(ds).toBeInstanceOf(ValueDataSource);
118
+ });
119
+
120
+ it('should pass idField option to ValueDataSource', () => {
121
+ const viewData: ViewData = {
122
+ provider: 'value',
123
+ items: [{ code: 'A', label: 'Alpha' }],
124
+ };
125
+ const ds = resolveDataSource(viewData, null, { idField: 'code' });
126
+ expect(ds).toBeInstanceOf(ValueDataSource);
127
+ });
128
+
129
+ // -----------------------------------------------------------------------
130
+ // Unknown provider
131
+ // -----------------------------------------------------------------------
132
+
133
+ it('should return fallback for unknown provider', () => {
134
+ const viewData = { provider: 'graphql' } as unknown as ViewData;
135
+ const ds = resolveDataSource(viewData, mockFallback);
136
+ expect(ds).toBe(mockFallback);
137
+ });
138
+
139
+ it('should return null for unknown provider without fallback', () => {
140
+ const viewData = { provider: 'unknown' } as unknown as ViewData;
141
+ const ds = resolveDataSource(viewData);
142
+ expect(ds).toBeNull();
143
+ });
144
+ });
@@ -7,4 +7,9 @@
7
7
  */
8
8
 
9
9
  // export { ObjectStackAdapter, createObjectStackAdapter } from './objectstack-adapter';
10
- // Adapters have been moved to separate packages (e.g. @object-ui/data-objectstack)
10
+ // ObjectStack adapter has been moved to @object-ui/data-objectstack
11
+
12
+ // Generic data source adapters (no external SDK dependencies)
13
+ export { ApiDataSource, type ApiDataSourceConfig } from './ApiDataSource.js';
14
+ export { ValueDataSource, type ValueDataSourceConfig } from './ValueDataSource.js';
15
+ export { resolveDataSource, type ResolveDataSourceOptions } from './resolveDataSource.js';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * ObjectUI — resolveDataSource
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
+ * Factory function to create the right DataSource from a ViewData config.
9
+ */
10
+
11
+ import type { DataSource, ViewData } from '@object-ui/types';
12
+ import { ApiDataSource, type ApiDataSourceConfig } from './ApiDataSource.js';
13
+ import { ValueDataSource } from './ValueDataSource.js';
14
+
15
+ export interface ResolveDataSourceOptions {
16
+ /** Custom fetch implementation passed to ApiDataSource */
17
+ fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
18
+ /** Default headers for API requests */
19
+ defaultHeaders?: Record<string, string>;
20
+ /** Custom ID field for ValueDataSource */
21
+ idField?: string;
22
+ }
23
+
24
+ /**
25
+ * Resolve a ViewData configuration into a concrete DataSource instance.
26
+ *
27
+ * - `provider: 'object'` → returns `fallback` (the context DataSource — typically ObjectStackAdapter)
28
+ * - `provider: 'api'` → returns a new `ApiDataSource`
29
+ * - `provider: 'value'` → returns a new `ValueDataSource`
30
+ *
31
+ * @param viewData - The ViewData configuration from the schema
32
+ * @param fallback - The default DataSource from context (for `provider: 'object'`)
33
+ * @param options - Additional options for adapter construction
34
+ * @returns A DataSource instance, or null if neither viewData nor fallback is available
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * const ds = resolveDataSource(
39
+ * { provider: 'api', read: { url: '/api/users' } },
40
+ * contextDataSource,
41
+ * );
42
+ * const result = await ds.find('users');
43
+ * ```
44
+ */
45
+ export function resolveDataSource<T = any>(
46
+ viewData: ViewData | null | undefined,
47
+ fallback?: DataSource<T> | null,
48
+ options?: ResolveDataSourceOptions,
49
+ ): DataSource<T> | null {
50
+ if (!viewData) {
51
+ return fallback ?? null;
52
+ }
53
+
54
+ switch (viewData.provider) {
55
+ case 'object':
56
+ // Delegate to the context DataSource (ObjectStackAdapter, etc.)
57
+ return fallback ?? null;
58
+
59
+ case 'api': {
60
+ const config: ApiDataSourceConfig = {
61
+ read: viewData.read,
62
+ write: viewData.write,
63
+ fetch: options?.fetch,
64
+ defaultHeaders: options?.defaultHeaders,
65
+ };
66
+ return new ApiDataSource<T>(config);
67
+ }
68
+
69
+ case 'value':
70
+ return new ValueDataSource<T>({
71
+ items: (viewData.items ?? []) as T[],
72
+ idField: options?.idField,
73
+ });
74
+
75
+ default:
76
+ // Unknown provider — fall back to context
77
+ return fallback ?? null;
78
+ }
79
+ }