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
package/src/engine/conditions.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { assertConditionReferences, createConditionContext } from './condition-context.js';
|
|
2
2
|
|
|
3
3
|
const valuesEqual = (a, b) => {
|
|
4
4
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
@@ -81,37 +81,50 @@ function normalizeConditionValue(field, value) {
|
|
|
81
81
|
return value;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
export function evaluateConditions(conditions, values, allFields) {
|
|
84
|
+
export function evaluateConditions(conditions, values, allFields, scope = null) {
|
|
85
|
+
if (scope) {
|
|
86
|
+
assertConditionReferences(conditions, scope.owner, scope.property, scope.context);
|
|
87
|
+
}
|
|
88
|
+
return evaluateConditionExpression(conditions, values, allFields);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function evaluateConditionExpression(conditions, values, allFields) {
|
|
85
92
|
if (Array.isArray(conditions)) {
|
|
86
|
-
return conditions.every((c) =>
|
|
93
|
+
return conditions.every((c) => evaluateConditionExpression(c, values, allFields));
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
if (conditions.and) {
|
|
90
|
-
return conditions.and.every((c) =>
|
|
97
|
+
return conditions.and.every((c) => evaluateConditionExpression(c, values, allFields));
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
if (conditions.or) {
|
|
94
|
-
return conditions.or.some((c) =>
|
|
101
|
+
return conditions.or.some((c) => evaluateConditionExpression(c, values, allFields));
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
// Map field_id (key) to data_name for value lookup
|
|
98
105
|
const field = allFields ? allFields[conditions.field_id] : undefined;
|
|
99
106
|
const dataName = field ? field.data_name : undefined;
|
|
100
107
|
const val = normalizeConditionValue(field, dataName ? values[dataName] : undefined);
|
|
101
|
-
// Debug log: show what is being checked
|
|
102
|
-
//console.log('[evaluateConditions] field_id:', conditions.field_id, 'data_name:', dataName, 'value in values:', val, 'expected:', conditions.value, 'operator:', conditions.operator);
|
|
103
108
|
const fn = OPERATORS[conditions.operator];
|
|
104
109
|
return fn ? fn(val, conditions.value) : false;
|
|
105
110
|
}
|
|
106
111
|
|
|
112
|
+
function evaluateElementCondition(owner, property, values, context) {
|
|
113
|
+
return evaluateConditions(owner[property], values, context.allFields, {
|
|
114
|
+
owner,
|
|
115
|
+
property,
|
|
116
|
+
context,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
107
120
|
/**
|
|
108
121
|
* Recursively evaluates visibility for both fields and sections in bottom-up order
|
|
109
122
|
* @param {Array} elements - Form elements to evaluate
|
|
110
123
|
* @param {Object} values - Current form values
|
|
111
124
|
* @param {Object} visible - Visibility state object to populate
|
|
112
|
-
* @param {Object}
|
|
125
|
+
* @param {Object} context - Shared field index and ownership resolver
|
|
113
126
|
*/
|
|
114
|
-
function evaluateVisibilityRecursive(elements, values, visible,
|
|
127
|
+
function evaluateVisibilityRecursive(elements, values, visible, context) {
|
|
115
128
|
elements.forEach((element) => {
|
|
116
129
|
if (
|
|
117
130
|
element.type === 'Section' ||
|
|
@@ -120,7 +133,7 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
|
|
|
120
133
|
) {
|
|
121
134
|
// First evaluate children (bottom-up approach)
|
|
122
135
|
if (element.elements && element.elements.length > 0) {
|
|
123
|
-
evaluateVisibilityRecursive(element.elements, values, visible,
|
|
136
|
+
evaluateVisibilityRecursive(element.elements, values, visible, context);
|
|
124
137
|
|
|
125
138
|
// Check if any child is visible
|
|
126
139
|
const hasVisibleChild = element.elements.some(
|
|
@@ -133,7 +146,12 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
|
|
|
133
146
|
} else {
|
|
134
147
|
// At least one child visible → respect explicit section visibility settings
|
|
135
148
|
if (element.visible_conditions) {
|
|
136
|
-
const isVisible =
|
|
149
|
+
const isVisible = evaluateElementCondition(
|
|
150
|
+
element,
|
|
151
|
+
'visible_conditions',
|
|
152
|
+
values,
|
|
153
|
+
context
|
|
154
|
+
);
|
|
137
155
|
visible[element.data_name] = isVisible;
|
|
138
156
|
} else {
|
|
139
157
|
visible[element.data_name] = element.visible === true;
|
|
@@ -142,7 +160,12 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
|
|
|
142
160
|
} else {
|
|
143
161
|
// Empty section fallback (though schema validation should prevent this)
|
|
144
162
|
if (element.visible_conditions) {
|
|
145
|
-
const isVisible =
|
|
163
|
+
const isVisible = evaluateElementCondition(
|
|
164
|
+
element,
|
|
165
|
+
'visible_conditions',
|
|
166
|
+
values,
|
|
167
|
+
context
|
|
168
|
+
);
|
|
146
169
|
visible[element.data_name] = isVisible;
|
|
147
170
|
} else {
|
|
148
171
|
visible[element.data_name] = element.visible === true;
|
|
@@ -151,7 +174,7 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
|
|
|
151
174
|
} else {
|
|
152
175
|
// Regular field - existing logic
|
|
153
176
|
if (element.visible_conditions) {
|
|
154
|
-
const isVisible =
|
|
177
|
+
const isVisible = evaluateElementCondition(element, 'visible_conditions', values, context);
|
|
155
178
|
visible[element.data_name] = isVisible;
|
|
156
179
|
} else {
|
|
157
180
|
visible[element.data_name] = element.visible === true;
|
|
@@ -160,34 +183,24 @@ function evaluateVisibilityRecursive(elements, values, visible, allFields) {
|
|
|
160
183
|
});
|
|
161
184
|
}
|
|
162
185
|
|
|
163
|
-
export function evaluateVisibility(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (field.key) allFields[field.key] = field;
|
|
171
|
-
if (field.data_name) allFields[field.data_name] = field;
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Use recursive approach for single-pass visibility evaluation
|
|
175
|
-
evaluateVisibilityRecursive(schema.elements, values, visible, allFields);
|
|
186
|
+
export function evaluateVisibility(
|
|
187
|
+
schema,
|
|
188
|
+
values,
|
|
189
|
+
visible,
|
|
190
|
+
context = createConditionContext(schema)
|
|
191
|
+
) {
|
|
192
|
+
evaluateVisibilityRecursive(schema.elements, values, visible, context);
|
|
176
193
|
}
|
|
177
194
|
|
|
178
|
-
export function evaluateRequirement(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fields.forEach((field) => {
|
|
185
|
-
if (field.key) allFields[field.key] = field;
|
|
186
|
-
if (field.data_name) allFields[field.data_name] = field;
|
|
187
|
-
});
|
|
188
|
-
fields.forEach((field) => {
|
|
195
|
+
export function evaluateRequirement(
|
|
196
|
+
schema,
|
|
197
|
+
values,
|
|
198
|
+
required,
|
|
199
|
+
context = createConditionContext(schema)
|
|
200
|
+
) {
|
|
201
|
+
context.fields.forEach((field) => {
|
|
189
202
|
if (field.required_conditions) {
|
|
190
|
-
const isRequired =
|
|
203
|
+
const isRequired = evaluateElementCondition(field, 'required_conditions', values, context);
|
|
191
204
|
required[field.data_name] = isRequired;
|
|
192
205
|
} else {
|
|
193
206
|
required[field.data_name] = field.required === true;
|
|
@@ -195,19 +208,15 @@ export function evaluateRequirement(schema, values, required) {
|
|
|
195
208
|
});
|
|
196
209
|
}
|
|
197
210
|
|
|
198
|
-
export function evaluateReadOnly(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
fields.forEach((field) => {
|
|
205
|
-
if (field.key) allFields[field.key] = field;
|
|
206
|
-
if (field.data_name) allFields[field.data_name] = field;
|
|
207
|
-
});
|
|
208
|
-
fields.forEach((field) => {
|
|
211
|
+
export function evaluateReadOnly(
|
|
212
|
+
schema,
|
|
213
|
+
values,
|
|
214
|
+
read_only,
|
|
215
|
+
context = createConditionContext(schema)
|
|
216
|
+
) {
|
|
217
|
+
context.fields.forEach((field) => {
|
|
209
218
|
if (field.read_only_conditions) {
|
|
210
|
-
const isReadOnly =
|
|
219
|
+
const isReadOnly = evaluateElementCondition(field, 'read_only_conditions', values, context);
|
|
211
220
|
read_only[field.data_name] = isReadOnly;
|
|
212
221
|
} else {
|
|
213
222
|
read_only[field.data_name] = field.read_only === true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context-aware field resolution for calculations and events
|
|
2
|
+
* Context-aware field resolution for calculations, conditions and events
|
|
3
3
|
* Determines which fields are accessible based on execution context
|
|
4
4
|
* Reuses tree-building logic from record-transformer.js
|
|
5
5
|
*/
|
|
@@ -10,6 +10,7 @@ export class ContextResolver {
|
|
|
10
10
|
constructor(schema) {
|
|
11
11
|
// Maps to store field context information (similar to record-transformer.js)
|
|
12
12
|
this.fieldOwnership = new Map(); // Field data_name -> { preferredKey, field, parentPath }
|
|
13
|
+
this.elementOwnership = new Map(); // Includes containers in their containing record context.
|
|
13
14
|
this.repeatableSectionTree = new Map(); // RepeatableSection data_name -> tree info
|
|
14
15
|
this.sectionFields = new Set(); // Section field names (organizational only)
|
|
15
16
|
|
|
@@ -26,6 +27,12 @@ export class ContextResolver {
|
|
|
26
27
|
if (!Array.isArray(elements)) return;
|
|
27
28
|
|
|
28
29
|
elements.forEach((element) => {
|
|
30
|
+
const preferredKey =
|
|
31
|
+
typeof element.key === 'string' && element.key.trim() !== ''
|
|
32
|
+
? element.key
|
|
33
|
+
: element.data_name;
|
|
34
|
+
const ownership = { preferredKey, field: element, parentPath: [...parentPath] };
|
|
35
|
+
this.elementOwnership.set(element.data_name, ownership);
|
|
29
36
|
if (element.type === 'Section' || element.type === 'BuildingPlanSection') {
|
|
30
37
|
this.sectionFields.add(element.data_name);
|
|
31
38
|
// Recursively process Section children with same parentPath
|
|
@@ -34,8 +41,6 @@ export class ContextResolver {
|
|
|
34
41
|
this.buildFieldContext(element.elements, parentPath);
|
|
35
42
|
}
|
|
36
43
|
} else if (element.type === 'RepeatableSection') {
|
|
37
|
-
const preferredKey =
|
|
38
|
-
element.key && element.key.trim() !== '' ? element.key : element.data_name;
|
|
39
44
|
const currentPath = [...parentPath, preferredKey];
|
|
40
45
|
|
|
41
46
|
// Store this RepeatableSection in the tree
|
|
@@ -54,13 +59,7 @@ export class ContextResolver {
|
|
|
54
59
|
}
|
|
55
60
|
} else {
|
|
56
61
|
// This is a regular field - determine its ownership
|
|
57
|
-
|
|
58
|
-
element.key && element.key.trim() !== '' ? element.key : element.data_name;
|
|
59
|
-
this.fieldOwnership.set(element.data_name, {
|
|
60
|
-
preferredKey: preferredFieldKey,
|
|
61
|
-
field: element,
|
|
62
|
-
parentPath: [...parentPath], // Copy the current path
|
|
63
|
-
});
|
|
62
|
+
this.fieldOwnership.set(element.data_name, ownership);
|
|
64
63
|
}
|
|
65
64
|
});
|
|
66
65
|
}
|
|
@@ -146,6 +145,19 @@ export class ContextResolver {
|
|
|
146
145
|
checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo) {
|
|
147
146
|
const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
|
|
148
147
|
|
|
148
|
+
if (contextType === 'condition') {
|
|
149
|
+
const owner = this.elementOwnership.get(contextFieldName);
|
|
150
|
+
if (owner) {
|
|
151
|
+
return this.resolveRepeatableSectionRelationship(
|
|
152
|
+
owner.parentPath,
|
|
153
|
+
fieldInfo.parentPath,
|
|
154
|
+
fieldName,
|
|
155
|
+
fieldInfo,
|
|
156
|
+
executionContext
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
149
161
|
if (contextType === 'calculation') {
|
|
150
162
|
// CalculatedField can only access fields in same RepeatableSection context
|
|
151
163
|
const calculationFieldInfo = this.fieldOwnership.get(contextFieldName);
|
|
@@ -318,6 +330,10 @@ export class ContextResolver {
|
|
|
318
330
|
buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code) {
|
|
319
331
|
const { type: contextType, eventType } = executionContext;
|
|
320
332
|
|
|
333
|
+
if (contextType === 'condition') {
|
|
334
|
+
return `Conditions can only reference fields in their containing record or its ancestors. Field '${fieldName}' belongs to: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
321
337
|
if (contextType === 'calculation') {
|
|
322
338
|
if (code === 'different_repeatable_section') {
|
|
323
339
|
return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
|
package/src/engine/evaluator.js
CHANGED
|
@@ -54,8 +54,10 @@ export function runExpression(
|
|
|
54
54
|
|
|
55
55
|
// Create secure context based on security mode
|
|
56
56
|
const secureContext = createSecureContext(context, securityConfig);
|
|
57
|
-
const keys =
|
|
58
|
-
|
|
57
|
+
const keys = options.compileCache
|
|
58
|
+
? Object.keys(secureContext).sort()
|
|
59
|
+
: Object.keys(secureContext);
|
|
60
|
+
const values = keys.map((key) => secureContext[key]);
|
|
59
61
|
|
|
60
62
|
// Execute expression
|
|
61
63
|
const executeExpression = () => {
|
|
@@ -70,9 +72,11 @@ export function runExpression(
|
|
|
70
72
|
isMultilineCalculationExpression(sourceExpression);
|
|
71
73
|
|
|
72
74
|
if (isMultiLine) {
|
|
73
|
-
//
|
|
75
|
+
// Reuse a factory-owned compilation cache when supplied; values stay per evaluation.
|
|
74
76
|
phase = 'syntax';
|
|
75
|
-
const
|
|
77
|
+
const cacheKey = JSON.stringify(['body', keys, expr]);
|
|
78
|
+
const fn = options.compileCache?.get(cacheKey) ?? new Function(...keys, expr);
|
|
79
|
+
options.compileCache?.set(cacheKey, fn);
|
|
76
80
|
phase = 'runtime';
|
|
77
81
|
const result = fn(...values);
|
|
78
82
|
// Check for consumed result from SETRESULT() calls in multiline code
|
|
@@ -85,7 +89,11 @@ export function runExpression(
|
|
|
85
89
|
// Execute as expression (existing behavior)
|
|
86
90
|
const inlineExpression = normalizeInlineCalculationExpression(expr);
|
|
87
91
|
phase = 'syntax';
|
|
88
|
-
const
|
|
92
|
+
const cacheKey = JSON.stringify(['expression', keys, inlineExpression]);
|
|
93
|
+
const fn =
|
|
94
|
+
options.compileCache?.get(cacheKey) ??
|
|
95
|
+
new Function(...keys, `return (${inlineExpression});`);
|
|
96
|
+
options.compileCache?.set(cacheKey, fn);
|
|
89
97
|
phase = 'runtime';
|
|
90
98
|
const result = fn(...values);
|
|
91
99
|
const consumed = __consumeResult();
|
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import { parse } from 'acorn';
|
|
2
2
|
|
|
3
3
|
/** Retains authored callback source without depending on engine-specific toString output. */
|
|
4
|
-
export function createEventSourceRegistry() {
|
|
4
|
+
export function createEventSourceRegistry(preparationCache = null) {
|
|
5
5
|
const sources = new WeakMap();
|
|
6
6
|
|
|
7
|
+
function bindPrepared(prepared, context) {
|
|
8
|
+
const propertyNames = new Map();
|
|
9
|
+
const capture = (callback, source, name) => {
|
|
10
|
+
sources.set(callback, source);
|
|
11
|
+
if (name && !callback.name)
|
|
12
|
+
Object.defineProperty(callback, 'name', { value: name, configurable: true });
|
|
13
|
+
return callback;
|
|
14
|
+
};
|
|
15
|
+
capture.key = (value, id) => {
|
|
16
|
+
const key = Reflect.ownKeys({ [value]: null })[0];
|
|
17
|
+
propertyNames.set(
|
|
18
|
+
id,
|
|
19
|
+
typeof key === 'symbol'
|
|
20
|
+
? key.description === undefined
|
|
21
|
+
? ''
|
|
22
|
+
: `[${key.description}]`
|
|
23
|
+
: key
|
|
24
|
+
);
|
|
25
|
+
return key;
|
|
26
|
+
};
|
|
27
|
+
capture.nameFor = (id) => propertyNames.get(id);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
code: prepared.code,
|
|
31
|
+
context: {
|
|
32
|
+
...context,
|
|
33
|
+
[prepared.captureName]: capture,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
function prepare(code, context) {
|
|
39
|
+
const cached = preparationCache?.get(code);
|
|
40
|
+
if (cached && !Object.hasOwn(context, cached.captureName)) return bindPrepared(cached, context);
|
|
8
41
|
let tree;
|
|
9
42
|
try {
|
|
10
43
|
// Event scripts are Function bodies, rather than modules or global scripts.
|
|
@@ -27,7 +60,6 @@ export function createEventSourceRegistry() {
|
|
|
27
60
|
let captureName = '__form0CaptureEventSource';
|
|
28
61
|
while (identifiers.has(captureName) || Object.hasOwn(context, captureName)) captureName += '_';
|
|
29
62
|
const edits = [];
|
|
30
|
-
const propertyNames = new Map();
|
|
31
63
|
|
|
32
64
|
function inferredName(node, parent) {
|
|
33
65
|
if (node.id) return '';
|
|
@@ -120,33 +152,9 @@ export function createEventSourceRegistry() {
|
|
|
120
152
|
prepared = prepared.slice(0, position) + text + prepared.slice(position);
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Object.defineProperty(callback, 'name', { value: name, configurable: true });
|
|
127
|
-
return callback;
|
|
128
|
-
};
|
|
129
|
-
capture.key = (value, id) => {
|
|
130
|
-
const key = Reflect.ownKeys({ [value]: null })[0];
|
|
131
|
-
propertyNames.set(
|
|
132
|
-
id,
|
|
133
|
-
typeof key === 'symbol'
|
|
134
|
-
? key.description === undefined
|
|
135
|
-
? ''
|
|
136
|
-
: `[${key.description}]`
|
|
137
|
-
: key
|
|
138
|
-
);
|
|
139
|
-
return key;
|
|
140
|
-
};
|
|
141
|
-
capture.nameFor = (id) => propertyNames.get(id);
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
code: prepared,
|
|
145
|
-
context: {
|
|
146
|
-
...context,
|
|
147
|
-
[captureName]: capture,
|
|
148
|
-
},
|
|
149
|
-
};
|
|
155
|
+
const result = { code: prepared, captureName };
|
|
156
|
+
preparationCache?.set(code, result);
|
|
157
|
+
return bindPrepared(result, context);
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
function getSource(callback) {
|
package/src/engine/events.js
CHANGED
|
@@ -17,7 +17,7 @@ const _loggedHandlers = new Set();
|
|
|
17
17
|
* EventManager handles form event registration and execution
|
|
18
18
|
*/
|
|
19
19
|
export class EventManager {
|
|
20
|
-
constructor(schema = null, contextResolver = null, warningSystem = null) {
|
|
20
|
+
constructor(schema = null, contextResolver = null, warningSystem = null, preparation = {}) {
|
|
21
21
|
this.listeners = new Map(); // eventType -> Map<fieldKey, callback[]>
|
|
22
22
|
this.eventContext = null;
|
|
23
23
|
this.securityConfig = null;
|
|
@@ -26,7 +26,9 @@ export class EventManager {
|
|
|
26
26
|
this.contextResolver = contextResolver || (schema ? new ContextResolver(schema) : null);
|
|
27
27
|
this.warningSystem = warningSystem || new WarningSystem();
|
|
28
28
|
this.schema = schema;
|
|
29
|
-
this.callbackSources = createEventSourceRegistry();
|
|
29
|
+
this.callbackSources = createEventSourceRegistry(preparation.sourceCache);
|
|
30
|
+
this.compilationCache = preparation.compilationCache;
|
|
31
|
+
this.referenceSourceCache = preparation.referenceSourceCache;
|
|
30
32
|
this.initializationIssues = [];
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -65,6 +67,7 @@ export class EventManager {
|
|
|
65
67
|
const result = runInEventOperationScope(scope, () =>
|
|
66
68
|
runExpression(prepared.code, prepared.context, this.securityConfig, true, this.schema, {
|
|
67
69
|
sourceExpression: code,
|
|
70
|
+
compileCache: this.compilationCache,
|
|
68
71
|
onDiagnostic: (diagnostic) =>
|
|
69
72
|
this.initializationIssues.push({
|
|
70
73
|
code: 'initialization_failed',
|
|
@@ -305,6 +308,7 @@ export class EventManager {
|
|
|
305
308
|
const functionCall = `(${callbackCode})(event)`;
|
|
306
309
|
return runInEventOperationScope(scope, () =>
|
|
307
310
|
runExpression(functionCall, contextWithEvent, this.securityConfig, true, this.schema, {
|
|
311
|
+
compileCache: this.compilationCache,
|
|
308
312
|
evaluateAsExpression: true,
|
|
309
313
|
...(managed?.reporting || {}),
|
|
310
314
|
})
|
|
@@ -4,7 +4,8 @@ import { evaluateRequirement } from './conditions.js';
|
|
|
4
4
|
import { evaluateReadOnly } from './conditions.js';
|
|
5
5
|
import { validateFields } from './field-validation.js';
|
|
6
6
|
import { calculationBuiltins, eventBuiltins } from '../builtins/registry.js';
|
|
7
|
-
import {
|
|
7
|
+
import { validateSchemaWithContext } from '../schema/schema-validator.js';
|
|
8
|
+
import { createConditionContext } from './condition-context.js';
|
|
8
9
|
import { flattenFields } from '../utilities/field-helpers.js';
|
|
9
10
|
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
10
11
|
import { EventManager } from './events.js';
|
|
@@ -77,7 +78,8 @@ export function createFormEngine({
|
|
|
77
78
|
}) {
|
|
78
79
|
const { schema: preparedSchema, buildingPlanMeta } = expandBuildingPlanSchema(schema);
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
const contextResolver = new ContextResolver(preparedSchema.form);
|
|
82
|
+
validateSchemaWithContext(preparedSchema.form, contextResolver);
|
|
81
83
|
|
|
82
84
|
const { form } = preparedSchema;
|
|
83
85
|
const values = { ...initialValues };
|
|
@@ -147,9 +149,9 @@ export function createFormEngine({
|
|
|
147
149
|
const allHelpers = { ...calculationBuiltins, ...helpers };
|
|
148
150
|
|
|
149
151
|
// Initialize context resolution system
|
|
150
|
-
const contextResolver = new ContextResolver(form);
|
|
151
152
|
const calculationPlan = buildCalculationDependencyPlan(form, contextResolver);
|
|
152
153
|
const sharedWarningSystem = warningSystem || new WarningSystem();
|
|
154
|
+
const conditionContext = createConditionContext(form, contextResolver, sharedWarningSystem);
|
|
153
155
|
const calculationDiagnostics = createCalculationDiagnostics(
|
|
154
156
|
diagnostics,
|
|
155
157
|
onDiagnostics,
|
|
@@ -196,9 +198,9 @@ export function createFormEngine({
|
|
|
196
198
|
calculationPlan,
|
|
197
199
|
calculationDiagnostics
|
|
198
200
|
);
|
|
199
|
-
evaluateRequirement(form, values, required);
|
|
200
|
-
evaluateVisibility(form, values, visible);
|
|
201
|
-
evaluateReadOnly(form, values, read_only);
|
|
201
|
+
evaluateRequirement(form, values, required, conditionContext);
|
|
202
|
+
evaluateVisibility(form, values, visible, conditionContext);
|
|
203
|
+
evaluateReadOnly(form, values, read_only, conditionContext);
|
|
202
204
|
validateFields(form, values, errors);
|
|
203
205
|
} catch (error) {
|
|
204
206
|
calculationDiagnostics.abort(frame);
|
|
@@ -247,7 +249,7 @@ export function createFormEngine({
|
|
|
247
249
|
};
|
|
248
250
|
}
|
|
249
251
|
|
|
250
|
-
function getDefaultValue(field) {
|
|
252
|
+
export function getDefaultValue(field) {
|
|
251
253
|
const spec = FIELD_SPECS[field.type];
|
|
252
254
|
if (spec && spec.defaultProducer) {
|
|
253
255
|
return spec.defaultProducer(field);
|
|
@@ -30,6 +30,10 @@ function copy(value, seen = new Map()) {
|
|
|
30
30
|
return result;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
const managedReferenceCaches = new WeakMap();
|
|
34
|
+
const managedFieldCaches = new WeakMap();
|
|
35
|
+
const managedRepeatableCaches = new WeakMap();
|
|
36
|
+
|
|
33
37
|
/** Internal adapter; the public synchronous API is engine.triggerDetailed(). */
|
|
34
38
|
export function createManagedEventDispatcher(manager, constructionScope) {
|
|
35
39
|
const instancePath = copy(constructionScope?.instancePath);
|
|
@@ -37,18 +41,27 @@ export function createManagedEventDispatcher(manager, constructionScope) {
|
|
|
37
41
|
if (scopeId !== undefined && (typeof scopeId !== 'string' || !scopeId.trim()))
|
|
38
42
|
throw new Error('eventScope.scopeId must be a nonempty string when supplied.');
|
|
39
43
|
const resolver = manager.contextResolver;
|
|
40
|
-
|
|
44
|
+
if (!managedFieldCaches.has(resolver))
|
|
45
|
+
managedFieldCaches.set(resolver, new Map(resolver.fieldOwnership));
|
|
46
|
+
const fields = managedFieldCaches.get(resolver);
|
|
47
|
+
if (!managedRepeatableCaches.has(resolver))
|
|
48
|
+
managedRepeatableCaches.set(
|
|
49
|
+
resolver,
|
|
50
|
+
new Map(
|
|
51
|
+
[...resolver.repeatableSectionTree.values()].map((entry) => [
|
|
52
|
+
JSON.stringify([entry.parentPath, entry.preferredKey]),
|
|
53
|
+
entry,
|
|
54
|
+
])
|
|
55
|
+
)
|
|
56
|
+
);
|
|
57
|
+
const repeatableIndex = managedRepeatableCaches.get(resolver);
|
|
41
58
|
const statusField = manager.schema?.status_field;
|
|
42
59
|
if (statusField?.enabled && typeof statusField.data_name === 'string')
|
|
43
60
|
fields.set(statusField.data_name, { field: statusField, parentPath: [] });
|
|
44
61
|
if (!Array.isArray(instancePath)) throw new Error('eventScope.instancePath must be an array.');
|
|
45
62
|
const ownershipPath = [];
|
|
46
63
|
for (const segment of instancePath) {
|
|
47
|
-
const repeatable =
|
|
48
|
-
(entry) =>
|
|
49
|
-
entry.preferredKey === segment?.repeatableKey &&
|
|
50
|
-
JSON.stringify(entry.parentPath) === JSON.stringify(ownershipPath)
|
|
51
|
-
);
|
|
64
|
+
const repeatable = repeatableIndex.get(JSON.stringify([ownershipPath, segment?.repeatableKey]));
|
|
52
65
|
if (
|
|
53
66
|
!repeatable ||
|
|
54
67
|
JSON.stringify(repeatable.parentPath) !== JSON.stringify(ownershipPath) ||
|
|
@@ -70,7 +83,8 @@ export function createManagedEventDispatcher(manager, constructionScope) {
|
|
|
70
83
|
const readable = (path) =>
|
|
71
84
|
path.length <= ownershipPath.length && path.every((key, index) => ownershipPath[index] === key);
|
|
72
85
|
const ownedField = (name) => fields.get(name) || resolver.repeatableSectionTree.get(name);
|
|
73
|
-
|
|
86
|
+
if (!managedReferenceCaches.has(manager)) managedReferenceCaches.set(manager, new WeakMap());
|
|
87
|
+
const referenceCache = managedReferenceCaches.get(manager);
|
|
74
88
|
return function dispatch(eventType, fieldKey, metadata = {}) {
|
|
75
89
|
const result = { operations: [], validationIssues: [], issues: [] };
|
|
76
90
|
const issue = (code, message) =>
|
|
@@ -149,8 +163,12 @@ export function createManagedEventDispatcher(manager, constructionScope) {
|
|
|
149
163
|
let collected;
|
|
150
164
|
try {
|
|
151
165
|
const source = manager.callbackSources.getSource(callback);
|
|
152
|
-
if (referenceCache.get(callback)?.source !== source)
|
|
153
|
-
|
|
166
|
+
if (referenceCache.get(callback)?.source !== source) {
|
|
167
|
+
const references =
|
|
168
|
+
manager.referenceSourceCache?.get(source) ?? getEventFieldReferences(source);
|
|
169
|
+
manager.referenceSourceCache?.set(source, references);
|
|
170
|
+
referenceCache.set(callback, { source, references });
|
|
171
|
+
}
|
|
154
172
|
let rejectedReference = false;
|
|
155
173
|
for (const name of referenceCache.get(callback).references) {
|
|
156
174
|
const field = fields.get(name);
|