snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
# JSONPath
A filter plugin that enables [JSONPath](https://en.wikipedia.org/wiki/JSONPath)
extraction and filtering for JSON responses, making it easy to extract specific values
from complex JSON structures.
![Screenshot of JSONPath filtering](screenshot.png)
## Overview
This plugin provides JSONPath filtering for responses in Yaak. JSONPath is a query
language for JSON, similar to XPath for XML, that provides the ability to extract data
from JSON documents using a simple, expressive syntax. This is useful for working with
complex API responses where you need to only view a small subset of response data.
## How JSONPath Works
JSONPath uses a dot-notation syntax to navigate JSON structures:
- `$` - Root element
- `.` - Child element
- `..` - Recursive descent
- `*` - Wildcard
- `[]` - Array index or filter
## JSONPath Syntax Examples
### Basic Navigation
```
$.store.book[0].title # First book title
$.store.book[*].author # All book authors
$.store.book[-1] # Last book
$.store.book[0,1] # First two books
$.store.book[0:2] # First two books (slice)
```
### Filtering
```
$.store.book[?(@.price < 10)] # Books under $10
$.store.book[?(@.author == 'Tolkien')] # Books by Tolkien
$.store.book[?(@.category == 'fiction')] # Fiction books
```
### Recursive Search
```
$..author # All authors anywhere in the document
$..book[2] # Third book anywhere
$..price # All prices in the document
```
## Usage
1. Make an API request that returns JSON data
2. Below the response body, click the filter icon
3. Enter a JSONPath expression
4. View the extracted data in the results panel

View File

@@ -0,0 +1,24 @@
{
"name": "@yaak/filter-jsonpath",
"displayName": "JSONPath Filter",
"version": "0.1.0",
"private": true,
"description": "Filter JSON response data using JSONPath expressions",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/filter-jsonpath"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"test": "vp test --run tests"
},
"dependencies": {
"@yaakapp-internal/lib": "^1.0.0",
"jsonpath-plus": "^10.3.0"
},
"devDependencies": {
"@types/jsonpath": "^0.2.4"
}
}

View File

@@ -0,0 +1,39 @@
import type { PluginDefinition } from "@yaakapp/api";
import { jsonPathToSegments } from "@yaakapp-internal/lib/jsonPath";
import { JSONPath } from "jsonpath-plus";
export const plugin: PluginDefinition = {
filter: {
name: "JSONPath",
description: "Filter JSONPath",
onFilter(_ctx, args) {
const parsed = JSON.parse(args.payload);
try {
// jsonpath-plus doesn't decode JSON-quoted keys reliably and can treat
// their contents as operators. Only complete literal paths take this
// route; all other expressions retain the library's existing behavior.
const segments = args.filter.includes('["') ? jsonPathToSegments(args.filter) : null;
let filtered: unknown;
if (segments != null) {
let value: unknown = parsed;
for (const segment of segments) {
const key = segment.kind === "key" ? segment.key : segment.index;
if (value == null || !Object.prototype.hasOwnProperty.call(value, key)) {
return { content: "[]" };
}
value = (value as Record<string, unknown>)[key];
}
filtered = [value];
} else {
filtered = JSONPath({ path: args.filter, json: parsed });
}
return { content: JSON.stringify(filtered, null, 2) };
} catch (err) {
return {
content: "",
error: `Invalid filter: ${err instanceof Error ? err.message : String(err)}`,
};
}
},
},
};

View File

