donobu 3.19.2 → 3.19.3

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.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * A minimal, safe JSONPath evaluator that replaces the `jsonpath` npm package.
3
+ *
4
+ * The `jsonpath` library (v1.2.1) has CVE GHSA-87r5-mp6g-5w5j due to its use
5
+ * of `eval()` for script expressions. This implementation covers the subset of
6
+ * JSONPath used by this project with NO dynamic code execution.
7
+ *
8
+ * Supported features:
9
+ * - Property access: $.user.name
10
+ * - Array indexing (positive & negative): $.calls[0], $.calls[-1]
11
+ * - Slice notation: $.calls[-2:], $.calls[1:], $.calls[0:2]
12
+ * - Filter expressions: $.books[?(@.price < 10)]
13
+ * - Logical AND in filters: [?(@.a < 5 && @.b == "x")]
14
+ * - Property existence filters: [?(@.isbn)]
15
+ * - Filter + index: $.books[?(@.price < 10)][0]
16
+ * - Recursive descent: $..author
17
+ * - Wildcards: $[*], $..books[*].title
18
+ */
19
+ /**
20
+ * Evaluate a JSONPath expression against a data object.
21
+ *
22
+ * Drop-in replacement for `jsonpath.query()` covering the subset of JSONPath
23
+ * used by this project. Uses no `eval()` or dynamic code execution.
24
+ *
25
+ * @param data - The data to query
26
+ * @param expression - A JSONPath expression (must start with $)
27
+ * @returns An array of matching values
28
+ */
29
+ export declare function queryJsonPath(data: unknown, expression: string): unknown[];
30
+ //# sourceMappingURL=JsonPath.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JsonPath.d.ts","sourceRoot":"","sources":["../../../src/utils/JsonPath.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAssBH;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,EAAE,CAI1E"}
@@ -0,0 +1,622 @@
1
+ "use strict";
2
+ /**
3
+ * A minimal, safe JSONPath evaluator that replaces the `jsonpath` npm package.
4
+ *
5
+ * The `jsonpath` library (v1.2.1) has CVE GHSA-87r5-mp6g-5w5j due to its use
6
+ * of `eval()` for script expressions. This implementation covers the subset of
7
+ * JSONPath used by this project with NO dynamic code execution.
8
+ *
9
+ * Supported features:
10
+ * - Property access: $.user.name
11
+ * - Array indexing (positive & negative): $.calls[0], $.calls[-1]
12
+ * - Slice notation: $.calls[-2:], $.calls[1:], $.calls[0:2]
13
+ * - Filter expressions: $.books[?(@.price < 10)]
14
+ * - Logical AND in filters: [?(@.a < 5 && @.b == "x")]
15
+ * - Property existence filters: [?(@.isbn)]
16
+ * - Filter + index: $.books[?(@.price < 10)][0]
17
+ * - Recursive descent: $..author
18
+ * - Wildcards: $[*], $..books[*].title
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.queryJsonPath = queryJsonPath;
22
+ // ---------------------------------------------------------------------------
23
+ // Tokenizer
24
+ // ---------------------------------------------------------------------------
25
+ function tokenize(expression) {
26
+ const tokens = [];
27
+ let i = 0;
28
+ if (expression[i] !== '$') {
29
+ throw new Error(`JSONPath must start with $, got: ${expression}`);
30
+ }
31
+ tokens.push({ type: 'root' });
32
+ i++;
33
+ while (i < expression.length) {
34
+ if (expression[i] === '.') {
35
+ if (expression[i + 1] === '.') {
36
+ tokens.push({ type: 'dotdot' });
37
+ i += 2;
38
+ }
39
+ else {
40
+ tokens.push({ type: 'dot' });
41
+ i++;
42
+ }
43
+ }
44
+ else if (expression[i] === '[') {
45
+ i++; // skip '['
46
+ if (expression[i] === '?' && expression[i + 1] === '(') {
47
+ // Filter expression: find matching closing )]
48
+ i += 2; // skip '?('
49
+ let depth = 1;
50
+ let filterStart = i;
51
+ while (i < expression.length && depth > 0) {
52
+ if (expression[i] === '(') {
53
+ depth++;
54
+ }
55
+ else if (expression[i] === ')') {
56
+ depth--;
57
+ }
58
+ if (depth > 0) {
59
+ i++;
60
+ }
61
+ }
62
+ if (depth !== 0) {
63
+ throw new Error(`Unmatched filter parenthesis in: ${expression}`);
64
+ }
65
+ const raw = expression.substring(filterStart, i);
66
+ i++; // skip ')'
67
+ if (expression[i] !== ']') {
68
+ throw new Error(`Expected ] after filter expression in: ${expression}`);
69
+ }
70
+ i++; // skip ']'
71
+ tokens.push({ type: 'filter', raw });
72
+ }
73
+ else if (expression[i] === '*') {
74
+ i++; // skip '*'
75
+ if (expression[i] !== ']') {
76
+ throw new Error(`Expected ] after * in: ${expression}`);
77
+ }
78
+ i++; // skip ']'
79
+ tokens.push({ type: 'wildcard' });
80
+ }
81
+ else {
82
+ // Index or slice: read until ']'
83
+ const bracketStart = i;
84
+ while (i < expression.length && expression[i] !== ']') {
85
+ i++;
86
+ }
87
+ if (i >= expression.length) {
88
+ throw new Error(`Unmatched [ in: ${expression}`);
89
+ }
90
+ const content = expression.substring(bracketStart, i);
91
+ i++; // skip ']'
92
+ if (content.includes(':')) {
93
+ // Slice notation
94
+ const parts = content.split(':');
95
+ const start = parts[0] !== '' ? parseInt(parts[0], 10) : undefined;
96
+ const end = parts[1] !== '' ? parseInt(parts[1], 10) : undefined;
97
+ if ((parts[0] !== '' && isNaN(start)) ||
98
+ (parts[1] !== '' && isNaN(end))) {
99
+ throw new Error(`Invalid slice notation: [${content}]`);
100
+ }
101
+ tokens.push({ type: 'slice', start, end });
102
+ }
103
+ else {
104
+ // Integer index
105
+ const value = parseInt(content, 10);
106
+ if (isNaN(value)) {
107
+ throw new Error(`Invalid bracket expression: [${content}]`);
108
+ }
109
+ tokens.push({ type: 'index', value });
110
+ }
111
+ }
112
+ }
113
+ else if (expression[i] === '*') {
114
+ tokens.push({ type: 'wildcard' });
115
+ i++;
116
+ }
117
+ else if (isIdentStart(expression[i])) {
118
+ const start = i;
119
+ while (i < expression.length && isIdentChar(expression[i])) {
120
+ i++;
121
+ }
122
+ tokens.push({ type: 'property', name: expression.substring(start, i) });
123
+ }
124
+ else {
125
+ throw new Error(`Unexpected character '${expression[i]}' at position ${i} in: ${expression}`);
126
+ }
127
+ }
128
+ return tokens;
129
+ }
130
+ function isIdentStart(ch) {
131
+ return /[a-zA-Z_]/.test(ch);
132
+ }
133
+ function isIdentChar(ch) {
134
+ return /[a-zA-Z0-9_-]/.test(ch);
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // Filter expression parser
138
+ // ---------------------------------------------------------------------------
139
+ const COMPARISON_OPS = [
140
+ '!==',
141
+ '===',
142
+ '!=',
143
+ '==',
144
+ '<=',
145
+ '>=',
146
+ '<',
147
+ '>',
148
+ ];
149
+ function parseFilterCondition(raw) {
150
+ // Split on && (top-level only, respecting quotes)
151
+ const parts = splitOnLogicalAnd(raw);
152
+ if (parts.length > 1) {
153
+ return {
154
+ kind: 'and',
155
+ conditions: parts.map((p) => parseSingleCondition(p.trim())),
156
+ };
157
+ }
158
+ return parseSingleCondition(raw.trim());
159
+ }
160
+ function splitOnLogicalAnd(raw) {
161
+ const parts = [];
162
+ let current = '';
163
+ let inString = null;
164
+ let depth = 0;
165
+ for (let i = 0; i < raw.length; i++) {
166
+ const ch = raw[i];
167
+ if (inString) {
168
+ current += ch;
169
+ if (ch === inString && raw[i - 1] !== '\\') {
170
+ inString = null;
171
+ }
172
+ continue;
173
+ }
174
+ if (ch === '"' || ch === "'") {
175
+ inString = ch;
176
+ current += ch;
177
+ continue;
178
+ }
179
+ if (ch === '(') {
180
+ depth++;
181
+ current += ch;
182
+ continue;
183
+ }
184
+ if (ch === ')') {
185
+ depth--;
186
+ current += ch;
187
+ continue;
188
+ }
189
+ if (depth === 0 && ch === '&' && raw[i + 1] === '&') {
190
+ parts.push(current);
191
+ current = '';
192
+ i++; // skip second &
193
+ continue;
194
+ }
195
+ current += ch;
196
+ }
197
+ parts.push(current);
198
+ return parts;
199
+ }
200
+ function parseSingleCondition(expr) {
201
+ // Try to find a comparison operator
202
+ for (const op of COMPARISON_OPS) {
203
+ const idx = findOperatorIndex(expr, op);
204
+ if (idx !== -1) {
205
+ const left = expr.substring(0, idx).trim();
206
+ const right = expr.substring(idx + op.length).trim();
207
+ return {
208
+ kind: 'comparison',
209
+ left: parseOperand(left),
210
+ op,
211
+ right: parseOperand(right),
212
+ };
213
+ }
214
+ }
215
+ // No operator found — existence check
216
+ if (expr.startsWith('@.')) {
217
+ return { kind: 'existence', segments: expr.substring(2).split('.') };
218
+ }
219
+ throw new Error(`Cannot parse filter condition: ${expr}`);
220
+ }
221
+ function findOperatorIndex(expr, op) {
222
+ let inString = null;
223
+ for (let i = 0; i < expr.length - op.length + 1; i++) {
224
+ const ch = expr[i];
225
+ if (inString) {
226
+ if (ch === inString && expr[i - 1] !== '\\') {
227
+ inString = null;
228
+ }
229
+ continue;
230
+ }
231
+ if (ch === '"' || ch === "'") {
232
+ inString = ch;
233
+ continue;
234
+ }
235
+ if (expr.substring(i, i + op.length) === op) {
236
+ // For single-char ops (< > = !), make sure we're not part of a multi-char op
237
+ if (op.length === 1) {
238
+ const next = expr[i + 1];
239
+ if (next === '=' || (op === '!' && next === '=')) {
240
+ continue;
241
+ }
242
+ }
243
+ if (op.length === 2 && (op === '==' || op === '!=')) {
244
+ const next = expr[i + 2];
245
+ if (next === '=') {
246
+ continue;
247
+ }
248
+ }
249
+ return i;
250
+ }
251
+ }
252
+ return -1;
253
+ }
254
+ function parseOperand(raw) {
255
+ const trimmed = raw.trim();
256
+ // @.path
257
+ if (trimmed.startsWith('@.')) {
258
+ return { kind: 'path', segments: trimmed.substring(2).split('.') };
259
+ }
260
+ // String literal
261
+ if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
262
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
263
+ return { kind: 'literal', value: trimmed.slice(1, -1) };
264
+ }
265
+ // Boolean
266
+ if (trimmed === 'true') {
267
+ return { kind: 'literal', value: true };
268
+ }
269
+ if (trimmed === 'false') {
270
+ return { kind: 'literal', value: false };
271
+ }
272
+ // Null
273
+ if (trimmed === 'null') {
274
+ return { kind: 'literal', value: null };
275
+ }
276
+ // Number
277
+ const num = Number(trimmed);
278
+ if (!isNaN(num) && trimmed !== '') {
279
+ return { kind: 'literal', value: num };
280
+ }
281
+ throw new Error(`Cannot parse filter operand: ${raw}`);
282
+ }
283
+ // ---------------------------------------------------------------------------
284
+ // Parser: tokens → path segments
285
+ // ---------------------------------------------------------------------------
286
+ function parse(tokens) {
287
+ const segments = [];
288
+ let i = 0;
289
+ while (i < tokens.length) {
290
+ const tok = tokens[i];
291
+ switch (tok.type) {
292
+ case 'root':
293
+ segments.push({ type: 'root' });
294
+ i++;
295
+ break;
296
+ case 'dot':
297
+ // Next token should be a property or wildcard
298
+ i++;
299
+ break;
300
+ case 'dotdot': {
301
+ // Recursive descent — wrap the next meaningful segment
302
+ i++;
303
+ const next = tokens[i];
304
+ if (!next) {
305
+ throw new Error('Unexpected end after ..');
306
+ }
307
+ if (next.type === 'property') {
308
+ segments.push({
309
+ type: 'recursiveDescent',
310
+ target: { type: 'property', name: next.name },
311
+ });
312
+ }
313
+ else if (next.type === 'wildcard') {
314
+ segments.push({
315
+ type: 'recursiveDescent',
316
+ target: { type: 'wildcard' },
317
+ });
318
+ }
319
+ else if (next.type === 'filter') {
320
+ segments.push({
321
+ type: 'recursiveDescent',
322
+ target: {
323
+ type: 'filter',
324
+ condition: parseFilterCondition(next.raw),
325
+ },
326
+ });
327
+ }
328
+ else {
329
+ throw new Error(`Unexpected token after ..: ${next.type}`);
330
+ }
331
+ i++;
332
+ break;
333
+ }
334
+ case 'property':
335
+ segments.push({ type: 'property', name: tok.name });
336
+ i++;
337
+ break;
338
+ case 'index':
339
+ segments.push({ type: 'index', value: tok.value });
340
+ i++;
341
+ break;
342
+ case 'slice':
343
+ segments.push({ type: 'slice', start: tok.start, end: tok.end });
344
+ i++;
345
+ break;
346
+ case 'wildcard':
347
+ segments.push({ type: 'wildcard' });
348
+ i++;
349
+ break;
350
+ case 'filter': {
351
+ const condition = parseFilterCondition(tok.raw);
352
+ // Check if next token is an index — combine into filterThenIndex
353
+ if (i + 1 < tokens.length && tokens[i + 1].type === 'index') {
354
+ const idxTok = tokens[i + 1];
355
+ segments.push({
356
+ type: 'filterThenIndex',
357
+ condition,
358
+ index: idxTok.value,
359
+ });
360
+ i += 2;
361
+ }
362
+ else {
363
+ segments.push({ type: 'filter', condition });
364
+ i++;
365
+ }
366
+ break;
367
+ }
368
+ }
369
+ }
370
+ return segments;
371
+ }
372
+ // ---------------------------------------------------------------------------
373
+ // Evaluator
374
+ // ---------------------------------------------------------------------------
375
+ function evaluate(data, segments) {
376
+ let current = [];
377
+ for (const seg of segments) {
378
+ switch (seg.type) {
379
+ case 'root':
380
+ current = [data];
381
+ break;
382
+ case 'property':
383
+ current = applyProperty(current, seg.name);
384
+ break;
385
+ case 'index':
386
+ current = applyIndex(current, seg.value);
387
+ break;
388
+ case 'slice':
389
+ current = applySlice(current, seg.start, seg.end);
390
+ break;
391
+ case 'wildcard':
392
+ current = applyWildcard(current);
393
+ break;
394
+ case 'filter':
395
+ current = applyFilter(current, seg.condition);
396
+ break;
397
+ case 'filterThenIndex':
398
+ current = applyFilterThenIndex(current, seg.condition, seg.index);
399
+ break;
400
+ case 'recursiveDescent':
401
+ current = applyRecursiveDescent(current, seg.target);
402
+ break;
403
+ }
404
+ }
405
+ return current;
406
+ }
407
+ function applyProperty(nodes, name) {
408
+ const results = [];
409
+ for (const node of nodes) {
410
+ if (node !== null && typeof node === 'object' && !Array.isArray(node)) {
411
+ const val = node[name];
412
+ if (val !== undefined) {
413
+ results.push(val);
414
+ }
415
+ }
416
+ }
417
+ return results;
418
+ }
419
+ function applyIndex(nodes, index) {
420
+ const results = [];
421
+ for (const node of nodes) {
422
+ if (Array.isArray(node)) {
423
+ const actual = index < 0 ? node.length + index : index;
424
+ if (actual >= 0 && actual < node.length) {
425
+ results.push(node[actual]);
426
+ }
427
+ }
428
+ }
429
+ return results;
430
+ }
431
+ function applySlice(nodes, start, end) {
432
+ const results = [];
433
+ for (const node of nodes) {
434
+ if (Array.isArray(node)) {
435
+ const s = start !== undefined ? (start < 0 ? node.length + start : start) : 0;
436
+ const e = end !== undefined ? (end < 0 ? node.length + end : end) : node.length;
437
+ const sliced = node.slice(s, e);
438
+ for (const item of sliced) {
439
+ results.push(item);
440
+ }
441
+ }
442
+ }
443
+ return results;
444
+ }
445
+ function applyWildcard(nodes) {
446
+ const results = [];
447
+ for (const node of nodes) {
448
+ if (Array.isArray(node)) {
449
+ for (const item of node) {
450
+ results.push(item);
451
+ }
452
+ }
453
+ else if (node !== null && typeof node === 'object') {
454
+ for (const val of Object.values(node)) {
455
+ results.push(val);
456
+ }
457
+ }
458
+ }
459
+ return results;
460
+ }
461
+ function applyFilter(nodes, condition) {
462
+ const results = [];
463
+ for (const node of nodes) {
464
+ if (Array.isArray(node)) {
465
+ for (const item of node) {
466
+ if (evaluateCondition(item, condition)) {
467
+ results.push(item);
468
+ }
469
+ }
470
+ }
471
+ }
472
+ return results;
473
+ }
474
+ function applyFilterThenIndex(nodes, condition, index) {
475
+ const results = [];
476
+ for (const node of nodes) {
477
+ if (Array.isArray(node)) {
478
+ const filtered = node.filter((item) => evaluateCondition(item, condition));
479
+ const actual = index < 0 ? filtered.length + index : index;
480
+ if (actual >= 0 && actual < filtered.length) {
481
+ results.push(filtered[actual]);
482
+ }
483
+ }
484
+ }
485
+ return results;
486
+ }
487
+ function applyRecursiveDescent(nodes, target) {
488
+ // Collect all descendants from every current node
489
+ const allDescendants = [];
490
+ for (const node of nodes) {
491
+ collectDescendants(node, allDescendants);
492
+ }
493
+ // Apply the target segment to each descendant
494
+ switch (target.type) {
495
+ case 'property': {
496
+ const results = [];
497
+ for (const d of allDescendants) {
498
+ if (d !== null && typeof d === 'object' && !Array.isArray(d)) {
499
+ const val = d[target.name];
500
+ if (val !== undefined) {
501
+ results.push(val);
502
+ }
503
+ }
504
+ }
505
+ return results;
506
+ }
507
+ case 'wildcard': {
508
+ const results = [];
509
+ for (const d of allDescendants) {
510
+ if (Array.isArray(d)) {
511
+ for (const item of d) {
512
+ results.push(item);
513
+ }
514
+ }
515
+ }
516
+ return results;
517
+ }
518
+ case 'filter': {
519
+ const results = [];
520
+ for (const d of allDescendants) {
521
+ if (Array.isArray(d)) {
522
+ for (const item of d) {
523
+ if (evaluateCondition(item, target.condition)) {
524
+ results.push(item);
525
+ }
526
+ }
527
+ }
528
+ }
529
+ return results;
530
+ }
531
+ default:
532
+ throw new Error(`Unsupported recursive descent target: ${target.type}`);
533
+ }
534
+ }
535
+ function collectDescendants(node, out) {
536
+ out.push(node);
537
+ if (node !== null && typeof node === 'object') {
538
+ const values = Array.isArray(node)
539
+ ? node
540
+ : Object.values(node);
541
+ for (const val of values) {
542
+ collectDescendants(val, out);
543
+ }
544
+ }
545
+ }
546
+ // ---------------------------------------------------------------------------
547
+ // Filter condition evaluator
548
+ // ---------------------------------------------------------------------------
549
+ function evaluateCondition(node, condition) {
550
+ switch (condition.kind) {
551
+ case 'comparison': {
552
+ const left = resolveOperand(node, condition.left);
553
+ const right = resolveOperand(node, condition.right);
554
+ return compare(left, right, condition.op);
555
+ }
556
+ case 'and':
557
+ return condition.conditions.every((c) => evaluateCondition(node, c));
558
+ case 'existence': {
559
+ const val = resolvePathOnNode(node, condition.segments);
560
+ return val !== undefined;
561
+ }
562
+ }
563
+ }
564
+ function resolveOperand(node, operand) {
565
+ if (operand.kind === 'literal') {
566
+ return operand.value;
567
+ }
568
+ return resolvePathOnNode(node, operand.segments);
569
+ }
570
+ function resolvePathOnNode(node, segments) {
571
+ let current = node;
572
+ for (const seg of segments) {
573
+ if (current === null ||
574
+ current === undefined ||
575
+ typeof current !== 'object') {
576
+ return undefined;
577
+ }
578
+ current = current[seg];
579
+ }
580
+ return current;
581
+ }
582
+ function compare(left, right, op) {
583
+ switch (op) {
584
+ case '===':
585
+ return left === right;
586
+ case '!==':
587
+ return left !== right;
588
+ /* eslint-disable eqeqeq -- JSONPath == and != operators require loose equality */
589
+ case '==':
590
+ return left == right;
591
+ case '!=':
592
+ return left != right;
593
+ /* eslint-enable eqeqeq */
594
+ case '<':
595
+ return left < right;
596
+ case '<=':
597
+ return left <= right;
598
+ case '>':
599
+ return left > right;
600
+ case '>=':
601
+ return left >= right;
602
+ }
603
+ }
604
+ // ---------------------------------------------------------------------------
605
+ // Public API
606
+ // ---------------------------------------------------------------------------
607
+ /**
608
+ * Evaluate a JSONPath expression against a data object.
609
+ *
610
+ * Drop-in replacement for `jsonpath.query()` covering the subset of JSONPath
611
+ * used by this project. Uses no `eval()` or dynamic code execution.
612
+ *
613
+ * @param data - The data to query
614
+ * @param expression - A JSONPath expression (must start with $)
615
+ * @returns An array of matching values
616
+ */
617
+ function queryJsonPath(data, expression) {
618
+ const tokens = tokenize(expression);
619
+ const segments = parse(tokens);
620
+ return evaluate(data, segments);
621
+ }
622
+ //# sourceMappingURL=JsonPath.js.map