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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form0-core",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -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('accessible', 'main_form', fieldName, fieldInfo, executionContext);
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 this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !== 'different_repeatable_section';
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 (targetPath.length < contextPath.length && ContextResolver.isPrefix(targetPath, contextPath)) {
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(executionContext, fieldName, fieldInfo, code),
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.join(' -> ')}`;
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 || this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
428
+ accessInfo.suggestion ||
429
+ this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
385
430
  fieldContext: fieldInfo
386
431
  ? {
387
432
  parentPath: fieldInfo.parentPath,
@@ -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 fn = new Function(...keys, `return (${expr});`);
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
- new Function(`return (${expression});`);
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
+ }