form0-core 0.0.0 → 0.1.0-beta.2

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +38 -0
  3. package/SECURITY.md +179 -0
  4. package/package.json +49 -7
  5. package/scripts/postinstall.cjs +38 -0
  6. package/src/builtins/choice/choicelabel.js +43 -0
  7. package/src/builtins/choice/choicelabels.js +43 -0
  8. package/src/builtins/choice/choicevalue.js +43 -0
  9. package/src/builtins/choice/choicevalues.js +43 -0
  10. package/src/builtins/choice/hasother.js +37 -0
  11. package/src/builtins/choice/other.js +44 -0
  12. package/src/builtins/control/eval.js +223 -0
  13. package/src/builtins/control/setresult.js +33 -0
  14. package/src/builtins/docs/choice/choicelabel.mdx +110 -0
  15. package/src/builtins/docs/choice/choicelabels.mdx +230 -0
  16. package/src/builtins/docs/choice/choicevalue.mdx +109 -0
  17. package/src/builtins/docs/choice/choicevalues.mdx +196 -0
  18. package/src/builtins/docs/choice/hasother.mdx +133 -0
  19. package/src/builtins/docs/choice/other.mdx +150 -0
  20. package/src/builtins/docs/control/setresult.mdx +203 -0
  21. package/src/builtins/docs/event/field/setvalue.mdx +239 -0
  22. package/src/builtins/docs/index.mdx +234 -0
  23. package/src/builtins/docs/logical/and.mdx +141 -0
  24. package/src/builtins/docs/logical/if.mdx +94 -0
  25. package/src/builtins/docs/logical/or.mdx +191 -0
  26. package/src/builtins/docs/overview.mdx +255 -0
  27. package/src/builtins/event/control/off.js +60 -0
  28. package/src/builtins/event/control/on.js +47 -0
  29. package/src/builtins/event/event-operations-collector.js +20 -0
  30. package/src/builtins/event/field/setvalue.js +49 -0
  31. package/src/builtins/event/ui/alert.js +26 -0
  32. package/src/builtins/logical/and.js +13 -0
  33. package/src/builtins/logical/array.js +92 -0
  34. package/src/builtins/logical/count.js +24 -0
  35. package/src/builtins/logical/counta.js +24 -0
  36. package/src/builtins/logical/countblank.js +26 -0
  37. package/src/builtins/logical/if.js +15 -0
  38. package/src/builtins/logical/or.js +13 -0
  39. package/src/builtins/math/abs.js +16 -0
  40. package/src/builtins/math/ceiling.js +22 -0
  41. package/src/builtins/math/cos.js +16 -0
  42. package/src/builtins/math/round.js +27 -0
  43. package/src/builtins/math/sin.js +16 -0
  44. package/src/builtins/registry.js +99 -0
  45. package/src/builtins/schema/datanames.js +77 -0
  46. package/src/builtins/schema/form.js +20 -0
  47. package/src/builtins/string/upper.js +21 -0
  48. package/src/engine/calculation.js +206 -0
  49. package/src/engine/conditions.js +153 -0
  50. package/src/engine/context-resolver.js +318 -0
  51. package/src/engine/evaluator.js +75 -0
  52. package/src/engine/event-registry.js +76 -0
  53. package/src/engine/events.js +483 -0
  54. package/src/engine/field-validation.js +18 -0
  55. package/src/engine/form-engine.js +242 -0
  56. package/src/engine/warning-system.js +256 -0
  57. package/src/index.js +48 -0
  58. package/src/schema/ai-metadata-validator.js +297 -0
  59. package/src/schema/attribute-validator.js +237 -0
  60. package/src/schema/building-plan-blueprint.js +1204 -0
  61. package/src/schema/building-plan-expander.js +261 -0
  62. package/src/schema/field-schema-registry.js +232 -0
  63. package/src/schema/field-specs.js +1537 -0
  64. package/src/schema/field-value-registry.js +314 -0
  65. package/src/schema/form-link-validators.js +131 -0
  66. package/src/schema/operators.js +246 -0
  67. package/src/schema/schema-validator.js +284 -0
  68. package/src/security/config.js +37 -0
  69. package/src/security/validation.js +154 -0
  70. package/src/utilities/ai-metadata.js +93 -0
  71. package/src/utilities/field-helpers.js +189 -0
  72. package/src/utilities/field-types.js +7 -0
  73. package/src/utilities/form-link-helpers.js +301 -0
  74. package/src/utilities/hash.js +17 -0
  75. package/src/utilities/record-transformer.js +666 -0
  76. package/src/utilities/repeatable-helpers.js +142 -0
  77. package/src/utilities/uuid.js +45 -0
  78. package/src/utilities/version-utils.js +97 -0
  79. package/index.js +0 -1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 paqu.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # form0-core
