@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.
- package/.editorconfig +12 -0
- package/.github/copilot-instructions.md +64 -0
- package/.github/workflows/ci.yml +51 -0
- package/.husky/pre-commit +8 -0
- package/README.md +63 -0
- package/docs/api/README.md +32 -0
- package/docs/api/cell-renderers.md +121 -0
- package/docs/api/no-rows-component.md +71 -0
- package/docs/api/runtime.md +78 -0
- package/e2e/app.spec.ts +6 -0
- package/e2e/cell-renderer-setfilterstate.spec.ts +63 -0
- package/e2e/filter-sharing.spec.ts +113 -0
- package/e2e/filter-url-persistence.spec.ts +36 -0
- package/e2e/graphqlMock.ts +144 -0
- package/e2e/multi-field-filters.spec.ts +95 -0
- package/e2e/pagination.spec.ts +38 -0
- package/e2e/payment-request-email-filter.spec.ts +67 -0
- package/e2e/save-filter-splitbutton.spec.ts +68 -0
- package/e2e/simple-view-email-filter.spec.ts +67 -0
- package/e2e/simple-view-transforms.spec.ts +171 -0
- package/e2e/simple-view.spec.ts +104 -0
- package/e2e/transform-regression.spec.ts +108 -0
- package/eslint.config.js +30 -0
- package/index.html +17 -0
- package/jest.config.js +10 -0
- package/package.json +45 -0
- package/playwright.config.ts +54 -0
- package/public/vite.svg +1 -0
- package/src/App.externalRuntime.test.ts +190 -0
- package/src/App.tsx +540 -0
- package/src/assets/react.svg +1 -0
- package/src/components/AIAssistantForm.tsx +241 -0
- package/src/components/FilterForm.test.ts +82 -0
- package/src/components/FilterForm.tsx +375 -0
- package/src/components/PhoneNumberFilter.tsx +102 -0
- package/src/components/SavedFilterList.tsx +181 -0
- package/src/components/SpeechInput.tsx +67 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/TablePagination.tsx +40 -0
- package/src/components/aiAssistant.test.ts +270 -0
- package/src/components/aiAssistant.ts +291 -0
- package/src/framework/cell-renderer-components/CurrencyAmount.tsx +30 -0
- package/src/framework/cell-renderer-components/LayoutHelpers.tsx +74 -0
- package/src/framework/cell-renderer-components/Link.tsx +28 -0
- package/src/framework/cell-renderer-components/Mapping.tsx +11 -0
- package/src/framework/cell-renderer-components.test.ts +353 -0
- package/src/framework/column-definition.tsx +85 -0
- package/src/framework/currency.test.ts +46 -0
- package/src/framework/currency.ts +62 -0
- package/src/framework/data.staticConditions.test.ts +46 -0
- package/src/framework/data.test.ts +167 -0
- package/src/framework/data.ts +162 -0
- package/src/framework/filter-form-state.test.ts +189 -0
- package/src/framework/filter-form-state.ts +185 -0
- package/src/framework/filter-sharing.test.ts +135 -0
- package/src/framework/filter-sharing.ts +118 -0
- package/src/framework/filters.ts +194 -0
- package/src/framework/graphql.buildHasuraConditions.test.ts +473 -0
- package/src/framework/graphql.paginationKey.test.ts +29 -0
- package/src/framework/graphql.test.ts +286 -0
- package/src/framework/graphql.ts +462 -0
- package/src/framework/native-runtime/index.tsx +33 -0
- package/src/framework/native-runtime/nativeComponents.test.ts +108 -0
- package/src/framework/runtime-reference.test.ts +172 -0
- package/src/framework/runtime.ts +15 -0
- package/src/framework/saved-filters.test.ts +422 -0
- package/src/framework/saved-filters.ts +293 -0
- package/src/framework/state.test.ts +86 -0
- package/src/framework/state.ts +148 -0
- package/src/framework/transform.test.ts +51 -0
- package/src/framework/view-parser-initialvalues.test.ts +228 -0
- package/src/framework/view-parser.ts +714 -0
- package/src/framework/view.test.ts +1805 -0
- package/src/framework/view.ts +38 -0
- package/src/index.css +6 -0
- package/src/main.tsx +99 -0
- package/src/views/index.ts +12 -0
- package/src/views/payment-requests/components/NoRowsExtendDateRange.tsx +37 -0
- package/src/views/payment-requests/components/PaymentMethod.tsx +184 -0
- package/src/views/payment-requests/components/PaymentStatusTag.tsx +61 -0
- package/src/views/payment-requests/index.ts +1 -0
- package/src/views/payment-requests/runtime.tsx +145 -0
- package/src/views/payment-requests/view.json +692 -0
- package/src/views/payment-requests-initial-values.test.ts +73 -0
- package/src/views/request-log/index.ts +2 -0
- package/src/views/request-log/runtime.tsx +47 -0
- package/src/views/request-log/view.json +123 -0
- package/src/views/simple-test-view/index.ts +3 -0
- package/src/views/simple-test-view/runtime.tsx +85 -0
- package/src/views/simple-test-view/view.json +191 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +7 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.jest.json +6 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +24 -0
- 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
|
+
}
|