@@ -0,0 +1,130 @@
import { forceParsing } from "@codemirror/language";
import { EditorState } from "@codemirror/state";
import { jsonc } from "@shopify/lang-jsonc";
import { JSONPath } from "jsonpath-plus";
import { describe, expect, test } from "vite-plus/test";
import {
jsonPathSegmentsAt,
jsonPathToSegments,
segmentsToJsonPath,
} from "../../../apps/yaak-client/components/core/Editor/json/jsonPath";
import { plugin } from "../src";
async function filter(json: unknown, path: string) {
const result = await plugin.filter!.onFilter({} as never, {
payload: JSON.stringify(json),
filter: path,
mimeType: "application/json",
});
expect(result.error).toBeUndefined();
return JSON.parse(result.content);
}
/** Exercise the caret path and expression the breadcrumb button actually sends. */
function breadcrumb(json: unknown) {
const doc = JSON.stringify(json, null, 2);
const state = EditorState.create({ doc, extensions: [jsonc()] });
forceParsing({ state } as never, doc.length, 5000);
const segments = jsonPathSegmentsAt(state, doc.indexOf('"needle-value"') + 4);
expect(segments).not.toBeNull();
const path = segmentsToJsonPath(segments ?? []);
expect(jsonPathToSegments(path)).toEqual(segments);
return path;
}
describe("breadcrumb paths through the real filter plugin", () => {
test.each([
"identifier",
"a.b",
"space key",
"",
'quote"key',
"single'key",
"back\\slash",
"bracket]key",
"combined\"\\]'key",
"*",
"..",
"$",
"^",
"~",
"a,b",
"0:2",
"?(true)",
"(@.length-1)",
"@number()",
"`key",
"x)]y",
"x)']y",
"semi;colon",
"percent%@%key",
"line\nbreak\t\u0000",
"emoji 🤔",
"lone surrogate \ud800",
"__proto__",
"constructor",
])("selects the literal key %j, not an operator or expression", async (key) => {
const json = Object.fromEntries([
[key, "needle-value"],
["decoy", "wrong-value"],
]);
expect(await filter(json, breadcrumb(json))).toEqual(["needle-value"]);
});
test("nested unusual keys and arrays round-trip and filter at every crumb", async () => {
const inner = Object.fromEntries([["inner'\\]", { "": "needle-value" }]]);
const json = { outer: Object.fromEntries([['quote"key', ["decoy", inner]]]) };
const path = breadcrumb(json);
const segments = jsonPathToSegments(path)!;
expect(await filter(json, path)).toEqual(["needle-value"]);
expect(await filter(json, segmentsToJsonPath(segments, 4))).toEqual([{ "": "needle-value" }]);
expect(await filter(json, segmentsToJsonPath(segments, 3))).toEqual([inner]);
expect(await filter(json, segmentsToJsonPath(segments, 2))).toEqual([["decoy", inner]]);
expect(await filter(json, segmentsToJsonPath(segments, 1))).toEqual([json.outer]);
expect(await filter(json, segmentsToJsonPath(segments, 0))).toEqual([json]);
});
test("root arrays and quoted numeric object keys remain distinct", async () => {
const json = ["decoy", { "01": { "1": "needle-value" } }];
expect(await filter(json, breadcrumb(json))).toEqual(["needle-value"]);
expect(await filter(json, '$[1]["01"]["missing"]')).toEqual([]);
});
test.each([null, false, 0, "", [1, 2], { value: 3 }])(
"retains the matched value %j",
async (value) => {
expect(await filter({ 'quote"key': value }, '$["quote\\\"key"]')).toEqual([value]);
},
);
test("only follows own properties, while allowing actual prototype-named JSON keys", async () => {
const json = JSON.parse('{"a.b":{"__proto__":{"constructor":"own-value"}}}');
expect(await filter(json, '$["a.b"].__proto__.constructor')).toEqual(["own-value"]);
expect(await filter(json, '$["a.b"].constructor')).toEqual([]);
expect(await filter(json, '$["a.b"].__proto__.toString')).toEqual([]);
expect(await filter(json, '$["a.b"].__proto__.constructor.__proto__')).toEqual([]);
});
});
describe("existing JSONPath expressions", () => {
const json = { items: [{ id: 1 }, { id: 2 }, { id: 3 }], "space key": 4 };
test.each([
["$", [json]],
["$.items[1].id", [2]],
['$["space key"]', [4]],
["$['space key']", [4]],
["$.items[*].id", [1, 2, 3]],
["$..id", [1, 2, 3]],
["$.items[0:2].id", [1, 2]],
["$.items[0,2].id", [1, 3]],
["$.items[?(@.id > 1)].id", [2, 3]],
['$.items[?(@["id"] > 1)].id', [2, 3]],
["$.items[(@.length-1)].id", [3]],
['$["items"][*].id', [1, 2, 3]],
['$["items"].$', [json.items]],
["$.missing", []],
])("preserves %s", async (path, expected) => {
expect(await filter(json, path as string)).toEqual(expected);
expect(await filter(json, path as string)).toEqual(JSONPath({ path: path as string, json }));
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}