diff --git a/package-lock.json b/package-lock.json
index 0c24a8003bb..fe0cca26fb6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20275,6 +20275,12 @@
"jiti": "bin/jiti.js"
}
},
+ "node_modules/jq-wasm": {
+ "version": "1.1.0-jq-1.8.1",
+ "resolved": "https://registry.npmjs.org/jq-wasm/-/jq-wasm-1.1.0-jq-1.8.1.tgz",
+ "integrity": "sha512-lWfu34lpDFIygOYcL5TzxhZIApDR9iR5XywcVoyUAZ6jlQrj8HKHOKeCcHgUm2dE9RVdbP3eqNAKGLuj+k4seQ==",
+ "license": "MIT"
+ },
"node_modules/js-base64": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz",
@@ -30193,6 +30199,7 @@
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
+ "jq-wasm": "^1.1.0-jq-1.8.1",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
diff --git a/packages/bruno-app/package.json b/packages/bruno-app/package.json
index e5c95f83f70..3e0025b8d0b 100644
--- a/packages/bruno-app/package.json
+++ b/packages/bruno-app/package.json
@@ -45,6 +45,7 @@
"i18next": "24.1.2",
"idb": "^7.0.0",
"immer": "^9.0.15",
+ "jq-wasm": "1.1.0-jq-1.8.1",
"js-yaml": "^4.1.0",
"jsesc": "^3.0.2",
"jshint": "^2.13.6",
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js
index 5320b419576..3776ea63015 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/QueryResultFilter/index.js
@@ -4,7 +4,7 @@ import { useRef } from 'react';
import { useState } from 'react';
import { Tooltip as ReactInfotip } from 'react-tooltip';
-const QueryResultFilter = ({ filter, onChange, mode }) => {
+const QueryResultFilter = ({ filter, onChange, mode, filterType, onFilterTypeChange, jqError }) => {
const inputRef = useRef(null);
const [isExpanded, toggleExpand] = useState(false);
@@ -19,9 +19,19 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
};
+ const handleFilterTypeChange = (type) => {
+ if (type === filterType) return;
+ onFilterTypeChange(type);
+ // Clear input when switching filter type
+ onChange({ target: { value: '' } });
+ if (inputRef?.current) {
+ inputRef.current.value = '';
+ }
+ };
+
const infotipText = useMemo(() => {
if (mode.includes('json')) {
- return 'Filter with JSONPath';
+ return filterType === 'jq' ? 'Filter with jq' : 'Filter with JSONPath';
}
if (mode.includes('xml')) {
@@ -29,11 +39,11 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
return null;
- }, [mode]);
+ }, [mode, filterType]);
const placeholderText = useMemo(() => {
if (mode.includes('json')) {
- return '$.store.books..author';
+ return filterType === 'jq' ? '.store.books[].author' : '$.store.books..author';
}
if (mode.includes('xml')) {
@@ -41,13 +51,29 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
}
return null;
- }, [mode]);
+ }, [mode, filterType]);
return (
{infotipText && !isExpanded &&
}
+ {isExpanded && mode.includes('json') && (
+
+
+
+
+ )}
{
autoCorrect="off"
autoCapitalize="off"
spellCheck="false"
- className={`block ml-14 p-2 py-1 transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
+ className={`block ${isExpanded && mode.includes('json') ? 'ml-0' : 'ml-14'} p-2 py-1 transition-all duration-200 ease-in-out border border-gray-300 rounded-md ${
isExpanded ? 'w-full opacity-100 pointer-events-auto' : 'w-[0] opacity-0'
}`}
onChange={onChange}
@@ -66,6 +92,9 @@ const QueryResultFilter = ({ filter, onChange, mode }) => {
{isExpanded ? : }
+ {isExpanded && jqError && (
+
{jqError}
+ )}
);
};
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js
index 9ed72277fd5..453a1408fb0 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/StyledWrapper.js
@@ -58,6 +58,55 @@ const StyledWrapper = styled.div`
outline: none;
}
}
+
+ .jq-error {
+ position: absolute;
+ bottom: 100%;
+ right: 0;
+ margin-bottom: 4px;
+ padding: 2px 8px;
+ font-size: 11px;
+ color: ${(props) => props.theme.colors.text.danger};
+ background-color: ${(props) => props.theme.background.base};
+ border: solid 1px ${(props) => props.theme.colors.text.danger};
+ border-radius: ${(props) => props.theme.border.radius.sm};
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+
+ .filter-type-toggle {
+ flex-shrink: 0;
+
+ .toggle-btn {
+ font-size: 11px;
+ padding: 2px 8px;
+ border: solid 1px ${(props) => props.theme.border.border2};
+ background-color: ${(props) => props.theme.background.mantle};
+ color: ${(props) => props.theme.colors.text.muted};
+ cursor: pointer;
+ white-space: nowrap;
+
+ &:first-child {
+ border-radius: 4px 0 0 4px;
+ }
+
+ &:last-child {
+ border-radius: 0 4px 4px 0;
+ margin-left: -1px;
+ }
+
+ &.active {
+ background-color: ${(props) => props.theme.background.base};
+ color: ${(props) => props.theme.colors.text.yellow};
+ border-color: ${(props) => props.theme.colors.text.yellow};
+ font-weight: 600;
+ position: relative;
+ z-index: 1;
+ }
+ }
+ }
}
`;
diff --git a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
index b6f40be58d2..438ed67d1b5 100644
--- a/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
+++ b/packages/bruno-app/src/components/ResponsePane/QueryResult/index.js
@@ -1,7 +1,8 @@
import { debounce } from 'lodash';
import { useTheme } from 'providers/Theme/index';
-import React, { useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
import { formatResponse, getContentType } from 'utils/common';
+import { runJqFilter } from 'utils/common/jq-service';
import { getDefaultResponseFormat, detectContentTypeFromBase64 } from 'utils/response';
import LargeResponseWarning from '../LargeResponseWarning';
import QueryResultFilter from './QueryResultFilter';
@@ -102,6 +103,9 @@ const QueryResult = ({
}) => {
const contentType = getContentType(headers);
const [filter, setFilter] = useState(null);
+ const [filterType, setFilterType] = useState('jsonpath');
+ const [jqResult, setJqResult] = useState(null);
+ const [jqError, setJqError] = useState(null);
const [showLargeResponse, setShowLargeResponse] = useState(false);
const { displayedTheme } = useTheme();
@@ -124,14 +128,46 @@ const QueryResult = ({
return detectContentTypeFromBase64(dataBuffer);
}, [dataBuffer, isLargeResponse]);
+ // Run jq filter asynchronously when filterType is 'jq'
+ useEffect(() => {
+ if (filterType !== 'jq' || !filter) {
+ setJqResult(null);
+ setJqError(null);
+ return;
+ }
+ let cancelled = false;
+ setJqResult(null);
+ setJqError(null);
+ runJqFilter(data, filter)
+ .then((result) => {
+ if (!cancelled) {
+ setJqResult(result);
+ setJqError(null);
+ }
+ })
+ .catch((err) => {
+ if (!cancelled) {
+ setJqResult(null);
+ setJqError(err.message);
+ }
+ });
+ return () => { cancelled = true; };
+ }, [data, filter, filterType]);
+
const formattedData = useMemo(
() => {
if (isLargeResponse && !showLargeResponse) {
return '';
}
- return formatResponse(data, dataBuffer, selectedFormat, filter);
+ // For jq mode with an active filter, use the async result
+ if (filterType === 'jq' && filter) {
+ return jqResult ?? formatResponse(data, dataBuffer, selectedFormat, null);
+ }
+ // For JSONPath mode, pass filter to formatResponse as before
+ const jsonPathFilter = filterType === 'jsonpath' ? filter : null;
+ return formatResponse(data, dataBuffer, selectedFormat, jsonPathFilter);
},
- [data, dataBuffer, selectedFormat, filter, isLargeResponse, showLargeResponse]
+ [data, dataBuffer, selectedFormat, filter, filterType, jqResult, isLargeResponse, showLargeResponse]
);
const debouncedResultFilterOnChange = debounce((e) => {
@@ -213,7 +249,14 @@ const QueryResult = ({
/>
{queryFilterEnabled && (
-
+
)}
diff --git a/packages/bruno-app/src/utils/common/jq-service.js b/packages/bruno-app/src/utils/common/jq-service.js
new file mode 100644
index 00000000000..23ad966a18f
--- /dev/null
+++ b/packages/bruno-app/src/utils/common/jq-service.js
@@ -0,0 +1,14 @@
+import * as jq from 'jq-wasm';
+
+export async function runJqFilter(data, filter) {
+ try {
+ const result = await jq.raw(data, filter);
+ if (result.exitCode !== 0) {
+ throw new Error(result.stderr || 'jq error');
+ }
+ return result.stdout;
+ } catch (e) {
+ console.warn('Could not apply jq filter:', e.message);
+ throw e;
+ }
+}
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 60612751365..d0a52011e22 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -72,7 +72,7 @@ const contentSecurityPolicy = [
'connect-src \'self\' https://*.posthog.com',
'font-src \'self\' https: data:;',
'frame-src data:',
- 'script-src \'self\' data:',
+ 'script-src \'self\' data: \'wasm-unsafe-eval\'',
// this has been commented out to make oauth2 work
// "form-action 'none'",
// we make an exception and allow http for images so that