@kronor/dtv 0.2.9

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 (97) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/copilot-instructions.md +64 -0
  3. package/.github/workflows/ci.yml +51 -0
  4. package/.husky/pre-commit +8 -0
  5. package/README.md +63 -0
  6. package/docs/api/README.md +32 -0
  7. package/docs/api/cell-renderers.md +121 -0
  8. package/docs/api/no-rows-component.md +71 -0
  9. package/docs/api/runtime.md +78 -0
  10. package/e2e/app.spec.ts +6 -0
  11. package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
  12. package/e2e/filter-sharing.spec.ts +113 -0
  13. package/e2e/filter-url-persistence.spec.ts +36 -0
  14. package/e2e/graphqlMock.ts +144 -0
  15. package/e2e/multi-field-filters.spec.ts +95 -0
  16. package/e2e/pagination.spec.ts +38 -0
  17. package/e2e/payment-request-email-filter.spec.ts +67 -0
  18. package/e2e/save-filter-splitbutton.spec.ts +68 -0
  19. package/e2e/simple-view-email-filter.spec.ts +67 -0
  20. package/e2e/simple-view-transforms.spec.ts +171 -0
  21. package/e2e/simple-view.spec.ts +104 -0
  22. package/e2e/transform-regression.spec.ts +108 -0
  23. package/eslint.config.js +30 -0
  24. package/index.html +17 -0
  25. package/jest.config.js +10 -0
  26. package/package.json +45 -0
  27. package/playwright.config.ts +54 -0
  28. package/public/vite.svg +1 -0
  29. package/src/App.externalRuntime.test.ts +190 -0
  30. package/src/App.tsx +540 -0
  31. package/src/assets/react.svg +1 -0
  32. package/src/components/AIAssistantForm.tsx +241 -0
  33. package/src/components/FilterForm.test.ts +82 -0
  34. package/src/components/FilterForm.tsx +375 -0
  35. package/src/components/PhoneNumberFilter.tsx +102 -0
  36. package/src/components/SavedFilterList.tsx +181 -0
  37. package/src/components/SpeechInput.tsx +67 -0
  38. package/src/components/Table.tsx +119 -0
  39. package/src/components/TablePagination.tsx +40 -0
  40. package/src/components/aiAssistant.test.ts +270 -0
  41. package/src/components/aiAssistant.ts +291 -0
  42. package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
  43. package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
  44. package/src/framework/cell-renderer-components/Link.tsx +28 -0
  45. package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
  46. package/src/framework/cell-renderer-components.test.ts +353 -0
  47. package/src/framework/column-definition.tsx +85 -0
  48. package/src/framework/currency.test.ts +46 -0
  49. package/src/framework/currency.ts +62 -0
  50. package/src/framework/data.staticConditions.test.ts +46 -0
  51. package/src/framework/data.test.ts +167 -0
  52. package/src/framework/data.ts +162 -0
  53. package/src/framework/filter-form-state.test.ts +189 -0
  54. package/src/framework/filter-form-state.ts +185 -0
  55. package/src/framework/filter-sharing.test.ts +135 -0
  56. package/src/framework/filter-sharing.ts +118 -0
  57. package/src/framework/filters.ts +194 -0
  58. package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
  59. package/src/framework/graphql.paginationKey.test.ts +29 -0
  60. package/src/framework/graphql.test.ts +286 -0
  61. package/src/framework/graphql.ts +462 -0
  62. package/src/framework/native-runtime/index.tsx +33 -0
  63. package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
  64. package/src/framework/runtime-reference.test.ts +172 -0
  65. package/src/framework/runtime.ts +15 -0
  66. package/src/framework/saved-filters.test.ts +422 -0
  67. package/src/framework/saved-filters.ts +293 -0
  68. package/src/framework/state.test.ts +86 -0
  69. package/src/framework/state.ts +148 -0
  70. package/src/framework/transform.test.ts +51 -0
  71. package/src/framework/view-parser-initialvalues.test.ts +228 -0
  72. package/src/framework/view-parser.ts +714 -0
  73. package/src/framework/view.test.ts +1805 -0
  74. package/src/framework/view.ts +38 -0
  75. package/src/index.css +6 -0
  76. package/src/main.tsx +99 -0
  77. package/src/views/index.ts +12 -0
  78. package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
  79. package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
  80. package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
  81. package/src/views/payment-requests/index.ts +1 -0
  82. package/src/views/payment-requests/runtime.tsx +145 -0
  83. package/src/views/payment-requests/view.json +692 -0
  84. package/src/views/payment-requests-initial-values.test.ts +73 -0
  85. package/src/views/request-log/index.ts +2 -0
  86. package/src/views/request-log/runtime.tsx +47 -0
  87. package/src/views/request-log/view.json +123 -0
  88. package/src/views/simple-test-view/index.ts +3 -0
  89. package/src/views/simple-test-view/runtime.tsx +85 -0
  90. package/src/views/simple-test-view/view.json +191 -0
  91. package/src/vite-env.d.ts +1 -0
  92. package/tailwind.config.js +7 -0
  93. package/tsconfig.app.json +26 -0
  94. package/tsconfig.jest.json +6 -0
  95. package/tsconfig.json +7 -0
  96. package/tsconfig.node.json +24 -0
  97. package/vite.config.ts +11 -0
