form0-core 0.5.1 → 0.7.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 +33 -0
- package/package.json +5 -3
- package/src/authoring/form-authoring.js +16 -26
- package/src/builtins/builtin-definitions.js +22 -0
- package/src/builtins/control/eval.js +11 -2
- package/src/builtins/registry.js +4 -0
- package/src/builtins/repeatable/repeatablesum.js +12 -0
- package/src/builtins/repeatable/repeatablevalues.js +12 -0
- package/src/engine/calculation.js +100 -37
- package/src/engine/condition-context.js +90 -0
- package/src/engine/conditions.js +59 -50
- package/src/engine/context-resolver.js +26 -10
- package/src/engine/evaluator.js +13 -5
- package/src/engine/event-source.js +37 -29
- package/src/engine/events.js +6 -2
- package/src/engine/form-engine.js +9 -7
- package/src/engine/managed-events.js +27 -9
- package/src/engine/record-context.js +126 -0
- package/src/engine/record-evaluator.js +657 -0
- package/src/engine/record-freshness.js +19 -0
- package/src/engine/submission-validation.js +125 -0
- package/src/engine/warning-system.js +4 -0
- package/src/index.js +3 -0
- package/src/schema/operators.js +2 -24
- package/src/schema/schema-validator.js +25 -20
- package/src/utilities/calculation-dependencies.js +2 -1
- package/src/utilities/calculation-editor.js +94 -1
- package/src/utilities/record-data.js +23 -0
- package/src/utilities/repeatable-collections.js +170 -0
- package/src/utilities/schema-revision.js +30 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { ContextResolver } from './context-resolver.js';
|
|
2
|
+
import { expandBuildingPlanSchema } from '../schema/building-plan-expander.js';
|
|
3
|
+
import { validateSchemaWithContext } from '../schema/schema-validator.js';
|
|
4
|
+
import { buildCalculationDependencyPlan } from '../utilities/calculation-dependencies.js';
|
|
5
|
+
import { analyzeRepeatableCollections } from '../utilities/repeatable-collections.js';
|
|
6
|
+
import { createConditionContext } from './condition-context.js';
|
|
7
|
+
import { copyRecordData } from '../utilities/record-data.js';
|
|
8
|
+
|
|
9
|
+
/** Shared immutable schema preparation; never owns live row engines. */
|
|
10
|
+
export function prepareRecordContext(inputSchema, warningSystem = null) {
|
|
11
|
+
const sourceSchema = copyRecordData(inputSchema);
|
|
12
|
+
const { schema } = expandBuildingPlanSchema(sourceSchema);
|
|
13
|
+
const resolver = new ContextResolver(schema.form);
|
|
14
|
+
validateSchemaWithContext(schema.form, resolver);
|
|
15
|
+
const calculationPlan = buildCalculationDependencyPlan(schema.form, resolver);
|
|
16
|
+
const conditionContext = createConditionContext(schema.form, resolver, warningSystem);
|
|
17
|
+
const templates = new Map();
|
|
18
|
+
const repeatableByPath = new Map(
|
|
19
|
+
[...resolver.repeatableSectionTree.values()].map((entry) => [
|
|
20
|
+
JSON.stringify(entry.currentPath),
|
|
21
|
+
entry,
|
|
22
|
+
])
|
|
23
|
+
);
|
|
24
|
+
const calculations = new Map();
|
|
25
|
+
for (const [name, node] of calculationPlan.nodesByDataName) {
|
|
26
|
+
calculations.set(name, {
|
|
27
|
+
...node,
|
|
28
|
+
collection: analyzeRepeatableCollections(node.field.calculate, resolver, name),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function template(elements, path) {
|
|
32
|
+
const fields = [];
|
|
33
|
+
const chains = new Map();
|
|
34
|
+
function project(items, ancestors = []) {
|
|
35
|
+
return items.map((field) => {
|
|
36
|
+
fields.push(field);
|
|
37
|
+
chains.set(field.data_name, ancestors);
|
|
38
|
+
if (field.type === 'RepeatableSection') {
|
|
39
|
+
const key = resolver.repeatableSectionTree.get(field.data_name).preferredKey;
|
|
40
|
+
template(field.elements || [], [...path, key]);
|
|
41
|
+
return { ...field, elements: [] };
|
|
42
|
+
}
|
|
43
|
+
if (field.type === 'Section' || field.type === 'BuildingPlanSection')
|
|
44
|
+
return {
|
|
45
|
+
...field,
|
|
46
|
+
elements: project(field.elements || [], [...ancestors, field.data_name]),
|
|
47
|
+
};
|
|
48
|
+
return field;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const localForm = { elements: project(elements) };
|
|
52
|
+
const status = path.length === 0 ? schema.form.status_field : null;
|
|
53
|
+
if (status?.enabled) {
|
|
54
|
+
fields.push(status);
|
|
55
|
+
chains.set(status.data_name, []);
|
|
56
|
+
}
|
|
57
|
+
const result = {
|
|
58
|
+
path,
|
|
59
|
+
fields,
|
|
60
|
+
chains,
|
|
61
|
+
form: localForm,
|
|
62
|
+
conditions: { ...conditionContext, fields },
|
|
63
|
+
};
|
|
64
|
+
templates.set(JSON.stringify(path), result);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
template(schema.form.elements, []);
|
|
68
|
+
return {
|
|
69
|
+
schema,
|
|
70
|
+
sourceSchema,
|
|
71
|
+
resolver,
|
|
72
|
+
calculationPlan,
|
|
73
|
+
templates,
|
|
74
|
+
calculations,
|
|
75
|
+
repeatableByPath,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function visitRecordScopes(snapshot, context, visit) {
|
|
80
|
+
function walk(record, path, instancePath, ancestors, participating) {
|
|
81
|
+
const template = context.templates.get(JSON.stringify(path));
|
|
82
|
+
const scope = { record, template, instancePath, ancestors, participating };
|
|
83
|
+
visit(scope);
|
|
84
|
+
for (const field of template.fields) {
|
|
85
|
+
if (field.type !== 'RepeatableSection') continue;
|
|
86
|
+
const info = context.resolver.repeatableSectionTree.get(field.data_name);
|
|
87
|
+
const rows = record.repeatable?.[info.preferredKey] ?? [];
|
|
88
|
+
if (!Array.isArray(rows))
|
|
89
|
+
throw new Error(`RepeatableSection '${field.data_name}' must contain an array of rows.`);
|
|
90
|
+
const identities = new Set();
|
|
91
|
+
rows.forEach((row, index) => {
|
|
92
|
+
if (!row || typeof row !== 'object' || Array.isArray(row))
|
|
93
|
+
throw new Error('Repeatable rows must be record objects.');
|
|
94
|
+
const id = row.id ?? row.record_id;
|
|
95
|
+
const identified =
|
|
96
|
+
(typeof id === 'string' && id.trim() !== '') ||
|
|
97
|
+
(typeof id === 'number' && Number.isFinite(id));
|
|
98
|
+
if (identified) {
|
|
99
|
+
const identity = `${typeof id}:${id}`;
|
|
100
|
+
if (identities.has(identity))
|
|
101
|
+
throw new Error(`Duplicate row identity in '${field.data_name}'.`);
|
|
102
|
+
identities.add(identity);
|
|
103
|
+
}
|
|
104
|
+
const segment = {
|
|
105
|
+
repeatableKey: info.preferredKey,
|
|
106
|
+
instanceId: identified ? id : null,
|
|
107
|
+
index,
|
|
108
|
+
identity: identified ? 'id' : 'position',
|
|
109
|
+
};
|
|
110
|
+
const visible =
|
|
111
|
+
record.visible?.[field.data_name] !== false &&
|
|
112
|
+
(template.chains.get(field.data_name) || []).every(
|
|
113
|
+
(name) => record.visible?.[name] !== false
|
|
114
|
+
);
|
|
115
|
+
walk(
|
|
116
|
+
row,
|
|
117
|
+
info.currentPath,
|
|
118
|
+
[...instancePath, segment],
|
|
119
|
+
[...ancestors, scope],
|
|
120
|
+
participating && visible
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
walk(snapshot, [], [], [], true);
|
|
126
|
+
}
|