form0-core 0.3.0 → 0.3.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"registry": "https://registry.npmjs.org/"
|
|
6
6
|
},
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"SECURITY.md"
|
|
13
13
|
],
|
|
14
14
|
"scripts": {
|
|
15
|
-
"test": "node tests/form0-core.test.js && node tests/numeric-field-schema.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/title-fields.test.js && node tests/builtin-metadata.test.js && node tests/calculation-expression-utils.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/form-authoring.test.js && node tests/security-test.js",
|
|
15
|
+
"test": "node tests/form0-core.test.js && node tests/conditions.test.js && node tests/numeric-field-schema.test.js && node tests/dataset-descriptors.test.js && node tests/structured-record.test.js && node tests/title-fields.test.js && node tests/builtin-metadata.test.js && node tests/calculation-expression-utils.test.js && node tests/calculation-editor.test.js && node tests/form-event-editor.test.js && node tests/form-authoring.test.js && node tests/security-test.js",
|
|
16
16
|
"format": "prettier --write \"src/**/*.{js,jsx}\" \"tests/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" package.json README.md SECURITY.md SUPPORT.md CONTRIBUTING.md eslint.config.js .github/workflows/ci.yml .github/workflows/publish.yml",
|
|
17
17
|
"format:check": "prettier --check \"src/**/*.{js,jsx}\" \"tests/**/*.{js,jsx}\" \"scripts/**/*.{js,mjs}\" package.json README.md SECURITY.md SUPPORT.md CONTRIBUTING.md eslint.config.js .github/workflows/ci.yml .github/workflows/publish.yml",
|
|
18
18
|
"lint:check": "eslint src tests",
|
|
@@ -181,11 +181,14 @@ IF(user.age >= 18, "Adult", "Minor")
|
|
|
181
181
|
|
|
182
182
|
## Security
|
|
183
183
|
|
|
184
|
-
|
|
184
|
+
Builtins provide defined interfaces for common form operations, and their arguments are validated
|
|
185
|
+
where applicable. Event builtins such as `SETVALUE` and `ALERT` return operations for the host to
|
|
186
|
+
apply instead of mutating application state directly.
|
|
185
187
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
Using a builtin does not sandbox the JavaScript expression or event script that calls it. Schemas
|
|
189
|
+
containing executable code must come from trusted authors. `SAFE` and `CUSTOM` modes provide
|
|
190
|
+
validation and configuration controls, not isolation. See the project
|
|
191
|
+
[security policy](../../../SECURITY.md) for the trust model and application responsibilities.
|
|
189
192
|
|
|
190
193
|
## Extending Builtins
|
|
191
194
|
|
|
@@ -252,4 +255,4 @@ SETRESULT(
|
|
|
252
255
|
|
|
253
256
|
- Explore individual builtin documentation for detailed usage
|
|
254
257
|
- Check out the [form0 examples](../examples.mdx) for real-world scenarios
|
|
255
|
-
- Learn about [custom helpers](../custom-helpers.mdx) for extending functionality
|
|
258
|
+
- Learn about [custom helpers](../custom-helpers.mdx) for extending functionality
|
package/src/engine/conditions.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { flattenFields } from '../utilities/field-helpers.js';
|
|
2
2
|
|
|
3
|
+
const valuesEqual = (a, b) => {
|
|
4
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
5
|
+
if (a.length !== b.length) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const unmatched = [...b];
|
|
10
|
+
return a.every((value) => {
|
|
11
|
+
const matchIndex = unmatched.findIndex((candidate) => candidate === value);
|
|
12
|
+
if (matchIndex === -1) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
unmatched.splice(matchIndex, 1);
|
|
16
|
+
return true;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return a === b;
|
|
21
|
+
};
|
|
22
|
+
|
|
3
23
|
const OPERATORS = {
|
|
4
|
-
equal_to:
|
|
5
|
-
not_equal_to: (a, b) => a
|
|
24
|
+
equal_to: valuesEqual,
|
|
25
|
+
not_equal_to: (a, b) => !valuesEqual(a, b),
|
|
6
26
|
greater_than: (a, b) => a > b,
|
|
7
27
|
less_than: (a, b) => a < b,
|
|
8
28
|
greater_or_equal_than: (a, b) => a >= b,
|
|
@@ -15,6 +35,52 @@ const OPERATORS = {
|
|
|
15
35
|
!(a === null || a === undefined || a === '' || (Array.isArray(a) && a.length === 0)),
|
|
16
36
|
};
|
|
17
37
|
|
|
38
|
+
const hasValue = (entry, property) =>
|
|
39
|
+
entry &&
|
|
40
|
+
typeof entry === 'object' &&
|
|
41
|
+
Object.prototype.hasOwnProperty.call(entry, property) &&
|
|
42
|
+
entry[property] !== null &&
|
|
43
|
+
entry[property] !== undefined &&
|
|
44
|
+
entry[property] !== '';
|
|
45
|
+
|
|
46
|
+
const getOtherValue = (entry) => {
|
|
47
|
+
if (hasValue(entry, 'value')) {
|
|
48
|
+
return entry.value;
|
|
49
|
+
}
|
|
50
|
+
if (hasValue(entry, 'label')) {
|
|
51
|
+
return entry.label;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function normalizeConditionValue(field, value) {
|
|
57
|
+
if (!field || !value || typeof value !== 'object' || Array.isArray(value)) {
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (field.type === 'SingleChoiceField' || field.type === 'BooleanField') {
|
|
62
|
+
const selectedChoice = Array.isArray(value.choice) ? value.choice[0] : null;
|
|
63
|
+
if (hasValue(selectedChoice, 'value')) {
|
|
64
|
+
return selectedChoice.value;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const otherEntry = Array.isArray(value.other) ? value.other[0] : null;
|
|
68
|
+
return getOtherValue(otherEntry);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (field.type === 'MultiChoiceField') {
|
|
72
|
+
const selectedValues = Array.isArray(value.choices)
|
|
73
|
+
? value.choices.filter((choice) => hasValue(choice, 'value')).map((choice) => choice.value)
|
|
74
|
+
: [];
|
|
75
|
+
const otherValues = Array.isArray(value.other)
|
|
76
|
+
? value.other.map(getOtherValue).filter((otherValue) => otherValue !== null)
|
|
77
|
+
: [];
|
|
78
|
+
return [...selectedValues, ...otherValues];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
18
84
|
export function evaluateConditions(conditions, values, allFields) {
|
|
19
85
|
if (Array.isArray(conditions)) {
|
|
20
86
|
return conditions.every((c) => evaluateConditions(c, values, allFields));
|
|
@@ -31,7 +97,7 @@ export function evaluateConditions(conditions, values, allFields) {
|
|
|
31
97
|
// Map field_id (key) to data_name for value lookup
|
|
32
98
|
const field = allFields ? allFields[conditions.field_id] : undefined;
|
|
33
99
|
const dataName = field ? field.data_name : undefined;
|
|
34
|
-
const val = dataName ? values[dataName] : undefined;
|
|
100
|
+
const val = normalizeConditionValue(field, dataName ? values[dataName] : undefined);
|
|
35
101
|
// Debug log: show what is being checked
|
|
36
102
|
//console.log('[evaluateConditions] field_id:', conditions.field_id, 'data_name:', dataName, 'value in values:', val, 'expected:', conditions.value, 'operator:', conditions.operator);
|
|
37
103
|
const fn = OPERATORS[conditions.operator];
|
package/src/engine/events.js
CHANGED
|
@@ -415,14 +415,16 @@ export class EventManager {
|
|
|
415
415
|
|
|
416
416
|
// Consume any collected event operations
|
|
417
417
|
const collectedOps = __consumeEventOperations();
|
|
418
|
+
const runtimeOps = collectedOps.filter(
|
|
419
|
+
(operation) => operation?.type !== 'EVENT_OPERATION'
|
|
420
|
+
);
|
|
418
421
|
|
|
419
422
|
// Validate operations against context restrictions before adding them
|
|
420
|
-
const validOps = this.validateOperations(
|
|
423
|
+
const validOps = this.validateOperations(runtimeOps, executionContext);
|
|
421
424
|
operations.push(...validOps);
|
|
422
425
|
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
// but won't be processed here to avoid duplicate registrations
|
|
426
|
+
// EVENT_OPERATION processing is initialization-only. ON/OFF calls inside a handler neither
|
|
427
|
+
// mutate the listener registry nor leak internal registration descriptors to the host.
|
|
426
428
|
|
|
427
429
|
// For backward compatibility, still handle returned operations
|
|
428
430
|
if (result && result.type === 'UI_OPERATION') {
|