@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 +32 -0
- package/dist/{chunk-JJ72EVIZ.cjs → chunk-AGBOCJGV.cjs} +320 -89
- package/dist/chunk-AGBOCJGV.cjs.map +1 -0
- package/dist/{chunk-4JNGUHMF.js → chunk-FDIJPOVQ.js} +320 -90
- 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 +91 -1
- package/dist/formula-spec.cjs.map +1 -1
- package/dist/formula-spec.js +91 -1
- 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-4JNGUHMF.js.map +0 -1
- package/dist/chunk-JJ72EVIZ.cjs.map +0 -1
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
|
|
506
|
-
if (
|
|
507
|
-
|
|
508
|
-
|
|
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 (
|
|
513
|
-
|
|
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 = {
|
|
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
|
|
1160
|
+
function validateFormulaInContext(expression, fieldPath, rootSchema) {
|
|
948
1161
|
const syntaxResult = validateFormulaSyntax(expression);
|
|
949
1162
|
if (!syntaxResult.isValid) {
|
|
950
1163
|
return {
|
|
951
|
-
field:
|
|
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(
|
|
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:
|
|
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) ===
|
|
1189
|
+
if (parseResult.dependencies.some((d) => extractFieldRoot(d) === localFieldName)) {
|
|
968
1190
|
return {
|
|
969
|
-
field:
|
|
1191
|
+
field: fieldPath,
|
|
970
1192
|
error: `Formula cannot reference itself`
|
|
971
1193
|
};
|
|
972
1194
|
}
|
|
973
|
-
const fieldSchema =
|
|
1195
|
+
const fieldSchema = contextSchema.properties?.[localFieldName];
|
|
974
1196
|
const expectedType = schemaTypeToInferred(fieldSchema?.type);
|
|
975
|
-
const fieldTypes = getSchemaFieldTypes(
|
|
1197
|
+
const fieldTypes = getSchemaFieldTypes(contextSchema);
|
|
976
1198
|
const inferredType = inferFormulaType(expression, fieldTypes);
|
|
977
1199
|
if (!isTypeCompatible(inferredType, expectedType)) {
|
|
978
1200
|
return {
|
|
979
|
-
field:
|
|
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
|
-
|
|
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-
|
|
1035
|
-
//# sourceMappingURL=chunk-
|
|
1265
|
+
//# sourceMappingURL=chunk-AGBOCJGV.cjs.map
|
|
1266
|
+
//# sourceMappingURL=chunk-AGBOCJGV.cjs.map
|