form0-core 0.1.25 → 0.2.0
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 +111 -48
- package/SECURITY.md +38 -174
- package/package.json +27 -13
- package/src/builtins/registry.js +1 -4
- package/src/index.js +1 -0
- package/src/schema/ai-metadata-validator.js +1 -3
- package/src/schema/building-plan-expander.js +2 -1
- package/src/schema/field-specs.js +1 -0
- package/src/schema/schema-validator.js +78 -8
- package/src/utilities/ai-metadata.js +15 -3
- package/src/utilities/calculation-dependencies.js +1 -6
- package/src/utilities/calculation-expression-utils.js +11 -1
- package/src/utilities/dataset-descriptors.js +147 -0
- package/src/utilities/field-helpers.js +5 -1
- package/src/utilities/form-event-editor.js +2 -10
- package/src/utilities/form-link-helpers.js +16 -5
- package/src/utilities/record-transformer.js +49 -86
- package/src/utilities/repeatable-helpers.js +6 -5
- package/src/utilities/structured-record.js +80 -93
- package/src/utilities/uuid.js +2 -1
- package/scripts/legacy-postinstall-message.cjs +0 -42
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { createFormEngine } from '../engine/form-engine.js';
|
|
2
|
-
import {
|
|
3
|
-
buildDatasetDescriptors,
|
|
4
|
-
buildFieldIdentityMap,
|
|
5
|
-
projectDatasetRowValues,
|
|
6
|
-
} from './dataset-descriptors.js';
|
|
2
|
+
import { buildDatasetDescriptors, resolveDatasetRowTitle } from './dataset-descriptors.js';
|
|
7
3
|
import {
|
|
8
4
|
isChoiceFieldLike,
|
|
9
5
|
normalizeStoredChoiceValue,
|
|
@@ -81,10 +77,11 @@ const findFirstAliasEntry = (value, aliases) => {
|
|
|
81
77
|
return null;
|
|
82
78
|
};
|
|
83
79
|
|
|
84
|
-
const buildScopeFromElements = (elements) => {
|
|
80
|
+
const buildScopeFromElements = (elements, descriptorByRepeatable = new Map()) => {
|
|
85
81
|
const scope = {
|
|
86
82
|
valueFields: [],
|
|
87
83
|
repeatables: [],
|
|
84
|
+
datasetDescriptor: null,
|
|
88
85
|
};
|
|
89
86
|
|
|
90
87
|
if (!Array.isArray(elements)) {
|
|
@@ -98,7 +95,8 @@ const buildScopeFromElements = (elements) => {
|
|
|
98
95
|
|
|
99
96
|
if (CONTAINER_TYPES.has(element.type)) {
|
|
100
97
|
const childScope = buildScopeFromElements(
|
|
101
|
-
Array.isArray(element.elements) ? element.elements : []
|
|
98
|
+
Array.isArray(element.elements) ? element.elements : [],
|
|
99
|
+
descriptorByRepeatable
|
|
102
100
|
);
|
|
103
101
|
scope.valueFields.push(...childScope.valueFields);
|
|
104
102
|
scope.repeatables.push(...childScope.repeatables);
|
|
@@ -115,8 +113,15 @@ const buildScopeFromElements = (elements) => {
|
|
|
115
113
|
aliases: toAliasList(element),
|
|
116
114
|
outputKey,
|
|
117
115
|
repeatableKey: trimString(element.key) ?? trimString(element.data_name) ?? outputKey,
|
|
118
|
-
childScope: buildScopeFromElements(
|
|
116
|
+
childScope: buildScopeFromElements(
|
|
117
|
+
Array.isArray(element.elements) ? element.elements : [],
|
|
118
|
+
descriptorByRepeatable
|
|
119
|
+
),
|
|
119
120
|
});
|
|
121
|
+
scope.repeatables[scope.repeatables.length - 1].childScope.datasetDescriptor =
|
|
122
|
+
descriptorByRepeatable.get(trimString(element.key)) ||
|
|
123
|
+
descriptorByRepeatable.get(trimString(element.data_name)) ||
|
|
124
|
+
null;
|
|
120
125
|
continue;
|
|
121
126
|
}
|
|
122
127
|
|
|
@@ -136,86 +141,6 @@ const buildScopeFromElements = (elements) => {
|
|
|
136
141
|
return scope;
|
|
137
142
|
};
|
|
138
143
|
|
|
139
|
-
const buildFieldReferenceMap = (schema) => {
|
|
140
|
-
const identityMap = buildFieldIdentityMap(schema);
|
|
141
|
-
const references = new Map();
|
|
142
|
-
|
|
143
|
-
Object.values(identityMap).forEach((field) => {
|
|
144
|
-
if (!isRecordObject(field)) {
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
[field.field_id, field.output_key, field.key, field.data_name].forEach((ref) => {
|
|
149
|
-
const normalizedRef = trimString(ref);
|
|
150
|
-
if (normalizedRef && !references.has(normalizedRef)) {
|
|
151
|
-
references.set(normalizedRef, field.field_id);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
return references;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
const toTitlePart = (value) => {
|
|
160
|
-
if (Array.isArray(value)) {
|
|
161
|
-
const parts = value
|
|
162
|
-
.map((entry) => toTitlePart(entry))
|
|
163
|
-
.filter((entry) => typeof entry === 'string' && entry.length > 0);
|
|
164
|
-
return parts.length > 0 ? parts.join(', ') : null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
168
|
-
return String(value);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (typeof value === 'string') {
|
|
172
|
-
const normalized = value.trim();
|
|
173
|
-
return normalized.length > 0 ? normalized : null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return null;
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const computeStructuredRecordTitle = (schema, record) => {
|
|
180
|
-
const formNode = isRecordObject(schema?.form) ? schema.form : null;
|
|
181
|
-
const titleField = isRecordObject(formNode?.title_field) ? formNode.title_field : null;
|
|
182
|
-
const titleElements = Array.isArray(titleField?.elements) ? titleField.elements : [];
|
|
183
|
-
|
|
184
|
-
if (!titleField || titleField.enabled === false || titleElements.length === 0) {
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const descriptors = buildDatasetDescriptors(schema);
|
|
189
|
-
const rootDescriptor =
|
|
190
|
-
descriptors.find((descriptor) => descriptor.id === ROOT_DATASET_ID) ?? descriptors[0] ?? null;
|
|
191
|
-
if (!rootDescriptor) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const projected = projectDatasetRowValues(rootDescriptor, record);
|
|
196
|
-
const references = buildFieldReferenceMap(schema);
|
|
197
|
-
const parts = [];
|
|
198
|
-
|
|
199
|
-
titleElements.forEach((entry) => {
|
|
200
|
-
const normalizedEntry = trimString(entry);
|
|
201
|
-
if (!normalizedEntry) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const fieldId = references.get(normalizedEntry);
|
|
206
|
-
if (!fieldId) {
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const part = toTitlePart(projected.displayValues[fieldId]);
|
|
211
|
-
if (part) {
|
|
212
|
-
parts.push(part);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
return parts.length > 0 ? parts.join(', ') : null;
|
|
217
|
-
};
|
|
218
|
-
|
|
219
144
|
const normalizeRecordScope = (record, scope) => {
|
|
220
145
|
if (!isRecordObject(record)) {
|
|
221
146
|
return record;
|
|
@@ -256,9 +181,43 @@ const normalizeRecordScope = (record, scope) => {
|
|
|
256
181
|
});
|
|
257
182
|
});
|
|
258
183
|
|
|
184
|
+
if (scope.datasetDescriptor) {
|
|
185
|
+
record['@title'] = resolveDatasetRowTitle(scope.datasetDescriptor, record);
|
|
186
|
+
}
|
|
187
|
+
|
|
259
188
|
return record;
|
|
260
189
|
};
|
|
261
190
|
|
|
191
|
+
const findRepeatableElement = (elements, descriptor) => {
|
|
192
|
+
if (!Array.isArray(elements)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
for (const element of elements) {
|
|
197
|
+
if (!isRecordObject(element)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
element.type === 'RepeatableSection' &&
|
|
202
|
+
[element.key, element.data_name].some(
|
|
203
|
+
(reference) =>
|
|
204
|
+
reference === descriptor.repeatable_field_id ||
|
|
205
|
+
reference === descriptor.repeatable_output_key
|
|
206
|
+
)
|
|
207
|
+
) {
|
|
208
|
+
return element;
|
|
209
|
+
}
|
|
210
|
+
if (CONTAINER_TYPES.has(element.type) || element.type === 'RepeatableSection') {
|
|
211
|
+
const match = findRepeatableElement(element.elements, descriptor);
|
|
212
|
+
if (match) {
|
|
213
|
+
return match;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
|
|
262
221
|
const buildSnapshotState = (formValues, scope) => {
|
|
263
222
|
const rawValues = {};
|
|
264
223
|
const repeatable = {};
|
|
@@ -360,14 +319,42 @@ export function normalizeStructuredRecord(schema, record, options = {}) {
|
|
|
360
319
|
}
|
|
361
320
|
|
|
362
321
|
const formNode = isRecordObject(schema?.form) ? schema.form : null;
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
322
|
+
const descriptors = buildDatasetDescriptors(schema);
|
|
323
|
+
const descriptorByRepeatable = new Map();
|
|
324
|
+
descriptors.forEach((descriptor) => {
|
|
325
|
+
if (descriptor.kind !== 'repeatable') {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
[descriptor.repeatable_field_id, descriptor.repeatable_output_key].forEach((reference) => {
|
|
329
|
+
if (reference) {
|
|
330
|
+
descriptorByRepeatable.set(reference, descriptor);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
});
|
|
366
334
|
|
|
367
|
-
|
|
368
|
-
|
|
335
|
+
const requestedDatasetId = trimString(options.datasetId) ?? ROOT_DATASET_ID;
|
|
336
|
+
const datasetDescriptor = descriptors.find((descriptor) => descriptor.id === requestedDatasetId);
|
|
337
|
+
if (!datasetDescriptor) {
|
|
338
|
+
throw new Error(`[form0] normalizeStructuredRecord: unknown dataset "${requestedDatasetId}"`);
|
|
369
339
|
}
|
|
370
340
|
|
|
341
|
+
const datasetElement =
|
|
342
|
+
datasetDescriptor.kind === 'repeatable'
|
|
343
|
+
? findRepeatableElement(formNode?.elements, datasetDescriptor)
|
|
344
|
+
: null;
|
|
345
|
+
const scopeElements =
|
|
346
|
+
datasetDescriptor.kind === 'root'
|
|
347
|
+
? Array.isArray(formNode?.elements)
|
|
348
|
+
? formNode.elements
|
|
349
|
+
: []
|
|
350
|
+
: Array.isArray(datasetElement?.elements)
|
|
351
|
+
? datasetElement.elements
|
|
352
|
+
: [];
|
|
353
|
+
const scope = buildScopeFromElements(scopeElements, descriptorByRepeatable);
|
|
354
|
+
scope.datasetDescriptor = datasetDescriptor;
|
|
355
|
+
|
|
356
|
+
normalizeRecordScope(normalized, scope);
|
|
357
|
+
|
|
371
358
|
return normalized;
|
|
372
359
|
}
|
|
373
360
|
|
package/src/utilities/uuid.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
function getRandomBytes(length) {
|
|
8
8
|
const array = new Uint8Array(length);
|
|
9
|
-
const cryptoObj =
|
|
9
|
+
const cryptoObj =
|
|
10
|
+
(typeof globalThis !== 'undefined' && (globalThis.crypto || globalThis.msCrypto)) || null;
|
|
10
11
|
|
|
11
12
|
if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
|
|
12
13
|
cryptoObj.getRandomValues(array);
|
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
|
|
5
|
-
const PACKAGE_NAME = 'form0-core';
|
|
6
|
-
|
|
7
|
-
const npmUserAgent = process.env.npm_config_user_agent || '';
|
|
8
|
-
const npmArgv = process.env.npm_config_argv;
|
|
9
|
-
|
|
10
|
-
const isSupportedManager = /(npm|pnpm|yarn|bun)\//.test(npmUserAgent);
|
|
11
|
-
|
|
12
|
-
const parseNpmArgv = (raw) => {
|
|
13
|
-
if (!raw) return null;
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(raw);
|
|
16
|
-
} catch {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const includesExplicitPackage = (argv) => {
|
|
22
|
-
if (!argv) return false;
|
|
23
|
-
const args = Array.isArray(argv.original) ? argv.original : argv.cooked || [];
|
|
24
|
-
return args.some(
|
|
25
|
-
(arg) => arg === PACKAGE_NAME || arg.startsWith(`${PACKAGE_NAME}@`),
|
|
26
|
-
);
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
if (isSupportedManager && includesExplicitPackage(parseNpmArgv(npmArgv))) {
|
|
30
|
-
// Keep message short to avoid noisy installs.
|
|
31
|
-
console.warn(
|
|
32
|
-
[
|
|
33
|
-
'',
|
|
34
|
-
'form0-core is the engine only.',
|
|
35
|
-
'For the full form0 ecosystem, install the CLI:',
|
|
36
|
-
' npm install -g form0-cli',
|
|
37
|
-
'Docs: https://docs.form0.dev/getting-started/quickstart',
|
|
38
|
-
'If you are integrating the engine directly, you can ignore this warning.',
|
|
39
|
-
'',
|
|
40
|
-
].join('\n'),
|
|
41
|
-
);
|
|
42
|
-
}
|