@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.
@@ -294,6 +294,125 @@ token("*", 200, (a) => {
294
294
  if (!a) return createWildcardLiteral();
295
295
  return void 0;
296
296
  });
297
+ token("/", 200, (a) => {
298
+ if (a) return;
299
+ const name = next2(isIdChar);
300
+ if (!name) return;
301
+ return "/" + name;
302
+ });
303
+ var isRelativePathChar = (c) => isIdChar(c) || c === 47 || c === 46 ? 1 : 0;
304
+ token(".", 200, (a) => {
305
+ if (a) return;
306
+ const second = next2((c) => c === 46 ? 1 : 0);
307
+ if (!second) return;
308
+ const rest = next2(isRelativePathChar);
309
+ if (!rest) return;
310
+ return ".." + rest;
311
+ });
312
+ var BUILTIN_FUNCTIONS = {
313
+ and: (a, b) => Boolean(a) && Boolean(b),
314
+ or: (a, b) => Boolean(a) || Boolean(b),
315
+ not: (a) => !a,
316
+ concat: (...args) => args.map(String).join(""),
317
+ upper: (s) => String(s).toUpperCase(),
318
+ lower: (s) => String(s).toLowerCase(),
319
+ trim: (s) => String(s).trim(),
320
+ left: (s, n) => {
321
+ const count = Math.max(0, Math.floor(Number(n)));
322
+ return String(s).slice(0, count);
323
+ },
324
+ right: (s, n) => {
325
+ const str = String(s);
326
+ const count = Math.max(0, Math.floor(Number(n)));
327
+ return count === 0 ? "" : str.slice(-count);
328
+ },
329
+ replace: (s, search, replacement) => String(s).replace(String(search), String(replacement)),
330
+ tostring: String,
331
+ length: (s) => {
332
+ if (Array.isArray(s)) return s.length;
333
+ if (typeof s === "string") return s.length;
334
+ if (s !== null && typeof s === "object") return Object.keys(s).length;
335
+ return String(s).length;
336
+ },
337
+ contains: (s, search) => String(s).includes(String(search)),
338
+ startswith: (s, search) => String(s).startsWith(String(search)),
339
+ endswith: (s, search) => String(s).endsWith(String(search)),
340
+ tonumber: Number,
341
+ toboolean: Boolean,
342
+ isnull: (v) => v === null || v === void 0,
343
+ coalesce: (...args) => args.find((v) => v !== null && v !== void 0) ?? null,
344
+ round: (n, decimals) => {
345
+ const num2 = Number(n);
346
+ const dec = decimals === void 0 ? 0 : Number(decimals);
347
+ const factor = 10 ** dec;
348
+ return Math.round(num2 * factor) / factor;
349
+ },
350
+ floor: (n) => Math.floor(Number(n)),
351
+ ceil: (n) => Math.ceil(Number(n)),
352
+ abs: (n) => Math.abs(Number(n)),
353
+ sqrt: (n) => Math.sqrt(Number(n)),
354
+ pow: (base, exp) => Math.pow(Number(base), Number(exp)),
355
+ min: (...args) => args.length === 0 ? Number.NaN : Math.min(...args.map(Number)),
356
+ max: (...args) => args.length === 0 ? Number.NaN : Math.max(...args.map(Number)),
357
+ log: (n) => Math.log(Number(n)),
358
+ log10: (n) => Math.log10(Number(n)),
359
+ exp: (n) => Math.exp(Number(n)),
360
+ sign: (n) => Math.sign(Number(n)),
361
+ sum: (arr) => Array.isArray(arr) ? arr.reduce((a, b) => a + Number(b), 0) : 0,
362
+ avg: (arr) => Array.isArray(arr) && arr.length > 0 ? arr.reduce((a, b) => a + Number(b), 0) / arr.length : 0,
363
+ count: (arr) => Array.isArray(arr) ? arr.length : 0,
364
+ first: (arr) => Array.isArray(arr) ? arr[0] : void 0,
365
+ last: (arr) => Array.isArray(arr) ? arr.at(-1) : void 0,
366
+ join: (arr, separator) => {
367
+ if (!Array.isArray(arr)) return "";
368
+ if (separator === void 0) return arr.join(",");
369
+ if (typeof separator === "string") return arr.join(separator);
370
+ if (typeof separator === "number") return arr.join(String(separator));
371
+ return arr.join(",");
372
+ },
373
+ includes: (arr, value) => Array.isArray(arr) ? arr.includes(value) : false,
374
+ if: (condition, ifTrue, ifFalse) => condition ? ifTrue : ifFalse
375
+ };
376
+ function extractCallArgs(argsNode) {
377
+ if (!argsNode) return [];
378
+ if (!Array.isArray(argsNode)) return [argsNode];
379
+ if (argsNode[0] === ",") {
380
+ return argsNode.slice(1);
381
+ }
382
+ return [argsNode];
383
+ }
384
+ function compileNode(node) {
385
+ return compile(node);
386
+ }
387
+ function createGroupingEvaluator(fn) {
388
+ const compiledExpr = compileNode(fn);
389
+ return (ctx) => compiledExpr(ctx);
390
+ }
391
+ function createFunctionCallEvaluator(fn, argsNode) {
392
+ const args = extractCallArgs(argsNode);
393
+ const compiledArgs = args.map((arg) => compileNode(arg));
394
+ return (ctx) => {
395
+ const argValues = compiledArgs.map((a) => a(ctx));
396
+ if (typeof fn === "string") {
397
+ const builtinFn = BUILTIN_FUNCTIONS[fn.toLowerCase()];
398
+ if (builtinFn) {
399
+ return builtinFn(...argValues);
400
+ }
401
+ }
402
+ const fnValue = compileNode(fn)(ctx);
403
+ if (typeof fnValue === "function") {
404
+ return fnValue(...argValues);
405
+ }
406
+ const fnName = typeof fn === "string" ? fn : String(fn);
407
+ throw new Error(`'${fnName}' is not a function`);
408
+ };
409
+ }
410
+ operator("()", (fn, argsNode) => {
411
+ if (argsNode === void 0) {
412
+ return createGroupingEvaluator(fn);
413
+ }
414
+ return createFunctionCallEvaluator(fn, argsNode);
415
+ });
297
416
  var KEYWORDS = /* @__PURE__ */ new Set([
298
417
  "true",
299
418
  "false",
@@ -302,9 +421,6 @@ var KEYWORDS = /* @__PURE__ */ new Set([
302
421
  "or",
303
422
  "not",
304
423
  "if",
305
- "constructor",
306
- "__proto__",
307
- "prototype",
308
424
  "round",
309
425
  "floor",
310
426
  "ceil",
@@ -339,8 +455,6 @@ var KEYWORDS = /* @__PURE__ */ new Set([
339
455
  "first",
340
456
  "last",
341
457
  "join",
342
- "filter",
343
- "map",
344
458
  "includes"
345
459
  ]);
346
460
  var ARRAY_FUNCTIONS = /* @__PURE__ */ new Set([
@@ -350,8 +464,6 @@ var ARRAY_FUNCTIONS = /* @__PURE__ */ new Set([
350
464
  "first",
351
465
  "last",
352
466
  "join",
353
- "filter",
354
- "map",
355
467
  "includes"
356
468
  ]);
357
469
  function isKeyword(name) {
@@ -363,6 +475,12 @@ function isArrayFunction(name) {
363
475
  function isContextToken(name) {
364
476
  return name.startsWith("@") || name.startsWith("#");
365
477
  }
478
+ function isRootPath(name) {
479
+ return name.startsWith("/");
480
+ }
481
+ function isRelativePath(name) {
482
+ return name.startsWith("..");
483
+ }
366
484
  function isValidIdentifierRoot(rootName) {
367
485
  return !isKeyword(rootName) && !isContextToken(rootName);
368
486
  }
@@ -375,6 +493,10 @@ function addPathIfValid(path, identifiers) {
375
493
  }
376
494
  }
377
495
  function collectStringIdentifier(node, identifiers) {
496
+ if (isRootPath(node) || isRelativePath(node)) {
497
+ identifiers.add(node);
498
+ return;
499
+ }
378
500
  if (!isContextToken(node) && !isKeyword(node)) {
379
501
  identifiers.add(node);
380
502
  }
@@ -407,6 +529,9 @@ function collectIdentifiers(node, identifiers) {
407
529
  if (!Array.isArray(node)) {
408
530
  return;
409
531
  }
532
+ if (isLiteralArray(node)) {
533
+ return;
534
+ }
410
535
  const [op, ...args] = node;
411
536
  if (op === "." || op === "[]") {
412
537
  collectPathOrFallback(node, identifiers);
@@ -504,16 +629,25 @@ function detectFunctionCallFeatures(funcName, features) {
504
629
  features.add("array_function");
505
630
  }
506
631
  }
507
- function detectFeatures(node, features) {
508
- if (typeof node === "string") {
509
- if (isContextToken(node)) {
510
- features.add("context_token");
632
+ function detectStringFeatures(node, features) {
633
+ if (isContextToken(node)) {
634
+ features.add("context_token");
635
+ }
636
+ if (isRootPath(node)) {
637
+ features.add("root_path");
638
+ if (node.includes(".")) {
639
+ features.add("nested_path");
511
640
  }
512
- return;
513
641
  }
514
- if (!Array.isArray(node) || isLiteralArray(node)) {
515
- return;
642
+ if (isRelativePath(node)) {
643
+ features.add("relative_path");
644
+ const withoutPrefix = node.replace(/^(\.\.\/)+/, "");
645
+ if (withoutPrefix.includes(".")) {
646
+ features.add("nested_path");
647
+ }
516
648
  }
649
+ }
650
+ function detectOperatorFeatures(node, features) {
517
651
  const op = node[0];
518
652
  if (op === ".") {
519
653
  features.add("nested_path");
@@ -524,6 +658,16 @@ function detectFeatures(node, features) {
524
658
  if (op === "()") {
525
659
  detectFunctionCallFeatures(node[1], features);
526
660
  }
661
+ }
662
+ function detectFeatures(node, features) {
663
+ if (typeof node === "string") {
664
+ detectStringFeatures(node, features);
665
+ return;
666
+ }
667
+ if (!Array.isArray(node) || isLiteralArray(node)) {
668
+ return;
669
+ }
670
+ detectOperatorFeatures(node, features);
527
671
  for (let i = 1; i < node.length; i++) {
528
672
  detectFeatures(node[i], features);
529
673
  }
@@ -572,20 +716,63 @@ function evaluate(expression, context) {
572
716
  throw new Error("Empty expression");
573
717
  }
574
718
  const fn = subscript_default(trimmed);
575
- return fn(context);
719
+ const safeContext = {};
720
+ for (const [key, value] of Object.entries(context)) {
721
+ if (typeof value !== "function") {
722
+ safeContext[key] = value;
723
+ }
724
+ }
725
+ return fn(safeContext);
726
+ }
727
+ function getValueByPath(data, path) {
728
+ const segments = path.split(".");
729
+ let current = data;
730
+ for (const segment of segments) {
731
+ if (current === null || current === void 0) {
732
+ return void 0;
733
+ }
734
+ if (typeof current !== "object") {
735
+ return void 0;
736
+ }
737
+ current = current[segment];
738
+ }
739
+ return current;
740
+ }
741
+ function buildPathReferences(rootData, dependencies) {
742
+ const refs = {};
743
+ for (const dep of dependencies) {
744
+ if (dep.startsWith("/")) {
745
+ const fieldPath = dep.slice(1);
746
+ const rootField = fieldPath.split(".")[0] ?? fieldPath;
747
+ const contextKey = "/" + rootField;
748
+ if (!(contextKey in refs)) {
749
+ refs[contextKey] = getValueByPath(rootData, rootField);
750
+ }
751
+ } else if (dep.startsWith("../")) {
752
+ const fieldPath = dep.slice(3);
753
+ refs[dep] = getValueByPath(rootData, fieldPath);
754
+ }
755
+ }
756
+ return refs;
757
+ }
758
+ function evaluateWithContext(expression, options) {
759
+ const { rootData, itemData } = options;
760
+ const trimmed = expression.trim();
761
+ if (!trimmed) {
762
+ throw new Error("Empty expression");
763
+ }
764
+ const parsed = parseFormula(trimmed);
765
+ const pathRefs = buildPathReferences(rootData, parsed.dependencies);
766
+ const context = {
767
+ ...rootData,
768
+ ...itemData ?? {},
769
+ ...pathRefs
770
+ };
771
+ return evaluate(trimmed, context);
576
772
  }
577
773
  var ARITHMETIC_OPS = /* @__PURE__ */ new Set(["+", "-", "*", "/", "%"]);
578
- var COMPARISON_OPS = /* @__PURE__ */ new Set([
579
- "<",
580
- ">",
581
- "<=",
582
- ">=",
583
- "==",
584
- "!=",
585
- "===",
586
- "!=="
587
- ]);
588
- var LOGICAL_OPS = /* @__PURE__ */ new Set(["&&", "||", "!", "and", "or", "not"]);
774
+ var COMPARISON_OPS = /* @__PURE__ */ new Set(["<", ">", "<=", ">=", "==", "!="]);
775
+ var LOGICAL_OPS = /* @__PURE__ */ new Set(["&&", "||", "!"]);
589
776
  var NUMERIC_FUNCTIONS = /* @__PURE__ */ new Set([
590
777
  "round",
591
778
  "floor",
@@ -602,7 +789,8 @@ var NUMERIC_FUNCTIONS = /* @__PURE__ */ new Set([
602
789
  "sum",
603
790
  "avg",
604
791
  "count",
605
- "tonumber"
792
+ "tonumber",
793
+ "length"
606
794
  ]);
607
795
  var STRING_FUNCTIONS = /* @__PURE__ */ new Set([
608
796
  "concat",
@@ -616,6 +804,9 @@ var STRING_FUNCTIONS = /* @__PURE__ */ new Set([
616
804
  "join"
617
805
  ]);
618
806
  var BOOLEAN_FUNCTIONS = /* @__PURE__ */ new Set([
807
+ "and",
808
+ "or",
809
+ "not",
619
810
  "contains",
620
811
  "startswith",
621
812
  "endswith",
@@ -645,7 +836,12 @@ function inferLiteralArrayType(node) {
645
836
  if (typeof val === "boolean") return "boolean";
646
837
  return "unknown";
647
838
  }
648
- function inferOperatorType(op, argsLength) {
839
+ function inferOperatorType(op, argsLength, argTypes) {
840
+ if (op === "+" && argTypes) {
841
+ if (argTypes.includes("string")) return "string";
842
+ if (argTypes.includes("unknown")) return "unknown";
843
+ return "number";
844
+ }
649
845
  if (ARITHMETIC_OPS.has(op)) return "number";
650
846
  if (COMPARISON_OPS.has(op)) return "boolean";
651
847
  if (LOGICAL_OPS.has(op)) return "boolean";
@@ -670,7 +866,8 @@ function inferTypeFromNode(node, fieldTypes) {
670
866
  if (!Array.isArray(node)) return "unknown";
671
867
  if (isLiteralArray(node)) return inferLiteralArrayType(node);
672
868
  const [op, ...args] = node;
673
- const operatorType = inferOperatorType(op, args.length);
869
+ const argTypes = op === "+" ? args.map((arg) => inferTypeFromNode(arg, fieldTypes)) : void 0;
870
+ const operatorType = inferOperatorType(op, args.length, argTypes);
674
871
  if (operatorType !== null) return operatorType;
675
872
  if (op === "." || op === "[]") {
676
873
  return inferPropertyAccessType(node, fieldTypes);
@@ -826,21 +1023,104 @@ function processQueue(queue, graph, inDegree) {
826
1023
  // src/extract-schema.ts
827
1024
  function extractSchemaFormulas(schema) {
828
1025
  const formulas = [];
1026
+ extractFormulasRecursive(schema, "", formulas);
1027
+ return formulas;
1028
+ }
1029
+ function extractFormulasRecursive(schema, pathPrefix, formulas) {
1030
+ if (schema.type === "array" && schema.items) {
1031
+ extractFormulasRecursive(schema.items, `${pathPrefix}[]`, formulas);
1032
+ return;
1033
+ }
829
1034
  const properties = schema.properties ?? {};
830
1035
  for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1036
+ const fullPath = pathPrefix ? `${pathPrefix}.${fieldName}` : fieldName;
831
1037
  const xFormula = fieldSchema["x-formula"];
832
1038
  if (xFormula) {
833
1039
  formulas.push({
834
- fieldName,
1040
+ fieldName: fullPath,
835
1041
  expression: xFormula.expression,
836
1042
  fieldType: fieldSchema.type ?? "string"
837
1043
  });
838
1044
  }
1045
+ if (fieldSchema.type === "object" && fieldSchema.properties) {
1046
+ extractFormulasRecursive(fieldSchema, fullPath, formulas);
1047
+ }
1048
+ if (fieldSchema.type === "array" && fieldSchema.items) {
1049
+ extractFormulasRecursive(fieldSchema.items, `${fullPath}[]`, formulas);
1050
+ }
839
1051
  }
840
- return formulas;
841
1052
  }
842
1053
 
843
1054
  // src/validate-schema.ts
1055
+ function resolveSubSchema(schema, fieldPath) {
1056
+ if (!fieldPath) {
1057
+ return schema;
1058
+ }
1059
+ const segments = parsePathSegments(fieldPath);
1060
+ let current = schema;
1061
+ for (const segment of segments) {
1062
+ if (segment === "[]") {
1063
+ if (current.type === "array" && current.items) {
1064
+ current = current.items;
1065
+ } else {
1066
+ return null;
1067
+ }
1068
+ } else {
1069
+ if (current.properties?.[segment]) {
1070
+ current = current.properties[segment];
1071
+ } else {
1072
+ return null;
1073
+ }
1074
+ }
1075
+ }
1076
+ return current;
1077
+ }
1078
+ function parsePathSegments(path) {
1079
+ const segments = [];
1080
+ let current = "";
1081
+ let inBracket = false;
1082
+ for (const char of path) {
1083
+ if (char === "[") {
1084
+ if (current) {
1085
+ segments.push(current);
1086
+ current = "";
1087
+ }
1088
+ inBracket = true;
1089
+ } else if (char === "]") {
1090
+ inBracket = false;
1091
+ segments.push("[]");
1092
+ } else if (char === "." && !inBracket) {
1093
+ if (current) {
1094
+ segments.push(current);
1095
+ current = "";
1096
+ }
1097
+ } else if (!inBracket) {
1098
+ current += char;
1099
+ }
1100
+ }
1101
+ if (current) {
1102
+ segments.push(current);
1103
+ }
1104
+ return segments;
1105
+ }
1106
+ function getParentPath(fieldPath) {
1107
+ const lastDotIndex = fieldPath.lastIndexOf(".");
1108
+ const lastBracketIndex = fieldPath.lastIndexOf("[");
1109
+ const splitIndex = Math.max(lastDotIndex, lastBracketIndex);
1110
+ if (splitIndex <= 0) {
1111
+ return "";
1112
+ }
1113
+ return fieldPath.substring(0, splitIndex);
1114
+ }
1115
+ function getFieldName(fieldPath) {
1116
+ const lastDotIndex = fieldPath.lastIndexOf(".");
1117
+ const lastBracketIndex = fieldPath.lastIndexOf("]");
1118
+ const splitIndex = Math.max(lastDotIndex, lastBracketIndex);
1119
+ if (splitIndex === -1) {
1120
+ return fieldPath;
1121
+ }
1122
+ return fieldPath.substring(splitIndex + 1);
1123
+ }
844
1124
  function getSchemaFields(schema) {
845
1125
  const fields = /* @__PURE__ */ new Set();
846
1126
  const properties = schema.properties ?? {};
@@ -875,44 +1155,56 @@ function extractFieldRoot(dependency) {
875
1155
  const root = dependency.split(".")[0]?.split("[")[0];
876
1156
  return root || dependency;
877
1157
  }
878
- function validateFormulaAgainstSchema(expression, fieldName, schema) {
1158
+ function validateFormulaInContext(expression, fieldPath, rootSchema) {
879
1159
  const syntaxResult = validateFormulaSyntax(expression);
880
1160
  if (!syntaxResult.isValid) {
881
1161
  return {
882
- field: fieldName,
1162
+ field: fieldPath,
883
1163
  error: syntaxResult.error,
884
1164
  position: syntaxResult.position
885
1165
  };
886
1166
  }
1167
+ const parentPath = getParentPath(fieldPath);
1168
+ const localFieldName = getFieldName(fieldPath);
1169
+ const contextSchema = resolveSubSchema(rootSchema, parentPath);
1170
+ if (!contextSchema) {
1171
+ return {
1172
+ field: fieldPath,
1173
+ error: `Cannot resolve schema context for path '${parentPath}'`
1174
+ };
1175
+ }
887
1176
  const parseResult = parseExpression(expression);
888
- const schemaFields = getSchemaFields(schema);
1177
+ const schemaFields = getSchemaFields(contextSchema);
889
1178
  for (const dep of parseResult.dependencies) {
890
1179
  const rootField = extractFieldRoot(dep);
891
1180
  if (!schemaFields.has(rootField)) {
892
1181
  return {
893
- field: fieldName,
1182
+ field: fieldPath,
894
1183
  error: `Unknown field '${rootField}' in formula`
895
1184
  };
896
1185
  }
897
1186
  }
898
- if (parseResult.dependencies.some((d) => extractFieldRoot(d) === fieldName)) {
1187
+ if (parseResult.dependencies.some((d) => extractFieldRoot(d) === localFieldName)) {
899
1188
  return {
900
- field: fieldName,
1189
+ field: fieldPath,
901
1190
  error: `Formula cannot reference itself`
902
1191
  };
903
1192
  }
904
- const fieldSchema = schema.properties?.[fieldName];
1193
+ const fieldSchema = contextSchema.properties?.[localFieldName];
905
1194
  const expectedType = schemaTypeToInferred(fieldSchema?.type);
906
- const fieldTypes = getSchemaFieldTypes(schema);
1195
+ const fieldTypes = getSchemaFieldTypes(contextSchema);
907
1196
  const inferredType = inferFormulaType(expression, fieldTypes);
908
1197
  if (!isTypeCompatible(inferredType, expectedType)) {
909
1198
  return {
910
- field: fieldName,
1199
+ field: fieldPath,
911
1200
  error: `Type mismatch: formula returns '${inferredType}' but field expects '${expectedType}'`
912
1201
  };
913
1202
  }
914
1203
  return null;
915
1204
  }
1205
+ function validateFormulaAgainstSchema(expression, fieldName, schema) {
1206
+ return validateFormulaInContext(expression, fieldName, schema);
1207
+ }
916
1208
  function validateSchemaFormulas(schema) {
917
1209
  const errors = [];
918
1210
  const formulas = extractSchemaFormulas(schema);
@@ -932,7 +1224,12 @@ function validateSchemaFormulas(schema) {
932
1224
  const dependencies = {};
933
1225
  for (const formula of formulas) {
934
1226
  const parseResult = parseExpression(formula.expression);
935
- dependencies[formula.fieldName] = parseResult.dependencies.map(extractFieldRoot);
1227
+ const parentPath = getParentPath(formula.fieldName);
1228
+ const prefix = parentPath ? `${parentPath}.` : "";
1229
+ dependencies[formula.fieldName] = parseResult.dependencies.map((dep) => {
1230
+ const rootField = extractFieldRoot(dep);
1231
+ return `${prefix}${rootField}`;
1232
+ });
936
1233
  }
937
1234
  const graph = buildDependencyGraph(dependencies);
938
1235
  const circularCheck = detectCircularDependencies(graph);
@@ -950,6 +1247,6 @@ function validateSchemaFormulas(schema) {
950
1247
  return { isValid: true, errors: [] };
951
1248
  }
952
1249
 
953
- export { buildDependencyGraph, detectCircularDependencies, evaluate, extractSchemaFormulas, getTopologicalOrder, inferFormulaType, parseExpression, parseFormula, validateFormulaAgainstSchema, validateFormulaSyntax, validateSchemaFormulas, validateSyntax };
954
- //# sourceMappingURL=chunk-FGRNVE53.js.map
955
- //# sourceMappingURL=chunk-FGRNVE53.js.map
1250
+ export { buildDependencyGraph, detectCircularDependencies, evaluate, evaluateWithContext, extractSchemaFormulas, getTopologicalOrder, inferFormulaType, parseExpression, parseFormula, validateFormulaAgainstSchema, validateFormulaSyntax, validateSchemaFormulas, validateSyntax };
1251
+ //# sourceMappingURL=chunk-FDIJPOVQ.js.map
1252
+ //# sourceMappingURL=chunk-FDIJPOVQ.js.map