2
+
3
+ [![NPM Version](https://img.shields.io/npm/v/form0-core)](https://www.npmjs.com/package/form0-core)
4
+ [![NPM Downloads](https://img.shields.io/npm/dt/form0-core)](https://www.npmjs.com/package/form0-core)
5
+ [![License](https://img.shields.io/npm/l/form0-core)](LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-docs.form0.dev-2563eb)](https://docs.form0.dev)
7
+ [![Website](https://img.shields.io/badge/site-form0.dev-0f172a)](https://form0.dev)
8
+
9
+ > [!WARNING]
10
+ > form0 is in active, very early development. Do not use in production. Expect breaking
11
+ > changes and unstable behavior.
12
+
13
+ form0-core is the schema-driven engine that powers form0 ecosystem. It is framework-agnostic and runs in any JavaScript runtime (Node.js, browsers, React Native, etc.)
14
+
15
+ ## 🚀 Start with the CLI (recommended)
16
+
17
+ The entry point for most users is form0-cli. Follow the quickstart to create a project and preview
18
+ your schema:
19
+ - https://docs.form0.dev/getting-started/quickstart
20
+
21
+ ## 🗂️ Documentation
22
+
23
+ - Docs home: https://docs.form0.dev
24
+ - form0-core overview: https://docs.form0.dev/core/overview
25
+ - form0-core concepts: https://docs.form0.dev/core/concepts
26
+ - Main site: https://form0.dev
27
+
28
+ ## Direct usage (advanced)
29
+
30
+ If you are integrating the engine directly:
31
+
32
+ ```bash
33
+ npm install form0-core
34
+ ```
35
+
36
+ ## Security
37
+
38
+ See `SECURITY.md` for security modes and configuration.
package/SECURITY.md ADDED
@@ -0,0 +1,179 @@
1
+ # Security Features
2
+
3
+ form0-core provides configurable security options for expression evaluation to balance flexibility and safety.
4
+
5
+ ## Security Modes
6
+
7
+ ### TRUSTED (Default)
8
+
9
+ - **Full JavaScript access** - Current behavior maintained
10
+ - **No restrictions** - All expressions execute as-is
11
+ - **Use case**: Internal tools, trusted developers, client-side only
12
+
13
+ ```javascript
14
+ import { createFormEngine } from 'form0-core';
15
+
16
+ // Default behavior - no security parameter needed
17
+ const engine = createFormEngine({ schema });
18
+
19
+ // Or explicitly set trusted mode
20
+ const engine = createFormEngine({
21
+ schema,
22
+ security: { mode: 'trusted' },
23
+ });
24
+ ```
25
+
26
+ ### SAFE
27
+
28
+ - **Restricted context** - Only whitelisted globals available
29
+ - **Pattern blocking** - Dangerous patterns are blocked
30
+ - **Use case**: User-generated expressions, safer environments
31
+
32
+ ```javascript
33
+ import { createFormEngine, SECURITY_MODES, SAFE_SECURITY_CONFIG } from 'form0-core';
34
+
35
+ // Simple safe mode
36
+ const engine = createFormEngine({
37
+ schema,
38
+ security: { mode: SECURITY_MODES.SAFE },
39
+ });
40
+
41
+ // Or use predefined safe config
42
+ const engine = createFormEngine({
43
+ schema,
44
+ security: SAFE_SECURITY_CONFIG,
45
+ });
46
+ ```
47
+
48
+ ### CUSTOM
49
+
50
+ - **User-defined rules** - Configure your own security settings
51
+ - **Flexible restrictions** - Mix and match security features
52
+ - **Use case**: Specific security requirements
53
+
54
+ ```javascript
55
+ const engine = createFormEngine({
56
+ schema,
57
+ security: {
58
+ mode: SECURITY_MODES.CUSTOM,
59
+ maxExecutionTime: 1000,
60
+ allowedGlobals: ['Math', 'Date', 'Number'],
61
+ blockedPatterns: [/\bwindow\b/, /\bdocument\b/, /\bfetch\b/],
62
+ },
63
+ });
64
+ ```
65
+
66
+ ## Security Configuration Options
67
+
68
+ ```javascript
69
+ // TRUSTED mode (default) - no additional config needed
70
+ const trustedConfig = {
71
+ mode: 'trusted', // Full JavaScript access
72
+ };
73
+
74
+ // SAFE mode - uses predefined safe settings
75
+ const safeConfig = {
76
+ mode: 'safe', // Automatically applies safe defaults
77
+ };
78
+
79
+ // CUSTOM mode - define your own rules
80
+ const customConfig = {
81
+ mode: 'custom',
82
+ maxExecutionTime: 1000, // Milliseconds (future feature)
83
+ maxCallStackDepth: 100, // Maximum recursion depth (future feature)
84
+ allowedGlobals: ['Math', 'Date', 'JSON'], // Whitelisted global objects
85
+ blockedPatterns: [/\beval\b/, /\bwindow\b/], // Regex patterns to block
86
+ };
87
+ ```
88
+
89
+ ## Default Blocked Patterns (Safe Mode)
90
+
91
+ The following patterns are blocked by default in safe mode:
92
+
93
+ - `eval`, `Function` - Code execution
94
+ - `window`, `document` - Browser globals
95
+ - `process`, `require` - Node.js globals
96
+ - `fetch`, `XMLHttpRequest` - Network requests
97
+ - `localStorage`, `sessionStorage` - Storage APIs
98
+ - `__proto__`, `constructor`, `prototype` - Prototype pollution
99
+
100
+ ## Migration Guide
101
+
102
+ ### No Changes Needed
103
+
104
+ Existing code continues to work unchanged:
105
+
106
+ ```javascript
107
+ // This still works exactly as before
108
+ const engine = createFormEngine({ schema, initialValues });
109
+ ```
110
+
111
+ ### Adding Security
112
+
113
+ To add security, simply include the security parameter:
114
+
115
+ ```javascript
116
+ // Add safe mode
117
+ const engine = createFormEngine({
118
+ schema,
119
+ initialValues,
120
+ security: { mode: 'safe' },
121
+ });
122
+ ```
123
+
124
+ ## Examples
125
+
126
+ ### Safe Mathematical Calculations
127
+
128
+ ```javascript
129
+ const schema = {
130
+ form: {
131
+ elements: [
132
+ {
133
+ type: 'CalculatedField',
134
+ data_name: 'result',
135
+ calculate: 'Math.max($value1, $value2) * 1.1', // ✅ Works in safe mode
136
+ },
137
+ ],
138
+ },
139
+ };
140
+ ```
141
+
142
+ ### Blocked Dangerous Expressions
143
+
144
+ ```javascript
145
+ const schema = {
146
+ form: {
147
+ elements: [
148
+ {
149
+ type: 'CalculatedField',
150
+ data_name: 'result',
151
+ calculate: 'window.alert("hello")', // ❌ Blocked in safe mode
152
+ },
153
+ ],
154
+ },
155
+ };
156
+ ```
157
+
158
+ ### Custom Security Rules
159
+
160
+ ```javascript
161
+ const engine = createFormEngine({
162
+ schema,
163
+ security: {
164
+ mode: 'custom',
165
+ allowedGlobals: ['Math'], // Only Math allowed
166
+ blockedPatterns: [/\bDate\b/], // Block Date usage
167
+ },
168
+ });
169
+ ```
170
+
171
+ ## Testing Security
172
+
173
+ Use the included test file to verify security behavior:
174
+
175
+ ```bash
176
+ node test/security-test.js
177
+ ```
178
+
179
+ This will show how different expressions behave under different security modes.
package/package.json CHANGED
@@ -1,12 +1,54 @@
1
1
  {
2
2
  "name": "form0-core",
3
- "version": "0.0.0",
4
- "main": "index.js",
3
+ "version": "0.1.0-beta.2",
4
+ "publishConfig": {
5
+ "registry": "https://registry.npmjs.org/"
6
+ },
7
+ "description": "Schema-driven engine that powers form0 ecosystem.",
8
+ "main": "src/index.js",
9
+ "type": "module",
5
10
  "scripts": {
6
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "test": "node tests/form0-core.test.js && node tests/security-test.js",
12
+ "postinstall": "node scripts/postinstall.cjs",
13
+ "format": "prettier --write \"**/*.{js,jsx,json,ts,tsx,md}\"",
14
+ "format:check": "prettier --check \"**/*.{js,jsx,json,ts,tsx,md}\""
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "import": "./src/index.js"
19
+ }
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/paqu-io/form0-core.git"
24
+ },
25
+ "keywords": [
26
+ "form",
27
+ "form-builder",
28
+ "form-generator",
29
+ "form-engine",
30
+ "schema",
31
+ "json-schema",
32
+ "react",
33
+ "react-native",
34
+ "dynamic-forms"
35
+ ],
36
+ "author": "paqu.io <hello@paqu.io> (https://paqu.io/)",
37
+ "contributors": [
38
+ {
39
+ "name": "aragornii"
40
+ }
41
+ ],
42
+ "license": "MIT",
43
+ "bugs": {
44
+ "url": "https://github.com/paqu-io/form0-core/issues"
45
+ },
46
+ "homepage": "https://form0.dev/",
47
+ "engines": {
48
+ "node": ">=18.0.0"
7
49
  },
8
- "keywords": [],
9
- "author": "",
10
- "license": "ISC",
11
- "description": ""
50
+ "devDependencies": {
51
+ "ignore": "^7.0.5",
52
+ "prettier": "^3.5.3"
53
+ }
12
54
  }
@@ -0,0 +1,38 @@
1
+ const PACKAGE_NAME = 'form0-core';
2
+
3
+ const npmUserAgent = process.env.npm_config_user_agent || '';
4
+ const npmArgv = process.env.npm_config_argv;
5
+
6
+ const isSupportedManager = /(npm|pnpm|yarn|bun)\//.test(npmUserAgent);
7
+
8
+ const parseNpmArgv = (raw) => {
9
+ if (!raw) return null;
10
+ try {
11
+ return JSON.parse(raw);
12
+ } catch {
13
+ return null;
14
+ }
15
+ };
16
+
17
+ const includesExplicitPackage = (argv) => {
18
+ if (!argv) return false;
19
+ const args = Array.isArray(argv.original) ? argv.original : argv.cooked || [];
20
+ return args.some(
21
+ (arg) => arg === PACKAGE_NAME || arg.startsWith(`${PACKAGE_NAME}@`),
22
+ );
23
+ };
24
+
25
+ if (isSupportedManager && includesExplicitPackage(parseNpmArgv(npmArgv))) {
26
+ // Keep message short to avoid noisy installs.
27
+ console.warn(
28
+ [
29
+ '',
30
+ 'form0-core is the engine only.',
31
+ 'For the full form0 ecosystem, install the CLI:',
32
+ ' npm install -g form0-cli',
33
+ 'Docs: https://docs.form0.dev/getting-started/quickstart',
34
+ 'If you are integrating the engine directly, you can ignore this warning.',
35
+ '',
36
+ ].join('\n'),
37
+ );
38
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @builtin CHOICELABEL
3
+ * @description Retrieves the currently selected choice field label, preserving the type (if the label is a number preserves the number type, otherwise string). If no selection it returns null.
4
+ * @param {Object} choiceField - The choice field object with choice and other arrays
5
+ * @returns {*} The selected choice label with preserved type, or null if no selection
6
+ * @example
7
+ * // Get the selected city label
8
+ * CHOICELABEL($city)
9
+ * @example
10
+ * // Use in display logic
11
+ * "You selected: " + CHOICELABEL($city)
12
+ */
13
+ export const CHOICELABEL = (choiceField) => {
14
+ // Handle null/undefined input
15
+ if (!choiceField || typeof choiceField !== 'object') {
16
+ return null;
17
+ }
18
+
19
+ // Validate structure
20
+ if (!Array.isArray(choiceField.choice)) {
21
+ return null;
22
+ }
23
+
24
+ // Get the selected choice (single selection)
25
+ if (choiceField.choice.length > 0) {
26
+ const selectedChoice = choiceField.choice[0];
27
+ if (selectedChoice && selectedChoice.label !== undefined) {
28
+ // Preserve type - if it's a number, keep it as number
29
+ const label = selectedChoice.label;
30
+
31
+ // Try to parse as number if it's a string that represents a number
32
+ if (typeof label === 'string' && !isNaN(label) && !isNaN(parseFloat(label))) {
33
+ // Check if it's an integer or float
34
+ const numLabel = parseFloat(label);
35
+ return Number.isInteger(numLabel) ? parseInt(label, 10) : numLabel;
36
+ }
37
+
38
+ return label;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @builtin CHOICELABELS
3
+ * @description Retrieves an array of all selected choice field labels from a MultiChoiceField, preserving the type of each label. Returns an empty array if no selections.
4
+ * @param {Object} multiChoiceField - The multi choice field object with choices and other arrays
5
+ * @returns {Array} Array of selected choice labels with preserved types
6
+ * @example
7
+ * // Get all selected color labels
8
+ * CHOICELABELS($colors)
9
+ * @example
10
+ * // Display selected colors
11
+ * "Selected colors: " + CHOICELABELS($colors).join(", ")
12
+ */
13
+ export const CHOICELABELS = (multiChoiceField) => {
14
+ // Handle null/undefined input
15
+ if (!multiChoiceField || typeof multiChoiceField !== 'object') {
16
+ return [];
17
+ }
18
+
19
+ // Validate structure
20
+ if (!Array.isArray(multiChoiceField.choices)) {
21
+ return [];
22
+ }
23
+
24
+ // Get all selected choices
25
+ const labels = [];
26
+ for (const choice of multiChoiceField.choices) {
27
+ if (choice && choice.label !== undefined) {
28
+ // Preserve type - if it's a number, keep it as number
29
+ const label = choice.label;
30
+
31
+ // Try to parse as number if it's a string that represents a number
32
+ if (typeof label === 'string' && !isNaN(label) && !isNaN(parseFloat(label))) {
33
+ // Check if it's an integer or float
34
+ const numLabel = parseFloat(label);
35
+ labels.push(Number.isInteger(numLabel) ? parseInt(label, 10) : numLabel);
36
+ } else {
37
+ labels.push(label);
38
+ }
39
+ }
40
+ }
41
+
42
+ return labels;
43
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @builtin CHOICEVALUE
3
+ * @description Retrieves the currently selected choice field value, preserving the type (if the value is a number preserves the number type, otherwise string). If no selection it returns null.
4
+ * @param {Object} choiceField - The choice field object with choice and other arrays
5
+ * @returns {*} The selected choice value with preserved type, or null if no selection
6
+ * @example
7
+ * // Get the selected city value
8
+ * CHOICEVALUE($city)
9
+ * @example
10
+ * // Use in conditional logic
11
+ * IF(CHOICEVALUE($city) === "bogota", "Welcome to Bogotá!", "Welcome!")
12
+ */
13
+ export const CHOICEVALUE = (choiceField) => {
14
+ // Handle null/undefined input
15
+ if (!choiceField || typeof choiceField !== 'object') {
16
+ return null;
17
+ }
18
+
19
+ // Validate structure
20
+ if (!Array.isArray(choiceField.choice)) {
21
+ return null;
22
+ }
23
+
24
+ // Get the selected choice (single selection)
25
+ if (choiceField.choice.length > 0) {
26
+ const selectedChoice = choiceField.choice[0];
27
+ if (selectedChoice && selectedChoice.value !== undefined) {
28
+ // Preserve type - if it's a number, keep it as number
29
+ const value = selectedChoice.value;
30
+
31
+ // Try to parse as number if it's a string that represents a number
32
+ if (typeof value === 'string' && !isNaN(value) && !isNaN(parseFloat(value))) {
33
+ // Check if it's an integer or float
34
+ const numValue = parseFloat(value);
35
+ return Number.isInteger(numValue) ? parseInt(value, 10) : numValue;
36
+ }
37
+
38
+ return value;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @builtin CHOICEVALUES
3
+ * @description Retrieves an array of all selected choice field values from a MultiChoiceField, preserving the type of each value. Returns an empty array if no selections.
4
+ * @param {Object} multiChoiceField - The multi choice field object with choices and other arrays
5
+ * @returns {Array} Array of selected choice values with preserved types
6
+ * @example
7
+ * // Get all selected color values
8
+ * CHOICEVALUES($colors)
9
+ * @example
10
+ * // Check if "red" is selected
11
+ * CHOICEVALUES($colors).includes("red")
12
+ */
13
+ export const CHOICEVALUES = (multiChoiceField) => {
14
+ // Handle null/undefined input
15
+ if (!multiChoiceField || typeof multiChoiceField !== 'object') {
16
+ return [];
17
+ }
18
+
19
+ // Validate structure
20
+ if (!Array.isArray(multiChoiceField.choices)) {
21
+ return [];
22
+ }
23
+
24
+ // Get all selected choices
25
+ const values = [];
26
+ for (const choice of multiChoiceField.choices) {
27
+ if (choice && choice.value !== undefined) {
28
+ // Preserve type - if it's a number, keep it as number
29
+ const value = choice.value;
30
+
31
+ // Try to parse as number if it's a string that represents a number
32
+ if (typeof value === 'string' && !isNaN(value) && !isNaN(parseFloat(value))) {
33
+ // Check if it's an integer or float
34
+ const numValue = parseFloat(value);
35
+ values.push(Number.isInteger(numValue) ? parseInt(value, 10) : numValue);
36
+ } else {
37
+ values.push(value);
38
+ }
39
+ }
40
+ }
41
+
42
+ return values;
43
+ };
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @builtin HASOTHER
3
+ * @description Returns true if user entered an other option, false otherwise. Works with both SingleChoiceField and MultiChoiceField.
4
+ * @param {Object} choiceField - The choice field object (SingleChoiceField with choice array or MultiChoiceField with choices array) and other array
5
+ * @returns {boolean} True if user entered an other option, false otherwise
6
+ * @example
7
+ * // Check if user entered other option in single choice field
8
+ * HASOTHER($city)
9
+ * @example
10
+ * // Check if user entered other option in multi choice field
11
+ * HASOTHER($colors)
12
+ * @example
13
+ * // Use in conditional logic
14
+ * IF(HASOTHER($city), "Custom city: " + OTHER($city), "Selected city: " + CHOICELABEL($city))
15
+ */
16
+ export const HASOTHER = (choiceField) => {
17
+ // Handle null/undefined input
18
+ if (!choiceField || typeof choiceField !== 'object') {
19
+ return false;
20
+ }
21
+
22
+ // Validate structure - must have either choice array (SingleChoiceField) or choices array (MultiChoiceField)
23
+ const hasChoiceArray = Array.isArray(choiceField.choice);
24
+ const hasChoicesArray = Array.isArray(choiceField.choices);
25
+
26
+ if (!hasChoiceArray && !hasChoicesArray) {
27
+ return false;
28
+ }
29
+
30
+ // Validate other array structure
31
+ if (!Array.isArray(choiceField.other)) {
32
+ return false;
33
+ }
34
+
35
+ // Check if there are any other entries
36
+ return choiceField.other.length > 0;
37
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @builtin OTHER
3
+ * @description Retrieves the other label if user entered the other option, otherwise null. Works with both SingleChoiceField and MultiChoiceField.
4
+ * @param {Object} choiceField - The choice field object (SingleChoiceField with choice array or MultiChoiceField with choices array) and other array
5
+ * @returns {string|null} The other label if user entered an other option, null otherwise
6
+ * @example
7
+ * // Get the other value from single choice field
8
+ * OTHER($city)
9
+ * @example
10
+ * // Get the other value from multi choice field
11
+ * OTHER($colors)
12
+ * @example
13
+ * // Use in conditional logic
14
+ * IF(HASOTHER($city), "Custom city: " + OTHER($city), "No custom city entered")
15
+ */
16
+ export const OTHER = (choiceField) => {
17
+ // Handle null/undefined input
18
+ if (!choiceField || typeof choiceField !== 'object') {
19
+ return null;
20
+ }
21
+
22
+ // Validate structure - must have either choice array (SingleChoiceField) or choices array (MultiChoiceField)
23
+ const hasChoiceArray = Array.isArray(choiceField.choice);
24
+ const hasChoicesArray = Array.isArray(choiceField.choices);
25
+
26
+ if (!hasChoiceArray && !hasChoicesArray) {
27
+ return null;
28
+ }
29
+
30
+ // Validate other array structure
31
+ if (!Array.isArray(choiceField.other)) {
32
+ return null;
33
+ }
34
+
35
+ // Get the other entry (single selection - user can only add 1 other option)
36
+ if (choiceField.other.length > 0) {
37
+ const otherEntry = choiceField.other[0];
38
+ if (otherEntry && otherEntry.label !== undefined) {
39
+ return otherEntry.label;
40
+ }
41
+ }
42
+
43
+ return null;
44
+ };