form0-core 0.1.11 → 0.1.13
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/package.json +2 -2
- package/src/index.js +7 -0
- package/src/utilities/dataset-descriptors.js +773 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"main": "src/index.js",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node tests/form0-core.test.js && node tests/builtin-metadata.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/security-test.js",
|
|
11
|
+
"test": "node tests/form0-core.test.js && node tests/dataset-descriptors.test.js && node tests/builtin-metadata.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/security-test.js",
|
|
12
12
|
"postinstall": "node scripts/postinstall.cjs",
|
|
13
13
|
"format": "prettier --write \"**/*.{js,jsx,json,ts,tsx,md}\"",
|
|
14
14
|
"format:check": "prettier --check \"**/*.{js,jsx,json,ts,tsx,md}\""
|
package/src/index.js
CHANGED
|
@@ -23,6 +23,13 @@ export { getValidOperators, isValidOperator, validateFieldConditions } from './s
|
|
|
23
23
|
|
|
24
24
|
export { recordVersion, formVersion } from './utilities/version-utils.js';
|
|
25
25
|
export { buildRepeatableMetadata } from './utilities/repeatable-helpers.js';
|
|
26
|
+
export {
|
|
27
|
+
buildDatasetDescriptors,
|
|
28
|
+
buildFieldIdentityMap,
|
|
29
|
+
getFieldQuerySemantics,
|
|
30
|
+
projectDatasetRowValues,
|
|
31
|
+
resolveDatasetDescriptorById,
|
|
32
|
+
} from './utilities/dataset-descriptors.js';
|
|
26
33
|
export { generateUuidV7 } from './utilities/uuid.js';
|
|
27
34
|
export {
|
|
28
35
|
FORM_LINK_VALUE_DELIMITER,
|
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { flattenFields } from './field-helpers.js';
|
|
2
|
+
|
|
3
|
+
const ROOT_DATASET_ID = '__root__';
|
|
4
|
+
const CONTAINER_TYPES = new Set(['Section', 'BuildingPlanSection']);
|
|
5
|
+
|
|
6
|
+
const isRecord = (value) =>
|
|
7
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
|
|
9
|
+
const toTrimmedString = (value) => {
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const trimmed = value.trim();
|
|
15
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const toSchemaFormNode = (schema) => {
|
|
19
|
+
if (!isRecord(schema)) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (isRecord(schema.form)) {
|
|
24
|
+
return schema.form;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (Array.isArray(schema.elements)) {
|
|
28
|
+
return schema;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const uniqueAliases = (...values) => {
|
|
35
|
+
const aliases = [];
|
|
36
|
+
for (const value of values) {
|
|
37
|
+
const normalized = toTrimmedString(value);
|
|
38
|
+
if (!normalized || aliases.includes(normalized)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
aliases.push(normalized);
|
|
42
|
+
}
|
|
43
|
+
return aliases;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const toFieldId = (field) =>
|
|
47
|
+
toTrimmedString(field?.key) ?? toTrimmedString(field?.data_name);
|
|
48
|
+
|
|
49
|
+
const toOutputKey = (field) =>
|
|
50
|
+
toTrimmedString(field?.data_name) ?? toTrimmedString(field?.key);
|
|
51
|
+
|
|
52
|
+
const toFieldLabel = (field) =>
|
|
53
|
+
toTrimmedString(field?.label) ??
|
|
54
|
+
toTrimmedString(field?.data_name) ??
|
|
55
|
+
toTrimmedString(field?.key) ??
|
|
56
|
+
'Field';
|
|
57
|
+
|
|
58
|
+
const toRepeatableLabel = (field) =>
|
|
59
|
+
toTrimmedString(field?.label) ??
|
|
60
|
+
toTrimmedString(field?.data_name) ??
|
|
61
|
+
toTrimmedString(field?.key) ??
|
|
62
|
+
'Repeatable section';
|
|
63
|
+
|
|
64
|
+
const getCalculatedDisplayStyle = (field) => {
|
|
65
|
+
if (!isRecord(field)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const display = field.display;
|
|
70
|
+
if (typeof display === 'string') {
|
|
71
|
+
return toTrimmedString(display);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isRecord(display)) {
|
|
75
|
+
return toTrimmedString(display.style);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const QUERY_SEMANTICS_BY_FIELD_TYPE = {
|
|
82
|
+
TextField: {
|
|
83
|
+
query_kind: 'scalar',
|
|
84
|
+
display_kind: 'text',
|
|
85
|
+
sortable: true,
|
|
86
|
+
filterable: true,
|
|
87
|
+
default_operator: 'contains',
|
|
88
|
+
allowed_operators: [
|
|
89
|
+
'contains',
|
|
90
|
+
'not_contains',
|
|
91
|
+
'eq',
|
|
92
|
+
'neq',
|
|
93
|
+
'in',
|
|
94
|
+
'not_in',
|
|
95
|
+
'starts_with',
|
|
96
|
+
'ends_with',
|
|
97
|
+
'is_blank',
|
|
98
|
+
'is_not_blank',
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
NumericField: {
|
|
102
|
+
query_kind: 'scalar',
|
|
103
|
+
display_kind: 'number',
|
|
104
|
+
sortable: true,
|
|
105
|
+
filterable: true,
|
|
106
|
+
default_operator: 'between',
|
|
107
|
+
allowed_operators: [
|
|
108
|
+
'between',
|
|
109
|
+
'gte',
|
|
110
|
+
'lte',
|
|
111
|
+
'eq',
|
|
112
|
+
'neq',
|
|
113
|
+
'gt',
|
|
114
|
+
'lt',
|
|
115
|
+
'in',
|
|
116
|
+
'not_in',
|
|
117
|
+
'is_blank',
|
|
118
|
+
'is_not_blank',
|
|
119
|
+
],
|
|
120
|
+
},
|
|
121
|
+
DateField: {
|
|
122
|
+
query_kind: 'scalar',
|
|
123
|
+
display_kind: 'date',
|
|
124
|
+
sortable: true,
|
|
125
|
+
filterable: true,
|
|
126
|
+
default_operator: 'between',
|
|
127
|
+
allowed_operators: [
|
|
128
|
+
'between',
|
|
129
|
+
'gte',
|
|
130
|
+
'lte',
|
|
131
|
+
'eq',
|
|
132
|
+
'neq',
|
|
133
|
+
'gt',
|
|
134
|
+
'lt',
|
|
135
|
+
'in',
|
|
136
|
+
'not_in',
|
|
137
|
+
'is_blank',
|
|
138
|
+
'is_not_blank',
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
TimeField: {
|
|
142
|
+
query_kind: 'scalar',
|
|
143
|
+
display_kind: 'time',
|
|
144
|
+
sortable: true,
|
|
145
|
+
filterable: true,
|
|
146
|
+
default_operator: 'between',
|
|
147
|
+
allowed_operators: [
|
|
148
|
+
'between',
|
|
149
|
+
'gte',
|
|
150
|
+
'lte',
|
|
151
|
+
'eq',
|
|
152
|
+
'neq',
|
|
153
|
+
'gt',
|
|
154
|
+
'lt',
|
|
155
|
+
'in',
|
|
156
|
+
'not_in',
|
|
157
|
+
'is_blank',
|
|
158
|
+
'is_not_blank',
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
SingleChoiceField: {
|
|
162
|
+
query_kind: 'scalar',
|
|
163
|
+
display_kind: 'enum',
|
|
164
|
+
sortable: true,
|
|
165
|
+
filterable: true,
|
|
166
|
+
default_operator: 'in',
|
|
167
|
+
allowed_operators: ['in', 'eq', 'neq', 'not_in', 'is_blank', 'is_not_blank'],
|
|
168
|
+
},
|
|
169
|
+
BooleanField: {
|
|
170
|
+
query_kind: 'scalar',
|
|
171
|
+
display_kind: 'boolean',
|
|
172
|
+
sortable: true,
|
|
173
|
+
filterable: true,
|
|
174
|
+
default_operator: 'in',
|
|
175
|
+
allowed_operators: ['in', 'eq', 'neq', 'not_in', 'is_blank', 'is_not_blank'],
|
|
176
|
+
},
|
|
177
|
+
MultiChoiceField: {
|
|
178
|
+
query_kind: 'terms',
|
|
179
|
+
display_kind: 'enum_multi',
|
|
180
|
+
sortable: false,
|
|
181
|
+
filterable: true,
|
|
182
|
+
default_operator: 'has_any',
|
|
183
|
+
allowed_operators: ['has_any', 'has_all', 'has_none', 'is_blank', 'is_not_blank'],
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const DISPLAY_ONLY_SEMANTICS = {
|
|
188
|
+
query_kind: 'display_only',
|
|
189
|
+
display_kind: 'json',
|
|
190
|
+
sortable: false,
|
|
191
|
+
filterable: false,
|
|
192
|
+
default_operator: null,
|
|
193
|
+
allowed_operators: [],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getCalculatedFieldSemantics = (field) => {
|
|
197
|
+
const style = getCalculatedDisplayStyle(field);
|
|
198
|
+
|
|
199
|
+
if (style === 'numeric' || style === 'currency') {
|
|
200
|
+
return {
|
|
201
|
+
query_kind: 'scalar',
|
|
202
|
+
display_kind: 'number',
|
|
203
|
+
sortable: true,
|
|
204
|
+
filterable: true,
|
|
205
|
+
default_operator: 'between',
|
|
206
|
+
allowed_operators: [
|
|
207
|
+
'between',
|
|
208
|
+
'gte',
|
|
209
|
+
'lte',
|
|
210
|
+
'eq',
|
|
211
|
+
'neq',
|
|
212
|
+
'gt',
|
|
213
|
+
'lt',
|
|
214
|
+
'in',
|
|
215
|
+
'not_in',
|
|
216
|
+
'is_blank',
|
|
217
|
+
'is_not_blank',
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (style === 'date') {
|
|
223
|
+
return {
|
|
224
|
+
query_kind: 'scalar',
|
|
225
|
+
display_kind: 'date',
|
|
226
|
+
sortable: true,
|
|
227
|
+
filterable: true,
|
|
228
|
+
default_operator: 'between',
|
|
229
|
+
allowed_operators: [
|
|
230
|
+
'between',
|
|
231
|
+
'gte',
|
|
232
|
+
'lte',
|
|
233
|
+
'eq',
|
|
234
|
+
'neq',
|
|
235
|
+
'gt',
|
|
236
|
+
'lt',
|
|
237
|
+
'in',
|
|
238
|
+
'not_in',
|
|
239
|
+
'is_blank',
|
|
240
|
+
'is_not_blank',
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (style === 'time') {
|
|
246
|
+
return {
|
|
247
|
+
query_kind: 'scalar',
|
|
248
|
+
display_kind: 'time',
|
|
249
|
+
sortable: true,
|
|
250
|
+
filterable: true,
|
|
251
|
+
default_operator: 'between',
|
|
252
|
+
allowed_operators: [
|
|
253
|
+
'between',
|
|
254
|
+
'gte',
|
|
255
|
+
'lte',
|
|
256
|
+
'eq',
|
|
257
|
+
'neq',
|
|
258
|
+
'gt',
|
|
259
|
+
'lt',
|
|
260
|
+
'in',
|
|
261
|
+
'not_in',
|
|
262
|
+
'is_blank',
|
|
263
|
+
'is_not_blank',
|
|
264
|
+
],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
query_kind: 'scalar',
|
|
270
|
+
display_kind: 'text',
|
|
271
|
+
sortable: true,
|
|
272
|
+
filterable: true,
|
|
273
|
+
default_operator: 'contains',
|
|
274
|
+
allowed_operators: [
|
|
275
|
+
'contains',
|
|
276
|
+
'not_contains',
|
|
277
|
+
'eq',
|
|
278
|
+
'neq',
|
|
279
|
+
'in',
|
|
280
|
+
'not_in',
|
|
281
|
+
'starts_with',
|
|
282
|
+
'ends_with',
|
|
283
|
+
'is_blank',
|
|
284
|
+
'is_not_blank',
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export function getFieldQuerySemantics(field) {
|
|
290
|
+
if (!isRecord(field)) {
|
|
291
|
+
return { ...DISPLAY_ONLY_SEMANTICS };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (field.type === 'CalculatedField') {
|
|
295
|
+
return getCalculatedFieldSemantics(field);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const semantics = QUERY_SEMANTICS_BY_FIELD_TYPE[field.type];
|
|
299
|
+
if (!semantics) {
|
|
300
|
+
return { ...DISPLAY_ONLY_SEMANTICS };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return { ...semantics };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const toChoiceValueList = (field) => {
|
|
307
|
+
if (!Array.isArray(field?.choices)) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return field.choices.filter((choice) => isRecord(choice));
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const isChoicePrimitive = (value) =>
|
|
315
|
+
typeof value === 'string' ||
|
|
316
|
+
typeof value === 'number' ||
|
|
317
|
+
typeof value === 'boolean';
|
|
318
|
+
|
|
319
|
+
const normalizeChoiceEntryValue = (entry) => {
|
|
320
|
+
if (isChoicePrimitive(entry)) {
|
|
321
|
+
return entry;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!isRecord(entry)) {
|
|
325
|
+
return undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (isChoicePrimitive(entry.value)) {
|
|
329
|
+
return entry.value;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (isChoicePrimitive(entry.label)) {
|
|
333
|
+
return entry.label;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return undefined;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const normalizeChoiceEntryDisplayValue = (entry) => {
|
|
340
|
+
if (isChoicePrimitive(entry)) {
|
|
341
|
+
return entry;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (!isRecord(entry)) {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (isChoicePrimitive(entry.label)) {
|
|
349
|
+
return entry.label;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (isChoicePrimitive(entry.value)) {
|
|
353
|
+
return entry.value;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return undefined;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const normalizeChoiceEntryLabel = (field, entry) => {
|
|
360
|
+
const normalizedValue = normalizeChoiceEntryValue(entry);
|
|
361
|
+
if (typeof normalizedValue === 'undefined') {
|
|
362
|
+
return undefined;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const matched = toChoiceValueList(field).find(
|
|
366
|
+
(choice) =>
|
|
367
|
+
Object.prototype.hasOwnProperty.call(choice, 'value') &&
|
|
368
|
+
choice.value === normalizedValue,
|
|
369
|
+
);
|
|
370
|
+
if (matched) {
|
|
371
|
+
return toTrimmedString(matched.label) ?? normalizedValue;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (isRecord(entry) && isChoicePrimitive(entry.label)) {
|
|
375
|
+
return entry.label;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return normalizedValue;
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const toSingleChoiceSelectionValues = (rawValue) => {
|
|
382
|
+
if (isRecord(rawValue)) {
|
|
383
|
+
const choiceValues = Array.isArray(rawValue.choice)
|
|
384
|
+
? rawValue.choice
|
|
385
|
+
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
386
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
387
|
+
: [];
|
|
388
|
+
const otherValues = Array.isArray(rawValue.other)
|
|
389
|
+
? rawValue.other
|
|
390
|
+
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
391
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
392
|
+
: [];
|
|
393
|
+
|
|
394
|
+
return [...choiceValues, ...otherValues];
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const normalized = normalizeChoiceEntryValue(rawValue);
|
|
398
|
+
return typeof normalized === 'undefined' ? [] : [normalized];
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const toSingleChoiceDisplayValues = (field, rawValue) => {
|
|
402
|
+
if (isRecord(rawValue)) {
|
|
403
|
+
const choiceLabels = Array.isArray(rawValue.choice)
|
|
404
|
+
? rawValue.choice
|
|
405
|
+
.map((entry) => normalizeChoiceEntryLabel(field, entry))
|
|
406
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
407
|
+
: [];
|
|
408
|
+
const otherLabels = Array.isArray(rawValue.other)
|
|
409
|
+
? rawValue.other
|
|
410
|
+
.map((entry) => normalizeChoiceEntryDisplayValue(entry))
|
|
411
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
412
|
+
: [];
|
|
413
|
+
|
|
414
|
+
return [...choiceLabels, ...otherLabels];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const normalized = normalizeChoiceEntryLabel(field, rawValue);
|
|
418
|
+
return typeof normalized === 'undefined' ? [] : [normalized];
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const toMultiChoiceSelectionValues = (rawValue) => {
|
|
422
|
+
if (isRecord(rawValue)) {
|
|
423
|
+
const selectedValues = Array.isArray(rawValue.choices)
|
|
424
|
+
? rawValue.choices
|
|
425
|
+
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
426
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
427
|
+
: [];
|
|
428
|
+
const otherValues = Array.isArray(rawValue.other)
|
|
429
|
+
? rawValue.other
|
|
430
|
+
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
431
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
432
|
+
: [];
|
|
433
|
+
|
|
434
|
+
return [...selectedValues, ...otherValues];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (Array.isArray(rawValue)) {
|
|
438
|
+
return rawValue
|
|
439
|
+
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
440
|
+
.filter((entry) => typeof entry !== 'undefined');
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return [];
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const toMultiChoiceDisplayValues = (field, rawValue) => {
|
|
447
|
+
if (isRecord(rawValue)) {
|
|
448
|
+
const selectedLabels = Array.isArray(rawValue.choices)
|
|
449
|
+
? rawValue.choices
|
|
450
|
+
.map((entry) => normalizeChoiceEntryLabel(field, entry))
|
|
451
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
452
|
+
: [];
|
|
453
|
+
const otherLabels = Array.isArray(rawValue.other)
|
|
454
|
+
? rawValue.other
|
|
455
|
+
.map((entry) => normalizeChoiceEntryDisplayValue(entry))
|
|
456
|
+
.filter((entry) => typeof entry !== 'undefined')
|
|
457
|
+
: [];
|
|
458
|
+
|
|
459
|
+
return [...selectedLabels, ...otherLabels];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (Array.isArray(rawValue)) {
|
|
463
|
+
return rawValue
|
|
464
|
+
.map((entry) => normalizeChoiceEntryLabel(field, entry))
|
|
465
|
+
.filter((entry) => typeof entry !== 'undefined');
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return [];
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const toSingleChoiceLabel = (field, rawValue) => {
|
|
472
|
+
const labels = toSingleChoiceDisplayValues(field, rawValue);
|
|
473
|
+
if (labels.length === 0) {
|
|
474
|
+
return rawValue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return labels.length === 1 ? labels[0] : labels;
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const toMultiChoiceLabels = (field, rawValue) => {
|
|
481
|
+
const labels = toMultiChoiceDisplayValues(field, rawValue);
|
|
482
|
+
if (labels.length === 0) {
|
|
483
|
+
return rawValue;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return labels;
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const toDisplayValue = (field, rawValue) => {
|
|
490
|
+
if (typeof rawValue === 'undefined') {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
if (field?.field_type === 'SingleChoiceField' || field?.field_type === 'BooleanField') {
|
|
495
|
+
return toSingleChoiceLabel(field, rawValue);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (field?.field_type === 'MultiChoiceField') {
|
|
499
|
+
return toMultiChoiceLabels(field, rawValue);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return rawValue;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const buildDatasetFieldDescriptor = (field, dataset) => {
|
|
506
|
+
const fieldId = toFieldId(field);
|
|
507
|
+
const outputKey = toOutputKey(field);
|
|
508
|
+
if (!fieldId || !outputKey) {
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const semantics = getFieldQuerySemantics(field);
|
|
513
|
+
return {
|
|
514
|
+
dataset_id: dataset.id,
|
|
515
|
+
dataset_path: [...dataset.dataset_path],
|
|
516
|
+
field_id: fieldId,
|
|
517
|
+
output_key: outputKey,
|
|
518
|
+
key: toTrimmedString(field.key),
|
|
519
|
+
data_name: toTrimmedString(field.data_name),
|
|
520
|
+
label: toFieldLabel(field),
|
|
521
|
+
field_type: toTrimmedString(field.type) ?? 'UnknownField',
|
|
522
|
+
choices: Array.isArray(field.choices)
|
|
523
|
+
? field.choices.filter((choice) => isRecord(choice)).map((choice) => ({ ...choice }))
|
|
524
|
+
: undefined,
|
|
525
|
+
aliases: uniqueAliases(outputKey, field.data_name, field.key, fieldId),
|
|
526
|
+
repeatable_depth: dataset.dataset_path.length,
|
|
527
|
+
...semantics,
|
|
528
|
+
};
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const collectFieldsForDataset = (elements, dataset) => {
|
|
532
|
+
if (!Array.isArray(elements)) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
elements.forEach((element) => {
|
|
537
|
+
if (!isRecord(element) || typeof element.type !== 'string') {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (CONTAINER_TYPES.has(element.type)) {
|
|
542
|
+
collectFieldsForDataset(element.elements, dataset);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (element.type === 'RepeatableSection') {
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const descriptor = buildDatasetFieldDescriptor(element, dataset);
|
|
551
|
+
if (descriptor) {
|
|
552
|
+
dataset.fields.push(descriptor);
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const collectRepeatableDatasets = (elements, context, datasets) => {
|
|
558
|
+
if (!Array.isArray(elements)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
elements.forEach((element) => {
|
|
563
|
+
if (!isRecord(element) || typeof element.type !== 'string') {
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (CONTAINER_TYPES.has(element.type)) {
|
|
568
|
+
collectRepeatableDatasets(element.elements, context, datasets);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (element.type !== 'RepeatableSection') {
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const repeatableFieldId = toFieldId(element);
|
|
577
|
+
const repeatableOutputKey = toOutputKey(element);
|
|
578
|
+
if (!repeatableFieldId || !repeatableOutputKey) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const datasetPath = [...context.dataset_path, repeatableFieldId];
|
|
583
|
+
const descriptor = {
|
|
584
|
+
id: datasetPath.join('.'),
|
|
585
|
+
kind: 'repeatable',
|
|
586
|
+
label: [...context.dataset_labels, toRepeatableLabel(element)].join(' / '),
|
|
587
|
+
dataset_path: datasetPath,
|
|
588
|
+
dataset_labels: [...context.dataset_labels, toRepeatableLabel(element)],
|
|
589
|
+
repeatable_output_path: [...context.repeatable_output_path, repeatableOutputKey],
|
|
590
|
+
parent_dataset_id: context.parent_dataset_id,
|
|
591
|
+
repeatable_field_id: repeatableFieldId,
|
|
592
|
+
repeatable_output_key: repeatableOutputKey,
|
|
593
|
+
root_dataset_id: ROOT_DATASET_ID,
|
|
594
|
+
fields: [],
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
collectFieldsForDataset(element.elements, descriptor);
|
|
598
|
+
datasets.push(descriptor);
|
|
599
|
+
|
|
600
|
+
collectRepeatableDatasets(
|
|
601
|
+
element.elements,
|
|
602
|
+
{
|
|
603
|
+
dataset_path: descriptor.dataset_path,
|
|
604
|
+
dataset_labels: descriptor.dataset_labels,
|
|
605
|
+
repeatable_output_path: descriptor.repeatable_output_path,
|
|
606
|
+
parent_dataset_id: descriptor.id,
|
|
607
|
+
},
|
|
608
|
+
datasets,
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
export function buildDatasetDescriptors(schema) {
|
|
614
|
+
const formNode = toSchemaFormNode(schema);
|
|
615
|
+
if (!formNode) {
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const rootDataset = {
|
|
620
|
+
id: ROOT_DATASET_ID,
|
|
621
|
+
kind: 'root',
|
|
622
|
+
label: toTrimmedString(formNode.name) ?? 'Root form',
|
|
623
|
+
dataset_path: [],
|
|
624
|
+
dataset_labels: [],
|
|
625
|
+
repeatable_output_path: [],
|
|
626
|
+
parent_dataset_id: null,
|
|
627
|
+
repeatable_field_id: null,
|
|
628
|
+
repeatable_output_key: null,
|
|
629
|
+
root_dataset_id: ROOT_DATASET_ID,
|
|
630
|
+
fields: [],
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
collectFieldsForDataset(formNode.elements, rootDataset);
|
|
634
|
+
|
|
635
|
+
const datasets = [rootDataset];
|
|
636
|
+
collectRepeatableDatasets(
|
|
637
|
+
formNode.elements,
|
|
638
|
+
{
|
|
639
|
+
dataset_path: [],
|
|
640
|
+
dataset_labels: [],
|
|
641
|
+
repeatable_output_path: [],
|
|
642
|
+
parent_dataset_id: ROOT_DATASET_ID,
|
|
643
|
+
},
|
|
644
|
+
datasets,
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
return datasets;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const readFirstAliasValue = (value, aliases) => {
|
|
651
|
+
if (!isRecord(value)) {
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
for (const alias of aliases) {
|
|
656
|
+
if (Object.prototype.hasOwnProperty.call(value, alias)) {
|
|
657
|
+
return value[alias];
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return undefined;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const normalizeScalarValue = (field, value) => {
|
|
665
|
+
if (
|
|
666
|
+
field?.field_type === 'SingleChoiceField' ||
|
|
667
|
+
field?.field_type === 'BooleanField'
|
|
668
|
+
) {
|
|
669
|
+
return toSingleChoiceSelectionValues(value)[0];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (Array.isArray(value)) {
|
|
673
|
+
return undefined;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return value;
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const normalizeTermValues = (field, value) => {
|
|
680
|
+
if (field?.field_type === 'MultiChoiceField') {
|
|
681
|
+
return toMultiChoiceSelectionValues(value);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (!Array.isArray(value)) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return value.filter(
|
|
689
|
+
(entry) =>
|
|
690
|
+
typeof entry === 'string' ||
|
|
691
|
+
typeof entry === 'number' ||
|
|
692
|
+
typeof entry === 'boolean',
|
|
693
|
+
);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
export function projectDatasetRowValues(descriptor, rowValues) {
|
|
697
|
+
const sourceRecord = isRecord(rowValues) ? rowValues : {};
|
|
698
|
+
const source =
|
|
699
|
+
isRecord(sourceRecord.form_values) && !Array.isArray(sourceRecord.form_values)
|
|
700
|
+
? sourceRecord.form_values
|
|
701
|
+
: sourceRecord;
|
|
702
|
+
const datasetFields = Array.isArray(descriptor?.fields) ? descriptor.fields : [];
|
|
703
|
+
|
|
704
|
+
const displayValues = {};
|
|
705
|
+
const scalarValues = {};
|
|
706
|
+
const termValues = {};
|
|
707
|
+
|
|
708
|
+
datasetFields.forEach((field) => {
|
|
709
|
+
const rawValue = readFirstAliasValue(source, field.aliases ?? []);
|
|
710
|
+
if (typeof rawValue === 'undefined') {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
displayValues[field.field_id] = toDisplayValue(field, rawValue);
|
|
715
|
+
|
|
716
|
+
if (field.query_kind === 'scalar') {
|
|
717
|
+
const scalarValue = normalizeScalarValue(field, rawValue);
|
|
718
|
+
if (typeof scalarValue !== 'undefined') {
|
|
719
|
+
scalarValues[field.field_id] = scalarValue;
|
|
720
|
+
}
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (field.query_kind === 'terms') {
|
|
725
|
+
const terms = normalizeTermValues(field, rawValue);
|
|
726
|
+
if (terms.length > 0) {
|
|
727
|
+
termValues[field.field_id] = terms;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
return {
|
|
733
|
+
displayValues,
|
|
734
|
+
scalarValues,
|
|
735
|
+
termValues,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function resolveDatasetDescriptorById(schema, datasetId) {
|
|
740
|
+
return buildDatasetDescriptors(schema).find((dataset) => dataset.id === datasetId) ?? null;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function buildFieldIdentityMap(schema) {
|
|
744
|
+
const formNode = toSchemaFormNode(schema);
|
|
745
|
+
if (!formNode) {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const fields = flattenFields(formNode.elements);
|
|
750
|
+
const map = {};
|
|
751
|
+
fields.forEach((field) => {
|
|
752
|
+
if (!isRecord(field)) {
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const fieldId = toFieldId(field);
|
|
757
|
+
const outputKey = toOutputKey(field);
|
|
758
|
+
if (!fieldId || !outputKey) {
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
map[fieldId] = {
|
|
763
|
+
field_id: fieldId,
|
|
764
|
+
output_key: outputKey,
|
|
765
|
+
key: toTrimmedString(field.key),
|
|
766
|
+
data_name: toTrimmedString(field.data_name),
|
|
767
|
+
label: toFieldLabel(field),
|
|
768
|
+
field_type: toTrimmedString(field.type) ?? 'UnknownField',
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
return map;
|
|
773
|
+
}
|