UNPKG

21.7 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, '__esModule', { value: true });
4
5var vm = require('vm');
6
7function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
8
9var vm__default = /*#__PURE__*/_interopDefaultLegacy(vm);
10
11const {
12 hasOwnProperty: hasOwnProp
13} = Object.prototype;
14/**
15* @typedef {null|boolean|number|string|PlainObject|GenericArray} JSONObject
16*/
17
18/**
19 * Copies array and then pushes item into it.
20 * @param {GenericArray} arr Array to copy and into which to push
21 * @param {any} item Array item to add (to end)
22 * @returns {GenericArray} Copy of the original array
23 */
24
25function push(arr, item) {
26 arr = arr.slice();
27 arr.push(item);
28 return arr;
29}
30/**
31 * Copies array and then unshifts item into it.
32 * @param {any} item Array item to add (to beginning)
33 * @param {GenericArray} arr Array to copy and into which to unshift
34 * @returns {GenericArray} Copy of the original array
35 */
36
37
38function unshift(item, arr) {
39 arr = arr.slice();
40 arr.unshift(item);
41 return arr;
42}
43/**
44 * Caught when JSONPath is used without `new` but rethrown if with `new`
45 * @extends Error
46 */
47
48
49class NewError extends Error {
50 /**
51 * @param {any} value The evaluated scalar value
52 */
53 constructor(value) {
54 super('JSONPath should not be called with "new" (it prevents return ' + 'of (unwrapped) scalar values)');
55 this.avoidNew = true;
56 this.value = value;
57 this.name = 'NewError';
58 }
59
60}
61/**
62* @typedef {PlainObject} ReturnObject
63* @property {string} path
64* @property {JSONObject} value
65* @property {PlainObject|GenericArray} parent
66* @property {string} parentProperty
67*/
68
69/**
70* @callback JSONPathCallback
71* @param {string|PlainObject} preferredOutput
72* @param {"value"|"property"} type
73* @param {ReturnObject} fullRetObj
74* @returns {void}
75*/
76
77/**
78* @callback OtherTypeCallback
79* @param {JSONObject} val
80* @param {string} path
81* @param {PlainObject|GenericArray} parent
82* @param {string} parentPropName
83* @returns {boolean}
84*/
85
86/* eslint-disable max-len -- Can make multiline type after https://github.com/syavorsky/comment-parser/issues/109 */
87
88/**
89 * @typedef {PlainObject} JSONPathOptions
90 * @property {JSON} json
91 * @property {string|string[]} path
92 * @property {"value"|"path"|"pointer"|"parent"|"parentProperty"|"all"} [resultType="value"]
93 * @property {boolean} [flatten=false]
94 * @property {boolean} [wrap=true]
95 * @property {PlainObject} [sandbox={}]
96 * @property {boolean} [preventEval=false]
97 * @property {PlainObject|GenericArray|null} [parent=null]
98 * @property {string|null} [parentProperty=null]
99 * @property {JSONPathCallback} [callback]
100 * @property {OtherTypeCallback} [otherTypeCallback] Defaults to
101 * function which throws on encountering `@other`
102 * @property {boolean} [autostart=true]
103 */
104
105/* eslint-enable max-len -- Can make multiline type after https://github.com/syavorsky/comment-parser/issues/109 */
106
107/**
108 * @param {string|JSONPathOptions} opts If a string, will be treated as `expr`
109 * @param {string} [expr] JSON path to evaluate
110 * @param {JSON} [obj] JSON object to evaluate against
111 * @param {JSONPathCallback} [callback] Passed 3 arguments: 1) desired payload
112 * per `resultType`, 2) `"value"|"property"`, 3) Full returned object with
113 * all payloads
114 * @param {OtherTypeCallback} [otherTypeCallback] If `@other()` is at the end
115 * of one's query, this will be invoked with the value of the item, its
116 * path, its parent, and its parent's property name, and it should return
117 * a boolean indicating whether the supplied value belongs to the "other"
118 * type or not (or it may handle transformations and return `false`).
119 * @returns {JSONPath}
120 * @class
121 */
122
123
124function JSONPath(opts, expr, obj, callback, otherTypeCallback) {
125 // eslint-disable-next-line no-restricted-syntax
126 if (!(this instanceof JSONPath)) {
127 try {
128 return new JSONPath(opts, expr, obj, callback, otherTypeCallback);
129 } catch (e) {
130 if (!e.avoidNew) {
131 throw e;
132 }
133
134 return e.value;
135 }
136 }
137
138 if (typeof opts === 'string') {
139 otherTypeCallback = callback;
140 callback = obj;
141 obj = expr;
142 expr = opts;
143 opts = null;
144 }
145
146 const optObj = opts && typeof opts === 'object';
147 opts = opts || {};
148 this.json = opts.json || obj;
149 this.path = opts.path || expr;
150 this.resultType = opts.resultType || 'value';
151 this.flatten = opts.flatten || false;
152 this.wrap = hasOwnProp.call(opts, 'wrap') ? opts.wrap : true;
153 this.sandbox = opts.sandbox || {};
154 this.preventEval = opts.preventEval || false;
155 this.parent = opts.parent || null;
156 this.parentProperty = opts.parentProperty || null;
157 this.callback = opts.callback || callback || null;
158
159 this.otherTypeCallback = opts.otherTypeCallback || otherTypeCallback || function () {
160 throw new TypeError('You must supply an otherTypeCallback callback option ' + 'with the @other() operator.');
161 };
162
163 if (opts.autostart !== false) {
164 const args = {
165 path: optObj ? opts.path : expr
166 };
167
168 if (!optObj) {
169 args.json = obj;
170 } else if ('json' in opts) {
171 args.json = opts.json;
172 }
173
174 const ret = this.evaluate(args);
175
176 if (!ret || typeof ret !== 'object') {
177 throw new NewError(ret);
178 }
179
180 return ret;
181 }
182} // PUBLIC METHODS
183
184
185JSONPath.prototype.evaluate = function (expr, json, callback, otherTypeCallback) {
186 let currParent = this.parent,
187 currParentProperty = this.parentProperty;
188 let {
189 flatten,
190 wrap
191 } = this;
192 this.currResultType = this.resultType;
193 this.currPreventEval = this.preventEval;
194 this.currSandbox = this.sandbox;
195 callback = callback || this.callback;
196 this.currOtherTypeCallback = otherTypeCallback || this.otherTypeCallback;
197 json = json || this.json;
198 expr = expr || this.path;
199
200 if (expr && typeof expr === 'object' && !Array.isArray(expr)) {
201 if (!expr.path && expr.path !== '') {
202 throw new TypeError('You must supply a "path" property when providing an object ' + 'argument to JSONPath.evaluate().');
203 }
204
205 if (!hasOwnProp.call(expr, 'json')) {
206 throw new TypeError('You must supply a "json" property when providing an object ' + 'argument to JSONPath.evaluate().');
207 }
208
209 ({
210 json
211 } = expr);
212 flatten = hasOwnProp.call(expr, 'flatten') ? expr.flatten : flatten;
213 this.currResultType = hasOwnProp.call(expr, 'resultType') ? expr.resultType : this.currResultType;
214 this.currSandbox = hasOwnProp.call(expr, 'sandbox') ? expr.sandbox : this.currSandbox;
215 wrap = hasOwnProp.call(expr, 'wrap') ? expr.wrap : wrap;
216 this.currPreventEval = hasOwnProp.call(expr, 'preventEval') ? expr.preventEval : this.currPreventEval;
217 callback = hasOwnProp.call(expr, 'callback') ? expr.callback : callback;
218 this.currOtherTypeCallback = hasOwnProp.call(expr, 'otherTypeCallback') ? expr.otherTypeCallback : this.currOtherTypeCallback;
219 currParent = hasOwnProp.call(expr, 'parent') ? expr.parent : currParent;
220 currParentProperty = hasOwnProp.call(expr, 'parentProperty') ? expr.parentProperty : currParentProperty;
221 expr = expr.path;
222 }
223
224 currParent = currParent || null;
225 currParentProperty = currParentProperty || null;
226
227 if (Array.isArray(expr)) {
228 expr = JSONPath.toPathString(expr);
229 }
230
231 if (!expr && expr !== '' || !json) {
232 return undefined;
233 }
234
235 const exprList = JSONPath.toPathArray(expr);
236
237 if (exprList[0] === '$' && exprList.length > 1) {
238 exprList.shift();
239 }
240
241 this._hasParentSelector = null;
242
243 const result = this._trace(exprList, json, ['$'], currParent, currParentProperty, callback).filter(function (ea) {
244 return ea && !ea.isParentSelector;
245 });
246
247 if (!result.length) {
248 return wrap ? [] : undefined;
249 }
250
251 if (!wrap && result.length === 1 && !result[0].hasArrExpr) {
252 return this._getPreferredOutput(result[0]);
253 }
254
255 return result.reduce((rslt, ea) => {
256 const valOrPath = this._getPreferredOutput(ea);
257
258 if (flatten && Array.isArray(valOrPath)) {
259 rslt = rslt.concat(valOrPath);
260 } else {
261 rslt.push(valOrPath);
262 }
263
264 return rslt;
265 }, []);
266}; // PRIVATE METHODS
267
268
269JSONPath.prototype._getPreferredOutput = function (ea) {
270 const resultType = this.currResultType;
271
272 switch (resultType) {
273 case 'all':
274 {
275 const path = Array.isArray(ea.path) ? ea.path : JSONPath.toPathArray(ea.path);
276 ea.pointer = JSONPath.toPointer(path);
277 ea.path = typeof ea.path === 'string' ? ea.path : JSONPath.toPathString(ea.path);
278 return ea;
279 }
280
281 case 'value':
282 case 'parent':
283 case 'parentProperty':
284 return ea[resultType];
285
286 case 'path':
287 return JSONPath.toPathString(ea[resultType]);
288
289 case 'pointer':
290 return JSONPath.toPointer(ea.path);
291
292 default:
293 throw new TypeError('Unknown result type');
294 }
295};
296
297JSONPath.prototype._handleCallback = function (fullRetObj, callback, type) {
298 if (callback) {
299 const preferredOutput = this._getPreferredOutput(fullRetObj);
300
301 fullRetObj.path = typeof fullRetObj.path === 'string' ? fullRetObj.path : JSONPath.toPathString(fullRetObj.path); // eslint-disable-next-line node/callback-return
302
303 callback(preferredOutput, type, fullRetObj);
304 }
305};
306/**
307 *
308 * @param {string} expr
309 * @param {JSONObject} val
310 * @param {string} path
311 * @param {PlainObject|GenericArray} parent
312 * @param {string} parentPropName
313 * @param {JSONPathCallback} callback
314 * @param {boolean} hasArrExpr
315 * @param {boolean} literalPriority
316 * @returns {ReturnObject|ReturnObject[]}
317 */
318
319
320JSONPath.prototype._trace = function (expr, val, path, parent, parentPropName, callback, hasArrExpr, literalPriority) {
321 // No expr to follow? return path and value as the result of
322 // this trace branch
323 let retObj;
324
325 if (!expr.length) {
326 retObj = {
327 path,
328 value: val,
329 parent,
330 parentProperty: parentPropName,
331 hasArrExpr
332 };
333
334 this._handleCallback(retObj, callback, 'value');
335
336 return retObj;
337 }
338
339 const loc = expr[0],
340 x = expr.slice(1); // We need to gather the return value of recursive trace calls in order to
341 // do the parent sel computation.
342
343 const ret = [];
344 /**
345 *
346 * @param {ReturnObject|ReturnObject[]} elems
347 * @returns {void}
348 */
349
350 function addRet(elems) {
351 if (Array.isArray(elems)) {
352 // This was causing excessive stack size in Node (with or
353 // without Babel) against our performance test:
354 // `ret.push(...elems);`
355 elems.forEach(t => {
356 ret.push(t);
357 });
358 } else {
359 ret.push(elems);
360 }
361 }
362
363 if ((typeof loc !== 'string' || literalPriority) && val && hasOwnProp.call(val, loc)) {
364 // simple case--directly follow property
365 addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback, hasArrExpr));
366 } else if (loc === '*') {
367 // all child properties
368 this._walk(loc, x, val, path, parent, parentPropName, callback, (m, l, _x, v, p, par, pr, cb) => {
369 addRet(this._trace(unshift(m, _x), v, p, par, pr, cb, true, true));
370 });
371 } else if (loc === '..') {
372 // all descendent parent properties
373 // Check remaining expression with val's immediate children
374 addRet(this._trace(x, val, path, parent, parentPropName, callback, hasArrExpr));
375
376 this._walk(loc, x, val, path, parent, parentPropName, callback, (m, l, _x, v, p, par, pr, cb) => {
377 // We don't join m and x here because we only want parents,
378 // not scalar values
379 if (typeof v[m] === 'object') {
380 // Keep going with recursive descent on val's
381 // object children
382 addRet(this._trace(unshift(l, _x), v[m], push(p, m), v, m, cb, true));
383 }
384 }); // The parent sel computation is handled in the frame above using the
385 // ancestor object of val
386
387 } else if (loc === '^') {
388 // This is not a final endpoint, so we do not invoke the callback here
389 this._hasParentSelector = true;
390 return {
391 path: path.slice(0, -1),
392 expr: x,
393 isParentSelector: true
394 };
395 } else if (loc === '~') {
396 // property name
397 retObj = {
398 path: push(path, loc),
399 value: parentPropName,
400 parent,
401 parentProperty: null
402 };
403
404 this._handleCallback(retObj, callback, 'property');
405
406 return retObj;
407 } else if (loc === '$') {
408 // root only
409 addRet(this._trace(x, val, path, null, null, callback, hasArrExpr));
410 } else if (/^(-?\d*):(-?\d*):?(\d*)$/u.test(loc)) {
411 // [start:end:step] Python slice syntax
412 addRet(this._slice(loc, x, val, path, parent, parentPropName, callback));
413 } else if (loc.indexOf('?(') === 0) {
414 // [?(expr)] (filtering)
415 if (this.currPreventEval) {
416 throw new Error('Eval [?(expr)] prevented in JSONPath expression.');
417 }
418
419 this._walk(loc, x, val, path, parent, parentPropName, callback, (m, l, _x, v, p, par, pr, cb) => {
420 if (this._eval(l.replace(/^\?\((.*?)\)$/u, '$1'), v[m], m, p, par, pr)) {
421 addRet(this._trace(unshift(m, _x), v, p, par, pr, cb, true));
422 }
423 });
424 } else if (loc[0] === '(') {
425 // [(expr)] (dynamic property/index)
426 if (this.currPreventEval) {
427 throw new Error('Eval [(expr)] prevented in JSONPath expression.');
428 } // As this will resolve to a property name (but we don't know it
429 // yet), property and parent information is relative to the
430 // parent of the property to which this expression will resolve
431
432
433 addRet(this._trace(unshift(this._eval(loc, val, path[path.length - 1], path.slice(0, -1), parent, parentPropName), x), val, path, parent, parentPropName, callback, hasArrExpr));
434 } else if (loc[0] === '@') {
435 // value type: @boolean(), etc.
436 let addType = false;
437 const valueType = loc.slice(1, -2);
438
439 switch (valueType) {
440 case 'scalar':
441 if (!val || !['object', 'function'].includes(typeof val)) {
442 addType = true;
443 }
444
445 break;
446
447 case 'boolean':
448 case 'string':
449 case 'undefined':
450 case 'function':
451 // eslint-disable-next-line valid-typeof
452 if (typeof val === valueType) {
453 addType = true;
454 }
455
456 break;
457
458 case 'integer':
459 if (Number.isFinite(val) && !(val % 1)) {
460 addType = true;
461 }
462
463 break;
464
465 case 'number':
466 if (Number.isFinite(val)) {
467 addType = true;
468 }
469
470 break;
471
472 case 'nonFinite':
473 if (typeof val === 'number' && !Number.isFinite(val)) {
474 addType = true;
475 }
476
477 break;
478
479 case 'object':
480 // eslint-disable-next-line valid-typeof
481 if (val && typeof val === valueType) {
482 addType = true;
483 }
484
485 break;
486
487 case 'array':
488 if (Array.isArray(val)) {
489 addType = true;
490 }
491
492 break;
493
494 case 'other':
495 addType = this.currOtherTypeCallback(val, path, parent, parentPropName);
496 break;
497
498 case 'null':
499 if (val === null) {
500 addType = true;
501 }
502
503 break;
504
505 /* istanbul ignore next */
506
507 default:
508 throw new TypeError('Unknown value type ' + valueType);
509 }
510
511 if (addType) {
512 retObj = {
513 path,
514 value: val,
515 parent,
516 parentProperty: parentPropName
517 };
518
519 this._handleCallback(retObj, callback, 'value');
520
521 return retObj;
522 } // `-escaped property
523
524 } else if (loc[0] === '`' && val && hasOwnProp.call(val, loc.slice(1))) {
525 const locProp = loc.slice(1);
526 addRet(this._trace(x, val[locProp], push(path, locProp), val, locProp, callback, hasArrExpr, true));
527 } else if (loc.includes(',')) {
528 // [name1,name2,...]
529 const parts = loc.split(',');
530
531 for (const part of parts) {
532 addRet(this._trace(unshift(part, x), val, path, parent, parentPropName, callback, true));
533 } // simple case--directly follow property
534
535 } else if (!literalPriority && val && hasOwnProp.call(val, loc)) {
536 addRet(this._trace(x, val[loc], push(path, loc), val, loc, callback, hasArrExpr, true));
537 } // We check the resulting values for parent selections. For parent
538 // selections we discard the value object and continue the trace with the
539 // current val object
540
541
542 if (this._hasParentSelector) {
543 for (let t = 0; t < ret.length; t++) {
544 const rett = ret[t];
545
546 if (rett && rett.isParentSelector) {
547 const tmp = this._trace(rett.expr, val, rett.path, parent, parentPropName, callback, hasArrExpr);
548
549 if (Array.isArray(tmp)) {
550 ret[t] = tmp[0];
551 const tl = tmp.length;
552
553 for (let tt = 1; tt < tl; tt++) {
554 t++;
555 ret.splice(t, 0, tmp[tt]);
556 }
557 } else {
558 ret[t] = tmp;
559 }
560 }
561 }
562 }
563
564 return ret;
565};
566
567JSONPath.prototype._walk = function (loc, expr, val, path, parent, parentPropName, callback, f) {
568 if (Array.isArray(val)) {
569 const n = val.length;
570
571 for (let i = 0; i < n; i++) {
572 f(i, loc, expr, val, path, parent, parentPropName, callback);
573 }
574 } else if (val && typeof val === 'object') {
575 Object.keys(val).forEach(m => {
576 f(m, loc, expr, val, path, parent, parentPropName, callback);
577 });
578 }
579};
580
581JSONPath.prototype._slice = function (loc, expr, val, path, parent, parentPropName, callback) {
582 if (!Array.isArray(val)) {
583 return undefined;
584 }
585
586 const len = val.length,
587 parts = loc.split(':'),
588 step = parts[2] && Number.parseInt(parts[2]) || 1;
589 let start = parts[0] && Number.parseInt(parts[0]) || 0,
590 end = parts[1] && Number.parseInt(parts[1]) || len;
591 start = start < 0 ? Math.max(0, start + len) : Math.min(len, start);
592 end = end < 0 ? Math.max(0, end + len) : Math.min(len, end);
593 const ret = [];
594
595 for (let i = start; i < end; i += step) {
596 const tmp = this._trace(unshift(i, expr), val, path, parent, parentPropName, callback, true); // Should only be possible to be an array here since first part of
597 // ``unshift(i, expr)` passed in above would not be empty, nor `~`,
598 // nor begin with `@` (as could return objects)
599 // This was causing excessive stack size in Node (with or
600 // without Babel) against our performance test: `ret.push(...tmp);`
601
602
603 tmp.forEach(t => {
604 ret.push(t);
605 });
606 }
607
608 return ret;
609};
610
611JSONPath.prototype._eval = function (code, _v, _vname, path, parent, parentPropName) {
612 if (code.includes('@parentProperty')) {
613 this.currSandbox._$_parentProperty = parentPropName;
614 code = code.replace(/@parentProperty/gu, '_$_parentProperty');
615 }
616
617 if (code.includes('@parent')) {
618 this.currSandbox._$_parent = parent;
619 code = code.replace(/@parent/gu, '_$_parent');
620 }
621
622 if (code.includes('@property')) {
623 this.currSandbox._$_property = _vname;
624 code = code.replace(/@property/gu, '_$_property');
625 }
626
627 if (code.includes('@path')) {
628 this.currSandbox._$_path = JSONPath.toPathString(path.concat([_vname]));
629 code = code.replace(/@path/gu, '_$_path');
630 }
631
632 if (code.includes('@root')) {
633 this.currSandbox._$_root = this.json;
634 code = code.replace(/@root/gu, '_$_root');
635 }
636
637 if (/@([.\s)[])/u.test(code)) {
638 this.currSandbox._$_v = _v;
639 code = code.replace(/@([.\s)[])/gu, '_$_v$1');
640 }
641
642 try {
643 return this.vm.runInNewContext(code, this.currSandbox);
644 } catch (e) {
645 // eslint-disable-next-line no-console
646 console.log(e);
647 throw new Error('jsonPath: ' + e.message + ': ' + code);
648 }
649}; // PUBLIC CLASS PROPERTIES AND METHODS
650// Could store the cache object itself
651
652
653JSONPath.cache = {};
654/**
655 * @param {string[]} pathArr Array to convert
656 * @returns {string} The path string
657 */
658
659JSONPath.toPathString = function (pathArr) {
660 const x = pathArr,
661 n = x.length;
662 let p = '$';
663
664 for (let i = 1; i < n; i++) {
665 if (!/^(~|\^|@.*?\(\))$/u.test(x[i])) {
666 p += /^[0-9*]+$/u.test(x[i]) ? '[' + x[i] + ']' : "['" + x[i] + "']";
667 }
668 }
669
670 return p;
671};
672/**
673 * @param {string} pointer JSON Path
674 * @returns {string} JSON Pointer
675 */
676
677
678JSONPath.toPointer = function (pointer) {
679 const x = pointer,
680 n = x.length;
681 let p = '';
682
683 for (let i = 1; i < n; i++) {
684 if (!/^(~|\^|@.*?\(\))$/u.test(x[i])) {
685 p += '/' + x[i].toString().replace(/~/gu, '~0').replace(/\//gu, '~1');
686 }
687 }
688
689 return p;
690};
691/**
692 * @param {string} expr Expression to convert
693 * @returns {string[]}
694 */
695
696
697JSONPath.toPathArray = function (expr) {
698 const {
699 cache
700 } = JSONPath;
701
702 if (cache[expr]) {
703 return cache[expr].concat();
704 }
705
706 const subx = [];
707 const normalized = expr // Properties
708 .replace(/@(?:null|boolean|number|string|integer|undefined|nonFinite|scalar|array|object|function|other)\(\)/gu, ';$&;') // Parenthetical evaluations (filtering and otherwise), directly
709 // within brackets or single quotes
710 .replace(/[['](\??\(.*?\))[\]']/gu, function ($0, $1) {
711 return '[#' + (subx.push($1) - 1) + ']';
712 }) // Escape periods and tildes within properties
713 .replace(/\['([^'\]]*)'\]/gu, function ($0, prop) {
714 return "['" + prop.replace(/\./gu, '%@%').replace(/~/gu, '%%@@%%') + "']";
715 }) // Properties operator
716 .replace(/~/gu, ';~;') // Split by property boundaries
717 .replace(/'?\.'?(?![^[]*\])|\['?/gu, ';') // Reinsert periods within properties
718 .replace(/%@%/gu, '.') // Reinsert tildes within properties
719 .replace(/%%@@%%/gu, '~') // Parent
720 .replace(/(?:;)?(\^+)(?:;)?/gu, function ($0, ups) {
721 return ';' + ups.split('').join(';') + ';';
722 }) // Descendents
723 .replace(/;;;|;;/gu, ';..;') // Remove trailing
724 .replace(/;$|'?\]|'$/gu, '');
725 const exprList = normalized.split(';').map(function (exp) {
726 const match = exp.match(/#(\d+)/u);
727 return !match || !match[1] ? exp : subx[match[1]];
728 });
729 cache[expr] = exprList;
730 return cache[expr].concat();
731};
732
733JSONPath.prototype.vm = vm__default['default'];
734
735exports.JSONPath = JSONPath;