@stackone/expressions 0.13.0 → 0.15.1

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
@@ -111,7 +111,7 @@ When the expression starts with `$`, it is treated as a JSON Path expression and
111
111
  | `*` | Wildcard. All elements in an array, or all properties of an object |
112
112
  | `..` | Recursive descent |
113
113
  | `[]` | Subscript operator |
114
- | `[,]` | Union operator |
114
+ | `[,]` | Union operator (e.g., `$.a[b,c]` for multiple properties) |
115
115
  | `[start:end:step]` | Array slice operator |
116
116
  | `?(expression)` | Filter expression |
117
117
  | `()` | Script expression |
@@ -123,10 +123,11 @@ Examples:
123
123
  '$.user.name' // Returns "John"
124
124
  '$.user.age' // Returns 30
125
125
  '$.user[*]' // Returns ["John", 30]
126
+ '$.user[name,age]' // Returns ["John", 30] (union operator)
126
127
  '$["info/email"]' // Returns "info@email.com"
127
128
  ```
128
129
 
129
- For more information on JSON Path syntax, refer to the original [JSON Path documentation](https://goessner.net/articles/JsonPath/).
130
+ For more information on JSON Path syntax, refer to the [JSONPath Plus documentation](https://github.com/s3u/JSONPath) and the original [JSON Path specification](https://goessner.net/articles/JsonPath/).
130
131
 
131
132
  ### JEXL Expressions
132
133
 
@@ -0,0 +1,11 @@
1
+ //#region src/evaluate.d.ts
2
+ declare const evaluateExpression: (expression?: string | null, context?: unknown, options?: {
3
+ incrementalJsonPath?: boolean;
4
+ }) => unknown;
5
+ declare const safeEvaluateExpression: (expression?: string | null, context?: unknown) => unknown;
6
+ declare const safeEvaluateRecord: (record: Record<string, unknown>, context?: unknown) => Record<string, unknown>;
7
+ //#endregion
8
+ //#region src/validate.d.ts
9
+ declare const isValidExpression: (expression?: string | null) => boolean;
10
+ //#endregion
11
+ export { evaluateExpression as evaluate, isValidExpression, safeEvaluateExpression as safeEvaluate, safeEvaluateRecord };
@@ -0,0 +1,11 @@
1
+ //#region src/evaluate.d.ts
2
+ declare const evaluateExpression: (expression?: string | null, context?: unknown, options?: {
3
+ incrementalJsonPath?: boolean;
4
+ }) => unknown;
5
+ declare const safeEvaluateExpression: (expression?: string | null, context?: unknown) => unknown;
6
+ declare const safeEvaluateRecord: (record: Record<string, unknown>, context?: unknown) => Record<string, unknown>;
7
+ //#endregion
8
+ //#region src/validate.d.ts
9
+ declare const isValidExpression: (expression?: string | null) => boolean;
10
+ //#endregion
11
+ export { evaluateExpression as evaluate, isValidExpression, safeEvaluateExpression as safeEvaluate, safeEvaluateRecord };
package/dist/index.js CHANGED
@@ -1 +1,274 @@
1
- "use strict";var e=require("@stackone/utils"),r=require("jexl"),t=require("jsonpath"),n=require("lodash.get");function a(e){var r=Object.create(null);return e&&Object.keys(e).forEach((function(t){if("default"!==t){var n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(r,t,n.get?n:{enumerable:!0,get:function(){return e[t]}})}})),r.default=e,Object.freeze(r)}var i=a(r);const s=/\${([^}]+)}/g,o=(e,r)=>{try{return e.compile(r)}catch{return null}},c=(e,r)=>{t.parse(e);const n=t.query(r,e);if(0!==n.length)return 1===n.length?n[0]:n},l=(e,r,t)=>e.replace(r,String(t)),u=(r,t,a)=>{const i=r.match(s);if(!i)return;const c=i.length,u=new Array(c);for(let e=0;e<c;e++)u[e]={toReplace:i[e],path:i[e].slice(2,-1)};return u.reduce(((r,i)=>((r,t,a,i)=>{const s=r.path.trim();if(!s)return i;try{const c=o(t,s);if(c){const e=c.evalSync(a);if(void 0!==e)return l(i,r.toReplace,e)}const u=n(a,s);return e.notMissing(u)?l(i,r.toReplace,u):i}catch{return i}})(i,a,t,r)),String(r))},d=(r,t,n,a)=>{if(Array.isArray(t)){const e=parseInt(n,10);if(!isNaN(e)&&(e<0||e>=t.length))return{value:void 0,error:`Invalid array index '${n}' at '${a}'`,availableKeys:t.map(((e,r)=>r.toString()))}}return e.isObject(t)?{value:void 0,error:`Key '${n}' not found at '${a}'`,availableKeys:Object.keys(t)}:{value:void 0,error:`${r} at '${a}'`,availableKeys:[]}},y=(r,t)=>{const n=/(\.\.)|(\[\*\])|(\[\?\()|(\[\d+:\d+\])/;try{if(n.test(r)){const e=c(r,t);return void 0===e?{value:void 0,error:`Invalid or empty JSONPath: '${r}'`}:{value:e}}const a=r.match(/\$|(?:\['([^']+)'\])|(?:\["([^"]+)"\])|\[\d+\]|[^[\].]+/g)||[];return((r,t)=>{if("$"!==r[0])return{value:void 0,error:"JSON path must start with $"};let n=t,a="$";for(let t=1;t<r.length;t++){const i=r[t],s=i.startsWith("[")?`${a}${i}`:`${a}.${i}`;if(Array.isArray(n)){const e=parseInt(i,10);if(isNaN(e)||e<0||e>=n.length)return d("Invalid array index",n,i,a);n=n[e],a=s}else{if(!e.isObject(n))return{value:void 0,error:`Cannot access '${i}' at '${a}' - parent is not an object`,availableKeys:[]};if(!Object.prototype.hasOwnProperty.call(n,i))return d("Key not found",n,i,a);n=n[i],a=s}}return{value:n}})(a.map((e=>e.replace(/^\['([^']+)'\]$/,"$1").replace(/^\["([^"]+)"\]$/,"$1").replace(/^\[(\d+)\]$/,"$1"))),t)}catch(e){return{value:void 0,error:`Something went wrong with evaluation of JSON path: ${String(e)}`}}},p=(r,t,n)=>{const a=r?.trim();if(null==a||""===a)throw new Error("Empty expression");const s=e.isObject(t)?t:{},l=((r=()=>new i.Jexl)=>{const t=r();return t.addFunction("nextAnniversary",((r,t)=>e.calculateNextAnniversary({initialDate:r,format:t}))),t.addFunction("yearsElapsed",((r,t,n)=>e.calculateYearsElapsed({startDate:r,endDate:n,format:t}))),t.addFunction("hasPassed",((r,t,n)=>e.dateHasPassed({date:r,yearsToAdd:n,format:t}))),t.addFunction("now",(()=>(new Date).toISOString())),t.addFunction("includes",((r,t)=>!(e.isMissing(r)||!Array.isArray(r)||0===r.length||e.isMissing(t))&&(Array.isArray(t)?t.every((e=>r.includes(e))):r.includes(t)))),t.addFunction("includesSome",((r,t)=>!(e.isMissing(r)||!Array.isArray(r)||0===r.length||e.isMissing(t))&&(Array.isArray(t)?t.some((e=>r.includes(e))):r.includes(t)))),t.addFunction("present",(r=>e.notMissing(r))),t.addFunction("missing",(r=>e.isMissing(r))),t.addFunction("keys",(r=>e.isMissing(r)||"object"!=typeof r||Array.isArray(r)?[]:Object.keys(r))),t.addFunction("values",(r=>e.isMissing(r)||"object"!=typeof r||Array.isArray(r)?[]:Object.values(r))),t.addBinaryOp("??",0,((r,t)=>e.isMissing(r)?t:e.isMissing(t)?r:r??t)),t})(),d=(e=>{if(e.startsWith("{{")&&e.endsWith("}}"))return e.slice(2,-2)})(a),p=(e=>e.replace(/\$\./g,"").trim())(d??a),v=u(p,s,l);if(v)return v;if(!d&&a.startsWith("$"))return n?.incrementalJsonPath?((e,r)=>{const t=y(e,r);if(t.error)throw new Error(`${t.error}${t.availableKeys?.length?`. Available keys: ${t.availableKeys.join(", ")}`:""}`);return t.value})(a,s):((e,r)=>{try{return c(e,r)}catch{throw new Error(`Invalid JSON path: "${e}"`)}})(a,s);if(!v&&!d)return r;const f=o(l,p);if(!f||"."===p)throw new Error(`Invalid expression: "${a}"`);try{return f.evalSync(s)}catch{return}},v=(e,r)=>{try{return p(e,r)}catch{return null}},f=(e,r)=>{const t=e=>Array.isArray(e)?e.map(t):"object"==typeof e&&null!==e?f(e,r):"string"==typeof e?v(e,r):e;return Object.fromEntries(Object.entries(e).map((([e,r])=>[e,t(r)])))};exports.evaluate=p,exports.isValidExpression=e=>{try{return p(e),!0}catch{return!1}},exports.safeEvaluate=v,exports.safeEvaluateRecord=f;
1
+ //#region rolldown:runtime
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+
23
+ //#endregion
24
+ const __stackone_utils = __toESM(require("@stackone/utils"));
25
+ const jexl = __toESM(require("jexl"));
26
+ const jsonpath_plus = __toESM(require("jsonpath-plus"));
27
+ const lodash_get = __toESM(require("lodash.get"));
28
+
29
+ //#region src/factory.ts
30
+ const createExpressionHandler = (instanceBuilder = () => new jexl.Jexl()) => {
31
+ const expressionHandler = instanceBuilder();
32
+ expressionHandler.addFunction("nextAnniversary", (initialDate, format) => (0, __stackone_utils.calculateNextAnniversary)({
33
+ initialDate,
34
+ format
35
+ }));
36
+ expressionHandler.addFunction("yearsElapsed", (startDate, format, endDate) => (0, __stackone_utils.calculateYearsElapsed)({
37
+ startDate,
38
+ endDate,
39
+ format
40
+ }));
41
+ expressionHandler.addFunction("hasPassed", (date, format, yearsToAdd) => (0, __stackone_utils.dateHasPassed)({
42
+ date,
43
+ yearsToAdd,
44
+ format
45
+ }));
46
+ expressionHandler.addFunction("now", () => (/* @__PURE__ */ new Date()).toISOString());
47
+ expressionHandler.addFunction("includes", (array, value) => {
48
+ if ((0, __stackone_utils.isMissing)(array) || !Array.isArray(array) || array.length === 0 || (0, __stackone_utils.isMissing)(value)) return false;
49
+ if (Array.isArray(value)) return value.every((val) => array.includes(val));
50
+ return array.includes(value);
51
+ });
52
+ expressionHandler.addFunction("includesSome", (array, value) => {
53
+ if ((0, __stackone_utils.isMissing)(array) || !Array.isArray(array) || array.length === 0 || (0, __stackone_utils.isMissing)(value)) return false;
54
+ if (Array.isArray(value)) return value.some((val) => array.includes(val));
55
+ return array.includes(value);
56
+ });
57
+ expressionHandler.addFunction("present", (value) => (0, __stackone_utils.notMissing)(value));
58
+ expressionHandler.addFunction("missing", (value) => (0, __stackone_utils.isMissing)(value));
59
+ expressionHandler.addFunction("keys", (value) => {
60
+ if ((0, __stackone_utils.isMissing)(value)) return [];
61
+ else if (typeof value === "object" && !Array.isArray(value)) return Object.keys(value);
62
+ else return [];
63
+ });
64
+ expressionHandler.addFunction("values", (value) => {
65
+ if ((0, __stackone_utils.isMissing)(value)) return [];
66
+ else if (typeof value === "object" && !Array.isArray(value)) return Object.values(value);
67
+ else return [];
68
+ });
69
+ expressionHandler.addBinaryOp("??", 0, (left, right) => {
70
+ if ((0, __stackone_utils.isMissing)(left)) return right;
71
+ else if ((0, __stackone_utils.isMissing)(right)) return left;
72
+ else return left ?? right;
73
+ });
74
+ return expressionHandler;
75
+ };
76
+
77
+ //#endregion
78
+ //#region src/constants.ts
79
+ const INTERPOLATED_EXPRESSION_REGEX = /\${([^}]+)}/g;
80
+
81
+ //#endregion
82
+ //#region src/utils.ts
83
+ const cleanExpression = (expression) => {
84
+ return expression.replace(/\$\./g, "").trim();
85
+ };
86
+ const compileExpression = (expressionHandler, expression) => {
87
+ try {
88
+ const compiledExpression = expressionHandler.compile(expression);
89
+ return compiledExpression;
90
+ } catch {
91
+ return null;
92
+ }
93
+ };
94
+ const extractExpressionBetweenDoubleCurlyBraces = (expression) => {
95
+ if (expression.startsWith("{{") && expression.endsWith("}}")) return expression.slice(2, -2);
96
+ };
97
+ const evaluateJsonPath = (expression, context) => {
98
+ try {
99
+ if (!expression.startsWith("$")) throw new Error(`JSONPath expression must start with '$': ${expression}`);
100
+ if (expression.includes("$_") || expression.match(/\$[^.[]/g) || expression.includes("[") && !expression.includes("]") || expression.includes(" ") && !expression.includes("[")) throw new Error(`Invalid JSONPath expression: ${expression}`);
101
+ const result = (0, jsonpath_plus.JSONPath)({
102
+ path: expression,
103
+ json: context
104
+ });
105
+ if (result.length === 0) return void 0;
106
+ if (result.length === 1) return result[0];
107
+ return result;
108
+ } catch {
109
+ throw new Error(`Invalid JSONPath expression: ${expression}`);
110
+ }
111
+ };
112
+ const replaceInterpolation = (expression, toReplace, value) => expression.replace(toReplace, String(value));
113
+ const handleSingleInterpolation = (interpolation, expressionHandler, context, currentExpression) => {
114
+ const path = interpolation.path.trim();
115
+ if (!path) return currentExpression;
116
+ try {
117
+ const compiledExpression = compileExpression(expressionHandler, path);
118
+ if (compiledExpression) {
119
+ const value = compiledExpression.evalSync(context);
120
+ if (value !== void 0) return replaceInterpolation(currentExpression, interpolation.toReplace, value);
121
+ }
122
+ const pathValue = (0, lodash_get.default)(context, path);
123
+ return (0, __stackone_utils.notMissing)(pathValue) ? replaceInterpolation(currentExpression, interpolation.toReplace, pathValue) : currentExpression;
124
+ } catch {
125
+ return currentExpression;
126
+ }
127
+ };
128
+ const evaluateStringInterpolations = (expression, context, expressionHandler) => {
129
+ const matches = expression.match(INTERPOLATED_EXPRESSION_REGEX);
130
+ if (!matches) return void 0;
131
+ const len = matches.length;
132
+ const interpolations = new Array(len);
133
+ for (let i = 0; i < len; i++) interpolations[i] = {
134
+ toReplace: matches[i],
135
+ path: matches[i].slice(2, -1)
136
+ };
137
+ return interpolations.reduce((acc, interpolation) => handleSingleInterpolation(interpolation, expressionHandler, context, acc), String(expression));
138
+ };
139
+ const handleSegmentPathError = (err, parentVal, segment, parentPath) => {
140
+ if (Array.isArray(parentVal)) {
141
+ const parsedIndex = parseInt(segment, 10);
142
+ if (!isNaN(parsedIndex) && (parsedIndex < 0 || parsedIndex >= parentVal.length)) return {
143
+ value: void 0,
144
+ error: `Invalid array index '${segment}' at '${parentPath}'`,
145
+ availableKeys: parentVal.map((_, idx) => idx.toString())
146
+ };
147
+ }
148
+ if ((0, __stackone_utils.isObject)(parentVal)) return {
149
+ value: void 0,
150
+ error: `Key '${segment}' not found at '${parentPath}'`,
151
+ availableKeys: Object.keys(parentVal)
152
+ };
153
+ return {
154
+ value: void 0,
155
+ error: `${err} at '${parentPath}'`,
156
+ availableKeys: []
157
+ };
158
+ };
159
+ const handleJsonPathSegments = (segments, ctx) => {
160
+ if (segments[0] !== "$") return {
161
+ value: void 0,
162
+ error: "JSON path must start with $"
163
+ };
164
+ let currentValue = ctx;
165
+ let currentPath = "$";
166
+ for (let i = 1; i < segments.length; i++) {
167
+ const segment = segments[i];
168
+ const nextPath = segment.startsWith("[") ? `${currentPath}${segment}` : `${currentPath}.${segment}`;
169
+ if (Array.isArray(currentValue)) {
170
+ const idx = parseInt(segment, 10);
171
+ if (isNaN(idx) || idx < 0 || idx >= currentValue.length) return handleSegmentPathError("Invalid array index", currentValue, segment, currentPath);
172
+ currentValue = currentValue[idx];
173
+ currentPath = nextPath;
174
+ } else if ((0, __stackone_utils.isObject)(currentValue)) {
175
+ if (!Object.prototype.hasOwnProperty.call(currentValue, segment)) return handleSegmentPathError("Key not found", currentValue, segment, currentPath);
176
+ currentValue = currentValue[segment];
177
+ currentPath = nextPath;
178
+ } else return {
179
+ value: void 0,
180
+ error: `Cannot access '${segment}' at '${currentPath}' - parent is not an object`,
181
+ availableKeys: []
182
+ };
183
+ }
184
+ return { value: currentValue };
185
+ };
186
+ const evaluateJsonPathIncremental = (path, context) => {
187
+ const SPECIAL_JSONPATH_REGEX = /(\.\.)|(\[\*\])|(\[\?\()|(\[\d+:\d+\])/;
188
+ try {
189
+ if (SPECIAL_JSONPATH_REGEX.test(path)) {
190
+ const result = evaluateJsonPath(path, context);
191
+ if (result === void 0) return {
192
+ value: void 0,
193
+ error: `Invalid or empty JSONPath: '${path}'`
194
+ };
195
+ return { value: result };
196
+ }
197
+ const segments = path.match(/\$|(?:\['([^']+)'\])|(?:\["([^"]+)"\])|\[\d+\]|[^[\].]+/g) || [];
198
+ const cleanedSegments = segments.map((seg) => seg.replace(/^\['([^']+)'\]$/, "$1").replace(/^\["([^"]+)"\]$/, "$1").replace(/^\[(\d+)\]$/, "$1"));
199
+ return handleJsonPathSegments(cleanedSegments, context);
200
+ } catch (error) {
201
+ return {
202
+ value: void 0,
203
+ error: `Something went wrong with evaluation of JSON path: ${String(error)}`
204
+ };
205
+ }
206
+ };
207
+
208
+ //#endregion
209
+ //#region src/evaluate.ts
210
+ const handleIncrementalJsonPath = (expr, ctx) => {
211
+ const result = evaluateJsonPathIncremental(expr, ctx);
212
+ if (result.error) throw new Error(`${result.error}${result.availableKeys?.length ? `. Available keys: ${result.availableKeys.join(", ")}` : ""}`);
213
+ return result.value;
214
+ };
215
+ const handleNormalJsonPath = (expr, ctx) => {
216
+ try {
217
+ return evaluateJsonPath(expr, ctx);
218
+ } catch {
219
+ throw new Error(`Invalid JSON path: "${expr}"`);
220
+ }
221
+ };
222
+ const evaluateExpression = (expression, context, options) => {
223
+ const trimmedExpression = expression?.trim();
224
+ if (trimmedExpression === null || trimmedExpression === void 0 || trimmedExpression === "") throw new Error("Empty expression");
225
+ const expressionContext = (0, __stackone_utils.isObject)(context) ? context : {};
226
+ const expressionHandler = createExpressionHandler();
227
+ const expressionBetweenDoubleCurlyBraces = extractExpressionBetweenDoubleCurlyBraces(trimmedExpression);
228
+ const extractedExpression = expressionBetweenDoubleCurlyBraces ?? trimmedExpression;
229
+ const cleanedExpression = cleanExpression(extractedExpression);
230
+ const interpolatedExpression = evaluateStringInterpolations(cleanedExpression, expressionContext, expressionHandler);
231
+ if (interpolatedExpression) return interpolatedExpression;
232
+ if (!expressionBetweenDoubleCurlyBraces && trimmedExpression.startsWith("$")) return options?.incrementalJsonPath ? handleIncrementalJsonPath(trimmedExpression, expressionContext) : handleNormalJsonPath(trimmedExpression, expressionContext);
233
+ if (!interpolatedExpression && !expressionBetweenDoubleCurlyBraces) return expression;
234
+ const compiledExpression = compileExpression(expressionHandler, cleanedExpression);
235
+ if (!compiledExpression || cleanedExpression === ".") throw new Error(`Invalid expression: "${trimmedExpression}"`);
236
+ try {
237
+ return compiledExpression.evalSync(expressionContext);
238
+ } catch {
239
+ return void 0;
240
+ }
241
+ };
242
+ const safeEvaluateExpression = (expression, context) => {
243
+ try {
244
+ return evaluateExpression(expression, context);
245
+ } catch {
246
+ return null;
247
+ }
248
+ };
249
+ const safeEvaluateRecord = (record, context) => {
250
+ const evaluateValue = (value) => {
251
+ if (Array.isArray(value)) return value.map(evaluateValue);
252
+ else if (typeof value === "object" && value !== null) return safeEvaluateRecord(value, context);
253
+ else if (typeof value === "string") return safeEvaluateExpression(value, context);
254
+ return value;
255
+ };
256
+ return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, evaluateValue(value)]));
257
+ };
258
+
259
+ //#endregion
260
+ //#region src/validate.ts
261
+ const isValidExpression = (expression) => {
262
+ try {
263
+ evaluateExpression(expression);
264
+ return true;
265
+ } catch {
266
+ return false;
267
+ }
268
+ };
269
+
270
+ //#endregion
271
+ exports.evaluate = evaluateExpression;
272
+ exports.isValidExpression = isValidExpression;
273
+ exports.safeEvaluate = safeEvaluateExpression;
274
+ exports.safeEvaluateRecord = safeEvaluateRecord;
package/dist/index.mjs ADDED
@@ -0,0 +1,248 @@
1
+ import { calculateNextAnniversary, calculateYearsElapsed, dateHasPassed, isMissing, isObject, notMissing } from "@stackone/utils";
2
+ import * as jexl from "jexl";
3
+ import { JSONPath } from "jsonpath-plus";
4
+ import get from "lodash.get";
5
+
6
+ //#region src/factory.ts
7
+ const createExpressionHandler = (instanceBuilder = () => new jexl.Jexl()) => {
8
+ const expressionHandler = instanceBuilder();
9
+ expressionHandler.addFunction("nextAnniversary", (initialDate, format) => calculateNextAnniversary({
10
+ initialDate,
11
+ format
12
+ }));
13
+ expressionHandler.addFunction("yearsElapsed", (startDate, format, endDate) => calculateYearsElapsed({
14
+ startDate,
15
+ endDate,
16
+ format
17
+ }));
18
+ expressionHandler.addFunction("hasPassed", (date, format, yearsToAdd) => dateHasPassed({
19
+ date,
20
+ yearsToAdd,
21
+ format
22
+ }));
23
+ expressionHandler.addFunction("now", () => (/* @__PURE__ */ new Date()).toISOString());
24
+ expressionHandler.addFunction("includes", (array, value) => {
25
+ if (isMissing(array) || !Array.isArray(array) || array.length === 0 || isMissing(value)) return false;
26
+ if (Array.isArray(value)) return value.every((val) => array.includes(val));
27
+ return array.includes(value);
28
+ });
29
+ expressionHandler.addFunction("includesSome", (array, value) => {
30
+ if (isMissing(array) || !Array.isArray(array) || array.length === 0 || isMissing(value)) return false;
31
+ if (Array.isArray(value)) return value.some((val) => array.includes(val));
32
+ return array.includes(value);
33
+ });
34
+ expressionHandler.addFunction("present", (value) => notMissing(value));
35
+ expressionHandler.addFunction("missing", (value) => isMissing(value));
36
+ expressionHandler.addFunction("keys", (value) => {
37
+ if (isMissing(value)) return [];
38
+ else if (typeof value === "object" && !Array.isArray(value)) return Object.keys(value);
39
+ else return [];
40
+ });
41
+ expressionHandler.addFunction("values", (value) => {
42
+ if (isMissing(value)) return [];
43
+ else if (typeof value === "object" && !Array.isArray(value)) return Object.values(value);
44
+ else return [];
45
+ });
46
+ expressionHandler.addBinaryOp("??", 0, (left, right) => {
47
+ if (isMissing(left)) return right;
48
+ else if (isMissing(right)) return left;
49
+ else return left ?? right;
50
+ });
51
+ return expressionHandler;
52
+ };
53
+
54
+ //#endregion
55
+ //#region src/constants.ts
56
+ const INTERPOLATED_EXPRESSION_REGEX = /\${([^}]+)}/g;
57
+
58
+ //#endregion
59
+ //#region src/utils.ts
60
+ const cleanExpression = (expression) => {
61
+ return expression.replace(/\$\./g, "").trim();
62
+ };
63
+ const compileExpression = (expressionHandler, expression) => {
64
+ try {
65
+ const compiledExpression = expressionHandler.compile(expression);
66
+ return compiledExpression;
67
+ } catch {
68
+ return null;
69
+ }
70
+ };
71
+ const extractExpressionBetweenDoubleCurlyBraces = (expression) => {
72
+ if (expression.startsWith("{{") && expression.endsWith("}}")) return expression.slice(2, -2);
73
+ };
74
+ const evaluateJsonPath = (expression, context) => {
75
+ try {
76
+ if (!expression.startsWith("$")) throw new Error(`JSONPath expression must start with '$': ${expression}`);
77
+ if (expression.includes("$_") || expression.match(/\$[^.[]/g) || expression.includes("[") && !expression.includes("]") || expression.includes(" ") && !expression.includes("[")) throw new Error(`Invalid JSONPath expression: ${expression}`);
78
+ const result = JSONPath({
79
+ path: expression,
80
+ json: context
81
+ });
82
+ if (result.length === 0) return void 0;
83
+ if (result.length === 1) return result[0];
84
+ return result;
85
+ } catch {
86
+ throw new Error(`Invalid JSONPath expression: ${expression}`);
87
+ }
88
+ };
89
+ const replaceInterpolation = (expression, toReplace, value) => expression.replace(toReplace, String(value));
90
+ const handleSingleInterpolation = (interpolation, expressionHandler, context, currentExpression) => {
91
+ const path = interpolation.path.trim();
92
+ if (!path) return currentExpression;
93
+ try {
94
+ const compiledExpression = compileExpression(expressionHandler, path);
95
+ if (compiledExpression) {
96
+ const value = compiledExpression.evalSync(context);
97
+ if (value !== void 0) return replaceInterpolation(currentExpression, interpolation.toReplace, value);
98
+ }
99
+ const pathValue = get(context, path);
100
+ return notMissing(pathValue) ? replaceInterpolation(currentExpression, interpolation.toReplace, pathValue) : currentExpression;
101
+ } catch {
102
+ return currentExpression;
103
+ }
104
+ };
105
+ const evaluateStringInterpolations = (expression, context, expressionHandler) => {
106
+ const matches = expression.match(INTERPOLATED_EXPRESSION_REGEX);
107
+ if (!matches) return void 0;
108
+ const len = matches.length;
109
+ const interpolations = new Array(len);
110
+ for (let i = 0; i < len; i++) interpolations[i] = {
111
+ toReplace: matches[i],
112
+ path: matches[i].slice(2, -1)
113
+ };
114
+ return interpolations.reduce((acc, interpolation) => handleSingleInterpolation(interpolation, expressionHandler, context, acc), String(expression));
115
+ };
116
+ const handleSegmentPathError = (err, parentVal, segment, parentPath) => {
117
+ if (Array.isArray(parentVal)) {
118
+ const parsedIndex = parseInt(segment, 10);
119
+ if (!isNaN(parsedIndex) && (parsedIndex < 0 || parsedIndex >= parentVal.length)) return {
120
+ value: void 0,
121
+ error: `Invalid array index '${segment}' at '${parentPath}'`,
122
+ availableKeys: parentVal.map((_, idx) => idx.toString())
123
+ };
124
+ }
125
+ if (isObject(parentVal)) return {
126
+ value: void 0,
127
+ error: `Key '${segment}' not found at '${parentPath}'`,
128
+ availableKeys: Object.keys(parentVal)
129
+ };
130
+ return {
131
+ value: void 0,
132
+ error: `${err} at '${parentPath}'`,
133
+ availableKeys: []
134
+ };
135
+ };
136
+ const handleJsonPathSegments = (segments, ctx) => {
137
+ if (segments[0] !== "$") return {
138
+ value: void 0,
139
+ error: "JSON path must start with $"
140
+ };
141
+ let currentValue = ctx;
142
+ let currentPath = "$";
143
+ for (let i = 1; i < segments.length; i++) {
144
+ const segment = segments[i];
145
+ const nextPath = segment.startsWith("[") ? `${currentPath}${segment}` : `${currentPath}.${segment}`;
146
+ if (Array.isArray(currentValue)) {
147
+ const idx = parseInt(segment, 10);
148
+ if (isNaN(idx) || idx < 0 || idx >= currentValue.length) return handleSegmentPathError("Invalid array index", currentValue, segment, currentPath);
149
+ currentValue = currentValue[idx];
150
+ currentPath = nextPath;
151
+ } else if (isObject(currentValue)) {
152
+ if (!Object.prototype.hasOwnProperty.call(currentValue, segment)) return handleSegmentPathError("Key not found", currentValue, segment, currentPath);
153
+ currentValue = currentValue[segment];
154
+ currentPath = nextPath;
155
+ } else return {
156
+ value: void 0,
157
+ error: `Cannot access '${segment}' at '${currentPath}' - parent is not an object`,
158
+ availableKeys: []
159
+ };
160
+ }
161
+ return { value: currentValue };
162
+ };
163
+ const evaluateJsonPathIncremental = (path, context) => {
164
+ const SPECIAL_JSONPATH_REGEX = /(\.\.)|(\[\*\])|(\[\?\()|(\[\d+:\d+\])/;
165
+ try {
166
+ if (SPECIAL_JSONPATH_REGEX.test(path)) {
167
+ const result = evaluateJsonPath(path, context);
168
+ if (result === void 0) return {
169
+ value: void 0,
170
+ error: `Invalid or empty JSONPath: '${path}'`
171
+ };
172
+ return { value: result };
173
+ }
174
+ const segments = path.match(/\$|(?:\['([^']+)'\])|(?:\["([^"]+)"\])|\[\d+\]|[^[\].]+/g) || [];
175
+ const cleanedSegments = segments.map((seg) => seg.replace(/^\['([^']+)'\]$/, "$1").replace(/^\["([^"]+)"\]$/, "$1").replace(/^\[(\d+)\]$/, "$1"));
176
+ return handleJsonPathSegments(cleanedSegments, context);
177
+ } catch (error) {
178
+ return {
179
+ value: void 0,
180
+ error: `Something went wrong with evaluation of JSON path: ${String(error)}`
181
+ };
182
+ }
183
+ };
184
+
185
+ //#endregion
186
+ //#region src/evaluate.ts
187
+ const handleIncrementalJsonPath = (expr, ctx) => {
188
+ const result = evaluateJsonPathIncremental(expr, ctx);
189
+ if (result.error) throw new Error(`${result.error}${result.availableKeys?.length ? `. Available keys: ${result.availableKeys.join(", ")}` : ""}`);
190
+ return result.value;
191
+ };
192
+ const handleNormalJsonPath = (expr, ctx) => {
193
+ try {
194
+ return evaluateJsonPath(expr, ctx);
195
+ } catch {
196
+ throw new Error(`Invalid JSON path: "${expr}"`);
197
+ }
198
+ };
199
+ const evaluateExpression = (expression, context, options) => {
200
+ const trimmedExpression = expression?.trim();
201
+ if (trimmedExpression === null || trimmedExpression === void 0 || trimmedExpression === "") throw new Error("Empty expression");
202
+ const expressionContext = isObject(context) ? context : {};
203
+ const expressionHandler = createExpressionHandler();
204
+ const expressionBetweenDoubleCurlyBraces = extractExpressionBetweenDoubleCurlyBraces(trimmedExpression);
205
+ const extractedExpression = expressionBetweenDoubleCurlyBraces ?? trimmedExpression;
206
+ const cleanedExpression = cleanExpression(extractedExpression);
207
+ const interpolatedExpression = evaluateStringInterpolations(cleanedExpression, expressionContext, expressionHandler);
208
+ if (interpolatedExpression) return interpolatedExpression;
209
+ if (!expressionBetweenDoubleCurlyBraces && trimmedExpression.startsWith("$")) return options?.incrementalJsonPath ? handleIncrementalJsonPath(trimmedExpression, expressionContext) : handleNormalJsonPath(trimmedExpression, expressionContext);
210
+ if (!interpolatedExpression && !expressionBetweenDoubleCurlyBraces) return expression;
211
+ const compiledExpression = compileExpression(expressionHandler, cleanedExpression);
212
+ if (!compiledExpression || cleanedExpression === ".") throw new Error(`Invalid expression: "${trimmedExpression}"`);
213
+ try {
214
+ return compiledExpression.evalSync(expressionContext);
215
+ } catch {
216
+ return void 0;
217
+ }
218
+ };
219
+ const safeEvaluateExpression = (expression, context) => {
220
+ try {
221
+ return evaluateExpression(expression, context);
222
+ } catch {
223
+ return null;
224
+ }
225
+ };
226
+ const safeEvaluateRecord = (record, context) => {
227
+ const evaluateValue = (value) => {
228
+ if (Array.isArray(value)) return value.map(evaluateValue);
229
+ else if (typeof value === "object" && value !== null) return safeEvaluateRecord(value, context);
230
+ else if (typeof value === "string") return safeEvaluateExpression(value, context);
231
+ return value;
232
+ };
233
+ return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, evaluateValue(value)]));
234
+ };
235
+
236
+ //#endregion
237
+ //#region src/validate.ts
238
+ const isValidExpression = (expression) => {
239
+ try {
240
+ evaluateExpression(expression);
241
+ return true;
242
+ } catch {
243
+ return false;
244
+ }
245
+ };
246
+
247
+ //#endregion
248
+ export { evaluateExpression as evaluate, isValidExpression, safeEvaluateExpression as safeEvaluate, safeEvaluateRecord };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackone/expressions",
3
- "version": "0.13.0",
3
+ "version": "0.15.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.es.mjs",
@@ -10,12 +10,20 @@
10
10
  "package.json",
11
11
  "README.md"
12
12
  ],
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/types/index.d.ts",
16
+ "import": "./dist/index.es.mjs",
17
+ "require": "./dist/index.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
13
21
  "scripts": {
14
22
  "clean": "rimraf dist",
15
23
  "prebuild": "npm run clean",
16
- "build": "rollup -c --environment NODE_ENV:production",
24
+ "build": "tsdown --env.NODE_ENV=production",
17
25
  "prebuild:dev": "npm run clean",
18
- "build:dev": "rollup -c --environment NODE_ENV:development",
26
+ "build:dev": "tsdown --env.NODE_ENV=development",
19
27
  "code:format": "biome format ./src ./*.mjs",
20
28
  "code:format:fix": "biome format --write ./src ./*.mjs",
21
29
  "code:lint": "biome lint --error-on-warnings ./src ./*.mjs",
@@ -34,12 +42,18 @@
34
42
  "dependencies": {
35
43
  "@stackone/utils": "*",
36
44
  "jexl": "^2.3.0",
37
- "jsonpath": "^1.1.1",
45
+ "jsonpath-plus": "^10.3.0",
38
46
  "lodash.get": "^4.4.2"
39
47
  },
40
48
  "devDependencies": {
41
49
  "@types/jexl": "^2.3.4",
42
- "@types/jsonpath": "^0.2.4",
43
50
  "@types/lodash.get": "^4.4.9"
51
+ },
52
+ "tsdown": {
53
+ "dts": true,
54
+ "format": [
55
+ "esm",
56
+ "cjs"
57
+ ]
44
58
  }
45
59
  }
package/dist/index.es.mjs DELETED
@@ -1 +0,0 @@
1
- import{calculateNextAnniversary as r,calculateYearsElapsed as t,dateHasPassed as e,isMissing as a,notMissing as n,isObject as o}from"@stackone/utils";import*as i from"jexl";import s from"jsonpath";import l from"lodash.get";const c=/\${([^}]+)}/g,u=(r,t)=>{try{return r.compile(t)}catch{return null}},d=(r,t)=>{s.parse(r);const e=s.query(t,r);if(0!==e.length)return 1===e.length?e[0]:e},y=(r,t,e)=>r.replace(t,String(e)),p=(r,t,e)=>{const a=r.match(c);if(!a)return;const o=a.length,i=new Array(o);for(let r=0;r<o;r++)i[r]={toReplace:a[r],path:a[r].slice(2,-1)};return i.reduce(((r,a)=>((r,t,e,a)=>{const o=r.path.trim();if(!o)return a;try{const i=u(t,o);if(i){const t=i.evalSync(e);if(void 0!==t)return y(a,r.toReplace,t)}const s=l(e,o);return n(s)?y(a,r.toReplace,s):a}catch{return a}})(a,e,t,r)),String(r))},h=(r,t,e,a)=>{if(Array.isArray(t)){const r=parseInt(e,10);if(!isNaN(r)&&(r<0||r>=t.length))return{value:void 0,error:`Invalid array index '${e}' at '${a}'`,availableKeys:t.map(((r,t)=>t.toString()))}}return o(t)?{value:void 0,error:`Key '${e}' not found at '${a}'`,availableKeys:Object.keys(t)}:{value:void 0,error:`${r} at '${a}'`,availableKeys:[]}},v=(r,t)=>{const e=/(\.\.)|(\[\*\])|(\[\?\()|(\[\d+:\d+\])/;try{if(e.test(r)){const e=d(r,t);return void 0===e?{value:void 0,error:`Invalid or empty JSONPath: '${r}'`}:{value:e}}const a=r.match(/\$|(?:\['([^']+)'\])|(?:\["([^"]+)"\])|\[\d+\]|[^[\].]+/g)||[];return((r,t)=>{if("$"!==r[0])return{value:void 0,error:"JSON path must start with $"};let e=t,a="$";for(let t=1;t<r.length;t++){const n=r[t],i=n.startsWith("[")?`${a}${n}`:`${a}.${n}`;if(Array.isArray(e)){const r=parseInt(n,10);if(isNaN(r)||r<0||r>=e.length)return h("Invalid array index",e,n,a);e=e[r],a=i}else{if(!o(e))return{value:void 0,error:`Cannot access '${n}' at '${a}' - parent is not an object`,availableKeys:[]};if(!Object.prototype.hasOwnProperty.call(e,n))return h("Key not found",e,n,a);e=e[n],a=i}}return{value:e}})(a.map((r=>r.replace(/^\['([^']+)'\]$/,"$1").replace(/^\["([^"]+)"\]$/,"$1").replace(/^\[(\d+)\]$/,"$1"))),t)}catch(r){return{value:void 0,error:`Something went wrong with evaluation of JSON path: ${String(r)}`}}},f=(s,l,c)=>{const y=s?.trim();if(null==y||""===y)throw new Error("Empty expression");const h=o(l)?l:{},f=((o=()=>new i.Jexl)=>{const s=o();return s.addFunction("nextAnniversary",((t,e)=>r({initialDate:t,format:e}))),s.addFunction("yearsElapsed",((r,e,a)=>t({startDate:r,endDate:a,format:e}))),s.addFunction("hasPassed",((r,t,a)=>e({date:r,yearsToAdd:a,format:t}))),s.addFunction("now",(()=>(new Date).toISOString())),s.addFunction("includes",((r,t)=>!(a(r)||!Array.isArray(r)||0===r.length||a(t))&&(Array.isArray(t)?t.every((t=>r.includes(t))):r.includes(t)))),s.addFunction("includesSome",((r,t)=>!(a(r)||!Array.isArray(r)||0===r.length||a(t))&&(Array.isArray(t)?t.some((t=>r.includes(t))):r.includes(t)))),s.addFunction("present",(r=>n(r))),s.addFunction("missing",(r=>a(r))),s.addFunction("keys",(r=>a(r)||"object"!=typeof r||Array.isArray(r)?[]:Object.keys(r))),s.addFunction("values",(r=>a(r)||"object"!=typeof r||Array.isArray(r)?[]:Object.values(r))),s.addBinaryOp("??",0,((r,t)=>a(r)?t:a(t)?r:r??t)),s})(),$=(r=>{if(r.startsWith("{{")&&r.endsWith("}}"))return r.slice(2,-2)})(y),m=(r=>r.replace(/\$\./g,"").trim())($??y),g=p(m,h,f);if(g)return g;if(!$&&y.startsWith("$"))return c?.incrementalJsonPath?((r,t)=>{const e=v(r,t);if(e.error)throw new Error(`${e.error}${e.availableKeys?.length?`. Available keys: ${e.availableKeys.join(", ")}`:""}`);return e.value})(y,h):((r,t)=>{try{return d(r,t)}catch{throw new Error(`Invalid JSON path: "${r}"`)}})(y,h);if(!g&&!$)return s;const A=u(f,m);if(!A||"."===m)throw new Error(`Invalid expression: "${y}"`);try{return A.evalSync(h)}catch{return}},$=(r,t)=>{try{return f(r,t)}catch{return null}},m=(r,t)=>{const e=r=>Array.isArray(r)?r.map(e):"object"==typeof r&&null!==r?m(r,t):"string"==typeof r?$(r,t):r;return Object.fromEntries(Object.entries(r).map((([r,t])=>[r,e(t)])))},g=r=>{try{return f(r),!0}catch{return!1}};export{f as evaluate,g as isValidExpression,$ as safeEvaluate,m as safeEvaluateRecord};
@@ -1 +0,0 @@
1
- export declare const INTERPOLATED_EXPRESSION_REGEX: RegExp;
@@ -1,5 +0,0 @@
1
- export declare const evaluateExpression: (expression?: string | null, context?: unknown, options?: {
2
- incrementalJsonPath?: boolean;
3
- }) => unknown;
4
- export declare const safeEvaluateExpression: (expression?: string | null, context?: unknown) => unknown;
5
- export declare const safeEvaluateRecord: (record: Record<string, unknown>, context?: unknown) => Record<string, unknown>;
@@ -1,39 +0,0 @@
1
- export declare const createExpressionHandler: (instanceBuilder?: () => {
2
- addBinaryOp(operator: string, precedence: number, fn: (left: any, right: any) => any): void;
3
- addUnaryOp(operator: string, fn: (right: any) => any): void;
4
- addTransform(name: string, fn: (value: any, ...args: any[]) => any): void;
5
- addTransforms(map: {
6
- [key: string]: (value: any, ...args: any[]) => any;
7
- }): void;
8
- getTransform(name: string): (value: any, ...args: any[]) => any;
9
- addFunction(name: string, fn: (value: any, ...args: any[]) => any): void;
10
- addFunctions(map: {
11
- [key: string]: (value: any, ...args: any[]) => any;
12
- }): void;
13
- getFunction(name: string): (value: any, ...args: any[]) => any;
14
- eval(expression: string, context?: import("jexl/Expression").Context): Promise<any>;
15
- evalSync(expression: string, context?: import("jexl/Expression").Context): any;
16
- compile(expression: string): import("jexl/Expression").default;
17
- createExpression(expression: string): import("jexl/Expression").default;
18
- removeOp(operator: string): void;
19
- _grammar: import("jexl/Grammar").default;
20
- }) => {
21
- addBinaryOp(operator: string, precedence: number, fn: (left: any, right: any) => any): void;
22
- addUnaryOp(operator: string, fn: (right: any) => any): void;
23
- addTransform(name: string, fn: (value: any, ...args: any[]) => any): void;
24
- addTransforms(map: {
25
- [key: string]: (value: any, ...args: any[]) => any;
26
- }): void;
27
- getTransform(name: string): (value: any, ...args: any[]) => any;
28
- addFunction(name: string, fn: (value: any, ...args: any[]) => any): void;
29
- addFunctions(map: {
30
- [key: string]: (value: any, ...args: any[]) => any;
31
- }): void;
32
- getFunction(name: string): (value: any, ...args: any[]) => any;
33
- eval(expression: string, context?: import("jexl/Expression").Context): Promise<any>;
34
- evalSync(expression: string, context?: import("jexl/Expression").Context): any;
35
- compile(expression: string): import("jexl/Expression").default;
36
- createExpression(expression: string): import("jexl/Expression").default;
37
- removeOp(operator: string): void;
38
- _grammar: import("jexl/Grammar").default;
39
- };
@@ -1,3 +0,0 @@
1
- export { evaluateExpression as evaluate } from './evaluate';
2
- export { safeEvaluateExpression as safeEvaluate, safeEvaluateRecord } from './evaluate';
3
- export { isValidExpression } from './validate';
@@ -1,8 +0,0 @@
1
- export type ExpressionContext = {
2
- [key: string]: unknown;
3
- };
4
- export type JSONPathEvalResult = {
5
- value: unknown;
6
- error?: string;
7
- availableKeys?: string[];
8
- };
@@ -1,16 +0,0 @@
1
- import Expression from 'jexl/Expression';
2
- import { ExpressionContext } from './types';
3
- export declare const cleanExpression: (expression: string) => string;
4
- export declare const compileExpression: (expressionHandler: {
5
- compile: (expr: string) => Expression;
6
- }, expression: string) => Expression | null;
7
- export declare const extractExpressionBetweenDoubleCurlyBraces: (expression: string) => string | undefined;
8
- export declare const evaluateJsonPath: (expression: string, context: unknown) => unknown;
9
- export declare const evaluateStringInterpolations: (expression: string, context: ExpressionContext, expressionHandler: {
10
- compile: (expr: string) => Expression;
11
- }) => string | undefined;
12
- export declare const evaluateJsonPathIncremental: (path: string, context: unknown) => {
13
- value: unknown;
14
- error?: string;
15
- availableKeys?: string[];
16
- };
@@ -1 +0,0 @@
1
- export declare const isValidExpression: (expression?: string | null) => boolean;