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.
@@ -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(Array.isArray(element.elements) ? element.elements : []),
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 scope = buildScopeFromElements(Array.isArray(formNode?.elements) ? formNode.elements : []);
364
-
365
- normalizeRecordScope(normalized, scope);
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
- if (formNode) {
368
- normalized['@title'] = computeStructuredRecordTitle(schema, normalized);
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
 
@@ -6,7 +6,8 @@
6
6
 
7
7
  function getRandomBytes(length) {
8
8
  const array = new Uint8Array(length);
9
- const cryptoObj = (typeof globalThis !== 'undefined' && (globalThis.crypto || globalThis.msCrypto)) || null;
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
- }