option to parse large bru files using a regex based approach (#5324)

This commit is contained in:
lohit
2025-08-19 15:24:23 +05:30
committed by GitHub
parent 77c96c4821
commit 146c8462ea
11 changed files with 370 additions and 31 deletions

View File

@@ -1,16 +1,13 @@
import { IconLoader2, IconFile, IconAlertTriangle } from '@tabler/icons';
import { loadRequest, loadRequestViaWorker } from 'providers/ReduxStore/slices/collections/actions';
import { loadLargeRequest } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch } from 'react-redux';
import StyledWrapper from './StyledWrapper';
const RequestNotLoaded = ({ collection, item }) => {
const dispatch = useDispatch();
const handleLoadRequestViaWorker = () => {
!item?.loading && dispatch(loadRequestViaWorker({ collectionUid: collection?.uid, pathname: item?.pathname }));
}
const handleLoadRequest = () => {
!item?.loading && dispatch(loadRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
const handleLoadLargeRequest = () => {
!item?.loading && dispatch(loadLargeRequest({ collectionUid: collection?.uid, pathname: item?.pathname }));
}
return <StyledWrapper>
@@ -44,23 +41,14 @@ const RequestNotLoaded = ({ collection, item }) => {
<IconAlertTriangle size={16} className="text-yellow-500" />
<span>The request wasn't loaded due to its large size. Please try again with the following options:</span>
</div>
<div className='flex flex-row mt-6 gap-2 items-center w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequestViaWorker}
>
Load in background
</button>
<p>(Runs in background)</p>
</div>
<div className='flex flex-row mt-6 items-center gap-2 w-full'>
<button
className={`submit btn btn-sm btn-secondary w-fit h-fit flex flex-row gap-2 ${item?.loading? 'opacity-50 cursor-blocked': ''}`}
onClick={handleLoadRequest}
onClick={handleLoadLargeRequest}
>
Force load
Load Request
</button>
<p>(May cause the app to freeze temporarily while it runs)</p>
<p>(Uses a regex based parsing approach)</p>
</div>
</div>
)}

View File

@@ -1361,6 +1361,7 @@ export const clearOauth2Cache = (payload) => async (dispatch, getState) => {
});
};
// todo: could be removed
export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
@@ -1368,6 +1369,7 @@ export const loadRequestViaWorker = ({ collectionUid, pathname }) => (dispatch,
});
};
// todo: could be removed
export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
@@ -1375,6 +1377,13 @@ export const loadRequest = ({ collectionUid, pathname }) => (dispatch, getState)
});
};
export const loadLargeRequest = ({ collectionUid, pathname }) => (dispatch, getState) => {
return new Promise(async (resolve, reject) => {
const { ipcRenderer } = window;
ipcRenderer.invoke('renderer:load-large-request', { collectionUid, pathname }).then(resolve).catch(reject);
});
};
export const mountCollection = ({ collectionUid, collectionPathname, brunoConfig }) => (dispatch, getState) => {
dispatch(updateCollectionMountStatus({ collectionUid, mountStatus: 'mounting' }));
return new Promise(async (resolve, reject) => {

View File

@@ -20,6 +20,7 @@ const { setBrunoConfig } = require('../store/bruno-config');
const EnvironmentSecretsStore = require('../store/env-secrets');
const UiStateSnapshot = require('../store/ui-state-snapshot');
const { parseBruFileMeta, hydrateRequestWithUuid } = require('../utils/collection');
const { parseLargeRequestWithRedaction } = require('../utils/parse');
const MAX_FILE_SIZE = 2.5 * 1024 * 1024;
@@ -344,11 +345,17 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
let seq;
const folderBruFilePath = path.join(pathname, `folder.bru`);
if (fs.existsSync(folderBruFilePath)) {
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
let folderBruData = await parseFolder(folderBruFileContent);
name = folderBruData?.meta?.name || name;
seq = folderBruData?.meta?.seq;
try {
if (fs.existsSync(folderBruFilePath)) {
let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8');
let folderBruData = await parseFolder(folderBruFileContent);
name = folderBruData?.meta?.name || name;
seq = folderBruData?.meta?.seq;
}
}
catch(error) {
console.error('Error occured while parsing folder.bru file!');
console.error(error);
}
const directory = {
@@ -462,10 +469,20 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
};
const bru = fs.readFileSync(pathname, 'utf8');
file.data = await parseRequest(bru);
const fileStats = fs.statSync(pathname);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
if (fileStats.size >= MAX_FILE_SIZE) {
const parsedData = await parseLargeRequestWithRedaction(bru);
file.data = parsedData;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
} else {
file.data = await parseRequest(bru);
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'change', file);
}
} catch (err) {
console.error(err);
}

View File

@@ -19,6 +19,7 @@ const {
} = require('@usebruno/filestore');
const brunoConverters = require('@usebruno/converters');
const { postmanToBruno } = brunoConverters;
const { parseLargeRequestWithRedaction } = require('../utils/parse');
const {
writeFile,
@@ -1057,6 +1058,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
// todo: could be removed
ipcMain.handle('renderer:load-request-via-worker', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
@@ -1094,7 +1096,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseRequest(parseBruFileMeta(bruContent));
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.partial = true;
file.loading = false;
@@ -1132,6 +1134,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
// todo: could be removed
ipcMain.handle('renderer:load-request', async (event, { collectionUid, pathname }) => {
let fileStats;
try {
@@ -1145,7 +1148,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseRequest(parseBruFileMeta(bruContent));
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.loading = true;
file.partial = true;
@@ -1169,7 +1172,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
};
let bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseRequest(parseBruFileMeta(bruContent));
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.partial = true;
file.loading = false;
@@ -1181,6 +1184,56 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
ipcMain.handle('renderer:load-large-request', async (event, { collectionUid, pathname }) => {
let fileStats;
if (!hasBruExtension(pathname)) {
return;
}
const file = {
meta: {
collectionUid,
pathname,
name: path.basename(pathname)
}
};
try {
fileStats = fs.statSync(pathname);
const bruContent = fs.readFileSync(pathname, 'utf8');
const metaJson = parseBruFileMeta(bruContent);
file.data = metaJson;
file.partial = false;
file.loading = true;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
try {
const parsedData = await parseLargeRequestWithRedaction(bruContent);
file.data = parsedData;
file.loading = false;
file.partial = false;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (parseError) {
file.data = metaJson;
file.partial = true;
file.loading = false;
file.size = sizeInMB(fileStats?.size);
hydrateRequestWithUuid(file.data, pathname);
await mainWindow.webContents.send('main:collection-tree-updated', 'addFile', file);
throw parseError;
}
} catch (error) {
return Promise.reject(error);
}
});
ipcMain.handle('renderer:mount-collection', async (event, { collectionUid, collectionPathname, brunoConfig }) => {
const {
size,

View File

@@ -0,0 +1,42 @@
const { parseRequestAndRedactBody, parseRequestViaWorker } = require('@usebruno/filestore');
/**
* Parses a large BRU request string by redacting body blocks, parsing the remainder,
* and then reinserting extracted body content into the parsed structure.
* @param {string} bruContent
* @returns {Promise<any>} parsed request JSON
*/
async function parseLargeRequestWithRedaction(bruContent) {
const { bruFileStringWithRedactedBody, extractedBodyContent } = parseRequestAndRedactBody(bruContent);
const parsedData = await parseRequestViaWorker(bruFileStringWithRedactedBody);
if (!parsedData.request) {
parsedData.request = {};
}
if (!parsedData.request.body) {
parsedData.request.body = {};
}
if (extractedBodyContent.json) {
parsedData.request.body.json = extractedBodyContent.json;
}
if (extractedBodyContent.text) {
parsedData.request.body.text = extractedBodyContent.text;
}
if (extractedBodyContent.xml) {
parsedData.request.body.xml = extractedBodyContent.xml;
}
if (extractedBodyContent.sparql) {
parsedData.request.body.sparql = extractedBodyContent.sparql;
}
if (extractedBodyContent.graphql) {
if (!parsedData.request.body.graphql) {
parsedData.request.body.graphql = {};
}
parsedData.request.body.graphql.query = extractedBodyContent.graphql;
}
return parsedData;
}
module.exports = { parseLargeRequestWithRedaction };

View File

@@ -47,8 +47,8 @@ export const bruRequestToJson = (data: string | any, parsed: boolean = false): a
transformedJson.request.body.mode = _.get(json, 'http.body', 'none');
return transformedJson;
} catch (e) {
return Promise.reject(e);
} catch (error) {
throw error;
}
};

View File

@@ -0,0 +1,66 @@
meta {
name: echo request
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: json
auth: none
}
body:json {
{
"hello": "world"
}
}
body:text {
This is a text body
}
body:xml {
<xml>
<name>John</name>
<age>30</age>
</xml>
}
body:sparql {
SELECT * WHERE {
?subject ?predicate ?object .
}
LIMIT 10
}
body:graphql {
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
}
body:form-urlencoded {
apikey: secret
numbers: +91998877665
~message: hello
}
body:multipart-form {
apikey: secret
numbers: +91998877665
~message: hello
}
body:file {
file: @file(path/to/file.json) @contentType(application/json)
file: @file(path/to/file.json) @contentType(application/json)
~file: @file(path/to/file2.json) @contentType(application/json)
}
body:graphql:vars {
{
"limit": 5
}
}

View File

@@ -0,0 +1,35 @@
meta {
name: echo request
type: http
seq: 1
}
post {
url: https://echo.usebruno.com
body: json
auth: none
}
body:form-urlencoded {
apikey: secret
numbers: +91998877665
~message: hello
}
body:multipart-form {
apikey: secret
numbers: +91998877665
~message: hello
}
body:file {
file: @file(path/to/file.json) @contentType(application/json)
file: @file(path/to/file.json) @contentType(application/json)
~file: @file(path/to/file2.json) @contentType(application/json)
}
body:graphql:vars {
{
"limit": 5
}
}

View File

@@ -0,0 +1,44 @@
const fs = require('node:fs');
const path = require('node:path');
const { bruRequestParseAndRedactBodyData } = require("../utils/request-parse-and-redact-body-data");
describe("parse and redact body data", () => {
it("should redact body blocks from the bru file string", () => {
const fixturesPath = `/fixtures/request-parse-and-redact-body-data`;
const inputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './input.bru'), 'utf8');
const expectedOutputBruString = fs.readFileSync(path.join(__dirname, fixturesPath, './output.bru'), 'utf8');
const res = bruRequestParseAndRedactBodyData(inputBruString);
expect(res.bruFileStringWithRedactedBody).toBe(expectedOutputBruString);
expect(res.extractedBodyContent).toEqual({
graphql: `
{
launchesPast {
launch_site {
site_name
}
launch_success
}
}
`.trim(),
json: `
{
"hello": "world"
}
`.trim(),
sparql: `
SELECT * WHERE {
?subject ?predicate ?object .
}
LIMIT 10
`.trim(),
text: `This is a text body`,
xml: `
<xml>
<name>John</name>
<age>30</age>
</xml>
`.trim()
})
});
});

View File

@@ -0,0 +1,77 @@
/**
* Parses a .bru file and extracts body content while redacting it from the main content
* @param {string} bruFileContent - The raw content of the .bru file
* @returns {Object} Object containing redacted file content and extracted body data
*/
export const bruRequestParseAndRedactBodyData = (bruFileContent: string) => {
try {
// Define the patterns that indicate the start of different body types
const bodyTypePatterns = [
"body:json {",
"body:text {",
"body:xml {",
"body:sparql {",
"body:graphql {"
];
// Normalize line endings to LF
bruFileContent = (bruFileContent || '').replace(/\r\n/g, '\n');
const EOL = `\n`;
/**
* Removes the leading 2-space indentation from each line of a string
* @param {string} indentedString - The string with leading spaces to remove
* @returns {string} The string with indentation removed
*/
const removeLeadingIndentation = (indentedString: string) => {
if (!indentedString || !indentedString.length) {
return indentedString || '';
}
return indentedString
.split(EOL)
.map((line) => line.replace(/^ /, ''))
.join(EOL);
};
// Split the file content into blocks
let fileContentBlocks = bruFileContent.split(`${EOL}}${EOL}`);
fileContentBlocks = fileContentBlocks.filter(Boolean).map(_ => _.trim());
// Extract body blocks and their content
const extractedBodyBlocks = fileContentBlocks
.filter(block => bodyTypePatterns.some(pattern => block.startsWith(pattern)))
.reduce((bodyContentMap: Record<string, string>, bodyBlock) => {
// Extract the body type (json, text, xml, etc.) from the first line
const firstLine = bodyBlock.split(EOL)[0];
const bodyType = firstLine.split(`body:`)[1].split(/\s/)[0];
// Extract the body content (everything between the opening and closing braces)
const bodyContentLines = bodyBlock.split(EOL).slice(1);
const rawBodyContent = bodyContentLines.join(EOL);
// Remove indentation from the body content
const cleanBodyContent = removeLeadingIndentation(rawBodyContent);
bodyContentMap[bodyType] = cleanBodyContent;
return bodyContentMap;
}, {});
// Filter out body blocks to get the remaining file content
const fileContentWithoutBodyBlocks = fileContentBlocks.filter(block =>
!bodyTypePatterns.some(pattern => block.startsWith(pattern))
);
return {
bruFileStringWithRedactedBody: fileContentWithoutBodyBlocks.join(`${EOL}}${EOL}${EOL}`).concat(`${EOL}}${EOL}`),
extractedBodyContent: extractedBodyBlocks
};
} catch (error) {
console.error('Error parsing and redacting body data:', error);
return {
bruFileStringWithRedactedBody: bruFileContent,
extractedBodyContent: {}
};
}
};

View File

@@ -15,6 +15,7 @@ import {
ParsedCollection,
ParsedEnvironment
} from './types';
import { bruRequestParseAndRedactBodyData } from './formats/bru/utils/request-parse-and-redact-body-data';
export const parseRequest = (content: string, options: ParseOptions = { format: 'bru' }): any => {
if (options.format === 'bru') {
@@ -23,6 +24,13 @@ export const parseRequest = (content: string, options: ParseOptions = { format:
throw new Error(`Unsupported format: ${options.format}`);
};
export const parseRequestAndRedactBody = (content: string, options: ParseOptions = { format: 'bru' }): any => {
if (options.format === 'bru') {
return bruRequestParseAndRedactBodyData(content);
}
throw new Error(`Unsupported format: ${options.format}`);
};
export const stringifyRequest = (requestObj: ParsedRequest, options: StringifyOptions = { format: 'bru' }): string => {
if (options.format === 'bru') {
return jsonRequestToBru(requestObj);