form0-core 0.1.2 → 0.1.4
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 +2 -2
- package/src/builtins/builtin-metadata.js +44 -0
- package/src/builtins/choice/choicelabel.js +11 -0
- package/src/builtins/choice/choicelabels.js +11 -0
- package/src/builtins/choice/choicevalue.js +11 -0
- package/src/builtins/choice/choicevalues.js +11 -0
- package/src/builtins/choice/hasother.js +11 -0
- package/src/builtins/choice/other.js +11 -0
- package/src/builtins/control/eval.js +10 -0
- package/src/builtins/control/setresult.js +11 -0
- package/src/builtins/event/control/off.js +10 -0
- package/src/builtins/event/control/on.js +10 -0
- package/src/builtins/event/field/setvalue.js +10 -0
- package/src/builtins/event/ui/alert.js +10 -0
- package/src/builtins/logical/and.js +11 -0
- package/src/builtins/logical/array.js +11 -0
- package/src/builtins/logical/count.js +11 -0
- package/src/builtins/logical/counta.js +11 -0
- package/src/builtins/logical/countblank.js +12 -3
- package/src/builtins/logical/if.js +11 -0
- package/src/builtins/logical/or.js +11 -0
- package/src/builtins/math/abs.js +11 -0
- package/src/builtins/math/ceiling.js +11 -0
- package/src/builtins/math/cos.js +12 -1
- package/src/builtins/math/round.js +13 -2
- package/src/builtins/math/sin.js +12 -1
- package/src/builtins/registry.js +130 -92
- package/src/builtins/schema/datanames.js +10 -0
- package/src/builtins/schema/form.js +11 -0
- package/src/builtins/string/upper.js +12 -1
- package/src/engine/context-resolver.js +202 -56
- package/src/index.js +6 -0
- package/src/utilities/calculation-editor.js +534 -0
package/src/builtins/registry.js
CHANGED
|
@@ -1,99 +1,137 @@
|
|
|
1
|
-
|
|
2
|
-
import { IF } from './logical/if.js';
|
|
3
|
-
import { AND } from './logical/and.js';
|
|
4
|
-
import { OR } from './logical/or.js';
|
|
5
|
-
import { COUNT } from './logical/count.js';
|
|
6
|
-
import { COUNTA } from './logical/counta.js';
|
|
7
|
-
import { COUNTBLANK } from './logical/countblank.js';
|
|
8
|
-
import { ARRAY } from './logical/array.js';
|
|
9
|
-
import { SETRESULT, __consumeResult } from './control/setresult.js';
|
|
10
|
-
import { EVAL } from './control/eval.js';
|
|
11
|
-
import { CHOICEVALUE } from './choice/choicevalue.js';
|
|
12
|
-
import { CHOICELABEL } from './choice/choicelabel.js';
|
|
13
|
-
import { HASOTHER } from './choice/hasother.js';
|
|
14
|
-
import { OTHER } from './choice/other.js';
|
|
15
|
-
import { CHOICEVALUES } from './choice/choicevalues.js';
|
|
16
|
-
import { CHOICELABELS } from './choice/choicelabels.js';
|
|
17
|
-
import { FORM } from './schema/form.js';
|
|
18
|
-
import { DATANAMES } from './schema/datanames.js';
|
|
19
|
-
import { ABS } from './math/abs.js';
|
|
20
|
-
import { CEILING } from './math/ceiling.js';
|
|
21
|
-
import { COS } from './math/cos.js';
|
|
22
|
-
import { SIN } from './math/sin.js';
|
|
23
|
-
import { ROUND } from './math/round.js';
|
|
24
|
-
import { UPPER } from './string/upper.js';
|
|
1
|
+
import { cloneBuiltinMetadata, BUILTIN_CONTEXTS } from './builtin-metadata.js';
|
|
25
2
|
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
3
|
+
import { IF, IF_METADATA } from './logical/if.js';
|
|
4
|
+
import { AND, AND_METADATA } from './logical/and.js';
|
|
5
|
+
import { OR, OR_METADATA } from './logical/or.js';
|
|
6
|
+
import { COUNT, COUNT_METADATA } from './logical/count.js';
|
|
7
|
+
import { COUNTA, COUNTA_METADATA } from './logical/counta.js';
|
|
8
|
+
import { COUNTBLANK, COUNTBLANK_METADATA } from './logical/countblank.js';
|
|
9
|
+
import { ARRAY, ARRAY_METADATA } from './logical/array.js';
|
|
10
|
+
import { SETRESULT, SETRESULT_METADATA, __consumeResult } from './control/setresult.js';
|
|
11
|
+
import { EVAL, EVAL_METADATA } from './control/eval.js';
|
|
12
|
+
import { CHOICEVALUE, CHOICEVALUE_METADATA } from './choice/choicevalue.js';
|
|
13
|
+
import { CHOICELABEL, CHOICELABEL_METADATA } from './choice/choicelabel.js';
|
|
14
|
+
import { HASOTHER, HASOTHER_METADATA } from './choice/hasother.js';
|
|
15
|
+
import { OTHER, OTHER_METADATA } from './choice/other.js';
|
|
16
|
+
import { CHOICEVALUES, CHOICEVALUES_METADATA } from './choice/choicevalues.js';
|
|
17
|
+
import { CHOICELABELS, CHOICELABELS_METADATA } from './choice/choicelabels.js';
|
|
18
|
+
import { FORM, FORM_METADATA } from './schema/form.js';
|
|
19
|
+
import { DATANAMES, DATANAMES_METADATA } from './schema/datanames.js';
|
|
20
|
+
import { ABS, ABS_METADATA } from './math/abs.js';
|
|
21
|
+
import { CEILING, CEILING_METADATA } from './math/ceiling.js';
|
|
22
|
+
import { COS, COS_METADATA } from './math/cos.js';
|
|
23
|
+
import { SIN, SIN_METADATA } from './math/sin.js';
|
|
24
|
+
import { ROUND, ROUND_METADATA } from './math/round.js';
|
|
25
|
+
import { UPPER, UPPER_METADATA } from './string/upper.js';
|
|
26
|
+
|
|
27
|
+
import { ALERT, ALERT_METADATA } from './event/ui/alert.js';
|
|
28
|
+
import { SETVALUE, SETVALUE_METADATA } from './event/field/setvalue.js';
|
|
29
|
+
import { ON, ON_METADATA } from './event/control/on.js';
|
|
30
|
+
import { OFF, OFF_METADATA } from './event/control/off.js';
|
|
31
31
|
import { __consumeEventOperations } from './event/event-operations-collector.js';
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
export {
|
|
35
|
-
export {
|
|
36
|
-
export {
|
|
37
|
-
export {
|
|
38
|
-
export {
|
|
39
|
-
export {
|
|
40
|
-
export {
|
|
41
|
-
export {
|
|
42
|
-
export {
|
|
43
|
-
export {
|
|
44
|
-
export {
|
|
45
|
-
export {
|
|
46
|
-
export {
|
|
47
|
-
export {
|
|
48
|
-
export {
|
|
49
|
-
export {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export {
|
|
56
|
-
export {
|
|
33
|
+
export { IF, IF_METADATA } from './logical/if.js';
|
|
34
|
+
export { AND, AND_METADATA } from './logical/and.js';
|
|
35
|
+
export { OR, OR_METADATA } from './logical/or.js';
|
|
36
|
+
export { COUNT, COUNT_METADATA } from './logical/count.js';
|
|
37
|
+
export { COUNTA, COUNTA_METADATA } from './logical/counta.js';
|
|
38
|
+
export { COUNTBLANK, COUNTBLANK_METADATA } from './logical/countblank.js';
|
|
39
|
+
export { ARRAY, ARRAY_METADATA } from './logical/array.js';
|
|
40
|
+
export { SETRESULT, SETRESULT_METADATA, __consumeResult } from './control/setresult.js';
|
|
41
|
+
export { EVAL, EVAL_METADATA, __setEvalContext, __clearEvalContext } from './control/eval.js';
|
|
42
|
+
export { CHOICEVALUE, CHOICEVALUE_METADATA } from './choice/choicevalue.js';
|
|
43
|
+
export { CHOICELABEL, CHOICELABEL_METADATA } from './choice/choicelabel.js';
|
|
44
|
+
export { HASOTHER, HASOTHER_METADATA } from './choice/hasother.js';
|
|
45
|
+
export { OTHER, OTHER_METADATA } from './choice/other.js';
|
|
46
|
+
export { CHOICEVALUES, CHOICEVALUES_METADATA } from './choice/choicevalues.js';
|
|
47
|
+
export { CHOICELABELS, CHOICELABELS_METADATA } from './choice/choicelabels.js';
|
|
48
|
+
export { FORM, FORM_METADATA } from './schema/form.js';
|
|
49
|
+
export {
|
|
50
|
+
DATANAMES,
|
|
51
|
+
DATANAMES_METADATA,
|
|
52
|
+
__setDataNamesContext,
|
|
53
|
+
__clearDataNamesContext,
|
|
54
|
+
} from './schema/datanames.js';
|
|
55
|
+
export { ABS, ABS_METADATA } from './math/abs.js';
|
|
56
|
+
export { CEILING, CEILING_METADATA } from './math/ceiling.js';
|
|
57
|
+
export { COS, COS_METADATA } from './math/cos.js';
|
|
58
|
+
export { SIN, SIN_METADATA } from './math/sin.js';
|
|
59
|
+
export { ROUND, ROUND_METADATA } from './math/round.js';
|
|
60
|
+
export { UPPER, UPPER_METADATA } from './string/upper.js';
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
export {
|
|
60
|
-
export {
|
|
61
|
-
export {
|
|
62
|
-
export { OFF } from './event/control/off.js';
|
|
62
|
+
export { ALERT, ALERT_METADATA } from './event/ui/alert.js';
|
|
63
|
+
export { SETVALUE, SETVALUE_METADATA } from './event/field/setvalue.js';
|
|
64
|
+
export { ON, ON_METADATA } from './event/control/on.js';
|
|
65
|
+
export { OFF, OFF_METADATA } from './event/control/off.js';
|
|
63
66
|
export { __consumeEventOperations } from './event/event-operations-collector.js';
|
|
64
67
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
const CALCULATION_AND_EVENT_ENTRIES = Object.freeze([
|
|
69
|
+
{ implementation: IF, definition: IF_METADATA },
|
|
70
|
+
{ implementation: AND, definition: AND_METADATA },
|
|
71
|
+
{ implementation: OR, definition: OR_METADATA },
|
|
72
|
+
{ implementation: COUNT, definition: COUNT_METADATA },
|
|
73
|
+
{ implementation: COUNTA, definition: COUNTA_METADATA },
|
|
74
|
+
{ implementation: COUNTBLANK, definition: COUNTBLANK_METADATA },
|
|
75
|
+
{ implementation: ARRAY, definition: ARRAY_METADATA },
|
|
76
|
+
{ implementation: SETRESULT, definition: SETRESULT_METADATA },
|
|
77
|
+
{ implementation: EVAL, definition: EVAL_METADATA },
|
|
78
|
+
{ implementation: CHOICEVALUE, definition: CHOICEVALUE_METADATA },
|
|
79
|
+
{ implementation: CHOICELABEL, definition: CHOICELABEL_METADATA },
|
|
80
|
+
{ implementation: HASOTHER, definition: HASOTHER_METADATA },
|
|
81
|
+
{ implementation: OTHER, definition: OTHER_METADATA },
|
|
82
|
+
{ implementation: CHOICEVALUES, definition: CHOICEVALUES_METADATA },
|
|
83
|
+
{ implementation: CHOICELABELS, definition: CHOICELABELS_METADATA },
|
|
84
|
+
{ implementation: FORM, definition: FORM_METADATA },
|
|
85
|
+
{ implementation: DATANAMES, definition: DATANAMES_METADATA },
|
|
86
|
+
{ implementation: ABS, definition: ABS_METADATA },
|
|
87
|
+
{ implementation: CEILING, definition: CEILING_METADATA },
|
|
88
|
+
{ implementation: COS, definition: COS_METADATA },
|
|
89
|
+
{ implementation: SIN, definition: SIN_METADATA },
|
|
90
|
+
{ implementation: ROUND, definition: ROUND_METADATA },
|
|
91
|
+
{ implementation: UPPER, definition: UPPER_METADATA },
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const EVENT_ONLY_ENTRIES = Object.freeze([
|
|
95
|
+
{ implementation: ALERT, definition: ALERT_METADATA },
|
|
96
|
+
{ implementation: SETVALUE, definition: SETVALUE_METADATA },
|
|
97
|
+
{ implementation: ON, definition: ON_METADATA },
|
|
98
|
+
{ implementation: OFF, definition: OFF_METADATA },
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
function buildBuiltinObject(entries) {
|
|
102
|
+
return Object.freeze(
|
|
103
|
+
Object.fromEntries(
|
|
104
|
+
entries.map(({ implementation, definition }) => [definition.name, implementation])
|
|
105
|
+
)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function filterDefinitionsByContext(definitions, context) {
|
|
110
|
+
return Object.freeze(definitions.filter((definition) => definition.contexts.includes(context)));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const builtins = buildBuiltinObject(CALCULATION_AND_EVENT_ENTRIES);
|
|
114
|
+
|
|
115
|
+
export const eventBuiltins = buildBuiltinObject(EVENT_ONLY_ENTRIES);
|
|
116
|
+
|
|
117
|
+
export const BUILTIN_DEFINITIONS = Object.freeze(
|
|
118
|
+
[...CALCULATION_AND_EVENT_ENTRIES, ...EVENT_ONLY_ENTRIES].map(({ definition }) => definition)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export const CALCULATION_BUILTIN_DEFINITIONS = filterDefinitionsByContext(
|
|
122
|
+
BUILTIN_DEFINITIONS,
|
|
123
|
+
BUILTIN_CONTEXTS.CALCULATION
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
export const EVENT_BUILTIN_DEFINITIONS = filterDefinitionsByContext(
|
|
127
|
+
BUILTIN_DEFINITIONS,
|
|
128
|
+
BUILTIN_CONTEXTS.EVENT
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
export const BUILTIN_DEFINITION_BY_NAME = new Map(
|
|
132
|
+
BUILTIN_DEFINITIONS.map((definition) => [definition.name, definition])
|
|
133
|
+
);
|
|
91
134
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
SETVALUE,
|
|
96
|
-
ON,
|
|
97
|
-
OFF,
|
|
98
|
-
// Future: CONFIRM, NOTIFY, etc.
|
|
99
|
-
};
|
|
135
|
+
export function getBuiltinDefinitions() {
|
|
136
|
+
return BUILTIN_DEFINITIONS.map(cloneBuiltinMetadata);
|
|
137
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @builtin DATANAMES
|
|
3
5
|
* @description Returns the data names of form fields, optionally filtered by field type
|
|
@@ -13,6 +15,14 @@
|
|
|
13
15
|
* // Get only TextField data names
|
|
14
16
|
* DATANAMES('TextField')
|
|
15
17
|
*/
|
|
18
|
+
export const DATANAMES_METADATA = defineBuiltinMetadata({
|
|
19
|
+
name: 'DATANAMES',
|
|
20
|
+
category: 'schema',
|
|
21
|
+
signature: "DATANAMES(type = 'any')",
|
|
22
|
+
description: 'Return form field data names, optionally filtered by field type.',
|
|
23
|
+
examples: ["DATANAMES('NumericField')"],
|
|
24
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
25
|
+
});
|
|
16
26
|
|
|
17
27
|
// Global schema context for DATANAMES() - set during expression evaluation
|
|
18
28
|
let _dataNamesSchema = null;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @builtin FORM
|
|
3
5
|
* @description Access to the nested JSON form definition (future implementation)
|
|
@@ -12,6 +14,15 @@
|
|
|
12
14
|
* // Check if form has RepeatableSections
|
|
13
15
|
* FORM().elements.some(el => el.type === 'RepeatableSection')
|
|
14
16
|
*/
|
|
17
|
+
export const FORM_METADATA = defineBuiltinMetadata({
|
|
18
|
+
name: 'FORM',
|
|
19
|
+
category: 'schema',
|
|
20
|
+
signature: 'FORM()',
|
|
21
|
+
description: 'Access the form definition. Reserved for a future implementation.',
|
|
22
|
+
examples: ['FORM()'],
|
|
23
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
24
|
+
});
|
|
25
|
+
|
|
15
26
|
export function FORM() {
|
|
16
27
|
// Placeholder for future implementation
|
|
17
28
|
// Will provide access to nested JSON form definition
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { BUILTIN_CONTEXTS, defineBuiltinMetadata } from '../builtin-metadata.js';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @builtin UPPER
|
|
3
5
|
* @description Returns the uppercase version of a string
|
|
@@ -13,9 +15,18 @@
|
|
|
13
15
|
* // Returns "ABC123"
|
|
14
16
|
* UPPER("abc123")
|
|
15
17
|
*/
|
|
18
|
+
export const UPPER_METADATA = defineBuiltinMetadata({
|
|
19
|
+
name: 'UPPER',
|
|
20
|
+
category: 'string',
|
|
21
|
+
signature: 'UPPER(value)',
|
|
22
|
+
description: 'Convert a string to uppercase.',
|
|
23
|
+
examples: ['UPPER($city_name)'],
|
|
24
|
+
contexts: [BUILTIN_CONTEXTS.CALCULATION, BUILTIN_CONTEXTS.EVENT],
|
|
25
|
+
});
|
|
26
|
+
|
|
16
27
|
export const UPPER = (value) => {
|
|
17
28
|
if (value == null) {
|
|
18
29
|
return '';
|
|
19
30
|
}
|
|
20
31
|
return String(value).toUpperCase();
|
|
21
|
-
};
|
|
32
|
+
};
|
|
@@ -66,25 +66,48 @@ export class ContextResolver {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
*
|
|
69
|
+
* Resolve field access with structured metadata for editor integrations.
|
|
70
70
|
* @param {Object} executionContext - Current execution context
|
|
71
71
|
* @param {string} fieldName - Field data_name being accessed
|
|
72
|
-
* @
|
|
72
|
+
* @param {Object | null} [providedFieldInfo]
|
|
73
|
+
* @returns {{
|
|
74
|
+
* level: 'accessible' | 'restricted' | 'not_found',
|
|
75
|
+
* code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
|
|
76
|
+
* message: string | null,
|
|
77
|
+
* suggestion: string | null,
|
|
78
|
+
* fieldInfo: Object | null,
|
|
79
|
+
* }}
|
|
73
80
|
*/
|
|
74
|
-
|
|
75
|
-
const fieldInfo = this.fieldOwnership.get(fieldName);
|
|
81
|
+
resolveFieldAccessInfo(executionContext, fieldName, providedFieldInfo = null) {
|
|
82
|
+
const fieldInfo = providedFieldInfo || this.fieldOwnership.get(fieldName) || null;
|
|
76
83
|
|
|
77
84
|
if (!fieldInfo) {
|
|
78
|
-
return
|
|
85
|
+
return {
|
|
86
|
+
level: 'not_found',
|
|
87
|
+
code: null,
|
|
88
|
+
message: `Field '${fieldName}' does not exist in the form schema`,
|
|
89
|
+
suggestion: `Check that field '${fieldName}' exists in the form schema`,
|
|
90
|
+
fieldInfo: null,
|
|
91
|
+
};
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
// Main form fields (no parent RepeatableSection) are always accessible
|
|
82
95
|
if (fieldInfo.parentPath.length === 0) {
|
|
83
|
-
return 'accessible';
|
|
96
|
+
return this.buildAccessInfo('accessible', 'main_form', fieldName, fieldInfo, executionContext);
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
// RepeatableSection field access depends on execution context
|
|
87
|
-
return this.
|
|
100
|
+
return this.checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Determine if a field is accessible from the current execution context
|
|
105
|
+
* @param {Object} executionContext - Current execution context
|
|
106
|
+
* @param {string} fieldName - Field data_name being accessed
|
|
107
|
+
* @returns {string} 'accessible', 'restricted', or 'not_found'
|
|
108
|
+
*/
|
|
109
|
+
resolveFieldAccess(executionContext, fieldName) {
|
|
110
|
+
return this.resolveFieldAccessInfo(executionContext, fieldName).level;
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
/**
|
|
@@ -94,22 +117,49 @@ export class ContextResolver {
|
|
|
94
117
|
* @returns {string} 'accessible' or 'restricted'
|
|
95
118
|
*/
|
|
96
119
|
checkRepeatableSectionAccess(executionContext, fieldInfo) {
|
|
120
|
+
return this.checkRepeatableSectionAccessInfo(
|
|
121
|
+
executionContext,
|
|
122
|
+
fieldInfo.field?.data_name || fieldInfo.preferredKey,
|
|
123
|
+
fieldInfo
|
|
124
|
+
).level;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check RepeatableSection field access and return structured access metadata.
|
|
129
|
+
* @param {Object} executionContext - Current execution context
|
|
130
|
+
* @param {string} fieldName - Field data_name being accessed
|
|
131
|
+
* @param {Object} fieldInfo - Field ownership information
|
|
132
|
+
* @returns {{
|
|
133
|
+
* level: 'accessible' | 'restricted' | 'not_found',
|
|
134
|
+
* code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
|
|
135
|
+
* message: string | null,
|
|
136
|
+
* suggestion: string | null,
|
|
137
|
+
* fieldInfo: Object | null,
|
|
138
|
+
* }}
|
|
139
|
+
*/
|
|
140
|
+
checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo) {
|
|
97
141
|
const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
|
|
98
142
|
|
|
99
143
|
if (contextType === 'calculation') {
|
|
100
144
|
// CalculatedField can only access fields in same RepeatableSection context
|
|
101
145
|
const calculationFieldInfo = this.fieldOwnership.get(contextFieldName);
|
|
102
146
|
if (!calculationFieldInfo) {
|
|
103
|
-
return
|
|
147
|
+
return this.buildAccessInfo(
|
|
148
|
+
'restricted',
|
|
149
|
+
'different_repeatable_section',
|
|
150
|
+
fieldName,
|
|
151
|
+
fieldInfo,
|
|
152
|
+
executionContext
|
|
153
|
+
);
|
|
104
154
|
}
|
|
105
155
|
|
|
106
|
-
|
|
107
|
-
return this.haveSameRepeatableSectionContext(
|
|
156
|
+
return this.resolveRepeatableSectionRelationship(
|
|
108
157
|
calculationFieldInfo.parentPath,
|
|
109
|
-
fieldInfo.parentPath
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
158
|
+
fieldInfo.parentPath,
|
|
159
|
+
fieldName,
|
|
160
|
+
fieldInfo,
|
|
161
|
+
executionContext
|
|
162
|
+
);
|
|
113
163
|
}
|
|
114
164
|
|
|
115
165
|
if (contextType === 'event') {
|
|
@@ -118,38 +168,75 @@ export class ContextResolver {
|
|
|
118
168
|
switch (eventInfo.scope) {
|
|
119
169
|
case 'main':
|
|
120
170
|
// Record events can't access RepeatableSection fields
|
|
121
|
-
return
|
|
171
|
+
return this.buildAccessInfo(
|
|
172
|
+
'restricted',
|
|
173
|
+
'different_repeatable_section',
|
|
174
|
+
fieldName,
|
|
175
|
+
fieldInfo,
|
|
176
|
+
executionContext
|
|
177
|
+
);
|
|
122
178
|
|
|
123
179
|
case 'contextual':
|
|
124
180
|
// Field events can access fields in same RepeatableSection context
|
|
125
181
|
if (!contextFieldName) {
|
|
126
|
-
return
|
|
182
|
+
return this.buildAccessInfo(
|
|
183
|
+
'restricted',
|
|
184
|
+
'different_repeatable_section',
|
|
185
|
+
fieldName,
|
|
186
|
+
fieldInfo,
|
|
187
|
+
executionContext
|
|
188
|
+
);
|
|
127
189
|
}
|
|
128
190
|
|
|
129
191
|
const triggerFieldInfo = this.fieldOwnership.get(contextFieldName);
|
|
130
192
|
if (!triggerFieldInfo) {
|
|
131
|
-
return
|
|
193
|
+
return this.buildAccessInfo(
|
|
194
|
+
'restricted',
|
|
195
|
+
'different_repeatable_section',
|
|
196
|
+
fieldName,
|
|
197
|
+
fieldInfo,
|
|
198
|
+
executionContext
|
|
199
|
+
);
|
|
132
200
|
}
|
|
133
201
|
|
|
134
|
-
return this.
|
|
202
|
+
return this.resolveRepeatableSectionRelationship(
|
|
135
203
|
triggerFieldInfo.parentPath,
|
|
136
|
-
fieldInfo.parentPath
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
204
|
+
fieldInfo.parentPath,
|
|
205
|
+
fieldName,
|
|
206
|
+
fieldInfo,
|
|
207
|
+
executionContext
|
|
208
|
+
);
|
|
140
209
|
|
|
141
210
|
case 'repeatable':
|
|
142
211
|
// RepeatableSection events - future implementation
|
|
143
212
|
// For now, assume accessible (will be refined when implemented)
|
|
144
|
-
return
|
|
213
|
+
return this.buildAccessInfo(
|
|
214
|
+
'accessible',
|
|
215
|
+
'same_repeatable_section',
|
|
216
|
+
fieldName,
|
|
217
|
+
fieldInfo,
|
|
218
|
+
executionContext
|
|
219
|
+
);
|
|
145
220
|
|
|
146
221
|
default:
|
|
147
|
-
return
|
|
222
|
+
return this.buildAccessInfo(
|
|
223
|
+
'restricted',
|
|
224
|
+
'different_repeatable_section',
|
|
225
|
+
fieldName,
|
|
226
|
+
fieldInfo,
|
|
227
|
+
executionContext
|
|
228
|
+
);
|
|
148
229
|
}
|
|
149
230
|
}
|
|
150
231
|
|
|
151
232
|
// Unknown context type
|
|
152
|
-
return
|
|
233
|
+
return this.buildAccessInfo(
|
|
234
|
+
'restricted',
|
|
235
|
+
'different_repeatable_section',
|
|
236
|
+
fieldName,
|
|
237
|
+
fieldInfo,
|
|
238
|
+
executionContext
|
|
239
|
+
);
|
|
153
240
|
}
|
|
154
241
|
|
|
155
242
|
/**
|
|
@@ -159,25 +246,80 @@ export class ContextResolver {
|
|
|
159
246
|
* @returns {boolean} True if they share the same RepeatableSection context
|
|
160
247
|
*/
|
|
161
248
|
haveSameRepeatableSectionContext(parentPath1, parentPath2) {
|
|
162
|
-
|
|
163
|
-
|
|
249
|
+
return this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !== 'different_repeatable_section';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
resolveRepeatableSectionRelationship(
|
|
253
|
+
contextPath,
|
|
254
|
+
targetPath,
|
|
255
|
+
fieldName,
|
|
256
|
+
fieldInfo,
|
|
257
|
+
executionContext
|
|
258
|
+
) {
|
|
259
|
+
const code = this.resolveRepeatableSectionAccessCode(contextPath, targetPath);
|
|
260
|
+
const level = code === 'different_repeatable_section' ? 'restricted' : 'accessible';
|
|
261
|
+
|
|
262
|
+
return this.buildAccessInfo(level, code, fieldName, fieldInfo, executionContext);
|
|
263
|
+
}
|
|
164
264
|
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
return
|
|
265
|
+
resolveRepeatableSectionAccessCode(contextPath, targetPath) {
|
|
266
|
+
if (targetPath.length === 0) {
|
|
267
|
+
return 'main_form';
|
|
168
268
|
}
|
|
169
269
|
|
|
170
|
-
// Child context can access its own fields
|
|
171
270
|
if (ContextResolver.pathsEqual(contextPath, targetPath)) {
|
|
172
|
-
return
|
|
271
|
+
return contextPath.length === 0 ? 'main_form' : 'same_repeatable_section';
|
|
173
272
|
}
|
|
174
273
|
|
|
175
|
-
// Child context can access ancestors (including main form)
|
|
176
274
|
if (targetPath.length < contextPath.length && ContextResolver.isPrefix(targetPath, contextPath)) {
|
|
177
|
-
return
|
|
275
|
+
return targetPath.length === 0 ? 'main_form' : 'ancestor_repeatable_context';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return 'different_repeatable_section';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
buildAccessInfo(level, code, fieldName, fieldInfo, executionContext) {
|
|
282
|
+
if (level === 'accessible') {
|
|
283
|
+
return {
|
|
284
|
+
level,
|
|
285
|
+
code,
|
|
286
|
+
message: null,
|
|
287
|
+
suggestion: null,
|
|
288
|
+
fieldInfo,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
level,
|
|
294
|
+
code,
|
|
295
|
+
message: `Field '${fieldName}' is not accessible from current context`,
|
|
296
|
+
suggestion: this.buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code),
|
|
297
|
+
fieldInfo,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code) {
|
|
302
|
+
const { type: contextType, eventType } = executionContext;
|
|
303
|
+
|
|
304
|
+
if (contextType === 'calculation') {
|
|
305
|
+
if (code === 'different_repeatable_section') {
|
|
306
|
+
return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${fieldInfo.parentPath.join(' -> ')}`;
|
|
307
|
+
}
|
|
178
308
|
}
|
|
179
309
|
|
|
180
|
-
|
|
310
|
+
if (contextType === 'event') {
|
|
311
|
+
const eventInfo = getEventInfo(eventType);
|
|
312
|
+
|
|
313
|
+
if (eventInfo.scope === 'main') {
|
|
314
|
+
return `Record-level events (like '${eventType}') can only access main form fields, not RepeatableSection fields`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (eventInfo.scope === 'contextual') {
|
|
318
|
+
return `Field events can only access fields in the same RepeatableSection or ancestor contexts as the triggering field`;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return 'Check the field access rules for your current context';
|
|
181
323
|
}
|
|
182
324
|
|
|
183
325
|
static isPrefix(prefix, fullPath) {
|
|
@@ -221,19 +363,25 @@ export class ContextResolver {
|
|
|
221
363
|
* @returns {Object} Warning object with structured information
|
|
222
364
|
*/
|
|
223
365
|
generateAccessWarning(executionContext, fieldName, reason) {
|
|
224
|
-
const
|
|
225
|
-
|
|
366
|
+
const accessInfo =
|
|
367
|
+
reason === 'not_found'
|
|
368
|
+
? this.resolveFieldAccessInfo(executionContext, fieldName)
|
|
369
|
+
: this.resolveFieldAccessInfo(executionContext, fieldName);
|
|
370
|
+
const fieldInfo = accessInfo.fieldInfo;
|
|
226
371
|
|
|
227
372
|
return {
|
|
228
373
|
type: 'FIELD_ACCESS_WARNING',
|
|
229
374
|
fieldName,
|
|
230
375
|
executionContext,
|
|
231
376
|
reason,
|
|
377
|
+
accessCode: accessInfo.code,
|
|
232
378
|
message:
|
|
233
|
-
|
|
379
|
+
accessInfo.message ||
|
|
380
|
+
(reason === 'not_found'
|
|
234
381
|
? `Field '${fieldName}' does not exist in the form schema`
|
|
235
|
-
: `Field '${fieldName}' is not accessible from current context
|
|
236
|
-
suggestion
|
|
382
|
+
: `Field '${fieldName}' is not accessible from current context`),
|
|
383
|
+
suggestion:
|
|
384
|
+
accessInfo.suggestion || this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
|
|
237
385
|
fieldContext: fieldInfo
|
|
238
386
|
? {
|
|
239
387
|
parentPath: fieldInfo.parentPath,
|
|
@@ -252,28 +400,26 @@ export class ContextResolver {
|
|
|
252
400
|
* @returns {string} Helpful suggestion message
|
|
253
401
|
*/
|
|
254
402
|
generateAccessSuggestion(executionContext, fieldName, fieldInfo) {
|
|
255
|
-
|
|
256
|
-
|
|
403
|
+
const accessInfo = this.resolveFieldAccessInfo(executionContext, fieldName, fieldInfo);
|
|
404
|
+
|
|
405
|
+
if (accessInfo.suggestion) {
|
|
406
|
+
return accessInfo.suggestion;
|
|
257
407
|
}
|
|
258
408
|
|
|
259
|
-
|
|
409
|
+
if (accessInfo.level === 'not_found') {
|
|
410
|
+
return `Check that field '${fieldName}' exists in the form schema`;
|
|
411
|
+
}
|
|
260
412
|
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
return 'Main form fields are always accessible from calculations';
|
|
264
|
-
} else {
|
|
265
|
-
return `CalculatedField can only access fields in the same RepeatableSection. Field '${fieldName}' is in RepeatableSection: ${fieldInfo.parentPath.join(' -> ')}`;
|
|
266
|
-
}
|
|
413
|
+
if (accessInfo.code === 'main_form') {
|
|
414
|
+
return 'Main form fields are always accessible from calculations';
|
|
267
415
|
}
|
|
268
416
|
|
|
269
|
-
if (
|
|
270
|
-
|
|
417
|
+
if (accessInfo.code === 'same_repeatable_section') {
|
|
418
|
+
return 'Fields in the same RepeatableSection are accessible from this context';
|
|
419
|
+
}
|
|
271
420
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
} else if (eventInfo.scope === 'contextual') {
|
|
275
|
-
return `Field events can only access fields in the same RepeatableSection context as the triggering field`;
|
|
276
|
-
}
|
|
421
|
+
if (accessInfo.code === 'ancestor_repeatable_context') {
|
|
422
|
+
return 'Fields in ancestor RepeatableSection contexts are accessible from this context';
|
|
277
423
|
}
|
|
278
424
|
|
|
279
425
|
return 'Check the field access rules for your current context';
|