form0-core 0.1.15 → 0.1.17
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/README.md +23 -0
- package/package.json +2 -3
- package/scripts/{postinstall.cjs → legacy-postinstall-message.cjs} +4 -0
- package/src/index.js +6 -0
- package/src/schema/field-specs.js +13 -0
- package/src/schema/field-value-registry.js +42 -0
- package/src/utilities/choice-value-shapes.js +318 -0
- package/src/utilities/dataset-descriptors.js +27 -185
- package/src/utilities/structured-record.js +22 -103
package/README.md
CHANGED
|
@@ -40,6 +40,29 @@ scope fields like `main_org_id`, and media or location settings. Operational cou
|
|
|
40
40
|
application or API boundaries, not treated as engine-authored schema. A top-level
|
|
41
41
|
`form.version` may still exist, but the engine does not bump or consume it.
|
|
42
42
|
|
|
43
|
+
### Record-side contract
|
|
44
|
+
|
|
45
|
+
`form0-core` intentionally uses two different choice-value shapes:
|
|
46
|
+
|
|
47
|
+
- Live engine / renderer values use renderer shape:
|
|
48
|
+
- single / boolean: `{ choice, other }`
|
|
49
|
+
- multi: `{ choices, other }`
|
|
50
|
+
- Structured records use canonical stored shape:
|
|
51
|
+
- single / boolean: `{ choice_value, other_value }`
|
|
52
|
+
- multi: `{ choices_value, other_value }`
|
|
53
|
+
|
|
54
|
+
Record-side utilities follow this contract:
|
|
55
|
+
|
|
56
|
+
- `createStructuredRecord()` outputs canonical stored records
|
|
57
|
+
- `normalizeStructuredRecord()` consumes and returns canonical stored records
|
|
58
|
+
- `buildFormRecordSnapshot()` consumes canonical stored records and returns renderer snapshot values
|
|
59
|
+
- `projectDatasetRowValues()` consumes canonical stored rows
|
|
60
|
+
- Choice `FIELD_SPECS` keep separate validators by context:
|
|
61
|
+
- `valueValidator` validates live engine / renderer values
|
|
62
|
+
- `recordValueValidator` validates canonical stored record values
|
|
63
|
+
|
|
64
|
+
Record status remains top-level as `@status`; it is not stored inside `form_values`.
|
|
65
|
+
|
|
43
66
|
## Security
|
|
44
67
|
|
|
45
68
|
See `SECURITY.md` for security modes and configuration.
|
package/package.json
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
7
|
-
"description": "Schema-driven engine that powers form0 ecosystem.",
|
|
7
|
+
"description": "Schema-driven engine that powers form0 ecosystem. Looking for the full form0 ecosystem? Install the CLI: `npm install -g form0-cli`",
|
|
8
8
|
"main": "src/index.js",
|
|
9
9
|
"type": "module",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node tests/form0-core.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.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
|
-
"postinstall": "node scripts/postinstall.cjs",
|
|
13
12
|
"format": "prettier --write \"**/*.{js,jsx,json,ts,tsx,md}\"",
|
|
14
13
|
"format:check": "prettier --check \"**/*.{js,jsx,json,ts,tsx,md}\""
|
|
15
14
|
},
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Legacy reference only.
|
|
2
|
+
// This script is intentionally not wired into package.json lifecycle hooks.
|
|
3
|
+
// It is retained as a reference for the previous install-time guidance message.
|
|
4
|
+
|
|
1
5
|
const PACKAGE_NAME = 'form0-core';
|
|
2
6
|
|
|
3
7
|
const npmUserAgent = process.env.npm_config_user_agent || '';
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,12 @@ export {
|
|
|
6
6
|
buildFormRecordSnapshot,
|
|
7
7
|
} from './utilities/structured-record.js';
|
|
8
8
|
export { validateSchema } from './schema/schema-validator.js';
|
|
9
|
+
export {
|
|
10
|
+
validateFieldValue,
|
|
11
|
+
validateRecordFieldValue,
|
|
12
|
+
getFieldValueValidator,
|
|
13
|
+
getRecordFieldValueValidator,
|
|
14
|
+
} from './schema/field-value-registry.js';
|
|
9
15
|
//export { validateSchema, resolveSupportingImagePath } from './schema/schema-validator.js';
|
|
10
16
|
export { generateKey } from './utilities/hash.js';
|
|
11
17
|
export {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { validateChoiceFieldChoices } from '../utilities/field-helpers.js';
|
|
2
|
+
import { validateStoredChoiceValue } from '../utilities/choice-value-shapes.js';
|
|
2
3
|
import {
|
|
3
4
|
isNonEmptyString,
|
|
4
5
|
validateFormLinkRecordDefaults,
|
|
@@ -316,6 +317,10 @@ export const FIELD_SPECS = {
|
|
|
316
317
|
}
|
|
317
318
|
return null;
|
|
318
319
|
},
|
|
320
|
+
recordValueValidator: (field, value) =>
|
|
321
|
+
validateStoredChoiceValue(field, value, {
|
|
322
|
+
context: 'FIELD_SPECS.recordValueValidator',
|
|
323
|
+
}),
|
|
319
324
|
defaultProducer: (field) => {
|
|
320
325
|
if (field.default_value) {
|
|
321
326
|
const choice = field.choices.find((c) => c.value === field.default_value);
|
|
@@ -466,6 +471,10 @@ export const FIELD_SPECS = {
|
|
|
466
471
|
}
|
|
467
472
|
return null;
|
|
468
473
|
},
|
|
474
|
+
recordValueValidator: (field, value) =>
|
|
475
|
+
validateStoredChoiceValue(field, value, {
|
|
476
|
+
context: 'FIELD_SPECS.recordValueValidator',
|
|
477
|
+
}),
|
|
469
478
|
defaultProducer: (field) => {
|
|
470
479
|
if (field.default_value) {
|
|
471
480
|
const selectedChoices = field.choices
|
|
@@ -596,6 +605,10 @@ export const FIELD_SPECS = {
|
|
|
596
605
|
}
|
|
597
606
|
return null;
|
|
598
607
|
},
|
|
608
|
+
recordValueValidator: (field, value) =>
|
|
609
|
+
validateStoredChoiceValue(field, value, {
|
|
610
|
+
context: 'FIELD_SPECS.recordValueValidator',
|
|
611
|
+
}),
|
|
599
612
|
defaultProducer: (field) => {
|
|
600
613
|
if (field.default_value) {
|
|
601
614
|
const choice = field.choices.find((c) => c.value === field.default_value);
|
|
@@ -19,6 +19,29 @@ export function validateFieldValue(field, value) {
|
|
|
19
19
|
return null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Validates a structured-record field value against its specification
|
|
24
|
+
* @param {object} field - The field definition
|
|
25
|
+
* @param {any} value - The stored record value to validate
|
|
26
|
+
* @returns {string|null} - Error message if validation fails, or null if valid
|
|
27
|
+
*/
|
|
28
|
+
export function validateRecordFieldValue(field, value) {
|
|
29
|
+
const spec = FIELD_SPECS[field.type];
|
|
30
|
+
if (!spec) {
|
|
31
|
+
return `Unsupported field type: ${field.type}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof spec.recordValueValidator === 'function') {
|
|
35
|
+
return spec.recordValueValidator(field, value);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof spec.valueValidator === 'function') {
|
|
39
|
+
return spec.valueValidator(field, value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
22
45
|
/**
|
|
23
46
|
* Returns the value validator function for a field type
|
|
24
47
|
* @param {string} fieldType - The field type
|
|
@@ -29,6 +52,25 @@ export function getFieldValueValidator(fieldType) {
|
|
|
29
52
|
return spec ? spec.valueValidator : null;
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Returns the structured-record value validator function for a field type
|
|
57
|
+
* Falls back to the live value validator when no record-specific validator exists.
|
|
58
|
+
* @param {string} fieldType - The field type
|
|
59
|
+
* @returns {function|null} - The record value validator function or null if not found
|
|
60
|
+
*/
|
|
61
|
+
export function getRecordFieldValueValidator(fieldType) {
|
|
62
|
+
const spec = FIELD_SPECS[fieldType];
|
|
63
|
+
if (!spec) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof spec.recordValueValidator === 'function') {
|
|
68
|
+
return spec.recordValueValidator;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return typeof spec.valueValidator === 'function' ? spec.valueValidator : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
/**
|
|
33
75
|
* Validates TextField value
|
|
34
76
|
* @param {object} field - The field definition
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
const SINGLE_CHOICE_TYPES = new Set(['SingleChoiceField', 'BooleanField']);
|
|
2
|
+
const MULTI_CHOICE_TYPES = new Set(['MultiChoiceField']);
|
|
3
|
+
const RENDERER_ALIAS_KEYS = ['choice', 'choices', 'other'];
|
|
4
|
+
|
|
5
|
+
const isRecordObject = (value) =>
|
|
6
|
+
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
7
|
+
|
|
8
|
+
const cloneJson = (value) => {
|
|
9
|
+
if (typeof structuredClone === 'function') {
|
|
10
|
+
return structuredClone(value);
|
|
11
|
+
}
|
|
12
|
+
return JSON.parse(JSON.stringify(value));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const trimString = (value) => {
|
|
16
|
+
if (typeof value !== 'string') {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const trimmed = value.trim();
|
|
21
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const isChoicePrimitive = (value) =>
|
|
25
|
+
typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
26
|
+
|
|
27
|
+
const normalizeChoiceLabel = (value) => {
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return isChoicePrimitive(value) ? value : null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const toFieldType = (field) => trimString(field?.type) ?? trimString(field?.field_type);
|
|
37
|
+
|
|
38
|
+
const toFieldIdentifier = (field) =>
|
|
39
|
+
trimString(field?.data_name) ??
|
|
40
|
+
trimString(field?.output_key) ??
|
|
41
|
+
trimString(field?.key) ??
|
|
42
|
+
trimString(field?.field_id) ??
|
|
43
|
+
trimString(field?.label) ??
|
|
44
|
+
'choice field';
|
|
45
|
+
|
|
46
|
+
const getChoiceKind = (field) => {
|
|
47
|
+
const fieldType = toFieldType(field);
|
|
48
|
+
if (SINGLE_CHOICE_TYPES.has(fieldType)) {
|
|
49
|
+
return 'single';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (MULTI_CHOICE_TYPES.has(fieldType)) {
|
|
53
|
+
return 'multi';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const getCanonicalSelectionKey = (field) =>
|
|
60
|
+
getChoiceKind(field) === 'multi' ? 'choices_value' : 'choice_value';
|
|
61
|
+
|
|
62
|
+
const createChoiceShapeError = (context, field, message) => {
|
|
63
|
+
const prefix = trimString(context) ?? 'record-side choice utilities';
|
|
64
|
+
return new Error(`[form0] ${prefix}: field "${toFieldIdentifier(field)}" ${message}`);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const assertCanonicalChoiceShape = (field, rawValue, context) => {
|
|
68
|
+
if (rawValue === null || typeof rawValue === 'undefined') {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!isRecordObject(rawValue)) {
|
|
73
|
+
throw createChoiceShapeError(
|
|
74
|
+
context,
|
|
75
|
+
field,
|
|
76
|
+
'must be a canonical stored object using *_value arrays'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const rendererKeys = RENDERER_ALIAS_KEYS.filter((key) =>
|
|
81
|
+
Object.prototype.hasOwnProperty.call(rawValue, key)
|
|
82
|
+
);
|
|
83
|
+
if (rendererKeys.length > 0) {
|
|
84
|
+
throw createChoiceShapeError(
|
|
85
|
+
context,
|
|
86
|
+
field,
|
|
87
|
+
`must not use renderer keys (${rendererKeys.join(', ')})`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const kind = getChoiceKind(field);
|
|
92
|
+
if (kind === 'single' && Object.prototype.hasOwnProperty.call(rawValue, 'choices_value')) {
|
|
93
|
+
throw createChoiceShapeError(
|
|
94
|
+
context,
|
|
95
|
+
field,
|
|
96
|
+
'must use "choice_value" instead of "choices_value"'
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (kind === 'multi' && Object.prototype.hasOwnProperty.call(rawValue, 'choice_value')) {
|
|
101
|
+
throw createChoiceShapeError(
|
|
102
|
+
context,
|
|
103
|
+
field,
|
|
104
|
+
'must use "choices_value" instead of "choice_value"'
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const resolveSchemaChoice = (field, rawValue) => {
|
|
110
|
+
if (!Array.isArray(field?.choices)) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
field.choices.find(
|
|
116
|
+
(choice) =>
|
|
117
|
+
isRecordObject(choice) &&
|
|
118
|
+
Object.prototype.hasOwnProperty.call(choice, 'value') &&
|
|
119
|
+
choice.value === rawValue
|
|
120
|
+
) ?? null
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const normalizeStoredSelectedEntry = (field, entry, context) => {
|
|
125
|
+
if (!isRecordObject(entry)) {
|
|
126
|
+
throw createChoiceShapeError(context, field, 'must store selected entries as objects');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!Object.prototype.hasOwnProperty.call(entry, 'value')) {
|
|
130
|
+
throw createChoiceShapeError(context, field, 'must store selected entries with a "value"');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const matchedChoice = resolveSchemaChoice(field, entry.value);
|
|
134
|
+
const resolvedLabel = matchedChoice
|
|
135
|
+
? (normalizeChoiceLabel(matchedChoice.label) ?? entry.value)
|
|
136
|
+
: (normalizeChoiceLabel(entry.label) ?? entry.value);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
...entry,
|
|
140
|
+
value: entry.value,
|
|
141
|
+
label: resolvedLabel,
|
|
142
|
+
};
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const normalizeStoredOtherEntry = (field, entry, context) => {
|
|
146
|
+
if (!isRecordObject(entry)) {
|
|
147
|
+
throw createChoiceShapeError(context, field, 'must store other entries as objects');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hasPrimitiveValue = isChoicePrimitive(entry.value);
|
|
151
|
+
const normalizedLabel = normalizeChoiceLabel(entry.label);
|
|
152
|
+
|
|
153
|
+
if (!hasPrimitiveValue && normalizedLabel === null) {
|
|
154
|
+
throw createChoiceShapeError(
|
|
155
|
+
context,
|
|
156
|
+
field,
|
|
157
|
+
'must store other entries with a primitive "label" or "value"'
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...entry,
|
|
163
|
+
...(hasPrimitiveValue ? { value: entry.value } : {}),
|
|
164
|
+
label: normalizedLabel ?? entry.value,
|
|
165
|
+
};
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const normalizeChoiceEntryArray = (field, entries, normalizer, context, keyName) => {
|
|
169
|
+
if (typeof entries === 'undefined') {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!Array.isArray(entries)) {
|
|
174
|
+
throw createChoiceShapeError(context, field, `"${keyName}" must be an array`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return entries.map((entry) => normalizer(field, entry, context));
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export function isChoiceFieldLike(field) {
|
|
181
|
+
return getChoiceKind(field) !== null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function normalizeStoredChoiceValue(field, rawValue, options = {}) {
|
|
185
|
+
const context = options.context;
|
|
186
|
+
const kind = getChoiceKind(field);
|
|
187
|
+
|
|
188
|
+
if (!kind || rawValue === null || typeof rawValue === 'undefined') {
|
|
189
|
+
return rawValue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
assertCanonicalChoiceShape(field, rawValue, context);
|
|
193
|
+
|
|
194
|
+
const selectionKey = getCanonicalSelectionKey(field);
|
|
195
|
+
const normalized = {
|
|
196
|
+
...rawValue,
|
|
197
|
+
[selectionKey]: normalizeChoiceEntryArray(
|
|
198
|
+
field,
|
|
199
|
+
rawValue[selectionKey],
|
|
200
|
+
normalizeStoredSelectedEntry,
|
|
201
|
+
context,
|
|
202
|
+
selectionKey
|
|
203
|
+
),
|
|
204
|
+
other_value: normalizeChoiceEntryArray(
|
|
205
|
+
field,
|
|
206
|
+
rawValue.other_value,
|
|
207
|
+
normalizeStoredOtherEntry,
|
|
208
|
+
context,
|
|
209
|
+
'other_value'
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
return normalized;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function validateStoredChoiceValue(field, rawValue, options = {}) {
|
|
217
|
+
try {
|
|
218
|
+
const normalized = normalizeStoredChoiceValue(field, rawValue, options);
|
|
219
|
+
const kind = getChoiceKind(field);
|
|
220
|
+
const fieldIdentifier = toFieldIdentifier(field);
|
|
221
|
+
|
|
222
|
+
if (normalized && kind === 'single' && toFieldType(field) === 'BooleanField') {
|
|
223
|
+
if (normalized.other_value.length > 0) {
|
|
224
|
+
return `${fieldIdentifier} does not support 'other_value' values`;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (normalized && kind !== null && toFieldType(field) !== 'BooleanField') {
|
|
229
|
+
if (field.allow_other !== true && normalized.other_value.length > 0) {
|
|
230
|
+
return `${fieldIdentifier} does not allow 'other_value' values`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return null;
|
|
235
|
+
} catch (error) {
|
|
236
|
+
if (error instanceof Error) {
|
|
237
|
+
return error.message.replace(/^\[form0\] [^:]+: /, '');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return 'stored choice value is invalid';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function toRendererChoiceValue(field, rawValue, options = {}) {
|
|
245
|
+
const kind = getChoiceKind(field);
|
|
246
|
+
if (!kind || rawValue === null || typeof rawValue === 'undefined') {
|
|
247
|
+
return rawValue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const normalized = normalizeStoredChoiceValue(field, rawValue, options);
|
|
251
|
+
const selectionKey = getCanonicalSelectionKey(field);
|
|
252
|
+
|
|
253
|
+
if (kind === 'multi') {
|
|
254
|
+
return {
|
|
255
|
+
choices: cloneJson(normalized[selectionKey]),
|
|
256
|
+
other: cloneJson(normalized.other_value),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
choice: cloneJson(normalized[selectionKey]),
|
|
262
|
+
other: cloneJson(normalized.other_value),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function readStoredChoiceSelectionValues(field, rawValue, options = {}) {
|
|
267
|
+
const kind = getChoiceKind(field);
|
|
268
|
+
if (!kind) {
|
|
269
|
+
return [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const normalized = normalizeStoredChoiceValue(field, rawValue, options);
|
|
273
|
+
if (normalized === null || typeof normalized === 'undefined') {
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const selectionKey = getCanonicalSelectionKey(field);
|
|
278
|
+
const selectedValues = normalized[selectionKey]
|
|
279
|
+
.map((entry) => entry.value)
|
|
280
|
+
.filter((entry) => typeof entry !== 'undefined');
|
|
281
|
+
const otherValues = normalized.other_value
|
|
282
|
+
.map((entry) =>
|
|
283
|
+
isChoicePrimitive(entry.value) ? entry.value : normalizeChoiceLabel(entry.label)
|
|
284
|
+
)
|
|
285
|
+
.filter((entry) => typeof entry !== 'undefined' && entry !== null);
|
|
286
|
+
|
|
287
|
+
return [...selectedValues, ...otherValues];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function readStoredChoiceDisplayValues(field, rawValue, options = {}) {
|
|
291
|
+
const kind = getChoiceKind(field);
|
|
292
|
+
if (!kind) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const normalized = normalizeStoredChoiceValue(field, rawValue, options);
|
|
297
|
+
if (normalized === null || typeof normalized === 'undefined') {
|
|
298
|
+
return [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const selectionKey = getCanonicalSelectionKey(field);
|
|
302
|
+
const selectedLabels = normalized[selectionKey]
|
|
303
|
+
.map(
|
|
304
|
+
(entry) =>
|
|
305
|
+
normalizeChoiceLabel(entry.label) ??
|
|
306
|
+
(isChoicePrimitive(entry.value) ? entry.value : undefined)
|
|
307
|
+
)
|
|
308
|
+
.filter((entry) => typeof entry !== 'undefined' && entry !== null);
|
|
309
|
+
const otherLabels = normalized.other_value
|
|
310
|
+
.map(
|
|
311
|
+
(entry) =>
|
|
312
|
+
normalizeChoiceLabel(entry.label) ??
|
|
313
|
+
(isChoicePrimitive(entry.value) ? entry.value : undefined)
|
|
314
|
+
)
|
|
315
|
+
.filter((entry) => typeof entry !== 'undefined' && entry !== null);
|
|
316
|
+
|
|
317
|
+
return [...selectedLabels, ...otherLabels];
|
|
318
|
+
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { flattenFields } from './field-helpers.js';
|
|
2
|
+
import {
|
|
3
|
+
readStoredChoiceDisplayValues,
|
|
4
|
+
readStoredChoiceSelectionValues,
|
|
5
|
+
} from './choice-value-shapes.js';
|
|
2
6
|
|
|
3
7
|
const ROOT_DATASET_ID = '__root__';
|
|
4
8
|
const CONTAINER_TYPES = new Set(['Section', 'BuildingPlanSection']);
|
|
5
9
|
|
|
6
|
-
const isRecord = (value) =>
|
|
7
|
-
Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
10
|
+
const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
11
|
|
|
9
12
|
const toTrimmedString = (value) => {
|
|
10
13
|
if (typeof value !== 'string') {
|
|
@@ -57,11 +60,9 @@ const uniqueAliases = (...values) => {
|
|
|
57
60
|
return aliases;
|
|
58
61
|
};
|
|
59
62
|
|
|
60
|
-
const toFieldId = (field) =>
|
|
61
|
-
toTrimmedString(field?.key) ?? toTrimmedString(field?.data_name);
|
|
63
|
+
const toFieldId = (field) => toTrimmedString(field?.key) ?? toTrimmedString(field?.data_name);
|
|
62
64
|
|
|
63
|
-
const toOutputKey = (field) =>
|
|
64
|
-
toTrimmedString(field?.data_name) ?? toTrimmedString(field?.key);
|
|
65
|
+
const toOutputKey = (field) => toTrimmedString(field?.data_name) ?? toTrimmedString(field?.key);
|
|
65
66
|
|
|
66
67
|
const toFieldLabel = (field) =>
|
|
67
68
|
toTrimmedString(field?.label) ??
|
|
@@ -343,173 +344,10 @@ export function getFieldQuerySemantics(field) {
|
|
|
343
344
|
return { ...semantics };
|
|
344
345
|
}
|
|
345
346
|
|
|
346
|
-
const toChoiceValueList = (field) => {
|
|
347
|
-
if (!Array.isArray(field?.choices)) {
|
|
348
|
-
return [];
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return field.choices.filter((choice) => isRecord(choice));
|
|
352
|
-
};
|
|
353
|
-
|
|
354
|
-
const isChoicePrimitive = (value) =>
|
|
355
|
-
typeof value === 'string' ||
|
|
356
|
-
typeof value === 'number' ||
|
|
357
|
-
typeof value === 'boolean';
|
|
358
|
-
|
|
359
|
-
const normalizeChoiceEntryValue = (entry) => {
|
|
360
|
-
if (isChoicePrimitive(entry)) {
|
|
361
|
-
return entry;
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (!isRecord(entry)) {
|
|
365
|
-
return undefined;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (isChoicePrimitive(entry.value)) {
|
|
369
|
-
return entry.value;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (isChoicePrimitive(entry.label)) {
|
|
373
|
-
return entry.label;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
return undefined;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const normalizeChoiceEntryDisplayValue = (entry) => {
|
|
380
|
-
if (isChoicePrimitive(entry)) {
|
|
381
|
-
return entry;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (!isRecord(entry)) {
|
|
385
|
-
return undefined;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (isChoicePrimitive(entry.label)) {
|
|
389
|
-
return entry.label;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (isChoicePrimitive(entry.value)) {
|
|
393
|
-
return entry.value;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
return undefined;
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const normalizeChoiceEntryLabel = (field, entry) => {
|
|
400
|
-
const normalizedValue = normalizeChoiceEntryValue(entry);
|
|
401
|
-
if (typeof normalizedValue === 'undefined') {
|
|
402
|
-
return undefined;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const matched = toChoiceValueList(field).find(
|
|
406
|
-
(choice) =>
|
|
407
|
-
Object.prototype.hasOwnProperty.call(choice, 'value') &&
|
|
408
|
-
choice.value === normalizedValue,
|
|
409
|
-
);
|
|
410
|
-
if (matched) {
|
|
411
|
-
return toTrimmedString(matched.label) ?? normalizedValue;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (isRecord(entry) && isChoicePrimitive(entry.label)) {
|
|
415
|
-
return entry.label;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return normalizedValue;
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
const toSingleChoiceSelectionValues = (rawValue) => {
|
|
422
|
-
if (isRecord(rawValue)) {
|
|
423
|
-
const choiceValues = Array.isArray(rawValue.choice)
|
|
424
|
-
? rawValue.choice
|
|
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 [...choiceValues, ...otherValues];
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const normalized = normalizeChoiceEntryValue(rawValue);
|
|
438
|
-
return typeof normalized === 'undefined' ? [] : [normalized];
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
const toSingleChoiceDisplayValues = (field, rawValue) => {
|
|
442
|
-
if (isRecord(rawValue)) {
|
|
443
|
-
const choiceLabels = Array.isArray(rawValue.choice)
|
|
444
|
-
? rawValue.choice
|
|
445
|
-
.map((entry) => normalizeChoiceEntryLabel(field, entry))
|
|
446
|
-
.filter((entry) => typeof entry !== 'undefined')
|
|
447
|
-
: [];
|
|
448
|
-
const otherLabels = Array.isArray(rawValue.other)
|
|
449
|
-
? rawValue.other
|
|
450
|
-
.map((entry) => normalizeChoiceEntryDisplayValue(entry))
|
|
451
|
-
.filter((entry) => typeof entry !== 'undefined')
|
|
452
|
-
: [];
|
|
453
|
-
|
|
454
|
-
return [...choiceLabels, ...otherLabels];
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const normalized = normalizeChoiceEntryLabel(field, rawValue);
|
|
458
|
-
return typeof normalized === 'undefined' ? [] : [normalized];
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
const toMultiChoiceSelectionValues = (rawValue) => {
|
|
462
|
-
if (isRecord(rawValue)) {
|
|
463
|
-
const selectedValues = Array.isArray(rawValue.choices)
|
|
464
|
-
? rawValue.choices
|
|
465
|
-
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
466
|
-
.filter((entry) => typeof entry !== 'undefined')
|
|
467
|
-
: [];
|
|
468
|
-
const otherValues = Array.isArray(rawValue.other)
|
|
469
|
-
? rawValue.other
|
|
470
|
-
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
471
|
-
.filter((entry) => typeof entry !== 'undefined')
|
|
472
|
-
: [];
|
|
473
|
-
|
|
474
|
-
return [...selectedValues, ...otherValues];
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
if (Array.isArray(rawValue)) {
|
|
478
|
-
return rawValue
|
|
479
|
-
.map((entry) => normalizeChoiceEntryValue(entry))
|
|
480
|
-
.filter((entry) => typeof entry !== 'undefined');
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
return [];
|
|
484
|
-
};
|
|
485
|
-
|
|
486
|
-
const toMultiChoiceDisplayValues = (field, rawValue) => {
|
|
487
|
-
if (isRecord(rawValue)) {
|
|
488
|
-
const selectedLabels = Array.isArray(rawValue.choices)
|
|
489
|
-
? rawValue.choices
|
|
490
|
-
.map((entry) => normalizeChoiceEntryLabel(field, entry))
|
|
491
|
-
.filter((entry) => typeof entry !== 'undefined')
|
|
492
|
-
: [];
|
|
493
|
-
const otherLabels = Array.isArray(rawValue.other)
|
|
494
|
-
? rawValue.other
|
|
495
|
-
.map((entry) => normalizeChoiceEntryDisplayValue(entry))
|
|
496
|
-
.filter((entry) => typeof entry !== 'undefined')
|
|
497
|
-
: [];
|
|
498
|
-
|
|
499
|
-
return [...selectedLabels, ...otherLabels];
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (Array.isArray(rawValue)) {
|
|
503
|
-
return rawValue
|
|
504
|
-
.map((entry) => normalizeChoiceEntryLabel(field, entry))
|
|
505
|
-
.filter((entry) => typeof entry !== 'undefined');
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
return [];
|
|
509
|
-
};
|
|
510
|
-
|
|
511
347
|
const toSingleChoiceLabel = (field, rawValue) => {
|
|
512
|
-
const labels =
|
|
348
|
+
const labels = readStoredChoiceDisplayValues(field, rawValue, {
|
|
349
|
+
context: 'projectDatasetRowValues',
|
|
350
|
+
});
|
|
513
351
|
if (labels.length === 0) {
|
|
514
352
|
return rawValue;
|
|
515
353
|
}
|
|
@@ -518,7 +356,9 @@ const toSingleChoiceLabel = (field, rawValue) => {
|
|
|
518
356
|
};
|
|
519
357
|
|
|
520
358
|
const toMultiChoiceLabels = (field, rawValue) => {
|
|
521
|
-
const labels =
|
|
359
|
+
const labels = readStoredChoiceDisplayValues(field, rawValue, {
|
|
360
|
+
context: 'projectDatasetRowValues',
|
|
361
|
+
});
|
|
522
362
|
if (labels.length === 0) {
|
|
523
363
|
return rawValue;
|
|
524
364
|
}
|
|
@@ -645,7 +485,7 @@ const collectRepeatableDatasets = (elements, context, datasets) => {
|
|
|
645
485
|
repeatable_output_path: descriptor.repeatable_output_path,
|
|
646
486
|
parent_dataset_id: descriptor.id,
|
|
647
487
|
},
|
|
648
|
-
datasets
|
|
488
|
+
datasets
|
|
649
489
|
);
|
|
650
490
|
});
|
|
651
491
|
};
|
|
@@ -681,7 +521,7 @@ export function buildDatasetDescriptors(schema) {
|
|
|
681
521
|
repeatable_output_path: [],
|
|
682
522
|
parent_dataset_id: ROOT_DATASET_ID,
|
|
683
523
|
},
|
|
684
|
-
datasets
|
|
524
|
+
datasets
|
|
685
525
|
);
|
|
686
526
|
|
|
687
527
|
return datasets;
|
|
@@ -702,11 +542,10 @@ const readFirstAliasValue = (value, aliases) => {
|
|
|
702
542
|
};
|
|
703
543
|
|
|
704
544
|
const normalizeScalarValue = (field, value) => {
|
|
705
|
-
if (
|
|
706
|
-
field
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
return toSingleChoiceSelectionValues(value)[0];
|
|
545
|
+
if (field?.field_type === 'SingleChoiceField' || field?.field_type === 'BooleanField') {
|
|
546
|
+
return readStoredChoiceSelectionValues(field, value, {
|
|
547
|
+
context: 'projectDatasetRowValues',
|
|
548
|
+
})[0];
|
|
710
549
|
}
|
|
711
550
|
|
|
712
551
|
if (Array.isArray(value)) {
|
|
@@ -725,7 +564,9 @@ const normalizeScalarValue = (field, value) => {
|
|
|
725
564
|
|
|
726
565
|
const normalizeTermValues = (field, value) => {
|
|
727
566
|
if (field?.field_type === 'MultiChoiceField') {
|
|
728
|
-
return
|
|
567
|
+
return readStoredChoiceSelectionValues(field, value, {
|
|
568
|
+
context: 'projectDatasetRowValues',
|
|
569
|
+
});
|
|
729
570
|
}
|
|
730
571
|
|
|
731
572
|
if (!Array.isArray(value)) {
|
|
@@ -733,13 +574,14 @@ const normalizeTermValues = (field, value) => {
|
|
|
733
574
|
}
|
|
734
575
|
|
|
735
576
|
return value.filter(
|
|
736
|
-
(entry) =>
|
|
737
|
-
typeof entry === 'string' ||
|
|
738
|
-
typeof entry === 'number' ||
|
|
739
|
-
typeof entry === 'boolean',
|
|
577
|
+
(entry) => typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean'
|
|
740
578
|
);
|
|
741
579
|
};
|
|
742
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Project a canonical stored row into dataset display/scalar/term values.
|
|
583
|
+
* Choice fields must already use the stored `*_value` record shape.
|
|
584
|
+
*/
|
|
743
585
|
export function projectDatasetRowValues(descriptor, rowValues) {
|
|
744
586
|
const sourceRecord = isRecord(rowValues) ? rowValues : {};
|
|
745
587
|
const source =
|
|
@@ -4,6 +4,11 @@ import {
|
|
|
4
4
|
buildFieldIdentityMap,
|
|
5
5
|
projectDatasetRowValues,
|
|
6
6
|
} from './dataset-descriptors.js';
|
|
7
|
+
import {
|
|
8
|
+
isChoiceFieldLike,
|
|
9
|
+
normalizeStoredChoiceValue,
|
|
10
|
+
toRendererChoiceValue,
|
|
11
|
+
} from './choice-value-shapes.js';
|
|
7
12
|
|
|
8
13
|
const CONTAINER_TYPES = new Set(['Section', 'BuildingPlanSection']);
|
|
9
14
|
const TIMESTAMP_KEYS = [
|
|
@@ -131,102 +136,6 @@ const buildScopeFromElements = (elements) => {
|
|
|
131
136
|
return scope;
|
|
132
137
|
};
|
|
133
138
|
|
|
134
|
-
const resolveChoiceLabel = (field, rawValue, fallbackLabel) => {
|
|
135
|
-
const normalizedValue = trimString(rawValue);
|
|
136
|
-
if (!normalizedValue) {
|
|
137
|
-
return fallbackLabel ?? null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (Array.isArray(field?.choices)) {
|
|
141
|
-
const matched = field.choices.find(
|
|
142
|
-
(choice) =>
|
|
143
|
-
isRecordObject(choice) &&
|
|
144
|
-
Object.prototype.hasOwnProperty.call(choice, 'value') &&
|
|
145
|
-
choice.value === normalizedValue
|
|
146
|
-
);
|
|
147
|
-
if (matched) {
|
|
148
|
-
return trimString(matched.label) ?? normalizedValue;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return fallbackLabel ?? normalizedValue;
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
const normalizeChoiceEntry = (field, entry) => {
|
|
156
|
-
if (!isRecordObject(entry)) {
|
|
157
|
-
return entry;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const rawValue = trimString(entry.value) ?? trimString(entry.label) ?? trimString(entry.id);
|
|
161
|
-
if (!rawValue) {
|
|
162
|
-
return { ...entry };
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
return {
|
|
166
|
-
...entry,
|
|
167
|
-
value: rawValue,
|
|
168
|
-
label: resolveChoiceLabel(field, rawValue, trimString(entry.label)),
|
|
169
|
-
};
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const normalizeChoiceList = (field, entries) => {
|
|
173
|
-
if (!Array.isArray(entries)) {
|
|
174
|
-
return entries;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return entries.map((entry) => normalizeChoiceEntry(field, entry));
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
const normalizeChoiceStructuredValue = (field, rawValue) => {
|
|
181
|
-
if (!isRecordObject(rawValue)) {
|
|
182
|
-
return rawValue;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const normalized = { ...rawValue };
|
|
186
|
-
|
|
187
|
-
if (field?.type === 'SingleChoiceField' || field?.type === 'BooleanField') {
|
|
188
|
-
if (Array.isArray(rawValue.choice_value)) {
|
|
189
|
-
normalized.choice_value = normalizeChoiceList(field, rawValue.choice_value);
|
|
190
|
-
}
|
|
191
|
-
if (Array.isArray(rawValue.choice)) {
|
|
192
|
-
normalized.choice = normalizeChoiceList(field, rawValue.choice);
|
|
193
|
-
}
|
|
194
|
-
if (Array.isArray(rawValue.other_value)) {
|
|
195
|
-
normalized.other_value = rawValue.other_value.map((entry) =>
|
|
196
|
-
isRecordObject(entry) ? { ...entry } : entry
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
if (Array.isArray(rawValue.other)) {
|
|
200
|
-
normalized.other = rawValue.other.map((entry) =>
|
|
201
|
-
isRecordObject(entry) ? { ...entry } : entry
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
return normalized;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (field?.type === 'MultiChoiceField') {
|
|
208
|
-
if (Array.isArray(rawValue.choices_value)) {
|
|
209
|
-
normalized.choices_value = normalizeChoiceList(field, rawValue.choices_value);
|
|
210
|
-
}
|
|
211
|
-
if (Array.isArray(rawValue.choices)) {
|
|
212
|
-
normalized.choices = normalizeChoiceList(field, rawValue.choices);
|
|
213
|
-
}
|
|
214
|
-
if (Array.isArray(rawValue.other_value)) {
|
|
215
|
-
normalized.other_value = rawValue.other_value.map((entry) =>
|
|
216
|
-
isRecordObject(entry) ? { ...entry } : entry
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
if (Array.isArray(rawValue.other)) {
|
|
220
|
-
normalized.other = rawValue.other.map((entry) =>
|
|
221
|
-
isRecordObject(entry) ? { ...entry } : entry
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
return normalized;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return normalized;
|
|
228
|
-
};
|
|
229
|
-
|
|
230
139
|
const buildFieldReferenceMap = (schema) => {
|
|
231
140
|
const identityMap = buildFieldIdentityMap(schema);
|
|
232
141
|
const references = new Map();
|
|
@@ -323,12 +232,10 @@ const normalizeRecordScope = (record, scope) => {
|
|
|
323
232
|
return;
|
|
324
233
|
}
|
|
325
234
|
|
|
326
|
-
if (
|
|
327
|
-
field.field
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
) {
|
|
331
|
-
formValues[entry.key] = normalizeChoiceStructuredValue(field.field, entry.value);
|
|
235
|
+
if (isChoiceFieldLike(field.field)) {
|
|
236
|
+
formValues[entry.key] = normalizeStoredChoiceValue(field.field, entry.value, {
|
|
237
|
+
context: 'normalizeStructuredRecord',
|
|
238
|
+
});
|
|
332
239
|
}
|
|
333
240
|
});
|
|
334
241
|
|
|
@@ -363,7 +270,11 @@ const buildSnapshotState = (formValues, scope) => {
|
|
|
363
270
|
return;
|
|
364
271
|
}
|
|
365
272
|
|
|
366
|
-
rawValues[field.dataName] =
|
|
273
|
+
rawValues[field.dataName] = isChoiceFieldLike(field.field)
|
|
274
|
+
? toRendererChoiceValue(field.field, entry.value, {
|
|
275
|
+
context: 'buildFormRecordSnapshot',
|
|
276
|
+
})
|
|
277
|
+
: cloneJson(entry.value);
|
|
367
278
|
});
|
|
368
279
|
|
|
369
280
|
scope.repeatables.forEach((repeatableField) => {
|
|
@@ -436,6 +347,10 @@ const extractSnapshotTimestamps = (record) => ({
|
|
|
436
347
|
updated_at_server: toNullableString(record?.updated_at_server) ?? null,
|
|
437
348
|
});
|
|
438
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Normalize a canonical stored record for downstream record-side consumers.
|
|
352
|
+
* Renderer/live aliases are rejected; known choice labels are refreshed from the supplied schema.
|
|
353
|
+
*/
|
|
439
354
|
export function normalizeStructuredRecord(schema, record, options = {}) {
|
|
440
355
|
const normalized = isRecordObject(record) ? cloneJson(record) : {};
|
|
441
356
|
const mode = trimString(options.mode) ?? 'derived';
|
|
@@ -456,6 +371,10 @@ export function normalizeStructuredRecord(schema, record, options = {}) {
|
|
|
456
371
|
return normalized;
|
|
457
372
|
}
|
|
458
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Build a renderer snapshot from a canonical stored record.
|
|
376
|
+
* Choice fields are converted back to renderer/live shape while record metadata stays top-level.
|
|
377
|
+
*/
|
|
459
378
|
export function buildFormRecordSnapshot(schema, record, options = {}) {
|
|
460
379
|
const normalizedRecord = normalizeStructuredRecord(schema, record, {
|
|
461
380
|
mode: 'derived',
|