form0-core 0.1.3 → 0.1.5
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
|
@@ -66,25 +66,54 @@ 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
|
|
96
|
+
return this.buildAccessInfo(
|
|
97
|
+
'accessible',
|
|
98
|
+
'main_form',
|
|
99
|
+
fieldName,
|
|
100
|
+
fieldInfo,
|
|
101
|
+
executionContext
|
|
102
|
+
);
|
|
84
103
|
}
|
|
85
104
|
|
|
86
105
|
// RepeatableSection field access depends on execution context
|
|
87
|
-
return this.
|
|
106
|
+
return this.checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Determine if a field is accessible from the current execution context
|
|
111
|
+
* @param {Object} executionContext - Current execution context
|
|
112
|
+
* @param {string} fieldName - Field data_name being accessed
|
|
113
|
+
* @returns {string} 'accessible', 'restricted', or 'not_found'
|
|
114
|
+
*/
|
|
115
|
+
resolveFieldAccess(executionContext, fieldName) {
|
|
116
|
+
return this.resolveFieldAccessInfo(executionContext, fieldName).level;
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
/**
|
|
@@ -94,22 +123,49 @@ export class ContextResolver {
|
|
|
94
123
|
* @returns {string} 'accessible' or 'restricted'
|
|
95
124
|
*/
|
|
96
125
|
checkRepeatableSectionAccess(executionContext, fieldInfo) {
|
|
126
|
+
return this.checkRepeatableSectionAccessInfo(
|
|
127
|
+
executionContext,
|
|
128
|
+
fieldInfo.field?.data_name || fieldInfo.preferredKey,
|
|
129
|
+
fieldInfo
|
|
130
|
+
).level;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check RepeatableSection field access and return structured access metadata.
|
|
135
|
+
* @param {Object} executionContext - Current execution context
|
|
136
|
+
* @param {string} fieldName - Field data_name being accessed
|
|
137
|
+
* @param {Object} fieldInfo - Field ownership information
|
|
138
|
+
* @returns {{
|
|
139
|
+
* level: 'accessible' | 'restricted' | 'not_found',
|
|
140
|
+
* code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
|
|
141
|
+
* message: string | null,
|
|
142
|
+
* suggestion: string | null,
|
|
143
|
+
* fieldInfo: Object | null,
|
|
144
|
+
* }}
|
|
145
|
+
*/
|
|
146
|
+
checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo) {
|
|
97
147
|
const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
|
|
98
148
|
|
|
99
149
|
if (contextType === 'calculation') {
|
|
100
150
|
// CalculatedField can only access fields in same RepeatableSection context
|
|
101
151
|
const calculationFieldInfo = this.fieldOwnership.get(contextFieldName);
|
|
102
152
|
if (!calculationFieldInfo) {
|
|
103
|
-
return
|
|
153
|
+
return this.buildAccessInfo(
|
|
154
|
+
'restricted',
|
|
155
|
+
'different_repeatable_section',
|
|
156
|
+
fieldName,
|
|
157
|
+
fieldInfo,
|
|
158
|
+
executionContext
|
|
159
|
+
);
|
|
104
160
|
}
|
|
105
161
|
|
|
106
|
-
|
|
107
|
-
return this.haveSameRepeatableSectionContext(
|
|
162
|
+
return this.resolveRepeatableSectionRelationship(
|
|
108
163
|
calculationFieldInfo.parentPath,
|
|
109
|
-
fieldInfo.parentPath
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
164
|
+
fieldInfo.parentPath,
|
|
165
|
+
fieldName,
|
|
166
|
+
fieldInfo,
|
|
167
|
+
executionContext
|
|
168
|
+
);
|
|
113
169
|
}
|
|
114
170
|
|
|
115
171
|
if (contextType === 'event') {
|
|
@@ -118,38 +174,75 @@ export class ContextResolver {
|
|
|
118
174
|
switch (eventInfo.scope) {
|
|
119
175
|
case 'main':
|
|
120
176
|
// Record events can't access RepeatableSection fields
|
|
121
|
-
return
|
|
177
|
+
return this.buildAccessInfo(
|
|
178
|
+
'restricted',
|
|
179
|
+
'different_repeatable_section',
|
|
180
|
+
fieldName,
|
|
181
|
+
fieldInfo,
|
|
182
|
+
executionContext
|
|
183
|
+
);
|
|
122
184
|
|
|
123
185
|
case 'contextual':
|
|
124
186
|
// Field events can access fields in same RepeatableSection context
|
|
125
187
|
if (!contextFieldName) {
|
|
126
|
-
return
|
|
188
|
+
return this.buildAccessInfo(
|
|
189
|
+
'restricted',
|
|
190
|
+
'different_repeatable_section',
|
|
191
|
+
fieldName,
|
|
192
|
+
fieldInfo,
|
|
193
|
+
executionContext
|
|
194
|
+
);
|
|
127
195
|
}
|
|
128
196
|
|
|
129
197
|
const triggerFieldInfo = this.fieldOwnership.get(contextFieldName);
|
|
130
198
|
if (!triggerFieldInfo) {
|
|
131
|
-
return
|
|
199
|
+
return this.buildAccessInfo(
|
|
200
|
+
'restricted',
|
|
201
|
+
'different_repeatable_section',
|
|
202
|
+
fieldName,
|
|
203
|
+
fieldInfo,
|
|
204
|
+
executionContext
|
|
205
|
+
);
|
|
132
206
|
}
|
|
133
207
|
|
|
134
|
-
return this.
|
|
208
|
+
return this.resolveRepeatableSectionRelationship(
|
|
135
209
|
triggerFieldInfo.parentPath,
|
|
136
|
-
fieldInfo.parentPath
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
210
|
+
fieldInfo.parentPath,
|
|
211
|
+
fieldName,
|
|
212
|
+
fieldInfo,
|
|
213
|
+
executionContext
|
|
214
|
+
);
|
|
140
215
|
|
|
141
216
|
case 'repeatable':
|
|
142
217
|
// RepeatableSection events - future implementation
|
|
143
218
|
// For now, assume accessible (will be refined when implemented)
|
|
144
|
-
return
|
|
219
|
+
return this.buildAccessInfo(
|
|
220
|
+
'accessible',
|
|
221
|
+
'same_repeatable_section',
|
|
222
|
+
fieldName,
|
|
223
|
+
fieldInfo,
|
|
224
|
+
executionContext
|
|
225
|
+
);
|
|
145
226
|
|
|
146
227
|
default:
|
|
147
|
-
return
|
|
228
|
+
return this.buildAccessInfo(
|
|
229
|
+
'restricted',
|
|
230
|
+
'different_repeatable_section',
|
|
231
|
+
fieldName,
|
|
232
|
+
fieldInfo,
|
|
233
|
+
executionContext
|
|
234
|
+
);
|
|
148
235
|
}
|
|
149
236
|
}
|
|
150
237
|
|
|
151
238
|
// Unknown context type
|
|
152
|
-
return
|
|
239
|
+
return this.buildAccessInfo(
|
|
240
|
+
'restricted',
|
|
241
|
+
'different_repeatable_section',
|
|
242
|
+
fieldName,
|
|
243
|
+
fieldInfo,
|
|
244
|
+
executionContext
|
|
245
|
+
);
|
|
153
246
|
}
|
|
154
247
|
|
|
155
248
|
/**
|
|
@@ -159,25 +252,118 @@ export class ContextResolver {
|
|
|
159
252
|
* @returns {boolean} True if they share the same RepeatableSection context
|
|
160
253
|
*/
|
|
161
254
|
haveSameRepeatableSectionContext(parentPath1, parentPath2) {
|
|
162
|
-
|
|
163
|
-
|
|
255
|
+
return (
|
|
256
|
+
this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !==
|
|
257
|
+
'different_repeatable_section'
|
|
258
|
+
);
|
|
259
|
+
}
|
|
164
260
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
261
|
+
resolveRepeatableSectionRelationship(
|
|
262
|
+
contextPath,
|
|
263
|
+
targetPath,
|
|
264
|
+
fieldName,
|
|
265
|
+
fieldInfo,
|
|
266
|
+
executionContext
|
|
267
|
+
) {
|
|
268
|
+
const code = this.resolveRepeatableSectionAccessCode(contextPath, targetPath);
|
|
269
|
+
const level = code === 'different_repeatable_section' ? 'restricted' : 'accessible';
|
|
270
|
+
|
|
271
|
+
return this.buildAccessInfo(level, code, fieldName, fieldInfo, executionContext);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
resolveRepeatableSectionAccessCode(contextPath, targetPath) {
|
|
275
|
+
if (targetPath.length === 0) {
|
|
276
|
+
return 'main_form';
|
|
168
277
|
}
|
|
169
278
|
|
|
170
|
-
// Child context can access its own fields
|
|
171
279
|
if (ContextResolver.pathsEqual(contextPath, targetPath)) {
|
|
172
|
-
return
|
|
280
|
+
return contextPath.length === 0 ? 'main_form' : 'same_repeatable_section';
|
|
173
281
|
}
|
|
174
282
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
283
|
+
if (
|
|
284
|
+
targetPath.length < contextPath.length &&
|
|
285
|
+
ContextResolver.isPrefix(targetPath, contextPath)
|
|
286
|
+
) {
|
|
287
|
+
return targetPath.length === 0 ? 'main_form' : 'ancestor_repeatable_context';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return 'different_repeatable_section';
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
buildAccessInfo(level, code, fieldName, fieldInfo, executionContext) {
|
|
294
|
+
if (level === 'accessible') {
|
|
295
|
+
return {
|
|
296
|
+
level,
|
|
297
|
+
code,
|
|
298
|
+
message: null,
|
|
299
|
+
suggestion: null,
|
|
300
|
+
fieldInfo,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
level,
|
|
306
|
+
code,
|
|
307
|
+
message: `Field '${fieldName}' is not accessible from current context`,
|
|
308
|
+
suggestion: this.buildRestrictedAccessSuggestion(
|
|
309
|
+
executionContext,
|
|
310
|
+
fieldName,
|
|
311
|
+
fieldInfo,
|
|
312
|
+
code
|
|
313
|
+
),
|
|
314
|
+
fieldInfo,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code) {
|
|
319
|
+
const { type: contextType, eventType } = executionContext;
|
|
320
|
+
|
|
321
|
+
if (contextType === 'calculation') {
|
|
322
|
+
if (code === 'different_repeatable_section') {
|
|
323
|
+
return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
|
|
324
|
+
}
|
|
178
325
|
}
|
|
179
326
|
|
|
180
|
-
|
|
327
|
+
if (contextType === 'event') {
|
|
328
|
+
const eventInfo = getEventInfo(eventType);
|
|
329
|
+
|
|
330
|
+
if (eventInfo.scope === 'main') {
|
|
331
|
+
return `Record-level events (like '${eventType}') can only access main form fields, not RepeatableSection fields`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (eventInfo.scope === 'contextual') {
|
|
335
|
+
return `Field events can only access fields in the same RepeatableSection or ancestor contexts as the triggering field`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return 'Check the field access rules for your current context';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
formatRepeatableSectionPath(parentPath) {
|
|
343
|
+
if (!Array.isArray(parentPath) || parentPath.length === 0) {
|
|
344
|
+
return 'main form';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return parentPath
|
|
348
|
+
.map((_, index) => this.formatRepeatableSectionSegment(parentPath.slice(0, index + 1)))
|
|
349
|
+
.join(' -> ');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
formatRepeatableSectionSegment(currentPath) {
|
|
353
|
+
for (const treeInfo of this.repeatableSectionTree.values()) {
|
|
354
|
+
if (!ContextResolver.pathsEqual(treeInfo.currentPath, currentPath)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const dataName = treeInfo.field?.data_name || treeInfo.preferredKey;
|
|
359
|
+
if (dataName === treeInfo.preferredKey) {
|
|
360
|
+
return dataName;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return `${dataName} (${treeInfo.preferredKey})`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return currentPath[currentPath.length - 1];
|
|
181
367
|
}
|
|
182
368
|
|
|
183
369
|
static isPrefix(prefix, fullPath) {
|
|
@@ -221,19 +407,26 @@ export class ContextResolver {
|
|
|
221
407
|
* @returns {Object} Warning object with structured information
|
|
222
408
|
*/
|
|
223
409
|
generateAccessWarning(executionContext, fieldName, reason) {
|
|
224
|
-
const
|
|
225
|
-
|
|
410
|
+
const accessInfo =
|
|
411
|
+
reason === 'not_found'
|
|
412
|
+
? this.resolveFieldAccessInfo(executionContext, fieldName)
|
|
413
|
+
: this.resolveFieldAccessInfo(executionContext, fieldName);
|
|
414
|
+
const fieldInfo = accessInfo.fieldInfo;
|
|
226
415
|
|
|
227
416
|
return {
|
|
228
417
|
type: 'FIELD_ACCESS_WARNING',
|
|
229
418
|
fieldName,
|
|
230
419
|
executionContext,
|
|
231
420
|
reason,
|
|
421
|
+
accessCode: accessInfo.code,
|
|
232
422
|
message:
|
|
233
|
-
|
|
423
|
+
accessInfo.message ||
|
|
424
|
+
(reason === 'not_found'
|
|
234
425
|
? `Field '${fieldName}' does not exist in the form schema`
|
|
235
|
-
: `Field '${fieldName}' is not accessible from current context
|
|
236
|
-
suggestion
|
|
426
|
+
: `Field '${fieldName}' is not accessible from current context`),
|
|
427
|
+
suggestion:
|
|
428
|
+
accessInfo.suggestion ||
|
|
429
|
+
this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
|
|
237
430
|
fieldContext: fieldInfo
|
|
238
431
|
? {
|
|
239
432
|
parentPath: fieldInfo.parentPath,
|
|
@@ -252,28 +445,26 @@ export class ContextResolver {
|
|
|
252
445
|
* @returns {string} Helpful suggestion message
|
|
253
446
|
*/
|
|
254
447
|
generateAccessSuggestion(executionContext, fieldName, fieldInfo) {
|
|
255
|
-
|
|
256
|
-
|
|
448
|
+
const accessInfo = this.resolveFieldAccessInfo(executionContext, fieldName, fieldInfo);
|
|
449
|
+
|
|
450
|
+
if (accessInfo.suggestion) {
|
|
451
|
+
return accessInfo.suggestion;
|
|
257
452
|
}
|
|
258
453
|
|
|
259
|
-
|
|
454
|
+
if (accessInfo.level === 'not_found') {
|
|
455
|
+
return `Check that field '${fieldName}' exists in the form schema`;
|
|
456
|
+
}
|
|
260
457
|
|
|
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
|
-
}
|
|
458
|
+
if (accessInfo.code === 'main_form') {
|
|
459
|
+
return 'Main form fields are always accessible from calculations';
|
|
267
460
|
}
|
|
268
461
|
|
|
269
|
-
if (
|
|
270
|
-
|
|
462
|
+
if (accessInfo.code === 'same_repeatable_section') {
|
|
463
|
+
return 'Fields in the same RepeatableSection are accessible from this context';
|
|
464
|
+
}
|
|
271
465
|
|
|
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
|
-
}
|
|
466
|
+
if (accessInfo.code === 'ancestor_repeatable_context') {
|
|
467
|
+
return 'Fields in ancestor RepeatableSection contexts are accessible from this context';
|
|
277
468
|
}
|
|
278
469
|
|
|
279
470
|
return 'Check the field access rules for your current context';
|
package/src/engine/evaluator.js
CHANGED
|
@@ -3,6 +3,10 @@ import { __setEvalContext, __clearEvalContext } from '../builtins/control/eval.j
|
|
|
3
3
|
import { __setDataNamesContext, __clearDataNamesContext } from '../builtins/schema/datanames.js';
|
|
4
4
|
import { validateExpression, createSecureContext, withTimeout } from '../security/validation.js';
|
|
5
5
|
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
6
|
+
import {
|
|
7
|
+
isMultilineCalculationExpression,
|
|
8
|
+
normalizeInlineCalculationExpression,
|
|
9
|
+
} from '../utilities/calculation-expression-utils.js';
|
|
6
10
|
|
|
7
11
|
export function runExpression(
|
|
8
12
|
expr,
|
|
@@ -34,8 +38,7 @@ export function runExpression(
|
|
|
34
38
|
|
|
35
39
|
try {
|
|
36
40
|
// Handle both expressions and multi-line code (Windows-safe)
|
|
37
|
-
const isMultiLine =
|
|
38
|
-
expr.includes('\r\n') || expr.includes('\n') || expr.includes('function');
|
|
41
|
+
const isMultiLine = isMultilineCalculationExpression(expr);
|
|
39
42
|
|
|
40
43
|
if (isMultiLine) {
|
|
41
44
|
// Execute as code block (recompile each time for now)
|
|
@@ -46,7 +49,8 @@ export function runExpression(
|
|
|
46
49
|
return consumed.called ? consumed.value : result;
|
|
47
50
|
} else {
|
|
48
51
|
// Execute as expression (existing behavior)
|
|
49
|
-
const
|
|
52
|
+
const inlineExpression = normalizeInlineCalculationExpression(expr);
|
|
53
|
+
const fn = new Function(...keys, `return (${inlineExpression});`);
|
|
50
54
|
const result = fn(...values);
|
|
51
55
|
const consumed = __consumeResult();
|
|
52
56
|
return consumed.called ? consumed.value : result;
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
import { ContextResolver } from '../engine/context-resolver.js';
|
|
7
7
|
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
8
8
|
import { validateExpression } from '../security/validation.js';
|
|
9
|
+
import {
|
|
10
|
+
isMultilineCalculationExpression,
|
|
11
|
+
normalizeInlineCalculationExpression,
|
|
12
|
+
} from './calculation-expression-utils.js';
|
|
9
13
|
import { flattenFields } from './field-helpers.js';
|
|
10
14
|
|
|
11
15
|
const COMMON_GLOBAL_FUNCTIONS = new Set([
|
|
@@ -157,6 +161,40 @@ function cloneCatalogDefinition(definition) {
|
|
|
157
161
|
};
|
|
158
162
|
}
|
|
159
163
|
|
|
164
|
+
function toCalculationReferenceAccess(accessInfo) {
|
|
165
|
+
return {
|
|
166
|
+
level: accessInfo.level,
|
|
167
|
+
code: accessInfo.code,
|
|
168
|
+
message: accessInfo.message,
|
|
169
|
+
suggestion: accessInfo.suggestion,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function toCalculationReferenceDefinition(field, resolver, executionContext) {
|
|
174
|
+
const accessInfo = resolver.resolveFieldAccessInfo(executionContext, field.data_name);
|
|
175
|
+
|
|
176
|
+
if (accessInfo.level === 'not_found') {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
key: typeof field.key === 'string' ? field.key : field.data_name,
|
|
182
|
+
reference: `$${field.data_name}`,
|
|
183
|
+
dataName: field.data_name,
|
|
184
|
+
label: typeof field.label === 'string' ? field.label : field.data_name,
|
|
185
|
+
type: typeof field.type === 'string' ? field.type : 'UnknownField',
|
|
186
|
+
description: typeof field.description === 'string' ? field.description : null,
|
|
187
|
+
displayStyle:
|
|
188
|
+
field.display && typeof field.display === 'object' && typeof field.display.style === 'string'
|
|
189
|
+
? field.display.style
|
|
190
|
+
: typeof field.display === 'string'
|
|
191
|
+
? field.display
|
|
192
|
+
: null,
|
|
193
|
+
parentPath: resolver.getFieldInfo(field.data_name)?.parentPath || [],
|
|
194
|
+
access: toCalculationReferenceAccess(accessInfo),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
160
198
|
function createIssue({ code, severity, message, symbol = null, index = null, length = null }) {
|
|
161
199
|
return {
|
|
162
200
|
code,
|
|
@@ -178,15 +216,15 @@ function addIssue(issues, issueKeys, issue) {
|
|
|
178
216
|
}
|
|
179
217
|
|
|
180
218
|
function validateSyntax(expression) {
|
|
181
|
-
const isMultiLine =
|
|
182
|
-
expression.includes('\r\n') || expression.includes('\n') || expression.includes('function');
|
|
219
|
+
const isMultiLine = isMultilineCalculationExpression(expression);
|
|
183
220
|
|
|
184
221
|
if (isMultiLine) {
|
|
185
222
|
new Function(expression);
|
|
186
223
|
return;
|
|
187
224
|
}
|
|
188
225
|
|
|
189
|
-
|
|
226
|
+
const inlineExpression = normalizeInlineCalculationExpression(expression);
|
|
227
|
+
new Function(`return (${inlineExpression});`);
|
|
190
228
|
}
|
|
191
229
|
|
|
192
230
|
function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
@@ -220,6 +258,12 @@ function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
|
220
258
|
* @property {string | null} description
|
|
221
259
|
* @property {string | null} displayStyle
|
|
222
260
|
* @property {string[]} parentPath
|
|
261
|
+
* @property {{
|
|
262
|
+
* level: 'accessible' | 'restricted',
|
|
263
|
+
* code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section',
|
|
264
|
+
* message: string | null,
|
|
265
|
+
* suggestion: string | null,
|
|
266
|
+
* }} access
|
|
223
267
|
*
|
|
224
268
|
* @typedef {Object} CalculationExpressionIssue
|
|
225
269
|
* @property {string} code
|
|
@@ -246,9 +290,14 @@ export function getCalculationBuiltinCatalog() {
|
|
|
246
290
|
* @param {Object} options
|
|
247
291
|
* @param {Object} options.schema
|
|
248
292
|
* @param {string} options.fieldDataName
|
|
293
|
+
* @param {boolean} [options.includeRestricted=false]
|
|
249
294
|
* @returns {CalculationReferenceDefinition[]}
|
|
250
295
|
*/
|
|
251
|
-
export function getCalculationReferenceCatalog({
|
|
296
|
+
export function getCalculationReferenceCatalog({
|
|
297
|
+
schema,
|
|
298
|
+
fieldDataName,
|
|
299
|
+
includeRestricted = false,
|
|
300
|
+
}) {
|
|
252
301
|
const form = normalizeFormSchema(schema);
|
|
253
302
|
const resolver = new ContextResolver(form);
|
|
254
303
|
const executionContext = buildExecutionContext(form, fieldDataName, resolver);
|
|
@@ -262,26 +311,9 @@ export function getCalculationReferenceCatalog({ schema, fieldDataName }) {
|
|
|
262
311
|
field.type !== 'RepeatableSection' &&
|
|
263
312
|
field.type !== 'BuildingPlanSection'
|
|
264
313
|
)
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
)
|
|
268
|
-
.map((field) => ({
|
|
269
|
-
key: typeof field.key === 'string' ? field.key : field.data_name,
|
|
270
|
-
reference: `$${field.data_name}`,
|
|
271
|
-
dataName: field.data_name,
|
|
272
|
-
label: typeof field.label === 'string' ? field.label : field.data_name,
|
|
273
|
-
type: typeof field.type === 'string' ? field.type : 'UnknownField',
|
|
274
|
-
description: typeof field.description === 'string' ? field.description : null,
|
|
275
|
-
displayStyle:
|
|
276
|
-
field.display &&
|
|
277
|
-
typeof field.display === 'object' &&
|
|
278
|
-
typeof field.display.style === 'string'
|
|
279
|
-
? field.display.style
|
|
280
|
-
: typeof field.display === 'string'
|
|
281
|
-
? field.display
|
|
282
|
-
: null,
|
|
283
|
-
parentPath: resolver.getFieldInfo(field.data_name)?.parentPath || [],
|
|
284
|
-
}));
|
|
314
|
+
.map((field) => toCalculationReferenceDefinition(field, resolver, executionContext))
|
|
315
|
+
.filter((reference) => reference !== null)
|
|
316
|
+
.filter((reference) => includeRestricted || reference.access.level === 'accessible');
|
|
285
317
|
}
|
|
286
318
|
|
|
287
319
|
/**
|
|
@@ -311,6 +343,7 @@ export function analyzeCalculationExpression({
|
|
|
311
343
|
const usedBuiltinNames = new Set();
|
|
312
344
|
const referencedFields = [];
|
|
313
345
|
const referencedFieldNames = new Set();
|
|
346
|
+
const setResultCalls = [];
|
|
314
347
|
|
|
315
348
|
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
316
349
|
const form = normalizeFormSchema(schema);
|
|
@@ -407,6 +440,10 @@ export function analyzeCalculationExpression({
|
|
|
407
440
|
usedBuiltins.push(call.name);
|
|
408
441
|
}
|
|
409
442
|
|
|
443
|
+
if (call.name === 'SETRESULT') {
|
|
444
|
+
setResultCalls.push(call);
|
|
445
|
+
}
|
|
446
|
+
|
|
410
447
|
if (builtinMetadata.status === 'unavailable') {
|
|
411
448
|
addIssue(
|
|
412
449
|
issues,
|
|
@@ -452,31 +489,50 @@ export function analyzeCalculationExpression({
|
|
|
452
489
|
);
|
|
453
490
|
}
|
|
454
491
|
|
|
492
|
+
if (setResultCalls.length > 1) {
|
|
493
|
+
const duplicateSetResultCall = setResultCalls[1];
|
|
494
|
+
addIssue(
|
|
495
|
+
issues,
|
|
496
|
+
issueKeys,
|
|
497
|
+
createIssue({
|
|
498
|
+
code: 'multiple_setresult_calls',
|
|
499
|
+
severity: 'error',
|
|
500
|
+
message: 'SETRESULT() can only be used once in a CalculatedField expression.',
|
|
501
|
+
symbol: duplicateSetResultCall.name,
|
|
502
|
+
index: duplicateSetResultCall.index,
|
|
503
|
+
length: duplicateSetResultCall.length,
|
|
504
|
+
})
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
455
508
|
for (const reference of extractFieldReferenceMatches(normalizedExpression)) {
|
|
456
509
|
if (!referencedFieldNames.has(reference.fieldName)) {
|
|
457
510
|
referencedFieldNames.add(reference.fieldName);
|
|
458
511
|
referencedFields.push(reference.fieldName);
|
|
459
512
|
}
|
|
460
513
|
|
|
461
|
-
const
|
|
462
|
-
if (
|
|
514
|
+
const accessInfo = resolver.resolveFieldAccessInfo(executionContext, reference.fieldName);
|
|
515
|
+
if (accessInfo.level === 'restricted') {
|
|
463
516
|
addIssue(
|
|
464
517
|
issues,
|
|
465
518
|
issueKeys,
|
|
466
519
|
createIssue({
|
|
467
520
|
code: 'restricted_field_reference',
|
|
468
521
|
severity: 'error',
|
|
469
|
-
message:
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
resolver.
|
|
473
|
-
|
|
522
|
+
message:
|
|
523
|
+
accessInfo.suggestion ||
|
|
524
|
+
accessInfo.message ||
|
|
525
|
+
resolver.generateAccessSuggestion(
|
|
526
|
+
executionContext,
|
|
527
|
+
reference.fieldName,
|
|
528
|
+
resolver.getFieldInfo(reference.fieldName)
|
|
529
|
+
),
|
|
474
530
|
symbol: reference.reference,
|
|
475
531
|
index: reference.index,
|
|
476
532
|
length: reference.length,
|
|
477
533
|
})
|
|
478
534
|
);
|
|
479
|
-
} else if (
|
|
535
|
+
} else if (accessInfo.level === 'not_found') {
|
|
480
536
|
addIssue(
|
|
481
537
|
issues,
|
|
482
538
|
issueKeys,
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isMultilineCalculationExpression(expression) {
|
|
2
|
+
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
3
|
+
|
|
4
|
+
return (
|
|
5
|
+
normalizedExpression.includes('\r\n') ||
|
|
6
|
+
normalizedExpression.includes('\n') ||
|
|
7
|
+
normalizedExpression.includes('function')
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function normalizeInlineCalculationExpression(expression) {
|
|
12
|
+
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
13
|
+
|
|
14
|
+
return normalizedExpression.replace(/;+\s*$/u, '');
|
|
15
|
+
}
|