@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,714 @@
1
+ // Parser functions for view JSON schema types
2
+ // Separated from view.ts to avoid React import issues in tests
3
+
4
+ import type { FieldQuery, QueryConfig, Field, QueryConfigs, FieldAlias } from './column-definition';
5
+ import type { FilterControl, FilterExpr, FilterField, FilterFieldGroup, FilterSchema, FilterSchemasAndGroups } from './filters';
6
+ import { View } from './view';
7
+ import type { Runtime } from './runtime';
8
+
9
+ // Runtime reference type for referencing components/functions from runtime
10
+ export type RuntimeReference = {
11
+ section: 'cellRenderers' | 'noRowsComponents' | 'customFilterComponents' | 'queryTransforms' | 'initialValues';
12
+ key: string;
13
+ };
14
+
15
+ // Helper function to resolve a runtime reference with external runtime precedence
16
+ export function resolveRuntimeReference<T>(
17
+ reference: RuntimeReference,
18
+ externalRuntime: Runtime | undefined,
19
+ builtInRuntime: Runtime
20
+ ): T {
21
+ const { section, key } = reference;
22
+
23
+ // First check external runtime if available
24
+ if (externalRuntime && externalRuntime[section] && externalRuntime[section][key]) {
25
+ return externalRuntime[section][key] as T;
26
+ }
27
+
28
+ // Fall back to built-in runtime
29
+ if (builtInRuntime[section] && builtInRuntime[section][key]) {
30
+ return builtInRuntime[section][key] as T;
31
+ }
32
+
33
+ // Component not found in either runtime
34
+ const externalKeys = externalRuntime ? Object.keys(externalRuntime[section] || {}) : [];
35
+ const builtInKeys = Object.keys(builtInRuntime[section] || {});
36
+ const availableKeys = [...new Set([...externalKeys, ...builtInKeys])];
37
+
38
+ throw new Error(
39
+ `Reference "${key}" not found in ${section}. Available keys: ${availableKeys.join(', ')}`
40
+ );
41
+ }
42
+
43
+ // JSON Schema types - these are just aliases since the original types are already JSON-friendly
44
+ export type FieldJson = Field;
45
+ export type QueryConfigJson = QueryConfig;
46
+ export type QueryConfigsJson = QueryConfigs;
47
+ export type FieldAliasJson = FieldAlias;
48
+ export type FieldQueryJson = FieldQuery;
49
+
50
+ // JSON Schema types for FilterControl with RuntimeReference support for custom components
51
+ export type FilterControlJson =
52
+ | { type: 'text'; label?: string; placeholder?: string; initialValue?: any }
53
+ | { type: 'number'; label?: string; placeholder?: string; initialValue?: any }
54
+ | { type: 'date'; label?: string; placeholder?: string; initialValue?: any }
55
+ | { type: 'dropdown'; label?: string; items: { label: string; value: any }[]; initialValue?: any }
56
+ | { type: 'multiselect'; label?: string; items: { label: string; value: any }[], filterable?: boolean; initialValue?: any }
57
+ | { type: 'customOperator'; label?: string; operators: { label: string; value: string }[]; valueControl: FilterControlJson; initialValue?: any }
58
+ | { type: 'custom'; component: RuntimeReference; props?: Record<string, any>; label?: string; initialValue?: any };
59
+
60
+ // JSON Schema types for FilterField (multi-field support)
61
+ export type FilterFieldJson =
62
+ | string // Single field: "name" or "user.email"
63
+ | { and: string[] } // AND multiple fields: { and: ["name", "title", "description"] }
64
+ | { or: string[] }; // OR multiple fields: { or: ["name", "title", "description"] }
65
+
66
+ // JSON Schema types for FilterExpr with transform as RuntimeReference
67
+ export type FilterExprJson =
68
+ | { type: 'equals'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
69
+ | { type: 'notEquals'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
70
+ | { type: 'greaterThan'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
71
+ | { type: 'lessThan'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
72
+ | { type: 'greaterThanOrEqual'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
73
+ | { type: 'lessThanOrEqual'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
74
+ | { type: 'in'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
75
+ | { type: 'notIn'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
76
+ | { type: 'like'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
77
+ | { type: 'iLike'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
78
+ | { type: 'isNull'; field: FilterFieldJson; value: FilterControlJson; transform?: RuntimeReference }
79
+ | { type: 'and'; filters: FilterExprJson[] }
80
+ | { type: 'or'; filters: FilterExprJson[] }
81
+ | { type: 'not'; filter: FilterExprJson };
82
+
83
+ // JSON Schema types for FilterFieldSchema components
84
+ export type FilterFieldGroupJson = FilterFieldGroup;
85
+
86
+ export type FilterFieldSchemaFilterJson = {
87
+ id: string;
88
+ label: string;
89
+ expression: FilterExprJson;
90
+ group: string;
91
+ aiGenerated: boolean;
92
+ };
93
+
94
+ export type FilterFieldSchemaJson = {
95
+ groups: FilterFieldGroupJson[];
96
+ filters: FilterFieldSchemaFilterJson[];
97
+ };
98
+
99
+ // JSON Schema types for view definitions
100
+ export type ColumnDefinitionJson = {
101
+ data: FieldQueryJson[]; // Array of FieldQuery objects
102
+ name: string; // Column display name
103
+ cellRenderer: RuntimeReference; // Reference to cell renderer from runtime
104
+ };
105
+
106
+ export type ViewJson = {
107
+ title: string;
108
+ id: string;
109
+ collectionName: string;
110
+ paginationKey: string;
111
+ columns: ColumnDefinitionJson[];
112
+ filterSchema: FilterFieldSchemaJson;
113
+ boolExpType: string; // GraphQL boolean expression type for this view
114
+ orderByType: string; // GraphQL order by type for this view
115
+ noRowsComponent?: RuntimeReference; // Optional reference to no-rows component from runtime
116
+ };
117
+
118
+ // Conversion functions from JSON types to actual types
119
+ export function parseRuntimeReference(json: unknown): RuntimeReference {
120
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
121
+ throw new Error('Invalid RuntimeReference: Expected an object');
122
+ }
123
+
124
+ const obj = json as Record<string, unknown>;
125
+
126
+ if (typeof obj.section !== 'string') {
127
+ throw new Error('Invalid RuntimeReference: "section" must be a string');
128
+ }
129
+
130
+ if (typeof obj.key !== 'string') {
131
+ throw new Error('Invalid RuntimeReference: "key" must be a string');
132
+ }
133
+
134
+ const validSections: RuntimeReference['section'][] = ['cellRenderers', 'noRowsComponents', 'customFilterComponents', 'queryTransforms', 'initialValues'];
135
+ if (!validSections.includes(obj.section as RuntimeReference['section'])) {
136
+ throw new Error(`Invalid RuntimeReference: "section" must be one of: ${validSections.join(', ')}`);
137
+ }
138
+
139
+ return {
140
+ section: obj.section as RuntimeReference['section'],
141
+ key: obj.key
142
+ };
143
+ }
144
+
145
+ // Parser functions for FieldQuery structures
146
+ function parseOrderByConfig(obj: unknown): { key: string; direction: 'ASC' | 'DESC' } {
147
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
148
+ throw new Error('Invalid orderBy: Expected an object');
149
+ }
150
+
151
+ const orderBy = obj as Record<string, unknown>;
152
+
153
+ if (typeof orderBy.key !== 'string') {
154
+ throw new Error('Invalid orderBy: "key" field must be a string');
155
+ }
156
+
157
+ if (orderBy.direction !== 'ASC' && orderBy.direction !== 'DESC') {
158
+ throw new Error('Invalid orderBy: "direction" field must be "ASC" or "DESC"');
159
+ }
160
+
161
+ return {
162
+ key: orderBy.key,
163
+ direction: orderBy.direction as 'ASC' | 'DESC'
164
+ };
165
+ }
166
+
167
+ function parseQueryConfigJson(obj: unknown): QueryConfigJson {
168
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
169
+ throw new Error('Invalid QueryConfig: Expected an object');
170
+ }
171
+
172
+ const config = obj as Record<string, unknown>;
173
+
174
+ if (typeof config.field !== 'string') {
175
+ throw new Error('Invalid QueryConfig: "field" must be a string');
176
+ }
177
+
178
+ const result: QueryConfigJson = {
179
+ field: config.field
180
+ };
181
+
182
+ if (config.orderBy !== undefined && config.orderBy !== null) {
183
+ if (Array.isArray(config.orderBy)) {
184
+ result.orderBy = config.orderBy.map(parseOrderByConfig);
185
+ } else {
186
+ result.orderBy = parseOrderByConfig(config.orderBy);
187
+ }
188
+ }
189
+
190
+ if (config.limit !== undefined && config.limit !== null) {
191
+ if (typeof config.limit !== 'number' || config.limit < 0 || !Number.isInteger(config.limit)) {
192
+ throw new Error('Invalid QueryConfig: "limit" must be a non-negative integer');
193
+ }
194
+ result.limit = config.limit;
195
+ }
196
+
197
+ if (config.path !== undefined && config.path !== null) {
198
+ if (typeof config.path !== 'string') {
199
+ throw new Error('Invalid QueryConfig: "path" must be a string');
200
+ }
201
+ result.path = config.path;
202
+ }
203
+
204
+ return result;
205
+ }
206
+
207
+ function parseFieldQueryJson(obj: unknown): FieldQueryJson {
208
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
209
+ throw new Error('Invalid FieldQuery: Expected an object');
210
+ }
211
+
212
+ const fieldQuery = obj as Record<string, unknown>;
213
+
214
+ if (fieldQuery.type === 'field') {
215
+ if (typeof fieldQuery.path !== 'string') {
216
+ throw new Error('Invalid Field: "path" must be a string');
217
+ }
218
+ return {
219
+ type: 'field',
220
+ path: fieldQuery.path
221
+ };
222
+ } else if (fieldQuery.type === 'queryConfigs') {
223
+ if (!Array.isArray(fieldQuery.configs)) {
224
+ throw new Error('Invalid QueryConfigs: "configs" must be an array');
225
+ }
226
+ return {
227
+ type: 'queryConfigs',
228
+ configs: fieldQuery.configs.map(parseQueryConfigJson)
229
+ };
230
+ } else if (fieldQuery.type === 'fieldAlias') {
231
+ if (typeof fieldQuery.alias !== 'string') {
232
+ throw new Error('Invalid FieldAlias: "alias" must be a string');
233
+ }
234
+ if (!fieldQuery.field) {
235
+ throw new Error('Invalid FieldAlias: "field" is required');
236
+ }
237
+ return {
238
+ type: 'fieldAlias',
239
+ alias: fieldQuery.alias,
240
+ field: parseFieldQueryJson(fieldQuery.field)
241
+ } as FieldQueryJson;
242
+ } else {
243
+ throw new Error('Invalid FieldQuery: "type" must be "field", "queryConfigs", or "fieldAlias"');
244
+ }
245
+ }
246
+
247
+ // Parser function for ColumnDefinitionJson
248
+ export function parseColumnDefinitionJson(
249
+ json: unknown,
250
+ builtInRuntime: Runtime,
251
+ externalRuntime?: Runtime
252
+ ): ColumnDefinitionJson {
253
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
254
+ throw new Error('Invalid JSON: Expected an object');
255
+ }
256
+
257
+ const obj = json as Record<string, unknown>;
258
+
259
+ // Validate required fields
260
+ if (!Array.isArray(obj.data)) {
261
+ throw new Error('Invalid JSON: "data" field must be an array of FieldQuery objects');
262
+ }
263
+
264
+ if (typeof obj.name !== 'string') {
265
+ throw new Error('Invalid JSON: "name" field must be a string');
266
+ }
267
+
268
+ // Parse cellRenderer as RuntimeReference
269
+ if (!obj.cellRenderer) {
270
+ throw new Error('Invalid JSON: "cellRenderer" field is required');
271
+ }
272
+
273
+ const cellRenderer = parseRuntimeReference(obj.cellRenderer);
274
+ if (cellRenderer.section !== 'cellRenderers') {
275
+ throw new Error('Invalid cellRenderer: section must be "cellRenderers"');
276
+ }
277
+
278
+ // Parse and validate data array as FieldQuery objects
279
+ const parsedData: FieldQueryJson[] = obj.data.map((item, index) => {
280
+ try {
281
+ return parseFieldQueryJson(item);
282
+ } catch (error) {
283
+ throw new Error(`Invalid data[${index}]: ${error instanceof Error ? error.message : 'Unknown error'}`);
284
+ }
285
+ });
286
+
287
+ // Validate that cellRenderer key exists in at least one runtime
288
+ const externalKeys = externalRuntime ? Object.keys(externalRuntime.cellRenderers || {}) : [];
289
+ const builtInKeys = Object.keys(builtInRuntime.cellRenderers || {});
290
+ const allKeys = [...new Set([...externalKeys, ...builtInKeys])];
291
+
292
+ if (!allKeys.includes(cellRenderer.key)) {
293
+ throw new Error(
294
+ `Invalid cellRenderer reference: "${cellRenderer.key}". Valid keys are: ${allKeys.join(', ')}`
295
+ );
296
+ }
297
+
298
+ return {
299
+ data: parsedData,
300
+ name: obj.name,
301
+ cellRenderer
302
+ };
303
+ }
304
+
305
+ // Helper function to validate FilterFieldJson
306
+ function parseFilterFieldJson(field: unknown): FilterField {
307
+ // Handle string (single field)
308
+ if (typeof field === 'string') {
309
+ return field;
310
+ }
311
+
312
+ // Handle object (multi-field)
313
+ if (typeof field === 'object' && field !== null && !Array.isArray(field)) {
314
+ const obj = field as Record<string, unknown>;
315
+
316
+ // Check for 'and' format
317
+ if ('and' in obj) {
318
+ if (!Array.isArray(obj.and)) {
319
+ throw new Error('Invalid FilterField: "and" must be an array of strings');
320
+ }
321
+ if (!obj.and.every(item => typeof item === 'string')) {
322
+ throw new Error('Invalid FilterField: "and" array must contain only strings');
323
+ }
324
+ return { and: obj.and as string[] };
325
+ }
326
+
327
+ // Check for 'or' format
328
+ if ('or' in obj) {
329
+ if (!Array.isArray(obj.or)) {
330
+ throw new Error('Invalid FilterField: "or" must be an array of strings');
331
+ }
332
+ if (!obj.or.every(item => typeof item === 'string')) {
333
+ throw new Error('Invalid FilterField: "or" array must contain only strings');
334
+ }
335
+ return { or: obj.or as string[] };
336
+ }
337
+ }
338
+
339
+ throw new Error('Invalid FilterField: must be a string or object with "and" or "or" arrays');
340
+ }
341
+
342
+ // Helper function to parse initialValue that may be a RuntimeReference
343
+ export function parseInitialValue(
344
+ initialValue: unknown,
345
+ builtInRuntime: Runtime,
346
+ externalRuntime?: Runtime
347
+ ): any {
348
+ // If initialValue is undefined or null, return as-is
349
+ if (initialValue === undefined || initialValue === null) {
350
+ return initialValue;
351
+ }
352
+
353
+ // Try to parse as RuntimeReference - this handles all validation internally
354
+ let ref: RuntimeReference;
355
+ try {
356
+ ref = parseRuntimeReference(initialValue);
357
+ } catch {
358
+ // If parsing as RuntimeReference fails, treat as regular value
359
+ return initialValue;
360
+ }
361
+
362
+ if (ref.section === 'initialValues') {
363
+ // Resolve the runtime reference from initialValues section - this may throw if key doesn't exist
364
+ return resolveRuntimeReference<any>(ref, externalRuntime, builtInRuntime);
365
+ }
366
+
367
+ // If it's not an initialValues reference, return as-is (might be a component reference)
368
+ return initialValue;
369
+ }
370
+
371
+ // Parser function for FilterControlJson to FilterControl
372
+ export function parseFilterControlJson(
373
+ json: unknown,
374
+ builtInRuntime: Runtime,
375
+ externalRuntime?: Runtime
376
+ ): FilterControl {
377
+ if (!json || (typeof json !== 'object' && !Array.isArray(json))) {
378
+ throw new Error('Invalid FilterControl: Expected an object');
379
+ }
380
+
381
+ const filterControlJson = json as Record<string, unknown>;
382
+
383
+ // Parse initialValue if present
384
+ const parsedInitialValue = filterControlJson.initialValue !== undefined
385
+ ? parseInitialValue(filterControlJson.initialValue, builtInRuntime, externalRuntime)
386
+ : undefined;
387
+
388
+ // If it's a custom filter control, resolve the component from runtimes
389
+ if (filterControlJson.type === 'custom') {
390
+ // RuntimeReference-based component reference
391
+ const componentRef = parseRuntimeReference(filterControlJson.component);
392
+ if (componentRef.section !== 'customFilterComponents') {
393
+ throw new Error('Invalid custom filter component: section must be "customFilterComponents"');
394
+ }
395
+
396
+ const component = resolveRuntimeReference<any>(
397
+ componentRef,
398
+ externalRuntime,
399
+ builtInRuntime
400
+ );
401
+
402
+ return {
403
+ ...filterControlJson,
404
+ component,
405
+ initialValue: parsedInitialValue
406
+ } as FilterControl;
407
+ }
408
+
409
+ // Handle customOperator controls that might have nested FilterControlJson
410
+ if (filterControlJson.type === 'customOperator') {
411
+ return {
412
+ ...filterControlJson,
413
+ valueControl: parseFilterControlJson(filterControlJson.valueControl, builtInRuntime, externalRuntime),
414
+ initialValue: parsedInitialValue
415
+ } as FilterControl;
416
+ }
417
+
418
+ // Handle all other filter control types
419
+ return {
420
+ ...filterControlJson,
421
+ initialValue: parsedInitialValue
422
+ } as FilterControl;
423
+ }
424
+
425
+ // Parser function for FilterExprJson to FilterExpr
426
+ export function parseFilterExprJson(
427
+ json: unknown,
428
+ builtInRuntime: Runtime,
429
+ externalRuntime?: Runtime
430
+ ): FilterExpr {
431
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
432
+ throw new Error('Invalid FilterExpr: Expected an object');
433
+ }
434
+
435
+ const expr = json as Record<string, unknown>;
436
+
437
+ if (typeof expr.type !== 'string') {
438
+ throw new Error('Invalid FilterExpr: "type" must be a string');
439
+ }
440
+
441
+ // Handle composite expressions (and, or, not)
442
+ if (expr.type === 'and' || expr.type === 'or') {
443
+ if (!Array.isArray(expr.filters)) {
444
+ throw new Error(`Invalid ${expr.type} FilterExpr: "filters" must be an array`);
445
+ }
446
+ return {
447
+ type: expr.type as 'and' | 'or',
448
+ filters: expr.filters.map(filter => parseFilterExprJson(filter, builtInRuntime, externalRuntime))
449
+ };
450
+ }
451
+
452
+ if (expr.type === 'not') {
453
+ if (!expr.filter || typeof expr.filter !== 'object') {
454
+ throw new Error('Invalid not FilterExpr: "filter" must be an object');
455
+ }
456
+ return {
457
+ type: 'not',
458
+ filter: parseFilterExprJson(expr.filter, builtInRuntime, externalRuntime)
459
+ };
460
+ }
461
+
462
+ // Handle leaf expressions
463
+ const validLeafTypes = ['equals', 'notEquals', 'greaterThan', 'lessThan', 'greaterThanOrEqual', 'lessThanOrEqual', 'in', 'notIn', 'like', 'iLike', 'isNull'];
464
+ if (!validLeafTypes.includes(expr.type)) {
465
+ throw new Error(`Invalid FilterExpr type: "${expr.type}". Valid types are: ${validLeafTypes.join(', ')}, and, or, not`);
466
+ }
467
+
468
+ const parsedField = parseFilterFieldJson(expr.field);
469
+
470
+ if (!expr.value) {
471
+ throw new Error('Invalid FilterExpr: "value" is required');
472
+ }
473
+
474
+ // Parse the FilterControl using the new parsing function
475
+ const value = parseFilterControlJson(expr.value, builtInRuntime, externalRuntime);
476
+
477
+ // Build the result FilterExpr
478
+ const result: FilterExpr = {
479
+ type: expr.type as any,
480
+ field: parsedField,
481
+ value
482
+ };
483
+
484
+ // Handle transform reference if present
485
+ if (expr.transform) {
486
+ const transformRef = parseRuntimeReference(expr.transform);
487
+ if (transformRef.section !== 'queryTransforms') {
488
+ throw new Error('Invalid transform: section must be "queryTransforms"');
489
+ }
490
+
491
+ const transform = resolveRuntimeReference<any>(
492
+ transformRef,
493
+ externalRuntime,
494
+ builtInRuntime
495
+ );
496
+
497
+ (result as any).transform = transform;
498
+ }
499
+
500
+ return result;
501
+ }
502
+
503
+ // Parser function for FilterFieldSchemaJson
504
+ export function parseFilterFieldSchemaJson(
505
+ json: unknown,
506
+ builtInRuntime: Runtime,
507
+ externalRuntime?: Runtime
508
+ ): FilterSchemasAndGroups {
509
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
510
+ throw new Error('Invalid FilterFieldSchema: Expected an object');
511
+ }
512
+
513
+ const schema = json as Record<string, unknown>;
514
+
515
+ // Validate groups
516
+ if (!Array.isArray(schema.groups)) {
517
+ throw new Error('Invalid FilterFieldSchema: "groups" must be an array');
518
+ }
519
+
520
+ const groups: FilterFieldGroup[] = schema.groups.map((group, index) => {
521
+ if (!group || typeof group !== 'object' || Array.isArray(group)) {
522
+ throw new Error(`Invalid group[${index}]: Expected an object`);
523
+ }
524
+
525
+ const g = group as Record<string, unknown>;
526
+
527
+ if (typeof g.name !== 'string') {
528
+ throw new Error(`Invalid group[${index}]: "name" must be a string`);
529
+ }
530
+
531
+ if (g.label !== null && typeof g.label !== 'string') {
532
+ throw new Error(`Invalid group[${index}]: "label" must be a string or null`);
533
+ }
534
+
535
+ return {
536
+ name: g.name,
537
+ label: g.label as string | null
538
+ };
539
+ });
540
+
541
+ // Validate filters
542
+ if (!Array.isArray(schema.filters)) {
543
+ throw new Error('Invalid FilterFieldSchema: "filters" must be an array');
544
+ }
545
+
546
+ const filters: FilterSchema[] = schema.filters.map((filter, index) => {
547
+ if (!filter || typeof filter !== 'object' || Array.isArray(filter)) {
548
+ throw new Error(`Invalid filter[${index}]: Expected an object`);
549
+ }
550
+
551
+ const f = filter as Record<string, unknown>;
552
+
553
+ if (typeof f.id !== 'string') {
554
+ throw new Error(`Invalid filter[${index}]: "id" must be a string`);
555
+ }
556
+
557
+ if (typeof f.label !== 'string') {
558
+ throw new Error(`Invalid filter[${index}]: "label" must be a string`);
559
+ }
560
+
561
+ if (typeof f.group !== 'string') {
562
+ throw new Error(`Invalid filter[${index}]: "group" must be a string`);
563
+ }
564
+
565
+ if (typeof f.aiGenerated !== 'boolean') {
566
+ throw new Error(`Invalid filter[${index}]: "aiGenerated" must be a boolean`);
567
+ }
568
+
569
+ if (!f.expression) {
570
+ throw new Error(`Invalid filter[${index}]: "expression" is required`);
571
+ }
572
+
573
+ let expression: FilterExpr;
574
+ try {
575
+ expression = parseFilterExprJson(f.expression, builtInRuntime, externalRuntime);
576
+ } catch (error) {
577
+ throw new Error(`Invalid filter[${index}] expression: ${error instanceof Error ? error.message : 'Unknown error'}`);
578
+ }
579
+
580
+ return {
581
+ id: f.id,
582
+ label: f.label,
583
+ expression,
584
+ group: f.group,
585
+ aiGenerated: f.aiGenerated
586
+ };
587
+ });
588
+
589
+ return {
590
+ groups,
591
+ filters
592
+ };
593
+ }
594
+
595
+ // Parse ViewJson into a View object with separate built-in and external runtimes
596
+ export function parseViewJson(
597
+ json: unknown,
598
+ builtInRuntime: Runtime,
599
+ externalRuntime?: Runtime
600
+ ): View {
601
+ if (!json || typeof json !== 'object' || Array.isArray(json)) {
602
+ throw new Error('View JSON must be a non-null object');
603
+ }
604
+
605
+ const view = json as Record<string, unknown>;
606
+
607
+ // Validate required string fields
608
+ if (typeof view.title !== 'string') {
609
+ throw new Error('View "title" must be a string');
610
+ }
611
+
612
+ if (typeof view.id !== 'string') {
613
+ throw new Error('View "id" must be a string');
614
+ }
615
+
616
+ if (typeof view.collectionName !== 'string') {
617
+ throw new Error('View "collectionName" must be a string');
618
+ }
619
+
620
+ if (typeof view.paginationKey !== 'string') {
621
+ throw new Error('View "paginationKey" must be a string');
622
+ }
623
+
624
+ if (typeof view.boolExpType !== 'string') {
625
+ throw new Error('View "boolExpType" must be a string');
626
+ }
627
+
628
+ if (typeof view.orderByType !== 'string') {
629
+ throw new Error('View "orderByType" must be a string');
630
+ }
631
+
632
+ // Validate columns array
633
+ if (!Array.isArray(view.columns)) {
634
+ throw new Error('View "columns" must be an array');
635
+ }
636
+
637
+ // Validate filterSchema
638
+ if (!view.filterSchema) {
639
+ throw new Error('View "filterSchema" is required');
640
+ }
641
+
642
+ // Parse columns with runtime resolution
643
+ const columnDefinitions = view.columns.map((col, index) => {
644
+ let colJson;
645
+ try {
646
+ colJson = parseColumnDefinitionJson(col, builtInRuntime, externalRuntime);
647
+ } catch (error) {
648
+ throw new Error(`Invalid column[${index}]: ${error instanceof Error ? error.message : 'Unknown error'}`);
649
+ }
650
+
651
+ // Convert ColumnDefinitionJson to ColumnDefinition by resolving cellRenderer
652
+ const cellRenderer = resolveRuntimeReference<any>(
653
+ colJson.cellRenderer,
654
+ externalRuntime,
655
+ builtInRuntime
656
+ );
657
+
658
+ return {
659
+ data: colJson.data,
660
+ name: colJson.name,
661
+ cellRenderer
662
+ };
663
+ });
664
+
665
+ // Parse filter schema with runtime resolution
666
+ let filterSchema;
667
+ try {
668
+ filterSchema = parseFilterFieldSchemaJson(view.filterSchema, builtInRuntime, externalRuntime);
669
+ } catch (error) {
670
+ throw new Error(`Invalid filterSchema: ${error instanceof Error ? error.message : 'Unknown error'}`);
671
+ }
672
+
673
+ // Parse optional noRowsComponent with runtime resolution
674
+ let noRowsComponent;
675
+ if (view.noRowsComponent !== undefined) {
676
+ const noRowsRef = parseRuntimeReference(view.noRowsComponent);
677
+ if (noRowsRef.section !== 'noRowsComponents') {
678
+ throw new Error('Invalid noRowsComponent: section must be "noRowsComponents"');
679
+ }
680
+
681
+ noRowsComponent = resolveRuntimeReference<any>(
682
+ noRowsRef,
683
+ externalRuntime,
684
+ builtInRuntime
685
+ );
686
+ }
687
+
688
+ // Optional staticConditions: validate it's an array of objects (shallow validation)
689
+ let staticConditions;
690
+ if (view.staticConditions !== undefined) {
691
+ if (!Array.isArray(view.staticConditions)) {
692
+ throw new Error('View "staticConditions" must be an array when provided');
693
+ }
694
+ // Only allow plain objects (basic guard); deeper validation is left to runtime/Hasura
695
+ const invalidIndex = view.staticConditions.findIndex(c => typeof c !== 'object' || c === null || Array.isArray(c));
696
+ if (invalidIndex !== -1) {
697
+ throw new Error(`View "staticConditions" entry[${invalidIndex}] must be a non-null object`);
698
+ }
699
+ staticConditions = view.staticConditions as any;
700
+ }
701
+
702
+ return {
703
+ title: view.title,
704
+ id: view.id,
705
+ collectionName: view.collectionName,
706
+ columnDefinitions,
707
+ filterSchema,
708
+ boolExpType: view.boolExpType as string,
709
+ orderByType: view.orderByType as string,
710
+ paginationKey: view.paginationKey,
711
+ noRowsComponent,
712
+ staticConditions
713
+ };
714
+ }