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.0",
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
- All builtins are designed to be safe for use in form contexts:
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
- - **No side effects**: Builtins don't modify external state
187
- - **Sandboxed execution**: Cannot access system resources
188
- - **Input validation**: Arguments are validated before processing
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
@@ -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: (a, b) => a === b,
5
- not_equal_to: (a, b) => a !== b,
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];
@@ -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(collectedOps, executionContext);
423
+ const validOps = this.validateOperations(runtimeOps, executionContext);
421
424
  operations.push(...validOps);
422
425
 
423
- // Note: EVENT_OPERATION processing (ON/OFF) is handled only during initialization
424
- // If event handlers contain ON/OFF calls, they will be in the operations array
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') {