@revisium/formula 0.3.0 → 0.4.0

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 CHANGED
@@ -46,6 +46,13 @@ evaluate('items[0].price + items[1].price', { items: [{ price: 10 }, { price: 20
46
46
  evaluate('price > 100', { price: 150 });
47
47
  // true
48
48
 
49
+ // Fields named like functions
50
+ evaluate('max(max, 0)', { max: 10 });
51
+ // 10 (max() function is called with field "max" as argument)
52
+
53
+ evaluate('max(max - field.min, 0)', { max: 100, field: { min: 20 } });
54
+ // 80
55
+
49
56
  // Type inference
50
57
  import { inferFormulaType } from '@revisium/formula';
51
58
 
@@ -72,6 +79,25 @@ validateFormulaAgainstSchema('price * quantity', 'total', schema);
72
79
 
73
80
  validateFormulaAgainstSchema('price > 100', 'total', schema);
74
81
  // { field: 'total', error: "Type mismatch: formula returns 'boolean' but field expects 'number'" }
82
+
83
+ // Array item formulas with path resolution
84
+ import { evaluateWithContext } from '@revisium/formula';
85
+
86
+ // Absolute path: /field always resolves from root data
87
+ evaluateWithContext('price * (1 + /taxRate)', {
88
+ rootData: { taxRate: 0.1, items: [{ price: 100 }] },
89
+ itemData: { price: 100 },
90
+ currentPath: 'items[0]'
91
+ });
92
+ // 110
93
+
94
+ // Relative path: ../field resolves from parent (root)
95
+ evaluateWithContext('price * (1 - ../discount)', {
96
+ rootData: { discount: 0.2, items: [] },
97
+ itemData: { price: 100 },
98
+ currentPath: 'items[0]'
99
+ });
100
+ // 80
75
101
  ```
76
102
 
77
103
  ## API
@@ -83,6 +109,7 @@ validateFormulaAgainstSchema('price > 100', 'total', schema);
83
109
  | `parseFormula` | Low-level parser returning AST, dependencies, features |
84
110
  | `validateSyntax` | Validate expression syntax |
85
111
  | `evaluate` | Evaluate expression with context |
112
+ | `evaluateWithContext` | Evaluate with automatic `/` and `../` path resolution |
86
113
  | `inferFormulaType` | Infer return type of expression |
87
114
 
88
115
  ### Expression API
@@ -116,14 +143,19 @@ validateFormulaAgainstSchema('price > 100', 'total', schema);
116
143
  | `obj.field` | Nested object | `stats.damage` |
117
144
  | `arr[N]` | Array index | `items[0].price` |
118
145
  | `arr[-1]` | Last element | `items[-1]` |
146
+ | `/field` | Absolute path (from root) | `/taxRate`, `/config.tax` |
147
+ | `../field` | Relative path (parent scope) | `../discount`, `../settings.multiplier` |
119
148
 
120
149
  ## Version Detection
121
150
 
122
151
  | Feature | Min Version |
123
152
  |---------|-------------|
124
153
  | Simple refs (`field`) | 1.0 |
154
+ | Function-named fields | 1.0 |
125
155
  | Nested paths (`a.b`) | 1.1 |
126
156
  | Array index (`[0]`, `[-1]`) | 1.1 |
157
+ | Absolute paths (`/field`) | 1.1 |
158
+ | Relative paths (`../field`) | 1.1 |
127
159
 
128
160
  ## License
129
161
 
@@ -296,6 +296,125 @@ token("*", 200, (a) => {
296
296
  if (!a) return createWildcardLiteral();
297
297
  return void 0;
298
298
  });
299
+ token("/", 200, (a) => {
300
+ if (a) return;
301
+ const name = next2(isIdChar);
302
+ if (!name) return;
303
+ return "/" + name;
304
+ });
305
+ var isRelativePathChar = (c) => isIdChar(c) || c === 47 || c === 46 ? 1 : 0;
306
+ token(".", 200, (a) => {
307
+ if (a) return;
308
+ const second = next2((c) => c === 46 ? 1 : 0);
309
+ if (!second) return;
310
+ const rest = next2(isRelativePathChar);
311
+ if (!rest) return;
312
+ return ".." + rest;
313
+ });
314
+ var BUILTIN_FUNCTIONS = {
315
+ and: (a, b) => Boolean(a) && Boolean(b),
316
+ or: (a, b) => Boolean(a) || Boolean(b),
317
+ not: (a) => !a,
318
+ concat: (...args) => args.map(String).join(""),
319
+ upper: (s) => String(s).toUpperCase(),
320
+ lower: (s) => String(s).toLowerCase(),
321
+ trim: (s) => String(s).trim(),
322
+ left: (s, n) => {
323
+ const count = Math.max(0, Math.floor(Number(n)));
324
+ return String(s).slice(0, count);
325
+ },
326
+ right: (s, n) => {
327
+ const str = String(s);
328
+ const count = Math.max(0, Math.floor(Number(n)));
329
+ return count === 0 ? "" : str.slice(-count);
330
+ },
331
+ replace: (s, search, replacement) => String(s).replace(String(search), String(replacement)),
332
+ tostring: String,
333
+ length: (s) => {
334
+ if (Array.isArray(s)) return s.length;
335
+ if (typeof s === "string") return s.length;
336
+ if (s !== null && typeof s === "object") return Object.keys(s).length;
337
+ return String(s).length;
338
+ },
339
+ contains: (s, search) => String(s).includes(String(search)),
340
+ startswith: (s, search) => String(s).startsWith(String(search)),
341
+ endswith: (s, search) => String(s).endsWith(String(search)),
342
+ tonumber: Number,
343
+ toboolean: Boolean,
344
+ isnull: (v) => v === null || v === void 0,
345
+ coalesce: (...args) => args.find((v) => v !== null && v !== void 0) ?? null,
346
+ round: (n, decimals) => {
347
+ const num2 = Number(n);
348
+ const dec = decimals === void 0 ? 0 : Number(decimals);
349
+ const factor = 10 ** dec;
350
+ return Math.round(num2 * factor) / factor;
351
+ },
352
+ floor: (n) => Math.floor(Number(n)),
353
+ ceil: (n) => Math.ceil(Number(n)),
354
+ abs: (n) => Math.abs(Number(n)),
355
+ sqrt: (n) => Math.sqrt(Number(n)),
356
+ pow: (base, exp) => Math.pow(Number(base), Number(exp)),
357
+ min: (...args) => args.length === 0 ? Number.NaN : Math.min(...args.map(Number)),
358
+ max: (...args) => args.length === 0 ? Number.NaN : Math.max(...args.map(Number)),
359
+ log: (n) => Math.log(Number(n)),
360
+ log10: (n) => Math.log10(Number(n)),
361
+ exp: (n) => Math.exp(Number(n)),
362
+ sign: (n) => Math.sign(Number(n)),
363
+ sum: (arr) => Array.isArray(arr) ? arr.reduce((a, b) => a + Number(b), 0) : 0,
364
+ avg: (arr) => Array.isArray(arr) && arr.length > 0 ? arr.reduce((a, b) => a + Number(b), 0) / arr.length : 0,
365
+ count: (arr) => Array.isArray(arr) ? arr.length : 0,
366
+ first: (arr) => Array.isArray(arr) ? arr[0] : void 0,
367
+ last: (arr) => Array.isArray(arr) ? arr.at(-1) : void 0,
368
+ join: (arr, separator) => {
369
+ if (!Array.isArray(arr)) return "";
370
+ if (separator === void 0) return arr.join(",");
371
+ if (typeof separator === "string") return arr.join(separator);
372
+ if (typeof separator === "number") return arr.join(String(separator));
373
+ return arr.join(",");
374
+ },
375
+ includes: (arr, value) => Array.isArray(arr) ? arr.includes(value) : false,
376
+ if: (condition, ifTrue, ifFalse) => condition ? ifTrue : ifFalse
377
+ };
378
+ function extractCallArgs(argsNode) {
379
+ if (!argsNode) return [];
380
+ if (!Array.isArray(argsNode)) return [argsNode];
381
+ if (argsNode[0] === ",") {
382
+ return argsNode.slice(1);
383
+ }
384
+ return [argsNode];
385
+ }
386
+ function compileNode(node) {
387
+ return compile(node);
388
+ }
389
+ function createGroupingEvaluator(fn) {
390
+ const compiledExpr = compileNode(fn);
391
+ return (ctx) => compiledExpr(ctx);
392
+ }
393
+ function createFunctionCallEvaluator(fn, argsNode) {
394
+ const args = extractCallArgs(argsNode);
395
+ const compiledArgs = args.map((arg) => compileNode(arg));
396
+ return (ctx) => {
397
+ const argValues = compiledArgs.map((a) => a(ctx));
398
+ if (typeof fn === "string") {
399
+ const builtinFn = BUILTIN_FUNCTIONS[fn.toLowerCase()];
400
+ if (builtinFn) {
401
+ return builtinFn(...argValues);
402
+ }
403
+ }
404
+ const fnValue = compileNode(fn)(ctx);
405
+ if (typeof fnValue === "function") {
406
+ return fnValue(...argValues);
407
+ }
408
+ const fnName = typeof fn === "string" ? fn : String(fn);
409
+ throw new Error(`'${fnName}' is not a function`);
410
+ };
411
+ }
412
+ operator("()", (fn, argsNode) => {
413
+ if (argsNode === void 0) {
414
+ return createGroupingEvaluator(fn);
415
+ }
416
+ return createFunctionCallEvaluator(fn, argsNode);
417
+ });
299
418
  var KEYWORDS = /* @__PURE__ */ new Set([
300
419
  "true",
301
420
  "false",
@@ -304,9 +423,6 @@ var KEYWORDS = /* @__PURE__ */ new Set([
304
423
  "or",
305
424
  "not",
306
425
  "if",
307
- "constructor",
308
- "__proto__",
309
- "prototype",
310
426
  "round",
311
427
  "floor",
312
428
  "ceil",
@@ -361,6 +477,12 @@ function isArrayFunction(name) {
361
477
  function isContextToken(name) {
362
478
  return name.startsWith("@") || name.startsWith("#");
363
479
  }
480
+ function isRootPath(name) {
481
+ return name.startsWith("/");
482
+ }
483
+ function isRelativePath(name) {
484
+ return name.startsWith("..");
485
+ }
364
486
  function isValidIdentifierRoot(rootName) {
365
487
  return !isKeyword(rootName) && !isContextToken(rootName);
366
488
  }
@@ -373,6 +495,10 @@ function addPathIfValid(path, identifiers) {
373
495
  }
374
496
  }
375
497
  function collectStringIdentifier(node, identifiers) {
498
+ if (isRootPath(node) || isRelativePath(node)) {
499
+ identifiers.add(node);
500
+ return;
501
+ }
376
502
  if (!isContextToken(node) && !isKeyword(node)) {
377
503
  identifiers.add(node);
378
504
  }
@@ -405,6 +531,9 @@ function collectIdentifiers(node, identifiers) {
405
531
  if (!Array.isArray(node)) {
406
532
  return;
407
533
  }
534
+ if (isLiteralArray(node)) {
535
+ return;
536
+ }
408
537
  const [op, ...args] = node;
409
538
  if (op === "." || op === "[]") {
410
539
  collectPathOrFallback(node, identifiers);
@@ -502,16 +631,25 @@ function detectFunctionCallFeatures(funcName, features) {
502
631
  features.add("array_function");
503
632
  }
504
633
  }
505
- function detectFeatures(node, features) {
506
- if (typeof node === "string") {
507
- if (isContextToken(node)) {
508
- features.add("context_token");
634
+ function detectStringFeatures(node, features) {
635
+ if (isContextToken(node)) {
636
+ features.add("context_token");
637
+ }
638
+ if (isRootPath(node)) {
639
+ features.add("root_path");
640
+ if (node.includes(".")) {
641
+ features.add("nested_path");
509
642
  }
510
- return;
511
643
  }
512
- if (!Array.isArray(node) || isLiteralArray(node)) {
513
- return;
644
+ if (isRelativePath(node)) {
645
+ features.add("relative_path");
646
+ const withoutPrefix = node.replace(/^(\.\.\/)+/, "");
647
+ if (withoutPrefix.includes(".")) {
648
+ features.add("nested_path");
649
+ }
514
650
  }
651
+ }
652
+ function detectOperatorFeatures(node, features) {
515
653
  const op = node[0];
516
654
  if (op === ".") {
517
655
  features.add("nested_path");
@@ -522,6 +660,16 @@ function detectFeatures(node, features) {
522
660
  if (op === "()") {
523
661
  detectFunctionCallFeatures(node[1], features);
524
662
  }
663
+ }
664
+ function detectFeatures(node, features) {
665
+ if (typeof node === "string") {
666
+ detectStringFeatures(node, features);
667
+ return;
668
+ }
669
+ if (!Array.isArray(node) || isLiteralArray(node)) {
670
+ return;
671
+ }
672
+ detectOperatorFeatures(node, features);
525
673
  for (let i = 1; i < node.length; i++) {
526
674
  detectFeatures(node[i], features);
527
675
  }
@@ -564,77 +712,13 @@ function validateSyntax(expression) {
564
712
  };
565
713
  }
566
714
  }
567
- var BUILTIN_FUNCTIONS = {
568
- and: (a, b) => Boolean(a) && Boolean(b),
569
- or: (a, b) => Boolean(a) || Boolean(b),
570
- not: (a) => !a,
571
- concat: (...args) => args.map(String).join(""),
572
- upper: (s) => String(s).toUpperCase(),
573
- lower: (s) => String(s).toLowerCase(),
574
- trim: (s) => String(s).trim(),
575
- left: (s, n) => {
576
- const count = Math.max(0, Math.floor(Number(n)));
577
- return String(s).slice(0, count);
578
- },
579
- right: (s, n) => {
580
- const str = String(s);
581
- const count = Math.max(0, Math.floor(Number(n)));
582
- return count === 0 ? "" : str.slice(-count);
583
- },
584
- replace: (s, search, replacement) => String(s).replace(String(search), String(replacement)),
585
- tostring: String,
586
- length: (s) => {
587
- if (Array.isArray(s)) return s.length;
588
- if (typeof s === "string") return s.length;
589
- if (s !== null && typeof s === "object") return Object.keys(s).length;
590
- return String(s).length;
591
- },
592
- contains: (s, search) => String(s).includes(String(search)),
593
- startswith: (s, search) => String(s).startsWith(String(search)),
594
- endswith: (s, search) => String(s).endsWith(String(search)),
595
- tonumber: Number,
596
- toboolean: Boolean,
597
- isnull: (v) => v === null || v === void 0,
598
- coalesce: (...args) => args.find((v) => v !== null && v !== void 0) ?? null,
599
- round: (n, decimals) => {
600
- const num2 = Number(n);
601
- const dec = decimals === void 0 ? 0 : Number(decimals);
602
- const factor = 10 ** dec;
603
- return Math.round(num2 * factor) / factor;
604
- },
605
- floor: (n) => Math.floor(Number(n)),
606
- ceil: (n) => Math.ceil(Number(n)),
607
- abs: (n) => Math.abs(Number(n)),
608
- sqrt: (n) => Math.sqrt(Number(n)),
609
- pow: (base, exp) => Math.pow(Number(base), Number(exp)),
610
- min: (...args) => args.length === 0 ? Number.NaN : Math.min(...args.map(Number)),
611
- max: (...args) => args.length === 0 ? Number.NaN : Math.max(...args.map(Number)),
612
- log: (n) => Math.log(Number(n)),
613
- log10: (n) => Math.log10(Number(n)),
614
- exp: (n) => Math.exp(Number(n)),
615
- sign: (n) => Math.sign(Number(n)),
616
- sum: (arr) => Array.isArray(arr) ? arr.reduce((a, b) => a + Number(b), 0) : 0,
617
- avg: (arr) => Array.isArray(arr) && arr.length > 0 ? arr.reduce((a, b) => a + Number(b), 0) / arr.length : 0,
618
- count: (arr) => Array.isArray(arr) ? arr.length : 0,
619
- first: (arr) => Array.isArray(arr) ? arr[0] : void 0,
620
- last: (arr) => Array.isArray(arr) ? arr.at(-1) : void 0,
621
- join: (arr, separator) => {
622
- if (!Array.isArray(arr)) return "";
623
- if (separator === void 0) return arr.join(",");
624
- if (typeof separator === "string") return arr.join(separator);
625
- if (typeof separator === "number") return arr.join(String(separator));
626
- return arr.join(",");
627
- },
628
- includes: (arr, value) => Array.isArray(arr) ? arr.includes(value) : false,
629
- if: (condition, ifTrue, ifFalse) => condition ? ifTrue : ifFalse
630
- };
631
715
  function evaluate(expression, context) {
632
716
  const trimmed = expression.trim();
633
717
  if (!trimmed) {
634
718
  throw new Error("Empty expression");
635
719
  }
636
720
  const fn = subscript_default(trimmed);
637
- const safeContext = { ...BUILTIN_FUNCTIONS };
721
+ const safeContext = {};
638
722
  for (const [key, value] of Object.entries(context)) {
639
723
  if (typeof value !== "function") {
640
724
  safeContext[key] = value;
@@ -642,6 +726,52 @@ function evaluate(expression, context) {
642
726
  }
643
727
  return fn(safeContext);
644
728
  }
729
+ function getValueByPath(data, path) {
730
+ const segments = path.split(".");
731
+ let current = data;
732
+ for (const segment of segments) {
733
+ if (current === null || current === void 0) {
734
+ return void 0;
735
+ }
736
+ if (typeof current !== "object") {
737
+ return void 0;
738
+ }
739
+ current = current[segment];
740
+ }
741
+ return current;
742
+ }
743
+ function buildPathReferences(rootData, dependencies) {
744
+ const refs = {};
745
+ for (const dep of dependencies) {
746
+ if (dep.startsWith("/")) {
747
+ const fieldPath = dep.slice(1);
748
+ const rootField = fieldPath.split(".")[0] ?? fieldPath;
749
+ const contextKey = "/" + rootField;
750
+ if (!(contextKey in refs)) {
751
+ refs[contextKey] = getValueByPath(rootData, rootField);
752
+ }
753
+ } else if (dep.startsWith("../")) {
754
+ const fieldPath = dep.slice(3);
755
+ refs[dep] = getValueByPath(rootData, fieldPath);
756
+ }
757
+ }
758
+ return refs;
759
+ }
760
+ function evaluateWithContext(expression, options) {
761
+ const { rootData, itemData } = options;
762
+ const trimmed = expression.trim();
763
+ if (!trimmed) {
764
+ throw new Error("Empty expression");
765
+ }
766
+ const parsed = parseFormula(trimmed);
767
+ const pathRefs = buildPathReferences(rootData, parsed.dependencies);
768
+ const context = {
769
+ ...rootData,
770
+ ...itemData ?? {},
771
+ ...pathRefs
772
+ };
773
+ return evaluate(trimmed, context);
774
+ }
645
775
  var ARITHMETIC_OPS = /* @__PURE__ */ new Set(["+", "-", "*", "/", "%"]);
646
776
  var COMPARISON_OPS = /* @__PURE__ */ new Set(["<", ">", "<=", ">=", "==", "!="]);
647
777
  var LOGICAL_OPS = /* @__PURE__ */ new Set(["&&", "||", "!"]);
@@ -895,21 +1025,104 @@ function processQueue(queue, graph, inDegree) {
895
1025
  // src/extract-schema.ts
896
1026
  function extractSchemaFormulas(schema) {
897
1027
  const formulas = [];
1028
+ extractFormulasRecursive(schema, "", formulas);
1029
+ return formulas;
1030
+ }
1031
+ function extractFormulasRecursive(schema, pathPrefix, formulas) {
1032
+ if (schema.type === "array" && schema.items) {
1033
+ extractFormulasRecursive(schema.items, `${pathPrefix}[]`, formulas);
1034
+ return;
1035
+ }
898
1036
  const properties = schema.properties ?? {};
899
1037
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1038
+ const fullPath = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
900
1039
  const xFormula = fieldSchema["x-formula"];
901
1040
  if (xFormula) {
902
1041
  formulas.push({
903
- fieldName,
1042
+ fieldName: fullPath,
904
1043
  expression: xFormula.expression,
905
1044
  fieldType: fieldSchema.type ?? "string"
906
1045
  });
907
1046
  }
1047
+ if (fieldSchema.type === "object" && fieldSchema.properties) {
1048
+ extractFormulasRecursive(fieldSchema, fullPath, formulas);
1049
+ }
1050
+ if (fieldSchema.type === "array" && fieldSchema.items) {
1051
+ extractFormulasRecursive(fieldSchema.items, `${fullPath}[]`, formulas);
1052
+ }
908
1053
  }
909
- return formulas;
910
1054
  }
911
1055
 
912
1056
  // src/validate-schema.ts
1057
+ function resolveSubSchema(schema, fieldPath) {
1058
+ if (!fieldPath) {
1059
+ return schema;
1060
+ }
1061
+ const segments = parsePathSegments(fieldPath);
1062
+ let current = schema;
1063
+ for (const segment of segments) {
1064
+ if (segment === "[]") {
1065
+ if (current.type === "array" && current.items) {
1066
+ current = current.items;
1067
+ } else {
1068
+ return null;
1069
+ }
1070
+ } else {
1071
+ if (current.properties?.[segment]) {
1072
+ current = current.properties[segment];
1073
+ } else {
1074
+ return null;
1075
+ }
1076
+ }
1077
+ }
1078
+ return current;
1079
+ }
1080
+ function parsePathSegments(path) {
1081
+ const segments = [];
1082
+ let current = "";
1083
+ let inBracket = false;
1084
+ for (const char of path) {
1085
+ if (char === "[") {
1086
+ if (current) {
1087
+ segments.push(current);
1088
+ current = "";
1089
+ }
1090
+ inBracket = true;
1091
+ } else if (char === "]") {
1092
+ inBracket = false;
1093
+ segments.push("[]");
1094
+ } else if (char === "." && !inBracket) {
1095
+ if (current) {
1096
+ segments.push(current);
1097
+ current = "";
1098
+ }
1099
+ } else if (!inBracket) {
1100
+ current += char;
1101
+ }
1102
+ }
1103
+ if (current) {
1104
+ segments.push(current);
1105
+ }
1106
+ return segments;
1107
+ }
1108
+ function getParentPath(fieldPath) {
1109
+ const lastDotIndex = fieldPath.lastIndexOf(".");
1110
+ const lastBracketIndex = fieldPath.lastIndexOf("[");
1111
+ const splitIndex = Math.max(lastDotIndex, lastBracketIndex);
1112
+ if (splitIndex <= 0) {
1113
+ return "";
1114
+ }
1115
+ return fieldPath.substring(0, splitIndex);
1116
+ }
1117
+ function getFieldName(fieldPath) {
1118
+ const lastDotIndex = fieldPath.lastIndexOf(".");
1119
+ const lastBracketIndex = fieldPath.lastIndexOf("]");
1120
+ const splitIndex = Math.max(lastDotIndex, lastBracketIndex);
1121
+ if (splitIndex === -1) {
1122
+ return fieldPath;
1123
+ }
1124
+ return fieldPath.substring(splitIndex + 1);
1125
+ }
913
1126
  function getSchemaFields(schema) {
914
1127
  const fields = /* @__PURE__ */ new Set();
915
1128
  const properties = schema.properties ?? {};
@@ -944,44 +1157,56 @@ function extractFieldRoot(dependency) {
944
1157
  const root = dependency.split(".")[0]?.split("[")[0];
945
1158
  return root || dependency;
946
1159
  }
947
- function validateFormulaAgainstSchema(expression, fieldName, schema) {
1160
+ function validateFormulaInContext(expression, fieldPath, rootSchema) {
948
1161
  const syntaxResult = validateFormulaSyntax(expression);
949
1162
  if (!syntaxResult.isValid) {
950
1163
  return {
951
- field: fieldName,
1164
+ field: fieldPath,
952
1165
  error: syntaxResult.error,
953
1166
  position: syntaxResult.position
954
1167
  };
955
1168
  }
1169
+ const parentPath = getParentPath(fieldPath);
1170
+ const localFieldName = getFieldName(fieldPath);
1171
+ const contextSchema = resolveSubSchema(rootSchema, parentPath);
1172
+ if (!contextSchema) {
1173
+ return {
1174
+ field: fieldPath,
1175
+ error: `Cannot resolve schema context for path '${parentPath}'`
1176
+ };
1177
+ }
956
1178
  const parseResult = parseExpression(expression);
957
- const schemaFields = getSchemaFields(schema);
1179
+ const schemaFields = getSchemaFields(contextSchema);
958
1180
  for (const dep of parseResult.dependencies) {
959
1181
  const rootField = extractFieldRoot(dep);
960
1182
  if (!schemaFields.has(rootField)) {
961
1183
  return {
962
- field: fieldName,
1184
+ field: fieldPath,
963
1185
  error: `Unknown field '${rootField}' in formula`
964
1186
  };
965
1187
  }
966
1188
  }
967
- if (parseResult.dependencies.some((d) => extractFieldRoot(d) === fieldName)) {
1189
+ if (parseResult.dependencies.some((d) => extractFieldRoot(d) === localFieldName)) {
968
1190
  return {
969
- field: fieldName,
1191
+ field: fieldPath,
970
1192
  error: `Formula cannot reference itself`
971
1193
  };
972
1194
  }
973
- const fieldSchema = schema.properties?.[fieldName];
1195
+ const fieldSchema = contextSchema.properties?.[localFieldName];
974
1196
  const expectedType = schemaTypeToInferred(fieldSchema?.type);
975
- const fieldTypes = getSchemaFieldTypes(schema);
1197
+ const fieldTypes = getSchemaFieldTypes(contextSchema);
976
1198
  const inferredType = inferFormulaType(expression, fieldTypes);
977
1199
  if (!isTypeCompatible(inferredType, expectedType)) {
978
1200
  return {
979
- field: fieldName,
1201
+ field: fieldPath,
980
1202
  error: `Type mismatch: formula returns '${inferredType}' but field expects '${expectedType}'`
981
1203
  };
982
1204
  }
983
1205
  return null;
984
1206
  }
1207
+ function validateFormulaAgainstSchema(expression, fieldName, schema) {
1208
+ return validateFormulaInContext(expression, fieldName, schema);
1209
+ }
985
1210
  function validateSchemaFormulas(schema) {
986
1211
  const errors = [];
987
1212
  const formulas = extractSchemaFormulas(schema);
@@ -1001,7 +1226,12 @@ function validateSchemaFormulas(schema) {
1001
1226
  const dependencies = {};
1002
1227
  for (const formula of formulas) {
1003
1228
  const parseResult = parseExpression(formula.expression);
1004
- dependencies[formula.fieldName] = parseResult.dependencies.map(extractFieldRoot);
1229
+ const parentPath = getParentPath(formula.fieldName);
1230
+ const prefix = parentPath ? `${parentPath}.` : "";
1231
+ dependencies[formula.fieldName] = parseResult.dependencies.map((dep) => {
1232
+ const rootField = extractFieldRoot(dep);
1233
+ return `${prefix}${rootField}`;
1234
+ });
1005
1235
  }
1006
1236
  const graph = buildDependencyGraph(dependencies);
1007
1237
  const circularCheck = detectCircularDependencies(graph);
@@ -1022,6 +1252,7 @@ function validateSchemaFormulas(schema) {
1022
1252
  exports.buildDependencyGraph = buildDependencyGraph;
1023
1253
  exports.detectCircularDependencies = detectCircularDependencies;
1024
1254
  exports.evaluate = evaluate;
1255
+ exports.evaluateWithContext = evaluateWithContext;
1025
1256
  exports.extractSchemaFormulas = extractSchemaFormulas;
1026
1257
  exports.getTopologicalOrder = getTopologicalOrder;
1027
1258
  exports.inferFormulaType = inferFormulaType;
@@ -1031,5 +1262,5 @@ exports.validateFormulaAgainstSchema = validateFormulaAgainstSchema;
1031
1262
  exports.validateFormulaSyntax = validateFormulaSyntax;
1032
1263
  exports.validateSchemaFormulas = validateSchemaFormulas;
1033
1264
  exports.validateSyntax = validateSyntax;
1034
- //# sourceMappingURL=chunk-JJ72EVIZ.cjs.map
1035
- //# sourceMappingURL=chunk-JJ72EVIZ.cjs.map
1265
+ //# sourceMappingURL=chunk-AGBOCJGV.cjs.map
1266
+ //# sourceMappingURL=chunk-AGBOCJGV.cjs.map