form0-core 0.3.1 → 0.4.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/README.md +75 -0
- package/package.json +5 -2
- package/src/authoring/form-authoring.js +124 -13
- package/src/builtins/builtin-definitions.js +320 -0
- package/src/builtins/choice/choicelabel.js +2 -17
- package/src/builtins/choice/choicelabels.js +2 -17
- package/src/builtins/choice/choicevalue.js +2 -20
- package/src/builtins/choice/choicevalues.js +2 -17
- package/src/builtins/choice/hasother.js +2 -20
- package/src/builtins/choice/other.js +2 -20
- package/src/builtins/control/eval.js +78 -31
- package/src/builtins/control/setresult.js +29 -17
- package/src/builtins/date/dateadd.js +24 -0
- package/src/builtins/date/days.js +22 -0
- package/src/builtins/event/control/off.js +3 -27
- package/src/builtins/event/control/on.js +3 -19
- package/src/builtins/event/event-operations-collector.js +33 -9
- package/src/builtins/event/field/setvalue.js +3 -32
- package/src/builtins/event/ui/alert.js +3 -11
- package/src/builtins/logical/and.js +2 -17
- package/src/builtins/logical/array.js +2 -35
- package/src/builtins/logical/count.js +2 -20
- package/src/builtins/logical/counta.js +2 -20
- package/src/builtins/logical/countblank.js +2 -20
- package/src/builtins/logical/if.js +2 -20
- package/src/builtins/logical/or.js +2 -17
- package/src/builtins/math/abs.js +2 -20
- package/src/builtins/math/ceiling.js +2 -20
- package/src/builtins/math/cos.js +2 -20
- package/src/builtins/math/round.js +2 -23
- package/src/builtins/math/sin.js +2 -20
- package/src/builtins/registry.js +23 -40
- package/src/builtins/schema/datanames.js +9 -20
- package/src/builtins/schema/form.js +2 -20
- package/src/builtins/string/upper.js +2 -20
- package/src/engine/calculation-diagnostics.js +107 -0
- package/src/engine/calculation.js +65 -32
- package/src/engine/evaluator.js +56 -18
- package/src/engine/event-operation-scope.js +61 -0
- package/src/engine/event-source.js +164 -0
- package/src/engine/events.js +105 -19
- package/src/engine/form-engine.js +61 -16
- package/src/engine/warning-system.js +7 -5
- package/src/schema/field-specs.js +2 -2
- package/src/security/validation.js +10 -8
- package/src/utilities/calculation-editor.js +42 -6
- package/src/utilities/date-utils.js +72 -0
- package/src/builtins/docs/choice/choicelabel.mdx +0 -110
- package/src/builtins/docs/choice/choicelabels.mdx +0 -230
- package/src/builtins/docs/choice/choicevalue.mdx +0 -109
- package/src/builtins/docs/choice/choicevalues.mdx +0 -196
- package/src/builtins/docs/choice/hasother.mdx +0 -133
- package/src/builtins/docs/choice/other.mdx +0 -150
- package/src/builtins/docs/control/setresult.mdx +0 -203
- package/src/builtins/docs/event/field/setvalue.mdx +0 -239
- package/src/builtins/docs/index.mdx +0 -234
- package/src/builtins/docs/logical/and.mdx +0 -141
- package/src/builtins/docs/logical/if.mdx +0 -94
- package/src/builtins/docs/logical/or.mdx +0 -191
- package/src/builtins/docs/overview.mdx +0 -258
package/README.md
CHANGED
|
@@ -81,6 +81,69 @@ console.log(engine.getState());
|
|
|
81
81
|
The engine API exposes `eval()` for calculations, conditions, and validation; `trigger()` for form
|
|
82
82
|
events; and `getState()` for current values and evaluated field state.
|
|
83
83
|
|
|
84
|
+
Schema event callbacks retain their authored source, so ordinary functions and arrows defined in
|
|
85
|
+
`form.events.code` also work on Hermes. Each dispatch uses current scoped field values; initialization
|
|
86
|
+
runs once. Local initialization variables are not captured. Callbacks supplied by host helpers still
|
|
87
|
+
require executable `toString()` output; unavailable source produces an event warning.
|
|
88
|
+
|
|
89
|
+
## Calculation diagnostics
|
|
90
|
+
|
|
91
|
+
Diagnostics are collected automatically, including when console reporting is disabled:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
const engine = createFormEngine({ schema });
|
|
95
|
+
console.log(engine.getDiagnostics()); // [] before the first evaluation
|
|
96
|
+
engine.eval();
|
|
97
|
+
console.log(engine.getDiagnostics()); // latest evaluation, returned as a defensive copy
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Configure reporting in application code:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
const engine = createFormEngine({
|
|
104
|
+
schema,
|
|
105
|
+
diagnostics: { console: false },
|
|
106
|
+
onDiagnostics(snapshot) {
|
|
107
|
+
for (const diagnostic of snapshot) {
|
|
108
|
+
reportCalculationDiagnostic(diagnostic);
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
engine.eval();
|
|
114
|
+
// For a schema calculating quotient from divisor, correct the input and evaluate again:
|
|
115
|
+
engine.getState().values.divisor = 4;
|
|
116
|
+
engine.eval(); // replaces the snapshot; recovered failures disappear
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`onDiagnostics` runs once after every completed evaluation, including identical snapshots and
|
|
120
|
+
`[]`. Observer exceptions cannot interrupt evaluation and are reported according to the console
|
|
121
|
+
policy. Each diagnostic has `code`, `severity`, `message`, `fieldName`, `phase`, `suggestion`, and
|
|
122
|
+
`context`. Context identifies the schema parent path, related/referenced fields, and `expression`
|
|
123
|
+
versus `EVAL` origin. The engine adds no expression source, field values, or stack traces.
|
|
124
|
+
|
|
125
|
+
Identical diagnostics are deduplicated within an evaluation. Distinct failures are retained,
|
|
126
|
+
including failures observed in intermediate stabilization passes even if a later pass succeeds.
|
|
127
|
+
The snapshot records that evaluation; it is not a history or just the final unresolved errors.
|
|
128
|
+
Legitimate `null`, `undefined`, `NaN`, and `Infinity` results do not produce errors by themselves.
|
|
129
|
+
Diagnostics stay separate from `state.errors` and do not change submission validity.
|
|
130
|
+
|
|
131
|
+
Omit `diagnostics.console` to preserve existing defaults and supplied `WarningSystem` settings.
|
|
132
|
+
`false` silences calculation-owned reporting, including `EVAL()` warnings and debug logs; `true`
|
|
133
|
+
prints diagnostic reporting in development and production. Event reporting retains its existing
|
|
134
|
+
behavior. `WarningSystem` remains compatible.
|
|
135
|
+
|
|
136
|
+
The append-only `runtimeDiagnostics` option is deprecated but remains supported for compatibility.
|
|
137
|
+
For new integrations, omit that option and use `engine.getDiagnostics()` for the latest evaluation,
|
|
138
|
+
or `onDiagnostics(snapshot)` to receive every completed evaluation, including recovery (`[]`).
|
|
139
|
+
This is not a drop-in replacement for the legacy array: snapshots use the structured diagnostic
|
|
140
|
+
contract above and replace prior results rather than accumulating them. If you need a history,
|
|
141
|
+
store snapshots in application code. No removal version is scheduled.
|
|
142
|
+
|
|
143
|
+
See [calculation diagnostics](https://docs.form0.dev/core/engine/calculations-dependencies#seeing-errors-and-warnings)
|
|
144
|
+
for stable codes and the reporting contract. CLI and renderer diagnostics adoption is separate
|
|
145
|
+
from this core API.
|
|
146
|
+
|
|
84
147
|
## Schema and record ownership
|
|
85
148
|
|
|
86
149
|
`form0-core` owns behavioral schema concerns such as fields, conditions, calculations, events, and
|
|
@@ -109,6 +172,18 @@ Record utilities follow this contract:
|
|
|
109
172
|
|
|
110
173
|
Record status remains top-level as `@status`; it is not stored inside `form_values`.
|
|
111
174
|
|
|
175
|
+
## Authoring mutation validation
|
|
176
|
+
|
|
177
|
+
`getFormAuthoringContext({ schema })` includes `mutationOperationCatalog`: the installed operation
|
|
178
|
+
formats, required parameters, and examples. `mutationOperations` remains the list of supported names.
|
|
179
|
+
Use these formats with `applyFormMutationBatch`; for example, a rename uses
|
|
180
|
+
`{ op: 'updateForm', changes: { name: 'Renamed form' } }`, not a `patch` parameter.
|
|
181
|
+
|
|
182
|
+
Malformed operations, unknown operation parameters, and empty change objects are rejected atomically
|
|
183
|
+
with diagnostics, rather than silently ignored. Clearing a title/status field, calculation, or event
|
|
184
|
+
code requires its explicit parameter (for example, `field: null`), not an omitted value. Valid
|
|
185
|
+
idempotent batches remain valid in core; consumers should check actual changes before offering to save.
|
|
186
|
+
|
|
112
187
|
## ✅ Requirements
|
|
113
188
|
|
|
114
189
|
- Node.js 22 or newer
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "form0-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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/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",
|
|
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/builtin-import-isolation.test.js && node tests/builtin-catalog-examples.test.js && node tests/date-builtins.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/calculation-dependency-engine.test.js && node tests/calculation-diagnostics.characterization.test.js && node tests/calculation-diagnostics.test.js && node tests/calculation-missing-result.test.js && node tests/evaluator-reentry.test.js && node tests/event-compatibility.characterization.test.js && node tests/event-hermes.test.js && node tests/event-async-isolation.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",
|
|
@@ -63,5 +63,8 @@
|
|
|
63
63
|
"globals": "^17.12.0",
|
|
64
64
|
"ignore": "^7.0.5",
|
|
65
65
|
"prettier": "^3.5.3"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"acorn": "8.18.0"
|
|
66
69
|
}
|
|
67
70
|
}
|
|
@@ -16,17 +16,77 @@ import {
|
|
|
16
16
|
} from '../utilities/form-event-editor.js';
|
|
17
17
|
|
|
18
18
|
const CONTAINER_TYPES = new Set(['Section', 'RepeatableSection', 'BuildingPlanSection']);
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
'
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
const FIELD_KEY_PARAMETER = Object.freeze({ type: 'string', minLength: 1 });
|
|
20
|
+
const CHANGES_PARAMETER = Object.freeze({ type: 'object', minProperties: 1 });
|
|
21
|
+
const PLACEMENT_PARAMETERS = Object.freeze({
|
|
22
|
+
parentKey: { type: ['string', 'null'], minLength: 1 },
|
|
23
|
+
position: {
|
|
24
|
+
type: ['object', 'null'],
|
|
25
|
+
properties: { beforeKey: FIELD_KEY_PARAMETER, afterKey: FIELD_KEY_PARAMETER },
|
|
26
|
+
maxProperties: 1,
|
|
27
|
+
description: 'Use one sibling key, or omit position to append in the target container.',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const MUTATION_OPERATION_CATALOG = Object.freeze([
|
|
31
|
+
{
|
|
32
|
+
op: 'updateForm',
|
|
33
|
+
description:
|
|
34
|
+
'Update form attributes; elements, events, title_field and status_field use dedicated operations.',
|
|
35
|
+
required: ['changes'],
|
|
36
|
+
parameters: { changes: CHANGES_PARAMETER },
|
|
37
|
+
example: { op: 'updateForm', changes: { name: 'Renamed form' } },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
op: 'addField',
|
|
41
|
+
description: 'Add a complete field matching fieldSpecs; missing keys are generated.',
|
|
42
|
+
required: ['field'],
|
|
43
|
+
parameters: { field: { type: 'object', minProperties: 1 }, ...PLACEMENT_PARAMETERS },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
op: 'updateField',
|
|
47
|
+
description: 'Update an existing field by key; type changes require remove/add.',
|
|
48
|
+
required: ['fieldKey', 'changes'],
|
|
49
|
+
parameters: { fieldKey: FIELD_KEY_PARAMETER, changes: CHANGES_PARAMETER },
|
|
50
|
+
example: { op: 'updateField', fieldKey: 'existing-field-key', changes: { label: 'New label' } },
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
op: 'moveField',
|
|
54
|
+
description: 'Move an existing field by key to a container/sibling position.',
|
|
55
|
+
required: ['fieldKey'],
|
|
56
|
+
parameters: { fieldKey: FIELD_KEY_PARAMETER, ...PLACEMENT_PARAMETERS },
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
op: 'removeField',
|
|
60
|
+
description: 'Remove an existing field by key only if it is unreferenced.',
|
|
61
|
+
required: ['fieldKey'],
|
|
62
|
+
parameters: { fieldKey: FIELD_KEY_PARAMETER },
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
op: 'setTitleField',
|
|
66
|
+
description: 'Set a complete title field, or explicitly pass field: null to clear it.',
|
|
67
|
+
required: ['field'],
|
|
68
|
+
parameters: { field: { type: ['object', 'null'] } },
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
op: 'setStatusField',
|
|
72
|
+
description: 'Set a complete status field, or explicitly pass field: null to clear it.',
|
|
73
|
+
required: ['field'],
|
|
74
|
+
parameters: { field: { type: ['object', 'null'] } },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
op: 'setCalculation',
|
|
78
|
+
description: 'Set a CalculatedField expression by key; null explicitly clears it.',
|
|
79
|
+
required: ['fieldKey', 'expression'],
|
|
80
|
+
parameters: { fieldKey: FIELD_KEY_PARAMETER, expression: { type: ['string', 'null'] } },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
op: 'setFormEventCode',
|
|
84
|
+
description: 'Set form event code; an empty string or null explicitly clears it.',
|
|
85
|
+
required: ['code'],
|
|
86
|
+
parameters: { code: { type: ['string', 'null'] } },
|
|
87
|
+
},
|
|
29
88
|
]);
|
|
89
|
+
const MUTATION_OPERATIONS = Object.freeze(MUTATION_OPERATION_CATALOG.map(({ op }) => op));
|
|
30
90
|
|
|
31
91
|
const CALCULATION_GUIDANCE = Object.freeze({
|
|
32
92
|
version: 1,
|
|
@@ -306,16 +366,66 @@ function mutationError(index, operation, error) {
|
|
|
306
366
|
};
|
|
307
367
|
}
|
|
308
368
|
|
|
369
|
+
function validateMutationParameter(value, spec, path) {
|
|
370
|
+
const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
|
|
371
|
+
const types = Array.isArray(spec.type) ? spec.type : [spec.type];
|
|
372
|
+
if (!types.includes(type)) throw new Error(`${path} must be ${types.join(' or ')}`);
|
|
373
|
+
if (value === null) return;
|
|
374
|
+
if (spec.minLength && value.length < spec.minLength) {
|
|
375
|
+
throw new Error(`${path} must not be empty`);
|
|
376
|
+
}
|
|
377
|
+
if (spec.minProperties && Object.keys(value).length < spec.minProperties) {
|
|
378
|
+
throw new Error(`${path} must be a non-empty object`);
|
|
379
|
+
}
|
|
380
|
+
if (spec.maxProperties && Object.keys(value).length > spec.maxProperties) {
|
|
381
|
+
throw new Error(`${path} must specify at most one property`);
|
|
382
|
+
}
|
|
383
|
+
if (spec.properties) {
|
|
384
|
+
for (const key of Object.keys(value)) {
|
|
385
|
+
if (!Object.hasOwn(spec.properties, key)) {
|
|
386
|
+
throw new Error(`Unexpected mutation parameter '${path}.${key}'`);
|
|
387
|
+
}
|
|
388
|
+
validateMutationParameter(value[key], spec.properties[key], `${path}.${key}`);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function validateMutationOperation(operation) {
|
|
394
|
+
if (!operation || typeof operation !== 'object' || Array.isArray(operation)) {
|
|
395
|
+
throw new Error('A mutation operation must be an object with an op property');
|
|
396
|
+
}
|
|
397
|
+
const definition = MUTATION_OPERATION_CATALOG.find(({ op }) => op === operation.op);
|
|
398
|
+
if (!definition) throw new Error(`Unsupported mutation operation '${operation.op}'`);
|
|
399
|
+
for (const name of definition.required) {
|
|
400
|
+
if (!Object.hasOwn(operation, name) || operation[name] === undefined) {
|
|
401
|
+
const example = definition.example ? ` Example: ${JSON.stringify(definition.example)}` : '';
|
|
402
|
+
throw new Error(`${operation.op} requires '${name}'.${example}`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
for (const name of Object.keys(operation)) {
|
|
406
|
+
if (name === 'op') continue;
|
|
407
|
+
if (!Object.hasOwn(definition.parameters, name)) {
|
|
408
|
+
throw new Error(`Unexpected ${operation.op} parameter '${name}'`);
|
|
409
|
+
}
|
|
410
|
+
validateMutationParameter(
|
|
411
|
+
operation[name],
|
|
412
|
+
definition.parameters[name],
|
|
413
|
+
`${operation.op}.${name}`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
309
418
|
function applyMutation(root, operation) {
|
|
419
|
+
validateMutationOperation(operation);
|
|
310
420
|
const form = root.form;
|
|
311
421
|
const fields = indexFields(form.elements);
|
|
312
422
|
switch (operation.op) {
|
|
313
423
|
case 'updateForm': {
|
|
314
424
|
const forbidden = new Set(['elements', 'events', 'title_field', 'status_field']);
|
|
315
|
-
for (const key of Object.keys(operation.changes
|
|
425
|
+
for (const key of Object.keys(operation.changes)) {
|
|
316
426
|
if (forbidden.has(key)) throw new Error(`updateForm cannot change '${key}'`);
|
|
317
427
|
}
|
|
318
|
-
Object.assign(form, deepClone(operation.changes
|
|
428
|
+
Object.assign(form, deepClone(operation.changes));
|
|
319
429
|
return `Updated form attributes`;
|
|
320
430
|
}
|
|
321
431
|
case 'addField': {
|
|
@@ -340,7 +450,7 @@ function applyMutation(root, operation) {
|
|
|
340
450
|
}
|
|
341
451
|
const original = deepClone(target.field);
|
|
342
452
|
const children = target.field.elements;
|
|
343
|
-
Object.assign(target.field, deepClone(operation.changes
|
|
453
|
+
Object.assign(target.field, deepClone(operation.changes));
|
|
344
454
|
if (
|
|
345
455
|
target.field.data_name !== original.data_name &&
|
|
346
456
|
[...fields.values()].some(
|
|
@@ -473,6 +583,7 @@ export function getFormAuthoringContext({ schema, coreVersion = null }) {
|
|
|
473
583
|
revision: getFormSchemaRevision(root),
|
|
474
584
|
schema: deepClone(root),
|
|
475
585
|
mutationOperations: [...MUTATION_OPERATIONS],
|
|
586
|
+
mutationOperationCatalog: deepClone(MUTATION_OPERATION_CATALOG),
|
|
476
587
|
fieldSpecs,
|
|
477
588
|
operators,
|
|
478
589
|
eventTypes: getAllEventTypes(),
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from './builtin-metadata.js';
|
|
2
|
+
|
|
3
|
+
// Authoritative builtin metadata. Keep this catalog independent of implementations
|
|
4
|
+
// so security validation can inspect names and contexts without loading EVAL().
|
|
5
|
+
|
|
6
|
+
export const IF_METADATA = defineBuiltinMetadata({
|
|
7
|
+
name: 'IF',
|
|
8
|
+
category: 'logical',
|
|
9
|
+
signature: 'IF(condition, trueValue, falseValue)',
|
|
10
|
+
description: 'Return one of two values based on a condition.',
|
|
11
|
+
examples: [
|
|
12
|
+
'IF($age >= 18, "adult", "minor")',
|
|
13
|
+
'IF($country === "US", IF($state === "CA", 8.99, 5.99), 15.99)',
|
|
14
|
+
],
|
|
15
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const AND_METADATA = defineBuiltinMetadata({
|
|
19
|
+
name: 'AND',
|
|
20
|
+
category: 'logical',
|
|
21
|
+
signature: 'AND(...conditions)',
|
|
22
|
+
description: 'Return true only when all arguments are truthy.',
|
|
23
|
+
examples: ['AND($age >= 18, $country === "it")'],
|
|
24
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const OR_METADATA = defineBuiltinMetadata({
|
|
28
|
+
name: 'OR',
|
|
29
|
+
category: 'logical',
|
|
30
|
+
signature: 'OR(...conditions)',
|
|
31
|
+
description: 'Return true when at least one argument is truthy.',
|
|
32
|
+
examples: ['OR($email, $phone)'],
|
|
33
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const COUNT_METADATA = defineBuiltinMetadata({
|
|
37
|
+
name: 'COUNT',
|
|
38
|
+
category: 'logical',
|
|
39
|
+
signature: 'COUNT(values)',
|
|
40
|
+
description: 'Count numeric values.',
|
|
41
|
+
examples: ['COUNT(ARRAY($score_1, $score_2, $score_3))'],
|
|
42
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const COUNTA_METADATA = defineBuiltinMetadata({
|
|
46
|
+
name: 'COUNTA',
|
|
47
|
+
category: 'logical',
|
|
48
|
+
signature: 'COUNTA(values)',
|
|
49
|
+
description: 'Count non-empty values.',
|
|
50
|
+
examples: ['COUNTA(ARRAY($first_name, $last_name, $email))'],
|
|
51
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const COUNTBLANK_METADATA = defineBuiltinMetadata({
|
|
55
|
+
name: 'COUNTBLANK',
|
|
56
|
+
category: 'logical',
|
|
57
|
+
signature: 'COUNTBLANK(values)',
|
|
58
|
+
description: 'Count blank values.',
|
|
59
|
+
examples: ['COUNTBLANK(ARRAY($first_name, $last_name, $email))'],
|
|
60
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const ARRAY_METADATA = defineBuiltinMetadata({
|
|
64
|
+
name: 'ARRAY',
|
|
65
|
+
category: 'logical',
|
|
66
|
+
signature: 'ARRAY(...values)',
|
|
67
|
+
description: 'Build an array from individual arguments.',
|
|
68
|
+
examples: ['ARRAY($city, $country)'],
|
|
69
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export const EVAL_METADATA = defineBuiltinMetadata({
|
|
73
|
+
name: 'EVAL',
|
|
74
|
+
category: 'control',
|
|
75
|
+
signature: 'EVAL(expression)',
|
|
76
|
+
description: 'Evaluate a dynamically built expression string in a restricted context.',
|
|
77
|
+
examples: ['EVAL("$" + dynamicFieldName)'],
|
|
78
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const CHOICEVALUE_METADATA = defineBuiltinMetadata({
|
|
82
|
+
name: 'CHOICEVALUE',
|
|
83
|
+
category: 'choice',
|
|
84
|
+
signature: 'CHOICEVALUE(fieldValue)',
|
|
85
|
+
description: 'Return the selected choice value from a choice field.',
|
|
86
|
+
examples: [
|
|
87
|
+
'CHOICEVALUE($city)',
|
|
88
|
+
'IF(CHOICEVALUE($city) === "bogota", "Welcome to Bogota!", "Welcome!")',
|
|
89
|
+
],
|
|
90
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export const CHOICELABEL_METADATA = defineBuiltinMetadata({
|
|
94
|
+
name: 'CHOICELABEL',
|
|
95
|
+
category: 'choice',
|
|
96
|
+
signature: 'CHOICELABEL(fieldValue)',
|
|
97
|
+
description: 'Return the selected choice label from a choice field.',
|
|
98
|
+
examples: ['CHOICELABEL($city)'],
|
|
99
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const HASOTHER_METADATA = defineBuiltinMetadata({
|
|
103
|
+
name: 'HASOTHER',
|
|
104
|
+
category: 'choice',
|
|
105
|
+
signature: 'HASOTHER(fieldValue)',
|
|
106
|
+
description: 'Return true when the field has an "other" value.',
|
|
107
|
+
examples: ['HASOTHER($city)'],
|
|
108
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const OTHER_METADATA = defineBuiltinMetadata({
|
|
112
|
+
name: 'OTHER',
|
|
113
|
+
category: 'choice',
|
|
114
|
+
signature: 'OTHER(fieldValue)',
|
|
115
|
+
description: 'Return the "other" label from a choice field.',
|
|
116
|
+
examples: ['OTHER($city)'],
|
|
117
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const CHOICEVALUES_METADATA = defineBuiltinMetadata({
|
|
121
|
+
name: 'CHOICEVALUES',
|
|
122
|
+
category: 'choice',
|
|
123
|
+
signature: 'CHOICEVALUES(fieldValue)',
|
|
124
|
+
description: 'Return all selected values from a MultiChoiceField.',
|
|
125
|
+
examples: ['CHOICEVALUES($colors)', 'CHOICEVALUES($colors).includes("red")'],
|
|
126
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
export const CHOICELABELS_METADATA = defineBuiltinMetadata({
|
|
130
|
+
name: 'CHOICELABELS',
|
|
131
|
+
category: 'choice',
|
|
132
|
+
signature: 'CHOICELABELS(fieldValue)',
|
|
133
|
+
description: 'Return all selected labels from a MultiChoiceField.',
|
|
134
|
+
examples: ['CHOICELABELS($colors)'],
|
|
135
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const FORM_METADATA = defineBuiltinMetadata({
|
|
139
|
+
name: 'FORM',
|
|
140
|
+
category: 'schema',
|
|
141
|
+
signature: 'FORM()',
|
|
142
|
+
description: 'Access the form definition. Reserved for a future implementation.',
|
|
143
|
+
examples: ['FORM()'],
|
|
144
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export const DATANAMES_METADATA = defineBuiltinMetadata({
|
|
148
|
+
name: 'DATANAMES',
|
|
149
|
+
category: 'schema',
|
|
150
|
+
signature: "DATANAMES(type = 'any')",
|
|
151
|
+
description: 'Return form field data names, optionally filtered by field type.',
|
|
152
|
+
examples: ["DATANAMES('NumericField')"],
|
|
153
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
export const ABS_METADATA = defineBuiltinMetadata({
|
|
157
|
+
name: 'ABS',
|
|
158
|
+
category: 'math',
|
|
159
|
+
signature: 'ABS(value)',
|
|
160
|
+
description: 'Return the absolute value of a number.',
|
|
161
|
+
examples: ['ABS($variance)'],
|
|
162
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
export const CEILING_METADATA = defineBuiltinMetadata({
|
|
166
|
+
name: 'CEILING',
|
|
167
|
+
category: 'math',
|
|
168
|
+
signature: 'CEILING(value, factor)',
|
|
169
|
+
description: 'Round a number up to the nearest multiple.',
|
|
170
|
+
examples: ['CEILING($amount, 5)'],
|
|
171
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const COS_METADATA = defineBuiltinMetadata({
|
|
175
|
+
name: 'COS',
|
|
176
|
+
category: 'math',
|
|
177
|
+
signature: 'COS(value)',
|
|
178
|
+
description: 'Return the cosine of a value in radians.',
|
|
179
|
+
examples: ['COS($angle_radians)'],
|
|
180
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
export const SIN_METADATA = defineBuiltinMetadata({
|
|
184
|
+
name: 'SIN',
|
|
185
|
+
category: 'math',
|
|
186
|
+
signature: 'SIN(value)',
|
|
187
|
+
description: 'Return the sine of a value in radians.',
|
|
188
|
+
examples: ['SIN($angle_radians)'],
|
|
189
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
export const ROUND_METADATA = defineBuiltinMetadata({
|
|
193
|
+
name: 'ROUND',
|
|
194
|
+
category: 'math',
|
|
195
|
+
signature: 'ROUND(value, places)',
|
|
196
|
+
description: 'Round a number to a fixed number of decimal places.',
|
|
197
|
+
examples: ['ROUND($total, 2)'],
|
|
198
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const DATEADD_METADATA = defineBuiltinMetadata({
|
|
202
|
+
name: 'DATEADD',
|
|
203
|
+
category: 'date',
|
|
204
|
+
signature: 'DATEADD(date, days)',
|
|
205
|
+
description:
|
|
206
|
+
'Add whole days to a YYYY-MM-DD date from 0001-01-01 through 9999-12-31. Returns null when an argument is blank.',
|
|
207
|
+
examples: ['DATEADD($start_date, 30)', 'DATEADD($due_date, -7)'],
|
|
208
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
export const DAYS_METADATA = defineBuiltinMetadata({
|
|
212
|
+
name: 'DAYS',
|
|
213
|
+
category: 'date',
|
|
214
|
+
signature: 'DAYS(endDate, startDate)',
|
|
215
|
+
description:
|
|
216
|
+
'Return the days between YYYY-MM-DD dates from 0001-01-01 through 9999-12-31. Returns null when an argument is blank.',
|
|
217
|
+
examples: ['DAYS($end_date, $start_date)'],
|
|
218
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
export const UPPER_METADATA = defineBuiltinMetadata({
|
|
222
|
+
name: 'UPPER',
|
|
223
|
+
category: 'string',
|
|
224
|
+
signature: 'UPPER(value)',
|
|
225
|
+
description: 'Convert a string to uppercase.',
|
|
226
|
+
examples: ['UPPER($city_name)'],
|
|
227
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
export const SETRESULT_METADATA = defineBuiltinMetadata({
|
|
231
|
+
name: 'SETRESULT',
|
|
232
|
+
category: 'control',
|
|
233
|
+
signature: 'SETRESULT(value)',
|
|
234
|
+
description: 'Set the final return value for multiline calculations.',
|
|
235
|
+
examples: ['SETRESULT($price * $quantity)'],
|
|
236
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION],
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
export const ALERT_METADATA = defineBuiltinMetadata({
|
|
240
|
+
name: 'ALERT',
|
|
241
|
+
category: 'event',
|
|
242
|
+
signature: "ALERT(title, message = '')",
|
|
243
|
+
description: 'Display an alert from a form event handler.',
|
|
244
|
+
examples: ["ALERT('Saved', 'The record was saved successfully.')"],
|
|
245
|
+
contexts: [BUILTIN_CONTEXTS.EVENT],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
export const SETVALUE_METADATA = defineBuiltinMetadata({
|
|
249
|
+
name: 'SETVALUE',
|
|
250
|
+
category: 'event',
|
|
251
|
+
signature: 'SETVALUE(fieldDataName, valueToSet)',
|
|
252
|
+
description: 'Set a field value from a form event handler.',
|
|
253
|
+
examples: ["SETVALUE('field_dataname', 'value_to_set')"],
|
|
254
|
+
contexts: [BUILTIN_CONTEXTS.EVENT],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
export const ON_METADATA = defineBuiltinMetadata({
|
|
258
|
+
name: 'ON',
|
|
259
|
+
category: 'event',
|
|
260
|
+
signature: 'ON(eventType, fieldKeyOrCallback, callback)',
|
|
261
|
+
description: 'Register an event handler within form event code.',
|
|
262
|
+
examples: ["ON('change', 'city', function(event) { ALERT('City changed!'); })"],
|
|
263
|
+
contexts: [BUILTIN_CONTEXTS.EVENT],
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
export const OFF_METADATA = defineBuiltinMetadata({
|
|
267
|
+
name: 'OFF',
|
|
268
|
+
category: 'event',
|
|
269
|
+
signature: 'OFF(eventType, fieldKeyOrCallback, callback)',
|
|
270
|
+
description: 'Remove event handlers within form event code.',
|
|
271
|
+
examples: ["OFF('change', 'city', specificCallback)"],
|
|
272
|
+
contexts: [BUILTIN_CONTEXTS.EVENT],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export const BUILTIN_DEFINITIONS = Object.freeze([
|
|
276
|
+
IF_METADATA,
|
|
277
|
+
AND_METADATA,
|
|
278
|
+
OR_METADATA,
|
|
279
|
+
COUNT_METADATA,
|
|
280
|
+
COUNTA_METADATA,
|
|
281
|
+
COUNTBLANK_METADATA,
|
|
282
|
+
ARRAY_METADATA,
|
|
283
|
+
EVAL_METADATA,
|
|
284
|
+
CHOICEVALUE_METADATA,
|
|
285
|
+
CHOICELABEL_METADATA,
|
|
286
|
+
HASOTHER_METADATA,
|
|
287
|
+
OTHER_METADATA,
|
|
288
|
+
CHOICEVALUES_METADATA,
|
|
289
|
+
CHOICELABELS_METADATA,
|
|
290
|
+
FORM_METADATA,
|
|
291
|
+
DATANAMES_METADATA,
|
|
292
|
+
ABS_METADATA,
|
|
293
|
+
CEILING_METADATA,
|
|
294
|
+
COS_METADATA,
|
|
295
|
+
SIN_METADATA,
|
|
296
|
+
ROUND_METADATA,
|
|
297
|
+
DATEADD_METADATA,
|
|
298
|
+
DAYS_METADATA,
|
|
299
|
+
UPPER_METADATA,
|
|
300
|
+
SETRESULT_METADATA,
|
|
301
|
+
ALERT_METADATA,
|
|
302
|
+
SETVALUE_METADATA,
|
|
303
|
+
ON_METADATA,
|
|
304
|
+
OFF_METADATA,
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
function filterDefinitionsByContext(context) {
|
|
308
|
+
return Object.freeze(
|
|
309
|
+
BUILTIN_DEFINITIONS.filter((definition) => definition.contexts.includes(context))
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export const CALCULATION_BUILTIN_DEFINITIONS = filterDefinitionsByContext(
|
|
314
|
+
BUILTIN_CONTEXTS.CALCULATION
|
|
315
|
+
);
|
|
316
|
+
export const EVENT_BUILTIN_DEFINITIONS = filterDefinitionsByContext(BUILTIN_CONTEXTS.EVENT);
|
|
317
|
+
|
|
318
|
+
export const BUILTIN_DEFINITION_BY_NAME = new Map(
|
|
319
|
+
BUILTIN_DEFINITIONS.map((definition) => [definition.name, definition])
|
|
320
|
+
);
|
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
export { CHOICELABEL_METADATA } from '../builtin-definitions.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @builtin CHOICELABEL
|
|
5
|
-
*
|
|
5
|
+
* Name, signature, description, examples, and contexts: see builtin-definitions.js.
|
|
6
6
|
* @param {Object} choiceField - The choice field object with choice and other arrays
|
|
7
7
|
* @returns {*} The selected choice label with preserved type, or null if no selection
|
|
8
|
-
* @example
|
|
9
|
-
* // Get the selected city label
|
|
10
|
-
* CHOICELABEL($city)
|
|
11
|
-
* @example
|
|
12
|
-
* // Use in display logic
|
|
13
|
-
* "You selected: " + CHOICELABEL($city)
|
|
14
8
|
*/
|
|
15
|
-
export const CHOICELABEL_METADATA = defineBuiltinMetadata({
|
|
16
|
-
name: 'CHOICELABEL',
|
|
17
|
-
category: 'choice',
|
|
18
|
-
signature: 'CHOICELABEL(fieldValue)',
|
|
19
|
-
description: 'Return the selected choice label from a choice field.',
|
|
20
|
-
examples: ['CHOICELABEL($city)'],
|
|
21
|
-
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
22
|
-
});
|
|
23
|
-
|
|
24
9
|
export const CHOICELABEL = (choiceField) => {
|
|
25
10
|
// Handle null/undefined input
|
|
26
11
|
if (!choiceField || typeof choiceField !== 'object') {
|
|
@@ -1,26 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
export { CHOICELABELS_METADATA } from '../builtin-definitions.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @builtin CHOICELABELS
|
|
5
|
-
*
|
|
5
|
+
* Name, signature, description, examples, and contexts: see builtin-definitions.js.
|
|
6
6
|
* @param {Object} multiChoiceField - The multi choice field object with choices and other arrays
|
|
7
7
|
* @returns {Array} Array of selected choice labels with preserved types
|
|
8
|
-
* @example
|
|
9
|
-
* // Get all selected color labels
|
|
10
|
-
* CHOICELABELS($colors)
|
|
11
|
-
* @example
|
|
12
|
-
* // Display selected colors
|
|
13
|
-
* "Selected colors: " + CHOICELABELS($colors).join(", ")
|
|
14
8
|
*/
|
|
15
|
-
export const CHOICELABELS_METADATA = defineBuiltinMetadata({
|
|
16
|
-
name: 'CHOICELABELS',
|
|
17
|
-
category: 'choice',
|
|
18
|
-
signature: 'CHOICELABELS(fieldValue)',
|
|
19
|
-
description: 'Return all selected labels from a MultiChoiceField.',
|
|
20
|
-
examples: ['CHOICELABELS($colors)'],
|
|
21
|
-
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
22
|
-
});
|
|
23
|
-
|
|
24
9
|
export const CHOICELABELS = (multiChoiceField) => {
|
|
25
10
|
// Handle null/undefined input
|
|
26
11
|
if (!multiChoiceField || typeof multiChoiceField !== 'object') {
|