@revisium/formula 0.2.1 → 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
@@ -3,7 +3,7 @@
3
3
  # @revisium/formula
4
4
 
5
5
  [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=revisium_formula&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=revisium_formula)
6
- [![codecov](https://codecov.io/gh/revisium/formula/graph/badge.svg)](https://codecov.io/gh/revisium/formula)
6
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=revisium_formula&metric=coverage)](https://sonarcloud.io/summary/new_code?id=revisium_formula)
7
7
  [![GitHub License](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/revisium/formula/blob/master/LICENSE)
8
8
  [![GitHub Release](https://img.shields.io/github/v/release/revisium/formula)](https://github.com/revisium/formula/releases)
9
9
 
@@ -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",
@@ -341,8 +457,6 @@ var KEYWORDS = /* @__PURE__ */ new Set([
341
457
  "first",
342
458
  "last",
343
459
  "join",
344
- "filter",
345
- "map",
346
460
  "includes"
347
461
  ]);
348
462
  var ARRAY_FUNCTIONS = /* @__PURE__ */ new Set([
@@ -352,8 +466,6 @@ var ARRAY_FUNCTIONS = /* @__PURE__ */ new Set([
352
466
  "first",
353
467
  "last",
354
468
  "join",
355
- "filter",
356
- "map",
357
469
  "includes"
358
470
  ]);
359
471
  function isKeyword(name) {
@@ -365,6 +477,12 @@ function isArrayFunction(name) {
365
477
  function isContextToken(name) {
366
478
  return name.startsWith("@") || name.startsWith("#");
367
479
  }
480
+ function isRootPath(name) {
481
+ return name.startsWith("/");
482
+ }
483
+ function isRelativePath(name) {
484
+ return name.startsWith("..");
485
+ }
368
486
  function isValidIdentifierRoot(rootName) {
369
487
  return !isKeyword(rootName) && !isContextToken(rootName);
370
488
  }
@@ -377,6 +495,10 @@ function addPathIfValid(path, identifiers) {
377
495
  }
378
496
  }
379
497
  function collectStringIdentifier(node, identifiers) {
498
+ if (isRootPath(node) || isRelativePath(node)) {
499
+ identifiers.add(node);
500
+ return;
501
+ }
380
502
  if (!isContextToken(node) && !isKeyword(node)) {
381
503
  identifiers.add(node);
382
504
  }
@@ -409,6 +531,9 @@ function collectIdentifiers(node, identifiers) {
409
531
  if (!Array.isArray(node)) {
410
532
  return;
411
533
  }
534
+ if (isLiteralArray(node)) {
535
+ return;
536
+ }
412
537
  const [op, ...args] = node;
413
538
  if (op === "." || op === "[]") {
414
539
  collectPathOrFallback(node, identifiers);
@@ -506,16 +631,25 @@ function detectFunctionCallFeatures(funcName, features) {
506
631
  features.add("array_function");
507
632
  }
508
633
  }
509
- function detectFeatures(node, features) {
510
- if (typeof node === "string") {
511
- if (isContextToken(node)) {
512
- 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");
513
642
  }
514
- return;
515
643
  }
516
- if (!Array.isArray(node) || isLiteralArray(node)) {
517
- 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
+ }
518
650
  }
651
+ }
652
+ function detectOperatorFeatures(node, features) {
519
653
  const op = node[0];
520
654
  if (op === ".") {
521
655
  features.add("nested_path");
@@ -526,6 +660,16 @@ function detectFeatures(node, features) {
526
660
  if (op === "()") {
527
661
  detectFunctionCallFeatures(node[1], features);
528
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);
529
673
  for (let i = 1; i < node.length; i++) {
530
674
  detectFeatures(node[i], features);
531
675
  }
@@ -574,20 +718,63 @@ function evaluate(expression, context) {
574
718
  throw new Error("Empty expression");
575
719
  }
576
720
  const fn = subscript_default(trimmed);
577
- return fn(context);
721
+ const safeContext = {};
722
+ for (const [key, value] of Object.entries(context)) {
723
+ if (typeof value !== "function") {
724
+ safeContext[key] = value;
725
+ }
726
+ }
727
+ return fn(safeContext);
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);
578
774
  }
579
775
  var ARITHMETIC_OPS = /* @__PURE__ */ new Set(["+", "-", "*", "/", "%"]);
580
- var COMPARISON_OPS = /* @__PURE__ */ new Set([
581
- "<",
582
- ">",
583
- "<=",
584
- ">=",
585
- "==",
586
- "!=",
587
- "===",
588
- "!=="
589
- ]);
590
- var LOGICAL_OPS = /* @__PURE__ */ new Set(["&&", "||", "!", "and", "or", "not"]);
776
+ var COMPARISON_OPS = /* @__PURE__ */ new Set(["<", ">", "<=", ">=", "==", "!="]);
777
+ var LOGICAL_OPS = /* @__PURE__ */ new Set(["&&", "||", "!"]);
591
778
  var NUMERIC_FUNCTIONS = /* @__PURE__ */ new Set([
592
779
  "round",
593
780
  "floor",
@@ -604,7 +791,8 @@ var NUMERIC_FUNCTIONS = /* @__PURE__ */ new Set([
604
791
  "sum",
605
792
  "avg",
606
793
  "count",
607
- "tonumber"
794
+ "tonumber",
795
+ "length"
608
796
  ]);
609
797
  var STRING_FUNCTIONS = /* @__PURE__ */ new Set([
610
798
  "concat",
@@ -618,6 +806,9 @@ var STRING_FUNCTIONS = /* @__PURE__ */ new Set([
618
806
  "join"
619
807
  ]);
620
808
  var BOOLEAN_FUNCTIONS = /* @__PURE__ */ new Set([
809
+ "and",
810
+ "or",
811
+ "not",
621
812
  "contains",
622
813
  "startswith",
623
814
  "endswith",
@@ -647,7 +838,12 @@ function inferLiteralArrayType(node) {
647
838
  if (typeof val === "boolean") return "boolean";
648
839
  return "unknown";
649
840
  }
650
- function inferOperatorType(op, argsLength) {
841
+ function inferOperatorType(op, argsLength, argTypes) {
842
+ if (op === "+" && argTypes) {
843
+ if (argTypes.includes("string")) return "string";
844
+ if (argTypes.includes("unknown")) return "unknown";
845
+ return "number";
846
+ }
651
847
  if (ARITHMETIC_OPS.has(op)) return "number";
652
848
  if (COMPARISON_OPS.has(op)) return "boolean";
653
849
  if (LOGICAL_OPS.has(op)) return "boolean";
@@ -672,7 +868,8 @@ function inferTypeFromNode(node, fieldTypes) {
672
868
  if (!Array.isArray(node)) return "unknown";
673
869
  if (isLiteralArray(node)) return inferLiteralArrayType(node);
674
870
  const [op, ...args] = node;
675
- const operatorType = inferOperatorType(op, args.length);
871
+ const argTypes = op === "+" ? args.map((arg) => inferTypeFromNode(arg, fieldTypes)) : void 0;
872
+ const operatorType = inferOperatorType(op, args.length, argTypes);
676
873
  if (operatorType !== null) return operatorType;
677
874
  if (op === "." || op === "[]") {
678
875
  return inferPropertyAccessType(node, fieldTypes);
@@ -828,21 +1025,104 @@ function processQueue(queue, graph, inDegree) {
828
1025
  // src/extract-schema.ts
829
1026
  function extractSchemaFormulas(schema) {
830
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
+ }
831
1036
  const properties = schema.properties ?? {};
832
1037
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1038
+ const fullPath = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
833
1039
  const xFormula = fieldSchema["x-formula"];
834
1040
  if (xFormula) {
835
1041
  formulas.push({
836
- fieldName,
1042
+ fieldName: fullPath,
837
1043
  expression: xFormula.expression,
838
1044
  fieldType: fieldSchema.type ?? "string"
839
1045
  });
840
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
+ }
841
1053
  }
842
- return formulas;
843
1054
  }
844
1055
 
845
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
+ }
846
1126
  function getSchemaFields(schema) {
847
1127
  const fields = /* @__PURE__ */ new Set();
848
1128
  const properties = schema.properties ?? {};
@@ -877,44 +1157,56 @@ function extractFieldRoot(dependency) {
877
1157
  const root = dependency.split(".")[0]?.split("[")[0];
878
1158
  return root || dependency;
879
1159
  }
880
- function validateFormulaAgainstSchema(expression, fieldName, schema) {
1160
+ function validateFormulaInContext(expression, fieldPath, rootSchema) {
881
1161
  const syntaxResult = validateFormulaSyntax(expression);
882
1162
  if (!syntaxResult.isValid) {
883
1163
  return {
884
- field: fieldName,
1164
+ field: fieldPath,
885
1165
  error: syntaxResult.error,
886
1166
  position: syntaxResult.position
887
1167
  };
888
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
+ }
889
1178
  const parseResult = parseExpression(expression);
890
- const schemaFields = getSchemaFields(schema);
1179
+ const schemaFields = getSchemaFields(contextSchema);
891
1180
  for (const dep of parseResult.dependencies) {
892
1181
  const rootField = extractFieldRoot(dep);
893
1182
  if (!schemaFields.has(rootField)) {
894
1183
  return {
895
- field: fieldName,
1184
+ field: fieldPath,
896
1185
  error: `Unknown field '${rootField}' in formula`
897
1186
  };
898
1187
  }
899
1188
  }
900
- if (parseResult.dependencies.some((d) => extractFieldRoot(d) === fieldName)) {
1189
+ if (parseResult.dependencies.some((d) => extractFieldRoot(d) === localFieldName)) {
901
1190
  return {
902
- field: fieldName,
1191
+ field: fieldPath,
903
1192
  error: `Formula cannot reference itself`
904
1193
  };
905
1194
  }
906
- const fieldSchema = schema.properties?.[fieldName];
1195
+ const fieldSchema = contextSchema.properties?.[localFieldName];
907
1196
  const expectedType = schemaTypeToInferred(fieldSchema?.type);
908
- const fieldTypes = getSchemaFieldTypes(schema);
1197
+ const fieldTypes = getSchemaFieldTypes(contextSchema);
909
1198
  const inferredType = inferFormulaType(expression, fieldTypes);
910
1199
  if (!isTypeCompatible(inferredType, expectedType)) {
911
1200
  return {
912
- field: fieldName,
1201
+ field: fieldPath,
913
1202
  error: `Type mismatch: formula returns '${inferredType}' but field expects '${expectedType}'`
914
1203
  };
915
1204
  }
916
1205
  return null;
917
1206
  }
1207
+ function validateFormulaAgainstSchema(expression, fieldName, schema) {
1208
+ return validateFormulaInContext(expression, fieldName, schema);
1209
+ }
918
1210
  function validateSchemaFormulas(schema) {
919
1211
  const errors = [];
920
1212
  const formulas = extractSchemaFormulas(schema);
@@ -934,7 +1226,12 @@ function validateSchemaFormulas(schema) {
934
1226
  const dependencies = {};
935
1227
  for (const formula of formulas) {
936
1228
  const parseResult = parseExpression(formula.expression);
937
- 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
+ });
938
1235
  }
939
1236
  const graph = buildDependencyGraph(dependencies);
940
1237
  const circularCheck = detectCircularDependencies(graph);
@@ -955,6 +1252,7 @@ function validateSchemaFormulas(schema) {
955
1252
  exports.buildDependencyGraph = buildDependencyGraph;
956
1253
  exports.detectCircularDependencies = detectCircularDependencies;
957
1254
  exports.evaluate = evaluate;
1255
+ exports.evaluateWithContext = evaluateWithContext;
958
1256
  exports.extractSchemaFormulas = extractSchemaFormulas;
959
1257
  exports.getTopologicalOrder = getTopologicalOrder;
960
1258
  exports.inferFormulaType = inferFormulaType;
@@ -964,5 +1262,5 @@ exports.validateFormulaAgainstSchema = validateFormulaAgainstSchema;
964
1262
  exports.validateFormulaSyntax = validateFormulaSyntax;
965
1263
  exports.validateSchemaFormulas = validateSchemaFormulas;
966
1264
  exports.validateSyntax = validateSyntax;
967
- //# sourceMappingURL=chunk-5NMNSRHH.cjs.map
968
- //# sourceMappingURL=chunk-5NMNSRHH.cjs.map
1265
+ //# sourceMappingURL=chunk-AGBOCJGV.cjs.map
1266
+ //# sourceMappingURL=chunk-AGBOCJGV.cjs.map