@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 +33 -1
- package/dist/{chunk-5NMNSRHH.cjs → chunk-AGBOCJGV.cjs} +341 -43
- package/dist/chunk-AGBOCJGV.cjs.map +1 -0
- package/dist/{chunk-FGRNVE53.js → chunk-FDIJPOVQ.js} +341 -44
- package/dist/chunk-FDIJPOVQ.js.map +1 -0
- package/dist/editor/index.cjs +6 -6
- package/dist/editor/index.d.cts +1 -1
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/formula-spec.cjs +388 -8
- package/dist/formula-spec.cjs.map +1 -1
- package/dist/formula-spec.d.cts +16 -1
- package/dist/formula-spec.d.ts +16 -1
- package/dist/formula-spec.js +388 -8
- package/dist/formula-spec.js.map +1 -1
- package/dist/{index-DO2EZ7U6.d.cts → index-JZFJ9oT6.d.cts} +10 -1
- package/dist/{index-DO2EZ7U6.d.ts → index-JZFJ9oT6.d.ts} +10 -1
- package/dist/index.cjs +17 -13
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-5NMNSRHH.cjs.map +0 -1
- package/dist/chunk-FGRNVE53.js.map +0 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# @revisium/formula
|
|
4
4
|
|
|
5
5
|
[](https://sonarcloud.io/summary/new_code?id=revisium_formula)
|
|
6
|
-
[](https://sonarcloud.io/summary/new_code?id=revisium_formula)
|
|
7
7
|
[](https://github.com/revisium/formula/blob/master/LICENSE)
|
|
8
8
|
[](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
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
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 (
|
|
517
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1160
|
+
function validateFormulaInContext(expression, fieldPath, rootSchema) {
|
|
881
1161
|
const syntaxResult = validateFormulaSyntax(expression);
|
|
882
1162
|
if (!syntaxResult.isValid) {
|
|
883
1163
|
return {
|
|
884
|
-
field:
|
|
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(
|
|
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:
|
|
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) ===
|
|
1189
|
+
if (parseResult.dependencies.some((d) => extractFieldRoot(d) === localFieldName)) {
|
|
901
1190
|
return {
|
|
902
|
-
field:
|
|
1191
|
+
field: fieldPath,
|
|
903
1192
|
error: `Formula cannot reference itself`
|
|
904
1193
|
};
|
|
905
1194
|
}
|
|
906
|
-
const fieldSchema =
|
|
1195
|
+
const fieldSchema = contextSchema.properties?.[localFieldName];
|
|
907
1196
|
const expectedType = schemaTypeToInferred(fieldSchema?.type);
|
|
908
|
-
const fieldTypes = getSchemaFieldTypes(
|
|
1197
|
+
const fieldTypes = getSchemaFieldTypes(contextSchema);
|
|
909
1198
|
const inferredType = inferFormulaType(expression, fieldTypes);
|
|
910
1199
|
if (!isTypeCompatible(inferredType, expectedType)) {
|
|
911
1200
|
return {
|
|
912
|
-
field:
|
|
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
|
-
|
|
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-
|
|
968
|
-
//# sourceMappingURL=chunk-
|
|
1265
|
+
//# sourceMappingURL=chunk-AGBOCJGV.cjs.map
|
|
1266
|
+
//# sourceMappingURL=chunk-AGBOCJGV.cjs.map
|