form0-core 0.7.0 → 0.8.1
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 +17 -0
- package/package.json +3 -2
- package/src/builtins/builtin-definitions.js +6 -1
- package/src/builtins/builtin-metadata.js +12 -0
- package/src/engine/calculation-bindings.js +76 -0
- package/src/engine/calculation.js +15 -2
- package/src/engine/evaluator.js +10 -12
- package/src/engine/event-operation-scope.js +7 -3
- package/src/engine/event-policy.js +61 -0
- package/src/engine/event-registration.js +38 -0
- package/src/engine/event-source.js +15 -12
- package/src/engine/events.js +36 -12
- package/src/engine/expression-compilation.js +39 -0
- package/src/engine/form-engine.js +56 -21
- package/src/engine/managed-events.js +26 -18
- package/src/engine/record-context.js +13 -6
- package/src/engine/record-evaluator.js +282 -56
- package/src/engine/submission-validation.js +5 -0
- package/src/index.js +4 -1
- package/src/security/validation.js +4 -4
- package/src/utilities/form-event-editor.js +4 -0
- package/src/utilities/record-data.js +50 -4
- package/src/utilities/record-transformer.js +110 -39
|
@@ -8,6 +8,9 @@ import { recordVersion } from './version-utils.js';
|
|
|
8
8
|
import { buildRepeatableMetadata } from './repeatable-helpers.js';
|
|
9
9
|
import { buildDatasetDescriptors, resolveDatasetRowTitle } from './dataset-descriptors.js';
|
|
10
10
|
|
|
11
|
+
const resolveRecordId = (primary, fallback) =>
|
|
12
|
+
primary === 0 ? 0 : primary || (fallback === 0 ? 0 : fallback || null);
|
|
13
|
+
|
|
11
14
|
const hasMeaningfulValue = (value) => {
|
|
12
15
|
if (value === null || value === undefined) {
|
|
13
16
|
return false;
|
|
@@ -45,40 +48,8 @@ const hasMeaningfulRecord = (record) => {
|
|
|
45
48
|
return false;
|
|
46
49
|
};
|
|
47
50
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
* @param {Object} state - Form engine state {values, errors, visible, required, read_only}
|
|
51
|
-
* @param {Array} [fields] - Optional flattened fields array from schema (for key mapping)
|
|
52
|
-
* @param {Object} options - Additional options for record creation
|
|
53
|
-
* - mainRecordId: UUID for the main record
|
|
54
|
-
* - childRecordIds: Nested object structure for RepeatableSection child record UUIDs
|
|
55
|
-
* - originalElements: Original nested form elements (for proper RepeatableSection nesting)
|
|
56
|
-
* @param {string} [id] - DEPRECATED: Use options.mainRecordId instead
|
|
57
|
-
* @returns {Object} Structured record ready for submission
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* // Simple RepeatableSection:
|
|
61
|
-
* options = {
|
|
62
|
-
* mainRecordId: "main-uuid",
|
|
63
|
-
* childRecordIds: {
|
|
64
|
-
* "repA": ["child-1", "child-2", "child-3"]
|
|
65
|
-
* }
|
|
66
|
-
* }
|
|
67
|
-
*
|
|
68
|
-
* // Nested RepeatableSections:
|
|
69
|
-
* options = {
|
|
70
|
-
* mainRecordId: "main-uuid",
|
|
71
|
-
* childRecordIds: {
|
|
72
|
-
* "repA": {
|
|
73
|
-
* _records: ["repA-record-1", "repA-record-2"],
|
|
74
|
-
* "repA-record-1": {
|
|
75
|
-
* "repB": ["repB-child-1", "repB-child-2"]
|
|
76
|
-
* }
|
|
77
|
-
* }
|
|
78
|
-
* }
|
|
79
|
-
* }
|
|
80
|
-
*/
|
|
81
|
-
export function createStructuredRecord(state, fields = null, options = {}, id = null) {
|
|
51
|
+
/** Shared transformation steps; yielding does not change formatting or record metadata. */
|
|
52
|
+
function* structuredRecordSteps(state, fields = null, options = {}, id = null) {
|
|
82
53
|
// Compute final version values
|
|
83
54
|
const finalVersion = options.version || 1;
|
|
84
55
|
const finalChildVersion = options.childVersion || 1;
|
|
@@ -276,7 +247,11 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
276
247
|
}
|
|
277
248
|
|
|
278
249
|
// Enhanced recursive function to process nested RepeatableSections
|
|
279
|
-
const processRepeatableSection = (
|
|
250
|
+
const processRepeatableSection = function* (
|
|
251
|
+
repInfo,
|
|
252
|
+
currentPath = [],
|
|
253
|
+
structuredInstances = null
|
|
254
|
+
) {
|
|
280
255
|
const pathKey = repInfo.preferredKey;
|
|
281
256
|
|
|
282
257
|
// Get child record IDs from options using the full path
|
|
@@ -389,7 +364,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
389
364
|
? instanceRepeatable[childRepInfo.preferredKey]
|
|
390
365
|
: [];
|
|
391
366
|
const nestedInstances = Array.isArray(rawNestedInstances) ? rawNestedInstances : [];
|
|
392
|
-
const childRepeatableArray = processRepeatableSection(
|
|
367
|
+
const childRepeatableArray = yield* processRepeatableSection(
|
|
393
368
|
childRepInfo,
|
|
394
369
|
[...currentPath, pathKey],
|
|
395
370
|
nestedInstances
|
|
@@ -405,6 +380,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
405
380
|
if (hasMeaningfulRecord(childRecord)) {
|
|
406
381
|
childRecords.push(childRecord);
|
|
407
382
|
}
|
|
383
|
+
yield;
|
|
408
384
|
}
|
|
409
385
|
|
|
410
386
|
return childRecords;
|
|
@@ -499,7 +475,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
499
475
|
|
|
500
476
|
// Recursively process child RepeatableSections
|
|
501
477
|
for (const [childDataName, childRepInfo] of repInfo.children) {
|
|
502
|
-
const childRepeatableArray = processRepeatableSection(childRepInfo, [
|
|
478
|
+
const childRepeatableArray = yield* processRepeatableSection(childRepInfo, [
|
|
503
479
|
...currentPath,
|
|
504
480
|
pathKey,
|
|
505
481
|
]);
|
|
@@ -526,6 +502,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
526
502
|
if (hasMeaningfulRecord(childRecord)) {
|
|
527
503
|
childRecords.push(childRecord);
|
|
528
504
|
}
|
|
505
|
+
yield;
|
|
529
506
|
}
|
|
530
507
|
|
|
531
508
|
return childRecords;
|
|
@@ -549,7 +526,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
549
526
|
: []
|
|
550
527
|
: null;
|
|
551
528
|
|
|
552
|
-
const childRecords = processRepeatableSection(repInfo, [], structuredInstances);
|
|
529
|
+
const childRecords = yield* processRepeatableSection(repInfo, [], structuredInstances);
|
|
553
530
|
const repeatableOutputKey = resolveRepeatableOutputKey(repInfo);
|
|
554
531
|
if (hasStructuredRepeatableState) {
|
|
555
532
|
// In structured mode keep explicit repeatable keys, even when empty.
|
|
@@ -614,7 +591,7 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
614
591
|
),
|
|
615
592
|
|
|
616
593
|
// Ensure our specific handling isn't overridden (always set last)
|
|
617
|
-
id: options.mainRecordId
|
|
594
|
+
id: resolveRecordId(options.mainRecordId, id),
|
|
618
595
|
changeset_id: options.changeset_id || null, // Ensure changeset_id isn't overridden
|
|
619
596
|
created_at: clientCreatedAt, // Ensure canonical created_at isn't overridden
|
|
620
597
|
updated_at: serverUpdatedAt, // Ensure canonical updated_at isn't overridden
|
|
@@ -648,3 +625,97 @@ export function createStructuredRecord(state, fields = null, options = {}, id =
|
|
|
648
625
|
|
|
649
626
|
return record;
|
|
650
627
|
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Create structured record from form engine state with support for unlimited RepeatableSection nesting
|
|
631
|
+
* @param {Object} state - Form engine state {values, errors, visible, required, read_only}
|
|
632
|
+
* @param {Array} [fields] - Optional flattened fields array from schema (for key mapping)
|
|
633
|
+
* @param {Object} options - Additional options for record creation
|
|
634
|
+
* - mainRecordId: UUID for the main record
|
|
635
|
+
* - childRecordIds: Nested object structure for RepeatableSection child record UUIDs
|
|
636
|
+
* - originalElements: Original nested form elements (for proper RepeatableSection nesting)
|
|
637
|
+
* @param {string} [id] - DEPRECATED: Use options.mainRecordId instead
|
|
638
|
+
* @returns {Object} Structured record ready for submission
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* // Simple RepeatableSection:
|
|
642
|
+
* options = {
|
|
643
|
+
* mainRecordId: "main-uuid",
|
|
644
|
+
* childRecordIds: {
|
|
645
|
+
* "repA": ["child-1", "child-2", "child-3"]
|
|
646
|
+
* }
|
|
647
|
+
* }
|
|
648
|
+
*
|
|
649
|
+
* // Nested RepeatableSections:
|
|
650
|
+
* options = {
|
|
651
|
+
* mainRecordId: "main-uuid",
|
|
652
|
+
* childRecordIds: {
|
|
653
|
+
* "repA": {
|
|
654
|
+
* _records: ["repA-record-1", "repA-record-2"],
|
|
655
|
+
* "repA-record-1": {
|
|
656
|
+
* "repB": ["repB-child-1", "repB-child-2"]
|
|
657
|
+
* }
|
|
658
|
+
* }
|
|
659
|
+
* }
|
|
660
|
+
* }
|
|
661
|
+
*/
|
|
662
|
+
export function createStructuredRecord(state, fields = null, options = {}, id = null) {
|
|
663
|
+
const work = structuredRecordSteps(state, fields, options, id);
|
|
664
|
+
let next;
|
|
665
|
+
do {
|
|
666
|
+
next = work.next();
|
|
667
|
+
} while (!next.done);
|
|
668
|
+
return next.value;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Create the same structured record with host scheduling between rows.
|
|
673
|
+
* Supply a stable evaluated snapshot; this function never evaluates or mutates it.
|
|
674
|
+
* @param {Object} state
|
|
675
|
+
* @param {Object[]} [fields]
|
|
676
|
+
* @param {Object} [options] The synchronous serializer's options; use mainRecordId for identity.
|
|
677
|
+
* @param {{signal?: AbortSignal, budgetMs?: number, yieldToHost?: Function}} [scheduling]
|
|
678
|
+
* @returns {Promise<Object>} Cancellation rejects with an AbortError; no partial record is returned.
|
|
679
|
+
*/
|
|
680
|
+
export async function createStructuredRecordAsync(
|
|
681
|
+
state,
|
|
682
|
+
fields = null,
|
|
683
|
+
options = {},
|
|
684
|
+
scheduling = {}
|
|
685
|
+
) {
|
|
686
|
+
const clock = () => globalThis.performance?.now?.() ?? Date.now();
|
|
687
|
+
const check = () => {
|
|
688
|
+
if (scheduling.signal?.aborted)
|
|
689
|
+
throw Object.assign(new Error('Record serialization was cancelled.'), {
|
|
690
|
+
name: 'AbortError',
|
|
691
|
+
code: 'cancelled',
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
const yieldToHost =
|
|
695
|
+
scheduling.yieldToHost || (() => new Promise((resolve) => setTimeout(resolve, 0)));
|
|
696
|
+
const pause = async () => {
|
|
697
|
+
try {
|
|
698
|
+
await yieldToHost();
|
|
699
|
+
} catch (error) {
|
|
700
|
+
throw Object.assign(new Error(error?.message || 'Record serialization scheduling failed.'), {
|
|
701
|
+
code: 'record_scheduling_failed',
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
check();
|
|
706
|
+
// Finish prior editor/signature work before beginning this separate phase.
|
|
707
|
+
await pause();
|
|
708
|
+
check();
|
|
709
|
+
let started = clock();
|
|
710
|
+
const work = structuredRecordSteps(state, fields, options);
|
|
711
|
+
for (;;) {
|
|
712
|
+
const next = work.next();
|
|
713
|
+
check();
|
|
714
|
+
if (next.done) return next.value;
|
|
715
|
+
if (clock() - started >= (scheduling.budgetMs ?? 8)) {
|
|
716
|
+
await pause();
|
|
717
|
+
check();
|
|
718
|
+
started = clock();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|