form0-core 0.1.4 → 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
|
@@ -93,7 +93,13 @@ export class ContextResolver {
|
|
|
93
93
|
|
|
94
94
|
// Main form fields (no parent RepeatableSection) are always accessible
|
|
95
95
|
if (fieldInfo.parentPath.length === 0) {
|
|
96
|
-
return this.buildAccessInfo(
|
|
96
|
+
return this.buildAccessInfo(
|
|
97
|
+
'accessible',
|
|
98
|
+
'main_form',
|
|
99
|
+
fieldName,
|
|
100
|
+
fieldInfo,
|
|
101
|
+
executionContext
|
|
102
|
+
);
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
// RepeatableSection field access depends on execution context
|
|
@@ -246,7 +252,10 @@ export class ContextResolver {
|
|
|
246
252
|
* @returns {boolean} True if they share the same RepeatableSection context
|
|
247
253
|
*/
|
|
248
254
|
haveSameRepeatableSectionContext(parentPath1, parentPath2) {
|
|
249
|
-
return
|
|
255
|
+
return (
|
|
256
|
+
this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !==
|
|
257
|
+
'different_repeatable_section'
|
|
258
|
+
);
|
|
250
259
|
}
|
|
251
260
|
|
|
252
261
|
resolveRepeatableSectionRelationship(
|
|
@@ -271,7 +280,10 @@ export class ContextResolver {
|
|
|
271
280
|
return contextPath.length === 0 ? 'main_form' : 'same_repeatable_section';
|
|
272
281
|
}
|
|
273
282
|
|
|
274
|
-
if (
|
|
283
|
+
if (
|
|
284
|
+
targetPath.length < contextPath.length &&
|
|
285
|
+
ContextResolver.isPrefix(targetPath, contextPath)
|
|
286
|
+
) {
|
|
275
287
|
return targetPath.length === 0 ? 'main_form' : 'ancestor_repeatable_context';
|
|
276
288
|
}
|
|
277
289
|
|
|
@@ -293,7 +305,12 @@ export class ContextResolver {
|
|
|
293
305
|
level,
|
|
294
306
|
code,
|
|
295
307
|
message: `Field '${fieldName}' is not accessible from current context`,
|
|
296
|
-
suggestion: this.buildRestrictedAccessSuggestion(
|
|
308
|
+
suggestion: this.buildRestrictedAccessSuggestion(
|
|
309
|
+
executionContext,
|
|
310
|
+
fieldName,
|
|
311
|
+
fieldInfo,
|
|
312
|
+
code
|
|
313
|
+
),
|
|
297
314
|
fieldInfo,
|
|
298
315
|
};
|
|
299
316
|
}
|
|
@@ -303,7 +320,7 @@ export class ContextResolver {
|
|
|
303
320
|
|
|
304
321
|
if (contextType === 'calculation') {
|
|
305
322
|
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
|
|
323
|
+
return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
|
|
307
324
|
}
|
|
308
325
|
}
|
|
309
326
|
|
|
@@ -322,6 +339,33 @@ export class ContextResolver {
|
|
|
322
339
|
return 'Check the field access rules for your current context';
|
|
323
340
|
}
|
|
324
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];
|
|
367
|
+
}
|
|
368
|
+
|
|
325
369
|
static isPrefix(prefix, fullPath) {
|
|
326
370
|
if (prefix.length === 0) {
|
|
327
371
|
return true;
|
|
@@ -381,7 +425,8 @@ export class ContextResolver {
|
|
|
381
425
|
? `Field '${fieldName}' does not exist in the form schema`
|
|
382
426
|
: `Field '${fieldName}' is not accessible from current context`),
|
|
383
427
|
suggestion:
|
|
384
|
-
accessInfo.suggestion ||
|
|
428
|
+
accessInfo.suggestion ||
|
|
429
|
+
this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
|
|
385
430
|
fieldContext: fieldInfo
|
|
386
431
|
? {
|
|
387
432
|
parentPath: fieldInfo.parentPath,
|
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([
|
|
@@ -181,9 +185,7 @@ function toCalculationReferenceDefinition(field, resolver, executionContext) {
|
|
|
181
185
|
type: typeof field.type === 'string' ? field.type : 'UnknownField',
|
|
182
186
|
description: typeof field.description === 'string' ? field.description : null,
|
|
183
187
|
displayStyle:
|
|
184
|
-
field.display &&
|
|
185
|
-
typeof field.display === 'object' &&
|
|
186
|
-
typeof field.display.style === 'string'
|
|
188
|
+
field.display && typeof field.display === 'object' && typeof field.display.style === 'string'
|
|
187
189
|
? field.display.style
|
|
188
190
|
: typeof field.display === 'string'
|
|
189
191
|
? field.display
|
|
@@ -214,15 +216,15 @@ function addIssue(issues, issueKeys, issue) {
|
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
function validateSyntax(expression) {
|
|
217
|
-
const isMultiLine =
|
|
218
|
-
expression.includes('\r\n') || expression.includes('\n') || expression.includes('function');
|
|
219
|
+
const isMultiLine = isMultilineCalculationExpression(expression);
|
|
219
220
|
|
|
220
221
|
if (isMultiLine) {
|
|
221
222
|
new Function(expression);
|
|
222
223
|
return;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
|
|
226
|
+
const inlineExpression = normalizeInlineCalculationExpression(expression);
|
|
227
|
+
new Function(`return (${inlineExpression});`);
|
|
226
228
|
}
|
|
227
229
|
|
|
228
230
|
function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
@@ -341,6 +343,7 @@ export function analyzeCalculationExpression({
|
|
|
341
343
|
const usedBuiltinNames = new Set();
|
|
342
344
|
const referencedFields = [];
|
|
343
345
|
const referencedFieldNames = new Set();
|
|
346
|
+
const setResultCalls = [];
|
|
344
347
|
|
|
345
348
|
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
346
349
|
const form = normalizeFormSchema(schema);
|
|
@@ -437,6 +440,10 @@ export function analyzeCalculationExpression({
|
|
|
437
440
|
usedBuiltins.push(call.name);
|
|
438
441
|
}
|
|
439
442
|
|
|
443
|
+
if (call.name === 'SETRESULT') {
|
|
444
|
+
setResultCalls.push(call);
|
|
445
|
+
}
|
|
446
|
+
|
|
440
447
|
if (builtinMetadata.status === 'unavailable') {
|
|
441
448
|
addIssue(
|
|
442
449
|
issues,
|
|
@@ -482,6 +489,22 @@ export function analyzeCalculationExpression({
|
|
|
482
489
|
);
|
|
483
490
|
}
|
|
484
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
|
+
|
|
485
508
|
for (const reference of extractFieldReferenceMatches(normalizedExpression)) {
|
|
486
509
|
if (!referencedFieldNames.has(reference.fieldName)) {
|
|
487
510
|
referencedFieldNames.add(reference.fieldName);
|
|
@@ -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
|
+
}
|