@@ -0,0 +1,1805 @@
1
+ import { parseColumnDefinitionJson, parseFilterFieldSchemaJson, parseViewJson } from './view-parser';
2
+ import { Runtime } from './runtime';
3
+
4
+ describe('parseColumnDefinitionJson', () => {
5
+ const testRuntime: Runtime = {
6
+ cellRenderers: {
7
+ name: () => 'test',
8
+ email: () => 'test',
9
+ status: () => 'test',
10
+ amount: () => 'test'
11
+ },
12
+ queryTransforms: {},
13
+ noRowsComponents: {},
14
+ customFilterComponents: {},
15
+ initialValues: {}
16
+ };
17
+
18
+ describe('successful parsing', () => {
19
+ it('should parse valid JSON with single data field', () => {
20
+ const json = {
21
+ data: [{ type: 'field', path: 'user.name' }],
22
+ name: 'Name',
23
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
24
+ };
25
+
26
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
27
+
28
+ expect(result).toEqual({
29
+ data: [{ type: 'field', path: 'user.name' }],
30
+ name: 'Name',
31
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
32
+ });
33
+ });
34
+
35
+ it('should parse valid JSON with multiple data fields', () => {
36
+ const json = {
37
+ data: [
38
+ { type: 'field', path: 'user.firstName' },
39
+ { type: 'field', path: 'user.lastName' }
40
+ ],
41
+ name: 'Name',
42
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
43
+ };
44
+
45
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
46
+
47
+ expect(result).toEqual({
48
+ data: [
49
+ { type: 'field', path: 'user.firstName' },
50
+ { type: 'field', path: 'user.lastName' }
51
+ ],
52
+ name: 'Name',
53
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
54
+ });
55
+ });
56
+
57
+ it('should parse valid JSON with different cell renderer keys', () => {
58
+ const testCases = [
59
+ { cellRenderer: { section: 'cellRenderers', key: 'email' }, expected: 'email' },
60
+ { cellRenderer: { section: 'cellRenderers', key: 'status' }, expected: 'status' },
61
+ { cellRenderer: { section: 'cellRenderers', key: 'amount' }, expected: 'amount' }
62
+ ];
63
+
64
+ testCases.forEach(({ cellRenderer, expected }) => {
65
+ const json = {
66
+ data: [{ type: 'field', path: 'field' }],
67
+ name: 'Test',
68
+ cellRenderer
69
+ };
70
+
71
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
72
+ expect(result.cellRenderer.section).toBe('cellRenderers');
73
+ expect(result.cellRenderer.key).toBe(expected);
74
+ });
75
+ });
76
+
77
+ it('should parse valid JSON with different cell renderer keys (new format)', () => {
78
+ const testCases = [
79
+ { cellRenderer: { section: 'cellRenderers', key: 'email' }, expected: 'email' },
80
+ { cellRenderer: { section: 'cellRenderers', key: 'status' }, expected: 'status' },
81
+ { cellRenderer: { section: 'cellRenderers', key: 'amount' }, expected: 'amount' }
82
+ ];
83
+
84
+ testCases.forEach(({ cellRenderer, expected }) => {
85
+ const json = {
86
+ data: [{ type: 'field', path: 'field' }],
87
+ name: 'Test',
88
+ cellRenderer
89
+ };
90
+
91
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
92
+ expect(result.cellRenderer.section).toBe('cellRenderers');
93
+ expect(result.cellRenderer.key).toBe(expected);
94
+ });
95
+ });
96
+
97
+ it('should parse JSON with empty data array', () => {
98
+ const json = {
99
+ data: [],
100
+ name: 'Empty',
101
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
102
+ };
103
+
104
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
105
+ expect(result.data).toEqual([]);
106
+ expect(result.cellRenderer.section).toBe('cellRenderers');
107
+ expect(result.cellRenderer.key).toBe('name');
108
+ });
109
+
110
+ it('should parse JSON with queryConfigs data', () => {
111
+ const json = {
112
+ data: [{
113
+ type: 'queryConfigs',
114
+ configs: [
115
+ { field: 'posts', limit: 5 },
116
+ { field: 'title' }
117
+ ]
118
+ }],
119
+ name: 'Posts',
120
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
121
+ };
122
+
123
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
124
+ expect(result.data).toEqual([{
125
+ type: 'queryConfigs',
126
+ configs: [
127
+ { field: 'posts', limit: 5 },
128
+ { field: 'title' }
129
+ ]
130
+ }]);
131
+ });
132
+
133
+ it('should parse JSON with queryConfigs including orderBy', () => {
134
+ const json = {
135
+ data: [{
136
+ type: 'queryConfigs',
137
+ configs: [
138
+ {
139
+ field: 'posts',
140
+ limit: 5,
141
+ orderBy: { key: 'createdAt', direction: 'DESC' }
142
+ },
143
+ { field: 'title' }
144
+ ]
145
+ }],
146
+ name: 'Recent Posts',
147
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
148
+ };
149
+
150
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
151
+ expect(result.data).toEqual([{
152
+ type: 'queryConfigs',
153
+ configs: [
154
+ {
155
+ field: 'posts',
156
+ limit: 5,
157
+ orderBy: { key: 'createdAt', direction: 'DESC' }
158
+ },
159
+ { field: 'title' }
160
+ ]
161
+ }]);
162
+ });
163
+
164
+ it('should handle null orderBy and limit in queryConfigs', () => {
165
+ const json = {
166
+ data: [{
167
+ type: 'queryConfigs',
168
+ configs: [
169
+ {
170
+ field: 'posts',
171
+ limit: null,
172
+ orderBy: null
173
+ },
174
+ { field: 'title' }
175
+ ]
176
+ }],
177
+ name: 'Posts with null values',
178
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
179
+ };
180
+
181
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
182
+ expect(result.data).toEqual([{
183
+ type: 'queryConfigs',
184
+ configs: [
185
+ { field: 'posts' }, // null values should be omitted
186
+ { field: 'title' }
187
+ ]
188
+ }]);
189
+ });
190
+
191
+ it('should handle mixed null and valid values in queryConfigs', () => {
192
+ const json = {
193
+ data: [{
194
+ type: 'queryConfigs',
195
+ configs: [
196
+ {
197
+ field: 'posts',
198
+ limit: 5,
199
+ orderBy: null // null orderBy should be ignored
200
+ },
201
+ {
202
+ field: 'comments',
203
+ limit: null, // null limit should be ignored
204
+ orderBy: { key: 'createdAt', direction: 'ASC' }
205
+ }
206
+ ]
207
+ }],
208
+ name: 'Mixed null values',
209
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
210
+ };
211
+
212
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
213
+ expect(result.data).toEqual([{
214
+ type: 'queryConfigs',
215
+ configs: [
216
+ { field: 'posts', limit: 5 },
217
+ { field: 'comments', orderBy: { key: 'createdAt', direction: 'ASC' } }
218
+ ]
219
+ }]);
220
+ });
221
+
222
+ it('should parse JSON with queryConfigs including path property', () => {
223
+ const json = {
224
+ data: [{
225
+ type: 'queryConfigs',
226
+ configs: [
227
+ {
228
+ field: 'metadata',
229
+ path: '$.user.preferences.theme'
230
+ }
231
+ ]
232
+ }],
233
+ name: 'JSON Path Query',
234
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
235
+ };
236
+
237
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
238
+ expect(result.data).toEqual([{
239
+ type: 'queryConfigs',
240
+ configs: [
241
+ {
242
+ field: 'metadata',
243
+ path: '$.user.preferences.theme'
244
+ }
245
+ ]
246
+ }]);
247
+ });
248
+
249
+ it('should parse JSON with queryConfigs including path, limit, and orderBy', () => {
250
+ const json = {
251
+ data: [{
252
+ type: 'queryConfigs',
253
+ configs: [
254
+ {
255
+ field: 'activities',
256
+ path: '$.recent',
257
+ limit: 5,
258
+ orderBy: { key: 'timestamp', direction: 'DESC' }
259
+ }
260
+ ]
261
+ }],
262
+ name: 'Complex JSON Query',
263
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
264
+ };
265
+
266
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
267
+ expect(result.data).toEqual([{
268
+ type: 'queryConfigs',
269
+ configs: [
270
+ {
271
+ field: 'activities',
272
+ path: '$.recent',
273
+ limit: 5,
274
+ orderBy: { key: 'timestamp', direction: 'DESC' }
275
+ }
276
+ ]
277
+ }]);
278
+ });
279
+
280
+ it('should parse JSON with fieldAlias data', () => {
281
+ const json = {
282
+ data: [{
283
+ type: 'fieldAlias',
284
+ alias: 'userName',
285
+ field: { type: 'field', path: 'user.name' }
286
+ }],
287
+ name: 'User Name',
288
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
289
+ };
290
+
291
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
292
+ expect(result.data).toEqual([{
293
+ type: 'fieldAlias',
294
+ alias: 'userName',
295
+ field: { type: 'field', path: 'user.name' }
296
+ }]);
297
+ });
298
+
299
+ it('should parse JSON with nested fieldAlias data', () => {
300
+ const json = {
301
+ data: [{
302
+ type: 'fieldAlias',
303
+ alias: 'userPosts',
304
+ field: {
305
+ type: 'queryConfigs',
306
+ configs: [
307
+ { field: 'posts', limit: 3 }
308
+ ]
309
+ }
310
+ }],
311
+ name: 'User Posts',
312
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
313
+ };
314
+
315
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
316
+ expect(result.data).toEqual([{
317
+ type: 'fieldAlias',
318
+ alias: 'userPosts',
319
+ field: {
320
+ type: 'queryConfigs',
321
+ configs: [
322
+ { field: 'posts', limit: 3 }
323
+ ]
324
+ }
325
+ }]);
326
+ });
327
+ });
328
+
329
+ describe('input validation errors', () => {
330
+ it('should throw error for null input', () => {
331
+ expect(() => {
332
+ parseColumnDefinitionJson(null, testRuntime, undefined);
333
+ }).toThrow('Invalid JSON: Expected an object');
334
+ });
335
+
336
+ it('should throw error for undefined input', () => {
337
+ expect(() => {
338
+ parseColumnDefinitionJson(undefined, testRuntime, undefined);
339
+ }).toThrow('Invalid JSON: Expected an object');
340
+ });
341
+
342
+ it('should throw error for string input', () => {
343
+ expect(() => {
344
+ parseColumnDefinitionJson('invalid', testRuntime, undefined);
345
+ }).toThrow('Invalid JSON: Expected an object');
346
+ });
347
+
348
+ it('should throw error for number input', () => {
349
+ expect(() => {
350
+ parseColumnDefinitionJson(123, testRuntime, undefined);
351
+ }).toThrow('Invalid JSON: Expected an object');
352
+ });
353
+
354
+ it('should throw error for array input', () => {
355
+ expect(() => {
356
+ parseColumnDefinitionJson([1, 2, 3], testRuntime, undefined);
357
+ }).toThrow('Invalid JSON: Expected an object');
358
+ });
359
+ });
360
+
361
+ describe('data field validation', () => {
362
+ it('should throw error for missing data field', () => {
363
+ const json = {
364
+ name: 'Name',
365
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
366
+ };
367
+
368
+ expect(() => {
369
+ parseColumnDefinitionJson(json, testRuntime, undefined);
370
+ }).toThrow('Invalid JSON: "data" field must be an array of FieldQuery objects');
371
+ });
372
+
373
+ it('should throw error for null data field', () => {
374
+ const json = {
375
+ data: null,
376
+ name: 'Name',
377
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
378
+ };
379
+
380
+ expect(() => {
381
+ parseColumnDefinitionJson(json, testRuntime, undefined);
382
+ }).toThrow('Invalid JSON: "data" field must be an array of FieldQuery objects');
383
+ });
384
+
385
+ it('should throw error for string data field', () => {
386
+ const json = {
387
+ data: 'not an array',
388
+ name: 'Name',
389
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
390
+ };
391
+
392
+ expect(() => {
393
+ parseColumnDefinitionJson(json, testRuntime, undefined);
394
+ }).toThrow('Invalid JSON: "data" field must be an array of FieldQuery objects');
395
+ });
396
+
397
+ it('should throw error for object data field', () => {
398
+ const json = {
399
+ data: { field: 'value' },
400
+ name: 'Name',
401
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
402
+ };
403
+
404
+ expect(() => {
405
+ parseColumnDefinitionJson(json, testRuntime, undefined);
406
+ }).toThrow('Invalid JSON: "data" field must be an array of FieldQuery objects');
407
+ });
408
+ });
409
+
410
+ describe('name field validation', () => {
411
+ it('should throw error for missing name field', () => {
412
+ const json = {
413
+ data: ['field'],
414
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
415
+ };
416
+
417
+ expect(() => {
418
+ parseColumnDefinitionJson(json, testRuntime, undefined);
419
+ }).toThrow('Invalid JSON: "name" field must be a string');
420
+ });
421
+
422
+ it('should throw error for number name field', () => {
423
+ const json = {
424
+ data: ['field'],
425
+ name: 123,
426
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
427
+ };
428
+
429
+ expect(() => {
430
+ parseColumnDefinitionJson(json, testRuntime, undefined);
431
+ }).toThrow('Invalid JSON: "name" field must be a string');
432
+ });
433
+
434
+ it('should throw error for null name field', () => {
435
+ const json = {
436
+ data: ['field'],
437
+ name: null,
438
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
439
+ };
440
+
441
+ expect(() => {
442
+ parseColumnDefinitionJson(json, testRuntime, undefined);
443
+ }).toThrow('Invalid JSON: "name" field must be a string');
444
+ });
445
+
446
+ it('should throw error for object name field', () => {
447
+ const json = {
448
+ data: ['field'],
449
+ name: { value: 'Name' },
450
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
451
+ };
452
+
453
+ expect(() => {
454
+ parseColumnDefinitionJson(json, testRuntime, undefined);
455
+ }).toThrow('Invalid JSON: "name" field must be a string');
456
+ });
457
+
458
+ it('should throw error for array name field', () => {
459
+ const json = {
460
+ data: ['field'],
461
+ name: ['Name'],
462
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
463
+ };
464
+
465
+ expect(() => {
466
+ parseColumnDefinitionJson(json, testRuntime, undefined);
467
+ }).toThrow('Invalid JSON: "name" field must be a string');
468
+ });
469
+ });
470
+
471
+ describe('cellRenderer field validation', () => {
472
+ it('should throw error for missing cellRenderer field', () => {
473
+ const json = {
474
+ data: ['field'],
475
+ name: 'Name'
476
+ };
477
+
478
+ expect(() => {
479
+ parseColumnDefinitionJson(json, testRuntime, undefined);
480
+ }).toThrow('Invalid JSON: "cellRenderer" field is required');
481
+ });
482
+
483
+ it('should throw error for number cellRenderer field', () => {
484
+ const json = {
485
+ data: ['field'],
486
+ name: 'Name',
487
+ cellRenderer: { section: 'cellRenderers', key: 123 }
488
+ };
489
+
490
+ expect(() => {
491
+ parseColumnDefinitionJson(json, testRuntime, undefined);
492
+ }).toThrow('Invalid RuntimeReference: "key" must be a string');
493
+ });
494
+
495
+ it('should throw error for null cellRenderer field', () => {
496
+ const json = {
497
+ data: ['field'],
498
+ name: 'Name',
499
+ cellRenderer: { section: 'cellRenderers', key: null }
500
+ };
501
+
502
+ expect(() => {
503
+ parseColumnDefinitionJson(json, testRuntime, undefined);
504
+ }).toThrow('Invalid RuntimeReference: "key" must be a string');
505
+ });
506
+ });
507
+
508
+ describe('data array content validation', () => {
509
+ it('should throw error for number in data array', () => {
510
+ const json = {
511
+ data: [{ type: 'field', path: 'valid' }, 123, { type: 'field', path: 'valid2' }],
512
+ name: 'Name',
513
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
514
+ };
515
+
516
+ expect(() => {
517
+ parseColumnDefinitionJson(json, testRuntime, undefined);
518
+ }).toThrow('Invalid data[1]: Invalid FieldQuery: Expected an object');
519
+ });
520
+
521
+ it('should throw error for null in data array', () => {
522
+ const json = {
523
+ data: [{ type: 'field', path: 'valid' }, null, { type: 'field', path: 'valid2' }],
524
+ name: 'Name',
525
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
526
+ };
527
+
528
+ expect(() => {
529
+ parseColumnDefinitionJson(json, testRuntime, undefined);
530
+ }).toThrow('Invalid data[1]: Invalid FieldQuery: Expected an object');
531
+ });
532
+
533
+ it('should throw error for object in data array', () => {
534
+ const json = {
535
+ data: [{ type: 'field', path: 'valid' }, { field: 'value' }, { type: 'field', path: 'valid2' }],
536
+ name: 'Name',
537
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
538
+ };
539
+
540
+ expect(() => {
541
+ parseColumnDefinitionJson(json, testRuntime, undefined);
542
+ }).toThrow('Invalid data[1]: Invalid FieldQuery: "type" must be "field", "queryConfigs", or "fieldAlias"');
543
+ });
544
+
545
+ it('should throw error for array in data array', () => {
546
+ const json = {
547
+ data: [{ type: 'field', path: 'valid' }, ['nested'], { type: 'field', path: 'valid2' }],
548
+ name: 'Name',
549
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
550
+ };
551
+
552
+ expect(() => {
553
+ parseColumnDefinitionJson(json, testRuntime, undefined);
554
+ }).toThrow('Invalid data[1]: Invalid FieldQuery: Expected an object');
555
+ });
556
+
557
+ it('should throw error for undefined in data array', () => {
558
+ const json = {
559
+ data: [{ type: 'field', path: 'valid' }, undefined, { type: 'field', path: 'valid2' }],
560
+ name: 'Name',
561
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
562
+ };
563
+
564
+ expect(() => {
565
+ parseColumnDefinitionJson(json, testRuntime, undefined);
566
+ }).toThrow('Invalid data[1]: Invalid FieldQuery: Expected an object');
567
+ });
568
+
569
+ it('should throw error for invalid path type in queryConfigs', () => {
570
+ const json = {
571
+ data: [{
572
+ type: 'queryConfigs',
573
+ configs: [
574
+ {
575
+ field: 'metadata',
576
+ path: 123 // Invalid: path should be a string
577
+ }
578
+ ]
579
+ }],
580
+ name: 'Invalid Path',
581
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
582
+ };
583
+
584
+ expect(() => {
585
+ parseColumnDefinitionJson(json, testRuntime, undefined);
586
+ }).toThrow('Invalid data[0]: Invalid QueryConfig: "path" must be a string');
587
+ });
588
+
589
+ it('should throw error for null path in queryConfigs when provided', () => {
590
+ const json = {
591
+ data: [{
592
+ type: 'queryConfigs',
593
+ configs: [
594
+ {
595
+ field: 'metadata',
596
+ path: null // null path should be ignored, but this doesn't happen in parsing
597
+ }
598
+ ]
599
+ }],
600
+ name: 'Null Path',
601
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
602
+ };
603
+
604
+ // This should not throw because null path is handled and ignored
605
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
606
+ expect(result.data).toEqual([{
607
+ type: 'queryConfigs',
608
+ configs: [
609
+ { field: 'metadata' } // path should be omitted
610
+ ]
611
+ }]);
612
+ });
613
+
614
+ it('should throw error for fieldAlias missing alias property', () => {
615
+ const json = {
616
+ data: [{
617
+ type: 'fieldAlias',
618
+ field: { type: 'field', path: 'user.name' }
619
+ // missing alias property
620
+ }],
621
+ name: 'User Name',
622
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
623
+ };
624
+
625
+ expect(() => {
626
+ parseColumnDefinitionJson(json, testRuntime, undefined);
627
+ }).toThrow('Invalid data[0]: Invalid FieldAlias: "alias" must be a string');
628
+ });
629
+
630
+ it('should throw error for fieldAlias missing field property', () => {
631
+ const json = {
632
+ data: [{
633
+ type: 'fieldAlias',
634
+ alias: 'userName'
635
+ // missing field property
636
+ }],
637
+ name: 'User Name',
638
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
639
+ };
640
+
641
+ expect(() => {
642
+ parseColumnDefinitionJson(json, testRuntime, undefined);
643
+ }).toThrow('Invalid data[0]: Invalid FieldAlias: "field" is required');
644
+ });
645
+
646
+ it('should throw error for fieldAlias with invalid nested field', () => {
647
+ const json = {
648
+ data: [{
649
+ type: 'fieldAlias',
650
+ alias: 'userName',
651
+ field: { type: 'invalid', path: 'user.name' }
652
+ }],
653
+ name: 'User Name',
654
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
655
+ };
656
+
657
+ expect(() => {
658
+ parseColumnDefinitionJson(json, testRuntime, undefined);
659
+ }).toThrow('Invalid data[0]: Invalid FieldQuery: "type" must be "field", "queryConfigs", or "fieldAlias"');
660
+ });
661
+ });
662
+
663
+ describe('runtime key validation', () => {
664
+ it('should throw error for invalid cellRenderer reference', () => {
665
+ const json = {
666
+ data: [{ type: 'field', path: 'field' }],
667
+ name: 'Name',
668
+ cellRenderer: { section: 'cellRenderers', key: 'invalidKey' }
669
+ };
670
+
671
+ expect(() => {
672
+ parseColumnDefinitionJson(json, testRuntime, undefined);
673
+ }).toThrow('Invalid cellRenderer reference: "invalidKey". Valid keys are: name, email, status, amount');
674
+ });
675
+
676
+ it('should throw error for empty string cellRenderer reference', () => {
677
+ const json = {
678
+ data: [{ type: 'field', path: 'field' }],
679
+ name: 'Name',
680
+ cellRenderer: { section: 'cellRenderers', key: '' }
681
+ };
682
+
683
+ expect(() => {
684
+ parseColumnDefinitionJson(json, testRuntime, undefined);
685
+ }).toThrow('Invalid cellRenderer reference: "". Valid keys are: name, email, status, amount');
686
+ });
687
+
688
+ it('should throw error for case-sensitive mismatch', () => {
689
+ const json = {
690
+ data: [{ type: 'field', path: 'field' }],
691
+ name: 'Name',
692
+ cellRenderer: { section: 'cellRenderers', key: 'NAME' } // Wrong case
693
+ };
694
+
695
+ expect(() => {
696
+ parseColumnDefinitionJson(json, testRuntime, undefined);
697
+ }).toThrow('Invalid cellRenderer reference: "NAME". Valid keys are: name, email, status, amount');
698
+ });
699
+ });
700
+
701
+ describe('edge cases', () => {
702
+ it('should handle empty string in data array', () => {
703
+ const json = {
704
+ data: [{ type: 'field', path: '' }],
705
+ name: 'Name',
706
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
707
+ };
708
+
709
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
710
+ expect(result.data).toEqual([{ type: 'field', path: '' }]);
711
+ });
712
+
713
+ it('should handle empty string as name', () => {
714
+ const json = {
715
+ data: [{ type: 'field', path: 'field' }],
716
+ name: '',
717
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
718
+ };
719
+
720
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
721
+ expect(result.name).toBe('');
722
+ });
723
+
724
+ it('should handle extra properties in JSON', () => {
725
+ const json = {
726
+ data: [{ type: 'field', path: 'field' }],
727
+ name: 'Name',
728
+ cellRenderer: { section: 'cellRenderers', key: 'name' },
729
+ extraProperty: 'ignored'
730
+ };
731
+
732
+ const result = parseColumnDefinitionJson(json, testRuntime, undefined);
733
+ expect(result).toEqual({
734
+ data: [{ type: 'field', path: 'field' }],
735
+ name: 'Name',
736
+ cellRenderer: { section: 'cellRenderers', key: 'name' }
737
+ });
738
+ expect('extraProperty' in result).toBe(false);
739
+ });
740
+ });
741
+ });
742
+
743
+ describe('parseFilterFieldSchemaJson', () => {
744
+ const testRuntime: Runtime = {
745
+ cellRenderers: {},
746
+ noRowsComponents: {},
747
+ customFilterComponents: {},
748
+ queryTransforms: {
749
+ reference: {
750
+ toQuery: (input: any) => ({ value: `${input}%` })
751
+ },
752
+ amount: {
753
+ toQuery: (input: any) => ({ value: input * 100 })
754
+ },
755
+ creditCard: {
756
+ toQuery: (input: any) => ({ value: `%${input}%` })
757
+ }
758
+ },
759
+ initialValues: {}
760
+ };
761
+
762
+ describe('successful parsing', () => {
763
+ it('should parse valid FilterFieldSchema with basic filters', () => {
764
+ const json = {
765
+ groups: [
766
+ { name: 'default', label: null },
767
+ { name: 'advanced', label: 'Advanced Filters' }
768
+ ],
769
+ filters: [
770
+ {
771
+ id: 'name-filter',
772
+ label: 'Name',
773
+ expression: {
774
+ type: 'equals',
775
+ field: 'name',
776
+ value: { type: 'text' }
777
+ },
778
+ group: 'default',
779
+ aiGenerated: false
780
+ },
781
+ {
782
+ id: 'status-filter',
783
+ label: 'Status',
784
+ expression: {
785
+ type: 'in',
786
+ field: 'status',
787
+ value: {
788
+ type: 'multiselect',
789
+ items: [
790
+ { label: 'Active', value: 'active' },
791
+ { label: 'Inactive', value: 'inactive' }
792
+ ]
793
+ }
794
+ },
795
+ group: 'advanced',
796
+ aiGenerated: true
797
+ }
798
+ ]
799
+ };
800
+
801
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
802
+
803
+ expect(result.groups).toHaveLength(2);
804
+ expect(result.groups[0]).toEqual({ name: 'default', label: null });
805
+ expect(result.groups[1]).toEqual({ name: 'advanced', label: 'Advanced Filters' });
806
+
807
+ expect(result.filters).toHaveLength(2);
808
+ expect(result.filters[0]).toEqual({
809
+ id: 'name-filter',
810
+ label: 'Name',
811
+ expression: {
812
+ type: 'equals',
813
+ field: 'name',
814
+ value: { type: 'text' }
815
+ },
816
+ group: 'default',
817
+ aiGenerated: false
818
+ });
819
+ expect(result.filters[1].id).toBe('status-filter');
820
+ expect(result.filters[1].label).toBe('Status');
821
+ expect(result.filters[1].expression.type).toBe('in');
822
+ expect(result.filters[1].aiGenerated).toBe(true);
823
+ });
824
+
825
+ it('should parse filters with transform references', () => {
826
+ const json = {
827
+ groups: [{ name: 'default', label: null }],
828
+ filters: [
829
+ {
830
+ id: 'reference-filter',
831
+ label: 'Reference',
832
+ expression: {
833
+ type: 'equals',
834
+ field: 'reference',
835
+ value: { type: 'text' },
836
+ transform: { section: 'queryTransforms', key: 'reference' }
837
+ },
838
+ group: 'default',
839
+ aiGenerated: false
840
+ },
841
+ {
842
+ id: 'amount-filter',
843
+ label: 'Amount',
844
+ expression: {
845
+ type: 'greaterThan',
846
+ field: 'amount',
847
+ value: { type: 'number' },
848
+ transform: { section: 'queryTransforms', key: 'amount' }
849
+ },
850
+ group: 'default',
851
+ aiGenerated: false
852
+ }
853
+ ]
854
+ };
855
+
856
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
857
+
858
+ expect(result.filters).toHaveLength(2);
859
+
860
+ // Check that transforms are resolved
861
+ const referenceFilter = result.filters[0];
862
+ expect(referenceFilter.label).toBe('Reference');
863
+ expect('transform' in referenceFilter.expression).toBe(true);
864
+ expect((referenceFilter.expression as any).transform).toBe(testRuntime.queryTransforms.reference);
865
+
866
+ const amountFilter = result.filters[1];
867
+ expect(amountFilter.label).toBe('Amount');
868
+ expect('transform' in amountFilter.expression).toBe(true);
869
+ expect((amountFilter.expression as any).transform).toBe(testRuntime.queryTransforms.amount);
870
+ });
871
+
872
+ it('should parse complex expressions with and/or/not', () => {
873
+ const json = {
874
+ groups: [{ name: 'default', label: null }],
875
+ filters: [
876
+ {
877
+ id: 'complex-filter',
878
+ label: 'Complex Filter',
879
+ expression: {
880
+ type: 'and',
881
+ filters: [
882
+ {
883
+ type: 'equals',
884
+ field: 'status',
885
+ value: { type: 'text' }
886
+ },
887
+ {
888
+ type: 'or',
889
+ filters: [
890
+ {
891
+ type: 'greaterThan',
892
+ field: 'amount',
893
+ value: { type: 'number' },
894
+ transform: { section: 'queryTransforms', key: 'amount' }
895
+ },
896
+ {
897
+ type: 'not',
898
+ filter: {
899
+ type: 'equals',
900
+ field: 'disabled',
901
+ value: { type: 'text' }
902
+ }
903
+ }
904
+ ]
905
+ }
906
+ ]
907
+ },
908
+ group: 'default',
909
+ aiGenerated: false
910
+ }
911
+ ]
912
+ };
913
+
914
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
915
+
916
+ expect(result.filters).toHaveLength(1);
917
+ const filter = result.filters[0];
918
+ expect(filter.expression.type).toBe('and');
919
+
920
+ const andExpr = filter.expression as any;
921
+ expect(andExpr.filters).toHaveLength(2);
922
+ expect(andExpr.filters[0].type).toBe('equals');
923
+ expect(andExpr.filters[1].type).toBe('or');
924
+
925
+ const orExpr = andExpr.filters[1];
926
+ expect(orExpr.filters).toHaveLength(2);
927
+ expect(orExpr.filters[0].type).toBe('greaterThan');
928
+ expect('transform' in orExpr.filters[0]).toBe(true);
929
+ expect(orExpr.filters[1].type).toBe('not');
930
+ });
931
+
932
+ it('should parse filters with customOperator controls', () => {
933
+ const json = {
934
+ groups: [{ name: 'default', label: null }],
935
+ filters: [
936
+ {
937
+ id: 'search-filter',
938
+ label: 'Search',
939
+ expression: {
940
+ type: 'equals',
941
+ field: 'search',
942
+ value: {
943
+ type: 'customOperator',
944
+ operators: [
945
+ { label: 'equals', value: '_eq' },
946
+ { label: 'starts with', value: '_like' }
947
+ ],
948
+ valueControl: { type: 'text' }
949
+ },
950
+ transform: { section: 'queryTransforms', key: 'reference' }
951
+ },
952
+ group: 'default',
953
+ aiGenerated: false
954
+ }
955
+ ]
956
+ };
957
+
958
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
959
+
960
+ expect(result.filters).toHaveLength(1);
961
+ const filter = result.filters[0];
962
+ expect(filter.expression.type).toBe('equals');
963
+ expect((filter.expression as any).value.type).toBe('customOperator');
964
+ expect('transform' in filter.expression).toBe(true);
965
+ });
966
+ });
967
+
968
+ describe('error handling', () => {
969
+ it('should throw error for invalid JSON structure', () => {
970
+ expect(() => parseFilterFieldSchemaJson(null, testRuntime, undefined))
971
+ .toThrow('Invalid FilterFieldSchema: Expected an object');
972
+
973
+ expect(() => parseFilterFieldSchemaJson([], testRuntime, undefined))
974
+ .toThrow('Invalid FilterFieldSchema: Expected an object');
975
+
976
+ expect(() => parseFilterFieldSchemaJson('string', testRuntime, undefined))
977
+ .toThrow('Invalid FilterFieldSchema: Expected an object');
978
+ });
979
+
980
+ it('should throw error for missing or invalid groups', () => {
981
+ expect(() => parseFilterFieldSchemaJson({ filters: [] }, testRuntime, undefined))
982
+ .toThrow('Invalid FilterFieldSchema: "groups" must be an array');
983
+
984
+ expect(() => parseFilterFieldSchemaJson({ groups: 'not-array', filters: [] }, testRuntime))
985
+ .toThrow('Invalid FilterFieldSchema: "groups" must be an array');
986
+ });
987
+
988
+ it('should throw error for invalid group structure', () => {
989
+ const invalidGroup = {
990
+ groups: [{ label: 'Missing name' }],
991
+ filters: []
992
+ };
993
+
994
+ expect(() => parseFilterFieldSchemaJson(invalidGroup, testRuntime, undefined))
995
+ .toThrow('Invalid group[0]: "name" must be a string');
996
+
997
+ const invalidGroupLabel = {
998
+ groups: [{ name: 'test', label: 123 }],
999
+ filters: []
1000
+ };
1001
+
1002
+ expect(() => parseFilterFieldSchemaJson(invalidGroupLabel, testRuntime, undefined))
1003
+ .toThrow('Invalid group[0]: "label" must be a string or null');
1004
+ });
1005
+
1006
+ it('should throw error for missing or invalid filters', () => {
1007
+ expect(() => parseFilterFieldSchemaJson({ groups: [] }, testRuntime, undefined))
1008
+ .toThrow('Invalid FilterFieldSchema: "filters" must be an array');
1009
+
1010
+ expect(() => parseFilterFieldSchemaJson({ groups: [], filters: 'not-array' }, testRuntime))
1011
+ .toThrow('Invalid FilterFieldSchema: "filters" must be an array');
1012
+ });
1013
+
1014
+ it('should throw error for invalid filter structure', () => {
1015
+ const invalidFilter = {
1016
+ groups: [{ name: 'default', label: null }],
1017
+ filters: [{ expression: { type: 'equals', field: 'test', value: { type: 'text' } } }]
1018
+ };
1019
+
1020
+ expect(() => parseFilterFieldSchemaJson(invalidFilter, testRuntime, undefined))
1021
+ .toThrow('Invalid filter[0]: "id" must be a string');
1022
+
1023
+ const missingGroup = {
1024
+ groups: [{ name: 'default', label: null }],
1025
+ filters: [{ id: 'test', label: 'Test', expression: { type: 'equals', field: 'test', value: { type: 'text' } } }]
1026
+ };
1027
+
1028
+ expect(() => parseFilterFieldSchemaJson(missingGroup, testRuntime, undefined))
1029
+ .toThrow('Invalid filter[0]: "group" must be a string');
1030
+
1031
+ const missingAiGenerated = {
1032
+ groups: [{ name: 'default', label: null }],
1033
+ filters: [{ id: 'test', label: 'Test', group: 'default', expression: { type: 'equals', field: 'test', value: { type: 'text' } } }]
1034
+ };
1035
+
1036
+ expect(() => parseFilterFieldSchemaJson(missingAiGenerated, testRuntime, undefined))
1037
+ .toThrow('Invalid filter[0]: "aiGenerated" must be a boolean');
1038
+ });
1039
+
1040
+ it('should throw error for missing expression', () => {
1041
+ const missingExpression = {
1042
+ groups: [{ name: 'default', label: null }],
1043
+ filters: [{ id: 'test', label: 'Test', group: 'default', aiGenerated: false }]
1044
+ };
1045
+
1046
+ expect(() => parseFilterFieldSchemaJson(missingExpression, testRuntime, undefined))
1047
+ .toThrow('Invalid filter[0]: "expression" is required');
1048
+ });
1049
+
1050
+ it('should throw error for invalid transform reference', () => {
1051
+ const invalidTransformReference = {
1052
+ groups: [{ name: 'default', label: null }],
1053
+ filters: [
1054
+ {
1055
+ id: 'test',
1056
+ label: 'Test',
1057
+ expression: {
1058
+ type: 'equals',
1059
+ field: 'test',
1060
+ value: { type: 'text' },
1061
+ transform: { section: 'queryTransforms', key: 'nonExistentTransform' }
1062
+ },
1063
+ group: 'default',
1064
+ aiGenerated: false
1065
+ }
1066
+ ]
1067
+ };
1068
+
1069
+ expect(() => parseFilterFieldSchemaJson(invalidTransformReference, testRuntime, undefined))
1070
+ .toThrow('Invalid filter[0] expression: Reference "nonExistentTransform" not found in queryTransforms. Available keys: reference, amount, creditCard');
1071
+ });
1072
+
1073
+ it('should throw error for invalid expression structure', () => {
1074
+ const invalidExpression = {
1075
+ groups: [{ name: 'default', label: null }],
1076
+ filters: [
1077
+ {
1078
+ id: 'test',
1079
+ label: 'Test',
1080
+ expression: {
1081
+ type: 'invalidType',
1082
+ field: 'test',
1083
+ value: { type: 'text' }
1084
+ },
1085
+ group: 'default',
1086
+ aiGenerated: false
1087
+ }
1088
+ ]
1089
+ };
1090
+
1091
+ expect(() => parseFilterFieldSchemaJson(invalidExpression, testRuntime, undefined))
1092
+ .toThrow('Invalid FilterExpr type: "invalidType"');
1093
+ });
1094
+
1095
+ it('should throw error for invalid composite expression', () => {
1096
+ const invalidAnd = {
1097
+ groups: [{ name: 'default', label: null }],
1098
+ filters: [
1099
+ {
1100
+ id: 'test',
1101
+ label: 'Test',
1102
+ expression: {
1103
+ type: 'and',
1104
+ filters: 'not-array'
1105
+ },
1106
+ group: 'default',
1107
+ aiGenerated: false
1108
+ }
1109
+ ]
1110
+ };
1111
+
1112
+ expect(() => parseFilterFieldSchemaJson(invalidAnd, testRuntime, undefined))
1113
+ .toThrow('Invalid and FilterExpr: "filters" must be an array');
1114
+
1115
+ const invalidNot = {
1116
+ groups: [{ name: 'default', label: null }],
1117
+ filters: [
1118
+ {
1119
+ id: 'test-2',
1120
+ label: 'Test',
1121
+ expression: {
1122
+ type: 'not',
1123
+ filter: 'not-object'
1124
+ },
1125
+ group: 'default',
1126
+ aiGenerated: false
1127
+ }
1128
+ ]
1129
+ };
1130
+
1131
+ expect(() => parseFilterFieldSchemaJson(invalidNot, testRuntime, undefined))
1132
+ .toThrow('Invalid not FilterExpr: "filter" must be an object');
1133
+ });
1134
+ });
1135
+
1136
+ describe('multi-field format support', () => {
1137
+ it('should parse filters with AND multi-field format', () => {
1138
+ const json = {
1139
+ groups: [{ name: 'default', label: null }],
1140
+ filters: [
1141
+ {
1142
+ id: 'multi-field-and',
1143
+ label: 'Multi-field AND Filter',
1144
+ expression: {
1145
+ type: 'equals',
1146
+ field: {
1147
+ and: ['testField', 'email', 'name']
1148
+ },
1149
+ value: {
1150
+ type: 'text',
1151
+ placeholder: 'Match all fields'
1152
+ }
1153
+ },
1154
+ group: 'default',
1155
+ aiGenerated: false
1156
+ }
1157
+ ]
1158
+ };
1159
+
1160
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
1161
+
1162
+ expect(result.filters).toHaveLength(1);
1163
+ const filter = result.filters[0];
1164
+ expect(filter.label).toBe('Multi-field AND Filter');
1165
+ expect(filter.expression.type).toBe('equals');
1166
+
1167
+ const fieldValue = (filter.expression as any).field;
1168
+ expect(fieldValue).toEqual({ and: ['testField', 'email', 'name'] });
1169
+ });
1170
+
1171
+ it('should parse filters with OR multi-field format', () => {
1172
+ const json = {
1173
+ groups: [{ name: 'default', label: null }],
1174
+ filters: [
1175
+ {
1176
+ id: 'multi-field-or',
1177
+ label: 'Multi-field OR Filter',
1178
+ expression: {
1179
+ type: 'iLike',
1180
+ field: {
1181
+ or: ['title', 'description', 'tags']
1182
+ },
1183
+ value: {
1184
+ type: 'text',
1185
+ placeholder: 'Search in any field'
1186
+ }
1187
+ },
1188
+ group: 'default',
1189
+ aiGenerated: true
1190
+ }
1191
+ ]
1192
+ };
1193
+
1194
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
1195
+
1196
+ expect(result.filters).toHaveLength(1);
1197
+ const filter = result.filters[0];
1198
+ expect(filter.label).toBe('Multi-field OR Filter');
1199
+ expect(filter.expression.type).toBe('iLike');
1200
+ expect(filter.aiGenerated).toBe(true);
1201
+
1202
+ const fieldValue = (filter.expression as any).field;
1203
+ expect(fieldValue).toEqual({ or: ['title', 'description', 'tags'] });
1204
+ });
1205
+
1206
+ it('should parse filters with single field (string format)', () => {
1207
+ const json = {
1208
+ groups: [{ name: 'default', label: null }],
1209
+ filters: [
1210
+ {
1211
+ id: 'single-field',
1212
+ label: 'Single Field Filter',
1213
+ expression: {
1214
+ type: 'equals',
1215
+ field: 'email',
1216
+ value: {
1217
+ type: 'text'
1218
+ }
1219
+ },
1220
+ group: 'default',
1221
+ aiGenerated: false
1222
+ }
1223
+ ]
1224
+ };
1225
+
1226
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
1227
+
1228
+ expect(result.filters).toHaveLength(1);
1229
+ const filter = result.filters[0];
1230
+ expect(filter.expression.type).toBe('equals');
1231
+
1232
+ const fieldValue = (filter.expression as any).field;
1233
+ expect(fieldValue).toBe('email');
1234
+ });
1235
+
1236
+ it('should handle multi-field format with transforms', () => {
1237
+ const json = {
1238
+ groups: [{ name: 'default', label: null }],
1239
+ filters: [
1240
+ {
1241
+ id: 'multi-field-transform',
1242
+ label: 'Multi-field with Transform',
1243
+ expression: {
1244
+ type: 'greaterThan',
1245
+ field: {
1246
+ or: ['amount', 'total']
1247
+ },
1248
+ value: {
1249
+ type: 'number'
1250
+ },
1251
+ transform: {
1252
+ section: 'queryTransforms',
1253
+ key: 'amount'
1254
+ }
1255
+ },
1256
+ group: 'default',
1257
+ aiGenerated: false
1258
+ }
1259
+ ]
1260
+ };
1261
+
1262
+ const result = parseFilterFieldSchemaJson(json, testRuntime, undefined);
1263
+
1264
+ expect(result.filters).toHaveLength(1);
1265
+ const filter = result.filters[0];
1266
+ expect(filter.expression.type).toBe('greaterThan');
1267
+ expect('transform' in filter.expression).toBe(true);
1268
+ expect((filter.expression as any).transform).toBe(testRuntime.queryTransforms.amount);
1269
+
1270
+ const fieldValue = (filter.expression as any).field;
1271
+ expect(fieldValue).toEqual({ or: ['amount', 'total'] });
1272
+ });
1273
+
1274
+ it('should throw error for invalid multi-field format', () => {
1275
+ const invalidAndField = {
1276
+ groups: [{ name: 'default', label: null }],
1277
+ filters: [
1278
+ {
1279
+ id: 'invalid-and',
1280
+ label: 'Test',
1281
+ expression: {
1282
+ type: 'equals',
1283
+ field: {
1284
+ and: 'not-array'
1285
+ },
1286
+ value: { type: 'text' }
1287
+ },
1288
+ group: 'default',
1289
+ aiGenerated: false
1290
+ }
1291
+ ]
1292
+ };
1293
+
1294
+ expect(() => parseFilterFieldSchemaJson(invalidAndField, testRuntime, undefined))
1295
+ .toThrow('Invalid FilterField: "and" must be an array of strings');
1296
+
1297
+ const invalidOrField = {
1298
+ groups: [{ name: 'default', label: null }],
1299
+ filters: [
1300
+ {
1301
+ id: 'invalid-or',
1302
+ label: 'Test',
1303
+ expression: {
1304
+ type: 'equals',
1305
+ field: {
1306
+ or: [123, 456]
1307
+ },
1308
+ value: { type: 'text' }
1309
+ },
1310
+ group: 'default',
1311
+ aiGenerated: false
1312
+ }
1313
+ ]
1314
+ };
1315
+
1316
+ expect(() => parseFilterFieldSchemaJson(invalidOrField, testRuntime, undefined))
1317
+ .toThrow('Invalid FilterField: "or" array must contain only strings');
1318
+
1319
+ const invalidFieldFormat = {
1320
+ groups: [{ name: 'default', label: null }],
1321
+ filters: [
1322
+ {
1323
+ id: 'invalid-field',
1324
+ label: 'Test',
1325
+ expression: {
1326
+ type: 'equals',
1327
+ field: 123,
1328
+ value: { type: 'text' }
1329
+ },
1330
+ group: 'default',
1331
+ aiGenerated: false
1332
+ }
1333
+ ]
1334
+ };
1335
+
1336
+ expect(() => parseFilterFieldSchemaJson(invalidFieldFormat, testRuntime, undefined))
1337
+ .toThrow('Invalid FilterField: must be a string or object with "and" or "or" arrays');
1338
+ });
1339
+ });
1340
+ });
1341
+
1342
+ describe('parseViewJson', () => {
1343
+ const mockNoRowsComponent = () => null;
1344
+
1345
+ const viewTestRuntime = {
1346
+ cellRenderers: {
1347
+ text: () => 'text-renderer',
1348
+ number: () => 'number-renderer',
1349
+ custom: () => 'custom-renderer'
1350
+ },
1351
+ queryTransforms: {
1352
+ reference: {
1353
+ toQuery: (input: any) => ({ value: `${input}%` })
1354
+ },
1355
+ amount: {
1356
+ toQuery: (input: any) => ({ value: input * 100 })
1357
+ },
1358
+ creditCardNumber: {
1359
+ toQuery: (input: any) => ({ value: `%${input}%` })
1360
+ }
1361
+ },
1362
+ noRowsComponents: {
1363
+ noRowsExtendDateRange: mockNoRowsComponent
1364
+ },
1365
+ customFilterComponents: {},
1366
+ initialValues: {}
1367
+ };
1368
+
1369
+
1370
+ describe('successful parsing', () => {
1371
+ it('should parse valid ViewJson with all required fields', () => {
1372
+ const validJson = {
1373
+ title: 'Test View',
1374
+ id: 'test-view',
1375
+ collectionName: 'testCollection',
1376
+ paginationKey: 'createdAt',
1377
+ boolExpType: 'TestBoolExp',
1378
+ orderByType: '[TestOrderBy!]',
1379
+ staticConditions: [
1380
+ { status: { _eq: 'ACTIVE' } }
1381
+ ],
1382
+ columns: [
1383
+ {
1384
+ data: [{ type: 'field', path: 'id' }],
1385
+ name: 'ID',
1386
+ cellRenderer: { section: 'cellRenderers', key: 'text' }
1387
+ }
1388
+ ],
1389
+ filterSchema: {
1390
+ groups: [{ name: 'default', label: null }],
1391
+ filters: [
1392
+ {
1393
+ id: 'test-filter',
1394
+ label: 'Test Filter',
1395
+ expression: {
1396
+ type: 'equals',
1397
+ field: 'test',
1398
+ value: { type: 'text' }
1399
+ },
1400
+ group: 'default',
1401
+ aiGenerated: false
1402
+ }
1403
+ ]
1404
+ }
1405
+ };
1406
+
1407
+ const result = parseViewJson(validJson, viewTestRuntime);
1408
+
1409
+ expect(result.title).toBe('Test View');
1410
+ expect(result.id).toBe('test-view');
1411
+ expect(result.collectionName).toBe('testCollection');
1412
+ expect(result.paginationKey).toBe('createdAt');
1413
+ expect(result.boolExpType).toBe('TestBoolExp');
1414
+ expect(result.orderByType).toBe('[TestOrderBy!]');
1415
+ expect(result.columnDefinitions).toHaveLength(1);
1416
+ expect(result.filterSchema.groups).toHaveLength(1);
1417
+ expect(result.filterSchema.filters).toHaveLength(1);
1418
+ expect(result.noRowsComponent).toBeUndefined();
1419
+ expect(result.staticConditions).toEqual([{ status: { _eq: 'ACTIVE' } }]);
1420
+ });
1421
+
1422
+ it('should parse ViewJson with noRowsComponent', () => {
1423
+ const validJson = {
1424
+ title: 'Test View',
1425
+ id: 'test-view',
1426
+ collectionName: 'testCollection',
1427
+ paginationKey: 'createdAt',
1428
+ boolExpType: 'TestBoolExp',
1429
+ orderByType: '[TestOrderBy!]',
1430
+ noRowsComponent: { section: 'noRowsComponents', key: 'noRowsExtendDateRange' },
1431
+ columns: [
1432
+ {
1433
+ data: [{ type: 'field', path: 'id' }],
1434
+ name: 'ID',
1435
+ cellRenderer: { section: 'cellRenderers', key: 'text' }
1436
+ }
1437
+ ],
1438
+ filterSchema: {
1439
+ groups: [{ name: 'default', label: null }],
1440
+ filters: []
1441
+ }
1442
+ };
1443
+
1444
+ const result = parseViewJson(validJson, viewTestRuntime);
1445
+
1446
+ expect(result.noRowsComponent).toBe(mockNoRowsComponent);
1447
+ });
1448
+
1449
+ it('should parse ViewJson with multiple columns and complex filters', () => {
1450
+ const complexJson = {
1451
+ title: 'Complex View',
1452
+ id: 'complex-view',
1453
+ collectionName: 'complexCollection',
1454
+ paginationKey: 'updatedAt',
1455
+ boolExpType: 'ComplexBoolExp',
1456
+ orderByType: '[ComplexOrderBy!]',
1457
+ columns: [
1458
+ {
1459
+ data: [{ type: 'field', path: 'id' }],
1460
+ name: 'ID',
1461
+ cellRenderer: { section: 'cellRenderers', key: 'text' }
1462
+ },
1463
+ {
1464
+ data: [
1465
+ { type: 'field', path: 'amount' },
1466
+ {
1467
+ type: 'queryConfigs',
1468
+ configs: [
1469
+ { field: 'currency', limit: 1 }
1470
+ ]
1471
+ }
1472
+ ],
1473
+ name: 'Amount',
1474
+ cellRenderer: { section: 'cellRenderers', key: 'number' }
1475
+ }
1476
+ ],
1477
+ filterSchema: {
1478
+ groups: [
1479
+ { name: 'default', label: null },
1480
+ { name: 'advanced', label: 'Advanced Filters' }
1481
+ ],
1482
+ filters: [
1483
+ {
1484
+ id: 'reference-filter',
1485
+ label: 'Reference',
1486
+ expression: {
1487
+ type: 'equals',
1488
+ field: 'reference',
1489
+ value: { type: 'text' },
1490
+ transform: { section: 'queryTransforms', key: 'reference' }
1491
+ },
1492
+ group: 'default',
1493
+ aiGenerated: false
1494
+ },
1495
+ {
1496
+ id: 'complex-filter',
1497
+ label: 'Complex Filter',
1498
+ expression: {
1499
+ type: 'and',
1500
+ filters: [
1501
+ {
1502
+ type: 'greaterThan',
1503
+ field: 'amount',
1504
+ value: { type: 'number' }
1505
+ },
1506
+ {
1507
+ type: 'in',
1508
+ field: 'status',
1509
+ value: {
1510
+ type: 'multiselect',
1511
+ config: {
1512
+ items: [
1513
+ { label: 'Active', value: 'active' },
1514
+ { label: 'Inactive', value: 'inactive' }
1515
+ ]
1516
+ }
1517
+ }
1518
+ }
1519
+ ]
1520
+ },
1521
+ group: 'advanced',
1522
+ aiGenerated: true
1523
+ }
1524
+ ]
1525
+ }
1526
+ };
1527
+
1528
+ const result = parseViewJson(complexJson, viewTestRuntime);
1529
+
1530
+ expect(result.columnDefinitions).toHaveLength(2);
1531
+ expect(result.filterSchema.groups).toHaveLength(2);
1532
+ expect(result.filterSchema.filters).toHaveLength(2);
1533
+ expect(result.filterSchema.filters[0].expression.type).toBe('equals');
1534
+ expect(result.filterSchema.filters[1].expression.type).toBe('and');
1535
+ expect(result.filterSchema.filters[1].aiGenerated).toBe(true);
1536
+ });
1537
+ });
1538
+
1539
+ describe('error handling', () => {
1540
+ it('should throw error for invalid JSON structure', () => {
1541
+ expect(() => parseViewJson(null, viewTestRuntime))
1542
+ .toThrow('View JSON must be a non-null object');
1543
+
1544
+ expect(() => parseViewJson([], viewTestRuntime))
1545
+ .toThrow('View JSON must be a non-null object');
1546
+
1547
+ expect(() => parseViewJson('string', viewTestRuntime))
1548
+ .toThrow('View JSON must be a non-null object');
1549
+
1550
+ expect(() => parseViewJson(123, viewTestRuntime))
1551
+ .toThrow('View JSON must be a non-null object');
1552
+ });
1553
+
1554
+ it('should throw error for missing or invalid title', () => {
1555
+ const noTitle = {
1556
+ id: 'test',
1557
+ collectionName: 'test',
1558
+ paginationKey: 'id',
1559
+ boolExpType: 'TestBoolExp',
1560
+ orderByType: '[TestOrderBy!]',
1561
+ columns: [],
1562
+ filterSchema: { groups: [], filters: [] }
1563
+ };
1564
+
1565
+ expect(() => parseViewJson(noTitle, viewTestRuntime))
1566
+ .toThrow('View "title" must be a string');
1567
+
1568
+ const invalidTitle = { ...noTitle, title: 123 };
1569
+ expect(() => parseViewJson(invalidTitle, viewTestRuntime))
1570
+ .toThrow('View "title" must be a string');
1571
+ });
1572
+
1573
+ it('should throw error for missing or invalid id', () => {
1574
+ const noId = {
1575
+ title: 'Test',
1576
+ collectionName: 'test',
1577
+ paginationKey: 'id',
1578
+ boolExpType: 'TestBoolExp',
1579
+ orderByType: '[TestOrderBy!]',
1580
+ columns: [],
1581
+ filterSchema: { groups: [], filters: [] }
1582
+ };
1583
+
1584
+ expect(() => parseViewJson(noId, viewTestRuntime))
1585
+ .toThrow('View "id" must be a string');
1586
+
1587
+ const invalidId = { ...noId, id: null };
1588
+ expect(() => parseViewJson(invalidId, viewTestRuntime))
1589
+ .toThrow('View "id" must be a string');
1590
+ });
1591
+
1592
+ it('should throw error for missing or invalid collectionName', () => {
1593
+ const noCollectionName = {
1594
+ title: 'Test',
1595
+ id: 'test',
1596
+ paginationKey: 'id',
1597
+ boolExpType: 'TestBoolExp',
1598
+ orderByType: '[TestOrderBy!]',
1599
+ columns: [],
1600
+ filterSchema: { groups: [], filters: [] }
1601
+ };
1602
+
1603
+ expect(() => parseViewJson(noCollectionName, viewTestRuntime))
1604
+ .toThrow('View "collectionName" must be a string');
1605
+
1606
+ const invalidCollectionName = { ...noCollectionName, collectionName: {} };
1607
+ expect(() => parseViewJson(invalidCollectionName, viewTestRuntime))
1608
+ .toThrow('View "collectionName" must be a string');
1609
+ });
1610
+
1611
+ it('should throw error for missing or invalid paginationKey', () => {
1612
+ const noPaginationKey = {
1613
+ title: 'Test',
1614
+ id: 'test',
1615
+ collectionName: 'test',
1616
+ boolExpType: 'TestBoolExp',
1617
+ orderByType: '[TestOrderBy!]',
1618
+ columns: [],
1619
+ filterSchema: { groups: [], filters: [] }
1620
+ };
1621
+
1622
+ expect(() => parseViewJson(noPaginationKey, viewTestRuntime))
1623
+ .toThrow('View "paginationKey" must be a string');
1624
+
1625
+ const invalidPaginationKey = { ...noPaginationKey, paginationKey: [] };
1626
+ expect(() => parseViewJson(invalidPaginationKey, viewTestRuntime))
1627
+ .toThrow('View "paginationKey" must be a string');
1628
+ });
1629
+
1630
+ it('should throw error for missing or invalid GraphQL types', () => {
1631
+ const noBoolExpType = {
1632
+ title: 'Test',
1633
+ id: 'test',
1634
+ collectionName: 'test',
1635
+ paginationKey: 'id',
1636
+ orderByType: '[TestOrderBy!]',
1637
+ columns: [],
1638
+ filterSchema: { groups: [], filters: [] }
1639
+ };
1640
+
1641
+ expect(() => parseViewJson(noBoolExpType, viewTestRuntime))
1642
+ .toThrow('View "boolExpType" must be a string');
1643
+
1644
+ const noOrderByType = {
1645
+ title: 'Test',
1646
+ id: 'test',
1647
+ collectionName: 'test',
1648
+ paginationKey: 'id',
1649
+ boolExpType: 'TestBoolExp',
1650
+ columns: [],
1651
+ filterSchema: { groups: [], filters: [] }
1652
+ };
1653
+
1654
+ expect(() => parseViewJson(noOrderByType, viewTestRuntime))
1655
+ .toThrow('View "orderByType" must be a string');
1656
+ });
1657
+
1658
+ it('should throw error for missing or invalid columns', () => {
1659
+ const noColumns = {
1660
+ title: 'Test',
1661
+ id: 'test',
1662
+ collectionName: 'test',
1663
+ paginationKey: 'id',
1664
+ boolExpType: 'TestBoolExp',
1665
+ orderByType: '[TestOrderBy!]',
1666
+ filterSchema: { groups: [], filters: [] }
1667
+ };
1668
+
1669
+ expect(() => parseViewJson(noColumns, viewTestRuntime))
1670
+ .toThrow('View "columns" must be an array');
1671
+
1672
+ const invalidColumns = { ...noColumns, columns: 'not-array' };
1673
+ expect(() => parseViewJson(invalidColumns, viewTestRuntime))
1674
+ .toThrow('View "columns" must be an array');
1675
+ });
1676
+
1677
+ it('should throw error for missing filterSchema', () => {
1678
+ const noFilterSchema = {
1679
+ title: 'Test',
1680
+ id: 'test',
1681
+ collectionName: 'test',
1682
+ paginationKey: 'id',
1683
+ boolExpType: 'TestBoolExp',
1684
+ orderByType: '[TestOrderBy!]',
1685
+ columns: []
1686
+ };
1687
+
1688
+ expect(() => parseViewJson(noFilterSchema, viewTestRuntime))
1689
+ .toThrow('View "filterSchema" is required');
1690
+ });
1691
+
1692
+ it('should throw error for invalid column in columns array', () => {
1693
+ const invalidColumn = {
1694
+ title: 'Test',
1695
+ id: 'test',
1696
+ collectionName: 'test',
1697
+ paginationKey: 'id',
1698
+ boolExpType: 'TestBoolExp',
1699
+ orderByType: '[TestOrderBy!]',
1700
+ columns: [
1701
+ {
1702
+ data: [],
1703
+ name: 'Test',
1704
+ cellRenderer: { section: 'cellRenderers', key: 'nonexistent' }
1705
+ }
1706
+ ],
1707
+ filterSchema: { groups: [], filters: [] }
1708
+ };
1709
+
1710
+ expect(() => parseViewJson(invalidColumn, viewTestRuntime))
1711
+ .toThrow('Invalid column[0]:');
1712
+ });
1713
+
1714
+ it('should throw error for invalid filterSchema', () => {
1715
+ const invalidFilterSchema = {
1716
+ title: 'Test',
1717
+ id: 'test',
1718
+ collectionName: 'test',
1719
+ paginationKey: 'id',
1720
+ boolExpType: 'TestBoolExp',
1721
+ orderByType: '[TestOrderBy!]',
1722
+ columns: [],
1723
+ filterSchema: {
1724
+ groups: 'not-array',
1725
+ filters: []
1726
+ }
1727
+ };
1728
+
1729
+ expect(() => parseViewJson(invalidFilterSchema, viewTestRuntime))
1730
+ .toThrow('Invalid filterSchema:');
1731
+ });
1732
+
1733
+ it('should throw error for missing noRowsComponent in runtime', () => {
1734
+ const missingNoRowsComponent = {
1735
+ title: 'Test',
1736
+ id: 'test',
1737
+ collectionName: 'test',
1738
+ paginationKey: 'id',
1739
+ boolExpType: 'TestBoolExp',
1740
+ orderByType: '[TestOrderBy!]',
1741
+ columns: [],
1742
+ filterSchema: { groups: [], filters: [] },
1743
+ noRowsComponent: { section: 'noRowsComponents', key: 'nonexistent' }
1744
+ };
1745
+
1746
+ expect(() => parseViewJson(missingNoRowsComponent, viewTestRuntime))
1747
+ .toThrow('Reference "nonexistent" not found in noRowsComponents. Available keys: noRowsExtendDateRange');
1748
+ });
1749
+
1750
+ it('should throw error when runtime has no noRowsComponents', () => {
1751
+ const runtimeWithoutNoRows = {
1752
+ ...viewTestRuntime,
1753
+ noRowsComponents: {},
1754
+ customFilterComponents: {}
1755
+ };
1756
+
1757
+ const withNoRowsComponent = {
1758
+ title: 'Test',
1759
+ id: 'test',
1760
+ collectionName: 'test',
1761
+ paginationKey: 'id',
1762
+ boolExpType: 'TestBoolExp',
1763
+ orderByType: '[TestOrderBy!]',
1764
+ columns: [],
1765
+ filterSchema: { groups: [], filters: [] },
1766
+ noRowsComponent: { section: 'noRowsComponents', key: 'anything' }
1767
+ };
1768
+
1769
+ expect(() => parseViewJson(withNoRowsComponent, runtimeWithoutNoRows))
1770
+ .toThrow('Reference "anything" not found in noRowsComponents. Available keys:');
1771
+ });
1772
+
1773
+ it('should throw error for invalid staticConditions (not array)', () => {
1774
+ const invalidStatic = {
1775
+ title: 'Test',
1776
+ id: 'test',
1777
+ collectionName: 'test',
1778
+ paginationKey: 'id',
1779
+ boolExpType: 'TestBoolExp',
1780
+ orderByType: '[TestOrderBy!]',
1781
+ columns: [],
1782
+ filterSchema: { groups: [], filters: [] },
1783
+ staticConditions: 'not-an-array'
1784
+ };
1785
+ expect(() => parseViewJson(invalidStatic, viewTestRuntime))
1786
+ .toThrow('View "staticConditions" must be an array when provided');
1787
+ });
1788
+
1789
+ it('should throw error for invalid staticConditions entry', () => {
1790
+ const invalidStaticEntry = {
1791
+ title: 'Test',
1792
+ id: 'test',
1793
+ collectionName: 'test',
1794
+ paginationKey: 'id',
1795
+ boolExpType: 'TestBoolExp',
1796
+ orderByType: '[TestOrderBy!]',
1797
+ columns: [],
1798
+ filterSchema: { groups: [], filters: [] },
1799
+ staticConditions: [null]
1800
+ };
1801
+ expect(() => parseViewJson(invalidStaticEntry, viewTestRuntime))
1802
+ .toThrow('View "staticConditions" entry[0] must be a non-null object');
1803
+ });
1804
+ });
1805
+ });