mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 04:35:40 +00:00
option to parse large bru files using a regex based approach (#5324)
This commit is contained in:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
42
packages/bruno-electron/src/utils/parse.js
Normal file
42
packages/bruno-electron/src/utils/parse.js
Normal 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 };
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -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: {}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user