@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,462 @@
1
+ import { FilterFormState } from '../components/FilterForm';
2
+ import { FilterField, FilterSchemasAndGroups, FilterExpr } from './filters';
3
+ import { ColumnDefinition, FieldQuery, OrderByConfig, QueryConfig } from './column-definition';
4
+ import { FilterState } from './state';
5
+ import { traverseFilterSchemaAndState } from './filter-form-state';
6
+
7
+ // All supported Hasura operators for a field
8
+ export type HasuraOperator =
9
+ | { _eq?: any }
10
+ | { _neq?: any }
11
+ | { _gt?: any }
12
+ | { _lt?: any }
13
+ | { _gte?: any }
14
+ | { _lte?: any }
15
+ | { _in?: any[] }
16
+ | { _nin?: any[] }
17
+ | { _like?: string }
18
+ | { _ilike?: string }
19
+ | { _is_null?: boolean }
20
+ | { _similar?: string }
21
+ | { _nsimilar?: string }
22
+ | { _regex?: string }
23
+ | { _nregex?: string }
24
+ | { _iregex?: string }
25
+ | { _niregex?: string };
26
+
27
+ // Type for Hasura boolean expressions (conditions)
28
+ export type HasuraCondition =
29
+ | { _and: HasuraCondition[] }
30
+ | { _or: HasuraCondition[] }
31
+ | { _not: HasuraCondition }
32
+ | { [field: string]: HasuraOperator | HasuraOperator[] };
33
+
34
+ // Build Hasura conditions from FilterFormState and FilterFieldSchema using schema-driven approach
35
+ export function buildHasuraConditions(
36
+ filterState: FilterState,
37
+ filterSchema: FilterSchemasAndGroups
38
+ ): HasuraCondition {
39
+ // Support dot-separated keys by building nested objects and handle and/or field expressions
40
+ function buildNestedKey(field: FilterField, cond: any): HasuraCondition {
41
+ // Handle object format for multi-field expressions
42
+ if (typeof field === 'object') {
43
+ if ('and' in field) {
44
+ const conditions = field.and.map(fieldName => buildSingleNestedKey(fieldName, cond));
45
+ return { _and: conditions };
46
+ }
47
+ if ('or' in field) {
48
+ const conditions = field.or.map(fieldName => buildSingleNestedKey(fieldName, cond));
49
+ return { _or: conditions };
50
+ }
51
+ }
52
+
53
+ // Handle single field name (string)
54
+ if (typeof field === 'string') {
55
+ return buildSingleNestedKey(field, cond);
56
+ }
57
+
58
+ // Fallback
59
+ return {};
60
+ }
61
+
62
+ // Helper to build nested object from dot notation key for a single field
63
+ function buildSingleNestedKey(key: string, cond: any): HasuraCondition {
64
+ if (!key.includes('.')) return { [key]: cond };
65
+ const parts = key.split('.');
66
+ return parts.reverse().reduce((acc, k) => ({ [k]: acc }), cond);
67
+ }
68
+
69
+ // Recursive function that uses traversal helper to build conditions
70
+ function buildConditionsRecursive(
71
+ schemaNode: FilterExpr,
72
+ stateNode: FilterFormState
73
+ ): HasuraCondition | null {
74
+ return traverseFilterSchemaAndState(
75
+ schemaNode,
76
+ stateNode,
77
+ {
78
+ leaf: (schema, state): HasuraCondition | null => {
79
+ // Apply transforms if they exist
80
+ let transformedValue = state.value;
81
+ let transformedField = schema.field;
82
+
83
+ if (schema.transform?.toQuery !== undefined) {
84
+ const transformResult = schema.transform.toQuery(state.value);
85
+ if (transformResult.field !== undefined) transformedField = transformResult.field as FilterField;
86
+ if (transformResult.value !== undefined) transformedValue = transformResult.value;
87
+ }
88
+
89
+ // Handle customOperator from schema control info
90
+ if (schema.value && schema.value.type === 'customOperator') {
91
+ const opVal = transformedValue;
92
+ if (!opVal || !opVal.operator || opVal.value === undefined || opVal.value === '' || opVal.value === null || (Array.isArray(opVal.value) && opVal.value.length === 0)) return null;
93
+ return buildNestedKey(transformedField, { [opVal.operator]: opVal.value });
94
+ }
95
+
96
+ if (transformedValue === undefined || transformedValue === '' || transformedValue === null || (Array.isArray(transformedValue) && transformedValue.length === 0)) return null;
97
+
98
+ // Map filterType to Hasura operator using schema info
99
+ const opMap: Record<string, string> = {
100
+ equals: '_eq',
101
+ notEquals: '_neq',
102
+ greaterThan: '_gt',
103
+ lessThan: '_lt',
104
+ greaterThanOrEqual: '_gte',
105
+ lessThanOrEqual: '_lte',
106
+ in: '_in',
107
+ notIn: '_nin',
108
+ like: '_like',
109
+ iLike: '_ilike',
110
+ isNull: '_is_null',
111
+ };
112
+ const op = opMap[schema.type];
113
+ if (!op) return null;
114
+
115
+ // Support dot-separated keys by building nested objects
116
+ return buildNestedKey(transformedField, { [op]: transformedValue });
117
+ },
118
+ and: (_schema, _state, childResults): HasuraCondition | null => {
119
+ const validChildren = childResults.filter((c): c is HasuraCondition => c !== null);
120
+ if (validChildren.length === 0) return null;
121
+ return { _and: validChildren };
122
+ },
123
+ or: (_schema, _state, childResults): HasuraCondition | null => {
124
+ const validChildren = childResults.filter((c): c is HasuraCondition => c !== null);
125
+ if (validChildren.length === 0) return null;
126
+ return { _or: validChildren };
127
+ },
128
+ not: (_schema, _state, childResult): HasuraCondition | null => {
129
+ return childResult ? { _not: childResult } : null;
130
+ }
131
+ }
132
+ );
133
+ }
134
+
135
+ // Process each filter in the state
136
+ const conditions: HasuraCondition[] = [];
137
+
138
+ for (const [filterId, formState] of filterState.entries()) {
139
+ // Find the corresponding schema
140
+ const filterDef = filterSchema.filters.find(f => f.id === filterId);
141
+ if (!filterDef) continue;
142
+
143
+ const condition = buildConditionsRecursive(filterDef.expression, formState);
144
+ if (condition) {
145
+ conditions.push(condition);
146
+ }
147
+ }
148
+
149
+ if (conditions.length === 0) return {};
150
+ if (conditions.length === 1) return conditions[0];
151
+ return { _and: conditions };
152
+ }
153
+
154
+ // Helper to merge two GraphQLSelectionSets
155
+ function mergeSelectionSets(set1: GraphQLSelectionSet, set2: GraphQLSelectionSet): GraphQLSelectionSet {
156
+ const merged = [...set1];
157
+
158
+ for (const item2 of set2) {
159
+ // For aliased fields, we need to match on both alias and field name
160
+ const existingItem = merged.find(item1 =>
161
+ item1.field === item2.field &&
162
+ item1.alias === item2.alias &&
163
+ item1.path === item2.path
164
+ );
165
+ if (existingItem) {
166
+ // Deep merge selections if both have them
167
+ if (existingItem.selections && item2.selections) {
168
+ existingItem.selections = mergeSelectionSets(existingItem.selections, item2.selections);
169
+ }
170
+ // Naive merge of other properties, assuming they don't conflict or last one wins
171
+ if (item2.limit !== undefined) existingItem.limit = item2.limit;
172
+ if (item2.order_by) existingItem.order_by = item2.order_by;
173
+ if (item2.where) existingItem.where = item2.where;
174
+ if (item2.path) existingItem.path = item2.path;
175
+ // etc. for other properties
176
+ } else {
177
+ merged.push(item2);
178
+ }
179
+ }
180
+
181
+ return merged;
182
+ }
183
+
184
+ // Generates a GraphQL selection set from a FieldQuery[] (tagged ADT)
185
+ export function generateSelectionSetFromColumns(columns: ColumnDefinition[]): GraphQLSelectionSet {
186
+ // Helper to apply alias to the deepest field in a nested structure
187
+ function applyAliasToDeepestField(item: GraphQLSelectionSetItem, alias: string): GraphQLSelectionSetItem {
188
+ if (item.selections && item.selections.length > 0) {
189
+ // If there are nested selections, recursively apply to the deepest one
190
+ const lastSelection = item.selections[item.selections.length - 1];
191
+ const updatedLastSelection = applyAliasToDeepestField(lastSelection, alias);
192
+ return {
193
+ ...item,
194
+ selections: [
195
+ ...item.selections.slice(0, -1),
196
+ updatedLastSelection
197
+ ]
198
+ };
199
+ } else {
200
+ // This is the deepest field, apply the alias here
201
+ return { ...item, alias };
202
+ }
203
+ }
204
+
205
+ // Helper to process FieldQuery recursively
206
+ function processFieldQuery(fieldQuery: FieldQuery): GraphQLSelectionSetItem | null {
207
+ if (fieldQuery.type === 'field') {
208
+ const parts = fieldQuery.path.split('.');
209
+ const buildNested = (p: string[]): GraphQLSelectionSetItem => {
210
+ const [head, ...tail] = p;
211
+ const item: GraphQLSelectionSetItem = { field: head };
212
+ if (tail.length > 0) {
213
+ item.selections = [buildNested(tail)];
214
+ }
215
+ return item;
216
+ };
217
+ return buildNested(parts);
218
+ } else if (fieldQuery.type === 'queryConfigs') {
219
+ if (!fieldQuery.configs.length) return null;
220
+
221
+ // Recursive helper to build nested selection items
222
+ const buildNestedItem = (configs: QueryConfig[]): GraphQLSelectionSetItem => {
223
+ const [head, ...tail] = configs;
224
+ const item: GraphQLSelectionSetItem = { field: head.field };
225
+
226
+ if (head.orderBy) {
227
+ const toHasuraOrderBy = (ob: OrderByConfig | OrderByConfig[]): HasuraOrderBy | HasuraOrderBy[] => {
228
+ if (Array.isArray(ob)) {
229
+ return ob.map(o => ({ [o.key]: o.direction.toUpperCase() as 'ASC' | 'DESC' }));
230
+ }
231
+ return { [ob.key]: ob.direction.toUpperCase() as 'ASC' | 'DESC' };
232
+ };
233
+ item.order_by = toHasuraOrderBy(head.orderBy);
234
+ }
235
+
236
+ if (head.limit !== undefined) {
237
+ item.limit = head.limit;
238
+ }
239
+
240
+ if (head.path) {
241
+ item.path = head.path;
242
+ }
243
+
244
+ if (tail.length) {
245
+ item.selections = [buildNestedItem(tail)];
246
+ }
247
+
248
+ return item;
249
+ };
250
+
251
+ return buildNestedItem(fieldQuery.configs);
252
+ } else if (fieldQuery.type === 'fieldAlias') {
253
+ // Process the underlying field query
254
+ const underlyingItem = processFieldQuery(fieldQuery.field);
255
+ if (underlyingItem) {
256
+ // For different types of underlying fields, apply alias differently:
257
+ if (fieldQuery.field.type === 'field') {
258
+ // For simple fields (potentially nested), apply alias to the deepest field
259
+ return applyAliasToDeepestField(underlyingItem, fieldQuery.alias);
260
+ } else if (fieldQuery.field.type === 'queryConfigs') {
261
+ // For queryConfigs, apply alias to the root field
262
+ underlyingItem.alias = fieldQuery.alias;
263
+ return underlyingItem;
264
+ }
265
+ }
266
+ return underlyingItem;
267
+ }
268
+ return null;
269
+ }
270
+ // Build selection set for all columns and all FieldQuery[]
271
+ const allSelections = columns.flatMap(col => col.data.map(processFieldQuery).filter((item): item is GraphQLSelectionSetItem => !!item));
272
+
273
+ // Deep merge all generated selection sets
274
+ return allSelections.reduce((acc: GraphQLSelectionSet, current: GraphQLSelectionSetItem) => {
275
+ return mergeSelectionSets(acc, [current]);
276
+ }, []);
277
+ }
278
+
279
+ export function generateGraphQLQueryAST(
280
+ rootField: string,
281
+ columns: ColumnDefinition[],
282
+ boolExpType: string,
283
+ orderByType: string
284
+ ): GraphQLQueryAST {
285
+ const selectionSet = generateSelectionSetFromColumns(columns);
286
+ return {
287
+ operation: 'query',
288
+ variables: [
289
+ { name: 'conditions', type: boolExpType },
290
+ { name: 'limit', type: 'Int' },
291
+ { name: 'orderBy', type: orderByType }
292
+ ],
293
+ rootField: `${rootField}(where: $conditions, limit: $limit, orderBy: $orderBy)`,
294
+ selectionSet: selectionSet
295
+ };
296
+ }
297
+
298
+ // Generates a full GraphQL query string for a given root field and schema, supporting only limit (Int), conditions (Hasura condition), and orderBy (Hasura ordering)
299
+ // Generates a full GraphQL query string for a given root field and schema.
300
+ // If paginationKey is provided and not already part of the selection set derived from columns,
301
+ // it will be appended to ensure cursor-based pagination works even when the user has not defined
302
+ // a column for that field.
303
+ export function generateGraphQLQuery(
304
+ rootField: string,
305
+ columns: ColumnDefinition[],
306
+ boolExpType: string,
307
+ orderByType: string,
308
+ paginationKey: string
309
+ ): string {
310
+ const ast = generateGraphQLQueryAST(rootField, columns, boolExpType, orderByType);
311
+
312
+ // Check if the pagination key is already present in the top-level selection set
313
+ const hasPagKey = ast.selectionSet.some(sel => sel.field === paginationKey || sel.alias === paginationKey);
314
+ if (!hasPagKey) {
315
+ // Support dotted paths (e.g., parent.child.id) by building nested selections
316
+ const buildNested = (path: string): GraphQLSelectionSetItem => {
317
+ const parts = path.split('.');
318
+ const head = parts[0];
319
+ if (parts.length === 1) return { field: head };
320
+ return { field: head, selections: [buildNested(parts.slice(1).join('.'))] };
321
+ };
322
+ ast.selectionSet.push(buildNested(paginationKey));
323
+ }
324
+
325
+ return renderGraphQLQuery(ast);
326
+ }
327
+
328
+ // AST-like representation for GraphQL queries
329
+ export type GraphQLVariable = {
330
+ name: string;
331
+ type: string;
332
+ };
333
+
334
+ // Hasura order_by type: { field: 'asc' | 'desc' }
335
+ export type HasuraOrderBy = Record<string, 'ASC' | 'DESC'>;
336
+
337
+ export type GraphQLSelectionSetItem = {
338
+ field: string;
339
+ alias?: string; // field alias support
340
+ path?: string; // path for querying inside JSON columns
341
+ where?: HasuraCondition;
342
+ order_by?: HasuraOrderBy | HasuraOrderBy[];
343
+ limit?: number;
344
+ offset?: number;
345
+ distinct_on?: string[];
346
+ selections?: GraphQLSelectionSetItem[];
347
+ };
348
+
349
+ export type GraphQLSelectionSet = GraphQLSelectionSetItem[];
350
+
351
+ export type GraphQLQueryAST = {
352
+ operation: 'query';
353
+ name?: string;
354
+ variables: GraphQLVariable[];
355
+ rootField: string;
356
+ selectionSet: GraphQLSelectionSet;
357
+ };
358
+
359
+ // Renders a GraphQLQueryAST to a GraphQL query string
360
+ export function renderGraphQLQuery(ast: GraphQLQueryAST): string {
361
+ function renderVariables(vars: GraphQLVariable[]): string {
362
+ if (!vars.length) return '';
363
+ return '('
364
+ + vars.map(v => `$${v.name}: ${v.type}`).join(', ')
365
+ + ')';
366
+ }
367
+
368
+ function renderArgs(item: GraphQLSelectionSetItem): string {
369
+ const args: string[] = [];
370
+ if (item.where) {
371
+ // Recursively render HasuraCondition as GraphQL, not JSON
372
+ const renderWhere = (cond: HasuraCondition | HasuraOperator | HasuraOperator[]): string => {
373
+ if (Array.isArray(cond)) {
374
+ // Array of HasuraCondition or HasuraOperator
375
+ return `[${cond.map(renderWhere).join(", ")}]`;
376
+ } else if (typeof cond === 'object' && cond !== null) {
377
+ if ('_and' in cond && Array.isArray(cond._and)) {
378
+ return `_and: [${cond._and.map(renderWhere).join(", ")}]`;
379
+ }
380
+ if ('_or' in cond && Array.isArray(cond._or)) {
381
+ return `_or: [${cond._or.map(renderWhere).join(", ")}]`;
382
+ }
383
+ if ('_not' in cond) {
384
+ return `_not: {${renderWhere(cond._not)}}`;
385
+ }
386
+ // Field operators (HasuraOperator or HasuraOperator[])
387
+ return Object.entries(cond)
388
+ .map(([field, op]) => {
389
+ if (Array.isArray(op)) {
390
+ // Array of HasuraOperator
391
+ return `${field}: [${op.map(renderWhere).join(", ")}]`;
392
+ } else if (typeof op === 'object' && op !== null) {
393
+ // Single HasuraOperator
394
+ return `${field}: {${Object.entries(op)
395
+ .map(([k, v]) => `${k}: ${typeof v === 'string' ? `"${v}"` : JSON.stringify(v)}`)
396
+ .join(", ")}}`;
397
+ } else {
398
+ // Primitive value (should not happen for HasuraOperator, but fallback)
399
+ return `${field}: ${typeof op === 'string' ? `"${op}"` : JSON.stringify(op)}`;
400
+ }
401
+ })
402
+ .join(", ");
403
+ }
404
+ return JSON.stringify(cond);
405
+ };
406
+ args.push(`where: {${renderWhere(item.where)}}`);
407
+ }
408
+ if (item.limit !== undefined)
409
+ args.push(`limit: ${item.limit}`);
410
+ if (item.path)
411
+ args.push(`path: "${item.path}"`);
412
+ if (item.order_by) {
413
+ // Custom rendering for orderBy to avoid quotes around asc/desc
414
+ const renderOrderBy = (orderBy: HasuraOrderBy | HasuraOrderBy[] | undefined): string => {
415
+ if (Array.isArray(orderBy)) {
416
+ return (
417
+ '[' + orderBy.map(renderOrderBy).join(', ') + ']'
418
+ );
419
+ } else if (typeof orderBy === 'object' && orderBy !== undefined) {
420
+ return (
421
+ '{' + Object.entries(orderBy)
422
+ .map(([k, v]) => `${k}: ${String(v).toUpperCase()}`)
423
+ .join(', ') + '}'
424
+ );
425
+ }
426
+ return String(orderBy).toUpperCase();
427
+ };
428
+ args.push(`orderBy: ${renderOrderBy(item.order_by)}`);
429
+ }
430
+ return args.length ? `(${args.join(', ')})` : '';
431
+ }
432
+
433
+ function renderSelectionSet(set: GraphQLSelectionSet, indent = ' '): string {
434
+ return set
435
+ .map(item => {
436
+ const args = renderArgs(item);
437
+ const fieldName = item.alias ? `${item.alias}: ${item.field}` : item.field;
438
+ if (item.selections && item.selections.length) {
439
+ return (
440
+ `${indent}${fieldName}${args} {` +
441
+ renderSelectionSet(item.selections, indent + ' ') +
442
+ `${indent}}`
443
+ );
444
+ } else {
445
+ return `${indent}${fieldName}${args}`;
446
+ }
447
+ })
448
+ .join('\n');
449
+ }
450
+
451
+ const vars = renderVariables(ast.variables);
452
+ const selection = renderSelectionSet(ast.selectionSet);
453
+ const opName = ast.name ? ` ${ast.name}` : '';
454
+ return (
455
+ `${ast.operation}${opName}${vars} {` +
456
+ ` ${ast.rootField} {` +
457
+ selection +
458
+ `
459
+ }
460
+ }`
461
+ );
462
+ }
@@ -0,0 +1,33 @@
1
+ import { PhoneNumberFilter } from '../../components/PhoneNumberFilter';
2
+ import NoRowsExtendDateRange from '../../views/payment-requests/components/NoRowsExtendDateRange';
3
+ import { Runtime } from '../runtime';
4
+
5
+ export type NativeRuntime = Runtime & {
6
+ cellRenderers: {
7
+ text: ({ data }: { data: unknown }) => string;
8
+ json: ({ data }: { data: unknown }) => string;
9
+ };
10
+ };
11
+
12
+ export const nativeRuntime: NativeRuntime = {
13
+ cellRenderers: {
14
+ text: ({ data }) => {
15
+ if (typeof data === 'object' && data !== null) {
16
+ const value = Object.values(data)[0]
17
+ if (value != null) {
18
+ return String(value)
19
+ }
20
+ }
21
+ return ''
22
+ },
23
+ json: ({ data }) => JSON.stringify(data),
24
+ },
25
+ queryTransforms: {},
26
+ noRowsComponents: {
27
+ noRowsExtendDateRange: NoRowsExtendDateRange
28
+ },
29
+ customFilterComponents: {
30
+ PhoneNumberFilter
31
+ },
32
+ initialValues: {}
33
+ };
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import { nativeRuntime } from './index';
3
+
4
+ describe('NativeRuntime', () => {
5
+ describe('nativeRuntime', () => {
6
+ it('has the correct ViewRuntime structure', () => {
7
+ expect(nativeRuntime).toHaveProperty('cellRenderers');
8
+ expect(nativeRuntime.cellRenderers).toHaveProperty('text');
9
+ expect(nativeRuntime.cellRenderers).toHaveProperty('json');
10
+ });
11
+
12
+ it('exports text and json cell renderers', () => {
13
+ expect(nativeRuntime.cellRenderers.text).toBeDefined();
14
+ expect(typeof nativeRuntime.cellRenderers.text).toBe('function');
15
+ expect(nativeRuntime.cellRenderers.json).toBeDefined();
16
+ expect(typeof nativeRuntime.cellRenderers.json).toBe('function');
17
+ });
18
+
19
+ it('has all required runtime sections', () => {
20
+ expect(nativeRuntime).toHaveProperty('cellRenderers');
21
+ expect(nativeRuntime).toHaveProperty('queryTransforms');
22
+ expect(nativeRuntime).toHaveProperty('noRowsComponents');
23
+ expect(nativeRuntime).toHaveProperty('customFilterComponents');
24
+ expect(nativeRuntime).toHaveProperty('initialValues');
25
+ });
26
+ });
27
+
28
+ describe('text cell renderer', () => {
29
+ const textRenderer = nativeRuntime.cellRenderers.text;
30
+
31
+ it('returns empty string for primitive values', () => {
32
+ expect(textRenderer({ data: 'hello' })).toBe('');
33
+ expect(textRenderer({ data: 123 })).toBe('');
34
+ expect(textRenderer({ data: true })).toBe('');
35
+ expect(textRenderer({ data: false })).toBe('');
36
+ });
37
+
38
+ it('returns empty string for null and undefined', () => {
39
+ expect(textRenderer({ data: null })).toBe('');
40
+ expect(textRenderer({ data: undefined })).toBe('');
41
+ });
42
+
43
+ it('extracts first property value from objects', () => {
44
+ expect(textRenderer({ data: { name: 'John', age: 30 } })).toBe('John');
45
+ expect(textRenderer({ data: { id: 42 } })).toBe('42');
46
+ expect(textRenderer({ data: { active: true } })).toBe('true');
47
+ });
48
+
49
+ it('returns empty string for objects with null/undefined first value', () => {
50
+ expect(textRenderer({ data: { name: null, age: 30 } })).toBe('');
51
+ expect(textRenderer({ data: { name: undefined, age: 30 } })).toBe('');
52
+ });
53
+
54
+ it('returns empty string for empty objects', () => {
55
+ expect(textRenderer({ data: {} })).toBe('');
56
+ });
57
+
58
+ it('handles arrays as objects', () => {
59
+ expect(textRenderer({ data: ['first', 'second'] })).toBe('first');
60
+ expect(textRenderer({ data: [] })).toBe('');
61
+ });
62
+
63
+ it('converts nested objects to string', () => {
64
+ const nestedData = { user: { name: 'Alice', id: 1 } };
65
+ expect(textRenderer({ data: nestedData })).toBe('[object Object]');
66
+ });
67
+ });
68
+
69
+ describe('json cell renderer', () => {
70
+ const jsonRenderer = nativeRuntime.cellRenderers.json;
71
+
72
+ it('serializes primitive values', () => {
73
+ expect(jsonRenderer({ data: 'hello' })).toBe('"hello"');
74
+ expect(jsonRenderer({ data: 123 })).toBe('123');
75
+ expect(jsonRenderer({ data: true })).toBe('true');
76
+ expect(jsonRenderer({ data: false })).toBe('false');
77
+ });
78
+
79
+ it('handles null and undefined', () => {
80
+ expect(jsonRenderer({ data: null })).toBe('null');
81
+ expect(jsonRenderer({ data: undefined })).toBe(undefined);
82
+ });
83
+
84
+ it('serializes objects', () => {
85
+ const obj = { name: 'John', age: 30 };
86
+ expect(jsonRenderer({ data: obj })).toBe('{"name":"John","age":30}');
87
+ });
88
+
89
+ it('serializes arrays', () => {
90
+ const arr = ['apple', 'banana', 'orange'];
91
+ expect(jsonRenderer({ data: arr })).toBe('["apple","banana","orange"]');
92
+ });
93
+
94
+ it('handles empty objects and arrays', () => {
95
+ expect(jsonRenderer({ data: {} })).toBe('{}');
96
+ expect(jsonRenderer({ data: [] })).toBe('[]');
97
+ });
98
+
99
+ it('handles nested structures', () => {
100
+ const nested = {
101
+ user: { name: 'Alice', preferences: ['dark', 'compact'] },
102
+ settings: { theme: 'dark', notifications: true }
103
+ };
104
+ const expected = '{"user":{"name":"Alice","preferences":["dark","compact"]},"settings":{"theme":"dark","notifications":true}}';
105
+ expect(jsonRenderer({ data: nested })).toBe(expected);
106
+ });
107
+ });
108
+ });