Merge branch 'main' into fix/remove-jsonbigint-from-cli

This commit is contained in:
lohit 2024-12-15 18:00:38 +05:30 committed by GitHub
commit 6b6fc9a3dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 7365 additions and 1779 deletions

7653
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
{ {
"presets": ["next/babel"], "presets": ["@babel/preset-env"],
"plugins": [["styled-components", { "ssr": true }]] "plugins": [["styled-components", { "ssr": true }]]
} }

View File

@ -31,6 +31,6 @@ yarn-error.log*
# next.js # next.js
.next/ .next/
out/ dist/
.env .env

View File

@ -1,22 +0,0 @@
module.exports = {
output: 'export',
reactStrictMode: false,
publicRuntimeConfig: {
CI: process.env.CI,
PLAYWRIGHT: process.env.PLAYWRIGHT,
ENV: process.env.ENV
},
webpack: (config, { isServer }) => {
// Fixes npm packages that depend on `fs` module
if (!isServer) {
config.resolve.fallback.fs = false;
}
Object.defineProperty(config, 'devtool', {
get() {
return 'source-map';
},
set() {},
});
return config;
},
};

View File

@ -3,15 +3,15 @@
"version": "0.3.0", "version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "cross-env ENV=dev next dev -p 3000", "dev": "rsbuild dev",
"build": "next build", "build": "rsbuild build -m production",
"start": "next start", "preview": "rsbuild preview",
"lint": "next lint",
"test": "jest", "test": "jest",
"test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"", "test:prettier": "prettier --check \"./src/**/*.{js,jsx,json,ts,tsx}\"",
"prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\"" "prettier": "prettier --write \"./src/**/*.{js,jsx,json,ts,tsx}\""
}, },
"dependencies": { "dependencies": {
"@babel/preset-env": "^7.26.0",
"@fontsource/inter": "^5.0.15", "@fontsource/inter": "^5.0.15",
"@prantlf/jsonlint": "^16.0.0", "@prantlf/jsonlint": "^16.0.0",
"@reduxjs/toolkit": "^1.8.0", "@reduxjs/toolkit": "^1.8.0",
@ -49,7 +49,6 @@
"markdown-it-replace-link": "^1.2.0", "markdown-it-replace-link": "^1.2.0",
"mousetrap": "^1.6.5", "mousetrap": "^1.6.5",
"nanoid": "3.3.4", "nanoid": "3.3.4",
"next": "14.2.16",
"path": "^0.12.7", "path": "^0.12.7",
"pdfjs-dist": "4.4.168", "pdfjs-dist": "4.4.168",
"platform": "^1.3.6", "platform": "^1.3.6",
@ -57,17 +56,17 @@
"prettier": "^2.7.1", "prettier": "^2.7.1",
"qs": "^6.11.0", "qs": "^6.11.0",
"query-string": "^7.0.1", "query-string": "^7.0.1",
"react": "18.2.0", "react": "19.0.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "18.2.0", "react-dom": "19.0.0",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-i18next": "^15.0.1", "react-i18next": "^15.0.1",
"react-inspector": "^6.0.2", "react-inspector": "^6.0.2",
"react-pdf": "9.1.1", "react-pdf": "9.1.1",
"react-player": "^2.16.0", "react-player": "^2.16.0",
"react-redux": "^7.2.6", "react-redux": "^7.2.9",
"react-tooltip": "^5.5.2", "react-tooltip": "^5.5.2",
"sass": "^1.46.0", "sass": "^1.46.0",
"strip-json-comments": "^5.0.1", "strip-json-comments": "^5.0.1",
@ -79,13 +78,14 @@
"yup": "^0.32.11" "yup": "^0.32.11"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.0", "@rsbuild/core": "^1.1.2",
"@babel/plugin-transform-spread": "^7.16.7", "@rsbuild/plugin-babel": "^1.0.3",
"@babel/preset-env": "^7.16.4", "@rsbuild/plugin-node-polyfill": "^1.2.0",
"@babel/preset-react": "^7.16.0", "@rsbuild/plugin-react": "^1.0.7",
"@babel/runtime": "^7.16.3", "@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"babel-loader": "^8.2.3", "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",

View File

@ -0,0 +1,27 @@
import { defineConfig } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
import { pluginBabel } from '@rsbuild/plugin-babel';
import { pluginStyledComponents } from '@rsbuild/plugin-styled-components';
import { pluginSass } from '@rsbuild/plugin-sass';
import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill'
export default defineConfig({
plugins: [
pluginNodePolyfill(),
pluginReact(),
pluginStyledComponents(),
pluginSass(),
pluginBabel({
include: /\.(?:js|jsx|tsx)$/,
babelLoaderOptions(opts) {
opts.plugins?.unshift('babel-plugin-react-compiler');
}
})
],
source: {
tsconfigPath: './jsconfig.json', // Specifies the path to the JavaScript/TypeScript configuration file
},
html: {
title: 'Bruno'
},
});

View File

@ -11,7 +11,6 @@ const CreateEnvironment = ({ collection, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(); const inputRef = useRef();
// todo: Add this to global env too.
const validateEnvironmentName = (name) => { const validateEnvironmentName = (name) => {
return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim()); return !collection?.environments?.some((env) => env?.name?.toLowerCase().trim() === name?.toLowerCase().trim());
}; };

View File

@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.editing-mode {
cursor: pointer;
color: ${(props) => props.theme.colors.text.yellow};
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,63 @@
import 'github-markdown-css/github-markdown.css';
import get from 'lodash/get';
import { updateFolderDocs } from 'providers/ReduxStore/slices/collections';
import { useTheme } from 'providers/Theme';
import { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { saveFolderRoot } from 'providers/ReduxStore/slices/collections/actions';
import Markdown from 'components/MarkDown';
import CodeEditor from 'components/CodeEditor';
import StyledWrapper from './StyledWrapper';
const Documentation = ({ collection, folder }) => {
const dispatch = useDispatch();
const { displayedTheme } = useTheme();
const preferences = useSelector((state) => state.app.preferences);
const [isEditing, setIsEditing] = useState(false);
const docs = get(folder, 'root.docs', '');
const toggleViewMode = () => {
setIsEditing((prev) => !prev);
};
const onEdit = (value) => {
dispatch(
updateFolderDocs({
folderUid: folder.uid,
collectionUid: collection.uid,
docs: value
})
);
};
const onSave = () => dispatch(saveFolderRoot(collection.uid, folder.uid));
if (!folder) {
return null;
}
return (
<StyledWrapper className="flex flex-col gap-y-1 h-full w-full relative">
<div className="editing-mode" role="tab" onClick={toggleViewMode}>
{isEditing ? 'Preview' : 'Edit'}
</div>
{isEditing ? (
<CodeEditor
collection={collection}
theme={displayedTheme}
font={get(preferences, 'font.codeFont', 'default')}
fontSize={get(preferences, 'font.codeFontSize')}
value={docs || ''}
onEdit={onEdit}
onSave={onSave}
mode="application/text"
/>
) : (
<Markdown collectionPath={collection.pathname} onDoubleClick={toggleViewMode} content={docs} />
)}
</StyledWrapper>
);
};
export default Documentation;

View File

@ -7,6 +7,7 @@ import Script from './Script';
import Tests from './Tests'; import Tests from './Tests';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import Vars from './Vars'; import Vars from './Vars';
import Documentation from './Documentation';
import DotIcon from 'components/Icons/Dot'; import DotIcon from 'components/Icons/Dot';
const ContentIndicator = () => { const ContentIndicator = () => {
@ -60,6 +61,9 @@ const FolderSettings = ({ collection, folder }) => {
case 'vars': { case 'vars': {
return <Vars collection={collection} folder={folder} />; return <Vars collection={collection} folder={folder} />;
} }
case 'docs': {
return <Documentation collection={collection} folder={folder} />;
}
} }
}; };
@ -89,6 +93,9 @@ const FolderSettings = ({ collection, folder }) => {
Vars Vars
{activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>} {activeVarsCount > 0 && <sup className="ml-1 font-medium">{activeVarsCount}</sup>}
</div> </div>
<div className={getTabClassname('docs')} role="tab" onClick={() => setTab('docs')}>
Docs
</div>
</div> </div>
<section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section> <section className={`flex mt-4 h-full`}>{getTabPanel(tab)}</section>
</div> </div>

View File

@ -2,12 +2,19 @@ import React, { useEffect, useRef } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { useDispatch } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import Portal from 'components/Portal'; import Portal from 'components/Portal';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments'; import { addGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
const CreateEnvironment = ({ onClose }) => { const CreateEnvironment = ({ onClose }) => {
const globalEnvs = useSelector((state) => state?.globalEnvironments?.globalEnvironments);
const validateEnvironmentName = (name) => {
const trimmedName = name?.toLowerCase().trim();
return globalEnvs.every((env) => env?.name?.toLowerCase().trim() !== trimmedName);
};
const dispatch = useDispatch(); const dispatch = useDispatch();
const inputRef = useRef(); const inputRef = useRef();
const formik = useFormik({ const formik = useFormik({
@ -17,9 +24,10 @@ const CreateEnvironment = ({ onClose }) => {
}, },
validationSchema: Yup.object({ validationSchema: Yup.object({
name: Yup.string() name: Yup.string()
.min(1, 'must be at least 1 character') .min(1, 'Must be at least 1 character')
.max(50, 'must be 50 characters or less') .max(50, 'Must be 50 characters or less')
.required('name is required') .required('Name is required')
.test('duplicate-name', 'Global Environment already exists', validateEnvironmentName)
}), }),
onSubmit: (values) => { onSubmit: (values) => {
dispatch(addGlobalEnvironment({ name: values.name })) dispatch(addGlobalEnvironment({ name: values.name }))

View File

@ -1,8 +1,6 @@
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
function Portal({ children, wrapperId }) { function Portal({ children }) {
wrapperId = wrapperId || 'bruno-app-body'; return createPortal(children, document.body);
return createPortal(children, document.getElementById(wrapperId));
} }
export default Portal; export default Portal;

View File

@ -20,6 +20,7 @@ import React from 'react';
* endsWith : ends with * endsWith : ends with
* between : between * between : between
* isEmpty : is empty * isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null * isNull : is null
* isUndefined : is undefined * isUndefined : is undefined
* isDefined : is defined * isDefined : is defined
@ -51,6 +52,7 @@ const AssertionOperator = ({ operator, onChange }) => {
'endsWith', 'endsWith',
'between', 'between',
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',

View File

@ -24,6 +24,7 @@ import { useTheme } from 'providers/Theme';
* endsWith : ends with * endsWith : ends with
* between : between * between : between
* isEmpty : is empty * isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null * isNull : is null
* isUndefined : is undefined * isUndefined : is undefined
* isDefined : is defined * isDefined : is defined
@ -61,6 +62,7 @@ const parseAssertionOperator = (str = '') => {
'endsWith', 'endsWith',
'between', 'between',
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -75,6 +77,7 @@ const parseAssertionOperator = (str = '') => {
const unaryOperators = [ const unaryOperators = [
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -113,6 +116,7 @@ const parseAssertionOperator = (str = '') => {
const isUnaryOperator = (operator) => { const isUnaryOperator = (operator) => {
const unaryOperators = [ const unaryOperators = [
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',

View File

@ -70,7 +70,7 @@ const QueryUrl = ({ item, collection, handleRun }) => {
const handleGenerateCode = (e) => { const handleGenerateCode = (e) => {
e.stopPropagation(); e.stopPropagation();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) { if (item?.request?.url !== '' || (item.draft?.request?.url !== undefined && item.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true); setGenerateCodeItemModalOpen(true);
} else { } else {
toast.error('URL is required'); toast.error('URL is required');

View File

@ -20,7 +20,7 @@ import { DocExplorer } from '@usebruno/graphql-docs';
import StyledWrapper from './StyledWrapper'; import StyledWrapper from './StyledWrapper';
import SecuritySettings from 'components/SecuritySettings'; import SecuritySettings from 'components/SecuritySettings';
import FolderSettings from 'components/FolderSettings'; import FolderSettings from 'components/FolderSettings';
import { getGlobalEnvironmentVariables } from 'utils/collections/index'; import { getGlobalEnvironmentVariables, getGlobalEnvironmentVariablesMasked } from 'utils/collections/index';
import { produce } from 'immer'; import { produce } from 'immer';
const MIN_LEFT_PANE_WIDTH = 300; const MIN_LEFT_PANE_WIDTH = 300;
@ -45,7 +45,9 @@ const RequestTabPanel = () => {
if (collection) { if (collection) {
// add selected global env variables to the collection object // add selected global env variables to the collection object
const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid }); const globalEnvironmentVariables = getGlobalEnvironmentVariables({ globalEnvironments, activeGlobalEnvironmentUid });
const globalEnvSecrets = getGlobalEnvironmentVariablesMasked({ globalEnvironments, activeGlobalEnvironmentUid });
collection.globalEnvironmentVariables = globalEnvironmentVariables; collection.globalEnvironmentVariables = globalEnvironmentVariables;
collection.globalEnvSecrets = globalEnvSecrets;
} }
}); });

View File

@ -12,10 +12,15 @@ const DeleteCollectionItem = ({ onClose, item, collection }) => {
const isFolder = isItemAFolder(item); const isFolder = isItemAFolder(item);
const onConfirm = () => { const onConfirm = () => {
dispatch(deleteItem(item.uid, collection.uid)).then(() => { dispatch(deleteItem(item.uid, collection.uid)).then(() => {
if (isFolder) { if (isFolder) {
// close all tabs that belong to the folder
// including the folder itself and its children
const tabUids = [...recursivelyGetAllItemUids(item.items), item.uid]
dispatch( dispatch(
closeTabs({ closeTabs({
tabUids: recursivelyGetAllItemUids(item.items) tabUids: tabUids
}) })
); );
} else { } else {

View File

@ -6,6 +6,7 @@ import { useDispatch } from 'react-redux';
import { isItemAFolder } from 'utils/tabs'; import { isItemAFolder } from 'utils/tabs';
import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions'; import { renameItem, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { closeTabs } from 'providers/ReduxStore/slices/tabs';
const RenameCollectionItem = ({ collection, item, onClose }) => { const RenameCollectionItem = ({ collection, item, onClose }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -33,7 +34,8 @@ const RenameCollectionItem = ({ collection, item, onClose }) => {
} }
dispatch(renameItem(values.name, item.uid, collection.uid)) dispatch(renameItem(values.name, item.uid, collection.uid))
.then(() => { .then(() => {
toast.success('Request renamed'); dispatch(closeTabs({ tabUids: [item.uid] }));
toast.success(isFolder ? 'Folder renamed' : 'Request renamed');
onClose(); onClose();
}) })
.catch((err) => { .catch((err) => {

View File

@ -183,7 +183,7 @@ const CollectionItem = ({ item, collection, searchText }) => {
const handleGenerateCode = (e) => { const handleGenerateCode = (e) => {
e.stopPropagation(); e.stopPropagation();
dropdownTippyRef.current.hide(); dropdownTippyRef.current.hide();
if (item.request.url !== '' || (item.draft?.request.url !== undefined && item.draft?.request.url !== '')) { if (item?.request?.url !== '' || (item?.draft?.request?.url !== undefined && item?.draft?.request?.url !== '')) {
setGenerateCodeItemModalOpen(true); setGenerateCodeItemModalOpen(true);
} else { } else {
toast.error('URL is required'); toast.error('URL is required');

View File

@ -63,16 +63,16 @@ const Table = ({ minColumnWidth = 1, headers = [], children }) => {
[activeColumnIndex, columns, minColumnWidth] [activeColumnIndex, columns, minColumnWidth]
); );
const handleMouseUp = useCallback(() => {
setActiveColumnIndex(null);
removeListeners();
}, [removeListeners]);
const removeListeners = useCallback(() => { const removeListeners = useCallback(() => {
window.removeEventListener('mousemove', handleMouseMove); window.removeEventListener('mousemove', handleMouseMove);
window.removeEventListener('mouseup', removeListeners); window.removeEventListener('mouseup', removeListeners);
}, [handleMouseMove]); }, [handleMouseMove]);
const handleMouseUp = useCallback(() => {
setActiveColumnIndex(null);
removeListeners?.();
}, [removeListeners]);
useEffect(() => { useEffect(() => {
if (activeColumnIndex !== null) { if (activeColumnIndex !== null) {
window.addEventListener('mousemove', handleMouseMove); window.addEventListener('mousemove', handleMouseMove);

View File

@ -0,0 +1,14 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './pages/index';
const rootElement = document.getElementById('root');
if (rootElement) {
const root = ReactDOM.createRoot(rootElement);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
}

View File

@ -25,31 +25,7 @@ import '@fontsource/inter/900.css';
import { setupPolyfills } from 'utils/common/setupPolyfills'; import { setupPolyfills } from 'utils/common/setupPolyfills';
setupPolyfills(); setupPolyfills();
function SafeHydrate({ children }) { function Main({ children }) {
return <div suppressHydrationWarning>{typeof window === 'undefined' ? null : children}</div>;
}
function NoSsr({ children }) {
const SERVER_RENDERED = typeof window === 'undefined';
if (SERVER_RENDERED) {
return null;
}
return <>{children}</>;
}
function MyApp({ Component, pageProps }) {
const [domLoaded, setDomLoaded] = useState(false);
useEffect(() => {
setDomLoaded(true);
}, []);
if (!domLoaded) {
return null;
}
if (!window.ipcRenderer) { if (!window.ipcRenderer) {
return ( return (
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert"> <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 mx-10 my-10 rounded relative" role="alert">
@ -65,23 +41,21 @@ function MyApp({ Component, pageProps }) {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<SafeHydrate>
<NoSsr>
<Provider store={ReduxStore}> <Provider store={ReduxStore}>
<ThemeProvider> <ThemeProvider>
<ToastProvider> <ToastProvider>
<AppProvider> <AppProvider>
<HotkeysProvider> <HotkeysProvider>
<Component {...pageProps} /> {children}
</HotkeysProvider> </HotkeysProvider>
</AppProvider> </AppProvider>
</ToastProvider> </ToastProvider>
</ThemeProvider> </ThemeProvider>
</Provider> </Provider>
</NoSsr>
</SafeHydrate>
</ErrorBoundary> </ErrorBoundary>
); );
} }
export default MyApp; export default Main;

View File

@ -1,41 +0,0 @@
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />)
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
)
};
} finally {
sheet.seal();
}
}
render() {
return (
<Html>
<Head />
<body id="bruno-app-body">
<Main />
<NextScript />
</body>
</Html>
);
}
}

View File

@ -1,20 +1,16 @@
import Head from 'next/head';
import Bruno from './Bruno'; import Bruno from './Bruno';
import GlobalStyle from '../globalStyles'; import GlobalStyle from '../globalStyles';
import '../i18n'; import '../i18n';
import Main from './Main';
export default function Home() { export default function App() {
return ( return (
<div> <div>
<Head>
<title>Bruno</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<GlobalStyle />
<main> <main>
<Main>
<GlobalStyle />
<Bruno /> <Bruno />
</Main>
</main> </main>
</div> </div>
); );

View File

@ -7,21 +7,19 @@
*/ */
import { useEffect } from 'react'; import { useEffect } from 'react';
import getConfig from 'next/config';
import { PostHog } from 'posthog-node'; import { PostHog } from 'posthog-node';
import platformLib from 'platform'; import platformLib from 'platform';
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
const { publicRuntimeConfig } = getConfig();
const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY; const posthogApiKey = process.env.NEXT_PUBLIC_POSTHOG_API_KEY;
let posthogClient = null; let posthogClient = null;
const isPlaywrightTestRunning = () => { const isPlaywrightTestRunning = () => {
return publicRuntimeConfig.PLAYWRIGHT ? true : false; return process.env.PLAYWRIGHT ? true : false;
}; };
const isDevEnv = () => { const isDevEnv = () => {
return publicRuntimeConfig.ENV === 'dev'; return import.meta.env.MODE === 'development';
}; };
const getPosthogClient = () => { const getPosthogClient = () => {

View File

@ -6,7 +6,12 @@ import { useSelector, useDispatch } from 'react-redux';
import EnvironmentSettings from 'components/Environments/EnvironmentSettings'; import EnvironmentSettings from 'components/Environments/EnvironmentSettings';
import NetworkError from 'components/ResponsePane/NetworkError'; import NetworkError from 'components/ResponsePane/NetworkError';
import NewRequest from 'components/Sidebar/NewRequest'; import NewRequest from 'components/Sidebar/NewRequest';
import { sendRequest, saveRequest, saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions'; import {
sendRequest,
saveRequest,
saveCollectionRoot,
saveFolderRoot
} from 'providers/ReduxStore/slices/collections/actions';
import { findCollectionByUid, findItemInCollection } from 'utils/collections'; import { findCollectionByUid, findItemInCollection } from 'utils/collections';
import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs'; import { closeTabs, switchTab } from 'providers/ReduxStore/slices/tabs';
import { getKeyBindingsForActionAllOS } from './keyMappings'; import { getKeyBindingsForActionAllOS } from './keyMappings';
@ -43,7 +48,11 @@ export const HotkeysProvider = (props) => {
if (collection) { if (collection) {
const item = findItemInCollection(collection, activeTab.uid); const item = findItemInCollection(collection, activeTab.uid);
if (item && item.uid) { if (item && item.uid) {
if (activeTab.type === 'folder-settings') {
dispatch(saveFolderRoot(collection.uid, item.uid));
} else {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid)); dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
}
} else if (activeTab.type === 'collection-settings') { } else if (activeTab.type === 'collection-settings') {
dispatch(saveCollectionRoot(collection.uid)); dispatch(saveCollectionRoot(collection.uid));
} }

View File

@ -1,4 +1,3 @@
import getConfig from 'next/config';
import { configureStore } from '@reduxjs/toolkit'; import { configureStore } from '@reduxjs/toolkit';
import tasksMiddleware from './middlewares/tasks/middleware'; import tasksMiddleware from './middlewares/tasks/middleware';
import debugMiddleware from './middlewares/debug/middleware'; import debugMiddleware from './middlewares/debug/middleware';
@ -8,9 +7,8 @@ import tabsReducer from './slices/tabs';
import notificationsReducer from './slices/notifications'; import notificationsReducer from './slices/notifications';
import globalEnvironmentsReducer from './slices/global-environments'; import globalEnvironmentsReducer from './slices/global-environments';
const { publicRuntimeConfig } = getConfig();
const isDevEnv = () => { const isDevEnv = () => {
return publicRuntimeConfig.ENV === 'dev'; return import.meta.env.MODE === 'development';
}; };
let middleware = [tasksMiddleware.middleware]; let middleware = [tasksMiddleware.middleware];

View File

@ -1,6 +1,5 @@
import { uuid } from 'utils/common'; import { uuid } from 'utils/common';
import path from 'path'; import { find, map, forOwn, concat, filter, each, cloneDeep, get, set } from 'lodash';
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { import {
addDepth, addDepth,
@ -13,6 +12,7 @@ import {
findEnvironmentInCollection, findEnvironmentInCollection,
findItemInCollection, findItemInCollection,
findItemInCollectionByPathname, findItemInCollectionByPathname,
isItemAFolder,
isItemARequest isItemARequest
} from 'utils/collections'; } from 'utils/collections';
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url'; import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
@ -1733,6 +1733,15 @@ export const collectionsSlice = createSlice({
item.draft.request.docs = action.payload.docs; item.draft.request.docs = action.payload.docs;
} }
} }
},
updateFolderDocs: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
const folder = collection ? findItemInCollection(collection, action.payload.folderUid) : null;
if (folder) {
if (isItemAFolder(folder)) {
set(folder, 'root.docs', action.payload.docs);
}
}
} }
} }
}); });
@ -1827,7 +1836,8 @@ export const {
runRequestEvent, runRequestEvent,
runFolderEvent, runFolderEvent,
resetCollectionRunner, resetCollectionRunner,
updateRequestDocs updateRequestDocs,
updateFolderDocs
} = collectionsSlice.actions; } = collectionsSlice.actions;
export default collectionsSlice.reducer; export default collectionsSlice.reducer;

View File

@ -35,7 +35,6 @@ export const notificationSlice = createSlice({
state.loading = action.payload.fetching; state.loading = action.payload.fetching;
}, },
setNotifications: (state, action) => { setNotifications: (state, action) => {
console.log('notifications', notifications);
let notifications = action.payload.notifications || []; let notifications = action.payload.notifications || [];
let readNotificationIds = state.readNotificationIds; let readNotificationIds = state.readNotificationIds;

View File

@ -93,9 +93,6 @@ if (!SERVER_RENDERED) {
const box = target.getBoundingClientRect(); const box = target.getBoundingClientRect();
const hoverTime = getHoverTime(cm);
state.hoverTimeout = setTimeout(onHover, hoverTime);
const onMouseMove = function () { const onMouseMove = function () {
clearTimeout(state.hoverTimeout); clearTimeout(state.hoverTimeout);
state.hoverTimeout = setTimeout(onHover, hoverTime); state.hoverTimeout = setTimeout(onHover, hoverTime);
@ -115,6 +112,9 @@ if (!SERVER_RENDERED) {
onMouseHover(cm, box); onMouseHover(cm, box);
}; };
const hoverTime = getHoverTime(cm);
state.hoverTimeout = setTimeout(onHover, hoverTime);
CodeMirror.on(document, 'mousemove', onMouseMove); CodeMirror.on(document, 'mousemove', onMouseMove);
CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut); CodeMirror.on(cm.getWrapperElement(), 'mouseout', onMouseOut);
} }

View File

@ -404,7 +404,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
request: {} request: {}
}; };
let { request, meta } = si?.root || {}; let { request, meta, docs } = si?.root || {};
let { headers, script = {}, vars = {}, tests } = request || {}; let { headers, script = {}, vars = {}, tests } = request || {};
// folder level headers // folder level headers
@ -436,6 +436,11 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
di.root.request.tests = tests; di.root.request.tests = tests;
} }
// folder level docs
if (docs?.length) {
di.root.docs = docs;
}
if (meta?.name) { if (meta?.name) {
di.root.meta = {}; di.root.meta = {};
di.root.meta.name = meta?.name; di.root.meta.name = meta?.name;
@ -800,6 +805,19 @@ export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobal
return variables; return variables;
}; };
export const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
const environment = globalEnvironments?.find(env => env?.uid === activeGlobalEnvironmentUid);
if (environment && Array.isArray(environment.variables)) {
return environment.variables
.filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)
.map((variable) => variable.name);
}
return [];
};
export const getEnvironmentVariables = (collection) => { export const getEnvironmentVariables = (collection) => {
let variables = {}; let variables = {};
if (collection) { if (collection) {
@ -873,8 +891,22 @@ export const getAllVariables = (collection, item) => {
...requestVariables, ...requestVariables,
...runtimeVariables ...runtimeVariables
}; };
const maskedEnvVariables = getEnvironmentVariablesMasked(collection);
const mergedVariablesGlobal = {
...collectionVariables,
...envVariables,
...folderVariables,
...requestVariables,
...runtimeVariables,
}
const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];
const maskedGlobalEnvVariables = collection?.globalEnvSecrets || [];
const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables)); const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables));
const filteredMaskedGlobalEnvVariables = maskedGlobalEnvVariables.filter((key) => !(key in mergedVariablesGlobal));
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
return { return {
...globalEnvironmentVariables, ...globalEnvironmentVariables,
@ -886,7 +918,7 @@ export const getAllVariables = (collection, item) => {
pathParams: { pathParams: {
...pathParams ...pathParams
}, },
maskedEnvVariables: filteredMaskedEnvVariables, maskedEnvVariables: uniqueMaskedVariables,
process: { process: {
env: { env: {
...processEnvVariables ...processEnvVariables

View File

@ -166,6 +166,17 @@ const importScriptsFromEvents = (events, requestObject, options, pushTranslation
}); });
}; };
const importCollectionLevelVariables = (variables, requestObject) => {
const vars = variables.map((v) => ({
uid: uuid(),
name: v.key,
value: v.value,
enabled: true
}));
requestObject.vars.req = vars;
};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => { const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
brunoParent.items = brunoParent.items || []; brunoParent.items = brunoParent.items || [];
const folderMap = {}; const folderMap = {};
@ -494,6 +505,10 @@ const importPostmanV2Collection = (collection, options) => {
importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog); importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog);
} }
if (collection?.variable){
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
}
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options); importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
return brunoCollection; return brunoCollection;

View File

@ -1,207 +1,8 @@
const { get, each, filter, find, compact } = require('lodash'); const { get, each, filter, find, compact } = require('lodash');
const fs = require('fs'); const { get, each, filter } = require('lodash');
const os = require('os');
const decomment = require('decomment'); const decomment = require('decomment');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
}
}
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
if(request?.vars) {
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
if(request?.vars) {
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
}
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
}
}
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
if (scriptFlow === 'sequential') {
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
}
if (scriptFlow === 'sequential') {
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
}
};
const findItem = (items = [], pathname) => {
return find(items, (i) => i.pathname === pathname);
};
const findItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return findItem(flattenedItems, pathname);
};
const findParentItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return find(flattenedItems, (item) => {
return item.items && find(item.items, (i) => i.pathname === pathname);
});
};
const flattenItems = (items = []) => {
const flattenedItems = [];
const flatten = (itms, flattened) => {
each(itms, (i) => {
flattened.push(i);
if (i.items && i.items.length) {
flatten(i.items, flattened);
}
});
};
flatten(items, flattenedItems);
return flattenedItems;
};
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item.pathname);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item.pathname);
}
return path;
};
const prepareRequest = (item = {}, collection = {}) => { const prepareRequest = (item = {}, collection = {}) => {
const request = item?.request; const request = item?.request;
@ -312,7 +113,7 @@ const prepareRequest = (item = {}, collection = {}) => {
if (request.body.mode === 'xml') { if (request.body.mode === 'xml') {
if (!contentTypeDefined) { if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'text/xml'; axiosRequest.headers['content-type'] = 'application/xml';
} }
axiosRequest.data = request.body.xml; axiosRequest.data = request.body.xml;
} }

View File

@ -0,0 +1,208 @@
const { get, each, find, compact } = require('lodash');
const os = require('os');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
}
}
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
if(request?.vars) {
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
if(request?.vars) {
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
}
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
}
}
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
if (scriptFlow === 'sequential') {
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
}
if (scriptFlow === 'sequential') {
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
}
};
const findItem = (items = [], pathname) => {
return find(items, (i) => i.pathname === pathname);
};
const findItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return findItem(flattenedItems, pathname);
};
const findParentItemInCollection = (collection, pathname) => {
let flattenedItems = flattenItems(collection.items);
return find(flattenedItems, (item) => {
return item.items && find(item.items, (i) => i.pathname === pathname);
});
};
const flattenItems = (items = []) => {
const flattenedItems = [];
const flatten = (itms, flattened) => {
each(itms, (i) => {
flattened.push(i);
if (i.items && i.items.length) {
flatten(i.items, flattened);
}
});
};
flatten(items, flattenedItems);
return flattenedItems;
};
const getTreePathFromCollectionToItem = (collection, _item) => {
let path = [];
let item = findItemInCollection(collection, _item.pathname);
while (item) {
path.unshift(item);
item = findParentItemInCollection(collection, item.pathname);
}
return path;
};
module.exports = {
mergeHeaders,
mergeVars,
mergeScripts,
getTreePathFromCollectionToItem
}

View File

@ -389,6 +389,8 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
}; };
const unlink = (win, pathname, collectionUid, collectionPath) => { const unlink = (win, pathname, collectionUid, collectionPath) => {
console.log(`watcher unlink: ${pathname}`);
if (isBruEnvironmentConfig(pathname, collectionPath)) { if (isBruEnvironmentConfig(pathname, collectionPath)) {
return unlinkEnvironmentFile(win, pathname, collectionUid); return unlinkEnvironmentFile(win, pathname, collectionUid);
} }
@ -506,6 +508,33 @@ class Watcher {
this.watchers[watchPath] = null; this.watchers[watchPath] = null;
} }
} }
getWatcherByItemPath(itemPath) {
const paths = Object.keys(this.watchers);
const watcherPath = paths?.find(collectionPath => {
const absCollectionPath = path.resolve(collectionPath);
const absItemPath = path.resolve(itemPath);
return absItemPath.startsWith(absCollectionPath);
});
return watcherPath ? this.watchers[watcherPath] : null;
}
unlinkItemPathInWatcher(itemPath) {
const watcher = this.getWatcherByItemPath(itemPath);
if (watcher) {
watcher.unwatch(itemPath);
}
}
addItemPathInWatcher(itemPath) {
const watcher = this.getWatcherByItemPath(itemPath);
if (watcher && !watcher?.has?.(itemPath)) {
watcher?.add?.(itemPath);
}
}
} }
module.exports = Watcher; module.exports = Watcher;

View File

@ -20,8 +20,10 @@ const {
normalizeWslPath, normalizeWslPath,
normalizeAndResolvePath, normalizeAndResolvePath,
safeToRename, safeToRename,
sanitizeCollectionName,
isWindowsOS, isWindowsOS,
isValidFilename isValidFilename,
hasSubDirectories,
} = require('../utils/filesystem'); } = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections'); const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common'); const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
@ -67,6 +69,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
'renderer:create-collection', 'renderer:create-collection',
async (event, collectionName, collectionFolderName, collectionLocation) => { async (event, collectionName, collectionFolderName, collectionLocation) => {
try { try {
collectionFolderName = sanitizeDirectoryName(collectionFolderName);
collectionName = sanitizeCollectionName(collectionName);
const dirPath = path.join(collectionLocation, collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) { if (fs.existsSync(dirPath)) {
const files = fs.readdirSync(dirPath); const files = fs.readdirSync(dirPath);
@ -75,9 +79,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
throw new Error(`collection: ${dirPath} already exists and is not empty`); throw new Error(`collection: ${dirPath} already exists and is not empty`);
} }
} }
if (!isValidPathname(path.basename(dirPath))) {
if (!isValidPathname(dirPath)) { throw new Error(`collection: invalid pathname - ${dirPath}`);
throw new Error(`collection: invalid pathname - ${dir}`);
} }
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
@ -105,13 +108,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle( ipcMain.handle(
'renderer:clone-collection', 'renderer:clone-collection',
async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => { async (event, collectionName, collectionFolderName, collectionLocation, previousPath) => {
collectionFolderName = sanitizeCollectionName(collectionFolderName);
const dirPath = path.join(collectionLocation, collectionFolderName); const dirPath = path.join(collectionLocation, collectionFolderName);
if (fs.existsSync(dirPath)) { if (fs.existsSync(dirPath)) {
throw new Error(`collection: ${dirPath} already exists`); throw new Error(`collection: ${dirPath} already exists`);
} }
if (!isValidPathname(dirPath)) { if (!isValidPathname(path.basename(dirPath))) {
throw new Error(`collection: invalid pathname - ${dir}`); throw new Error(`collection: invalid pathname - ${dirPath}`);
} }
// create dir // create dir
@ -150,6 +154,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename collection // rename collection
ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => { ipcMain.handle('renderer:rename-collection', async (event, newName, collectionPathname) => {
try { try {
newName = sanitizeCollectionName(newName);
const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json'); const brunoJsonFilePath = path.join(collectionPathname, 'bruno.json');
const content = fs.readFileSync(brunoJsonFilePath, 'utf8'); const content = fs.readFileSync(brunoJsonFilePath, 'utf8');
const json = JSON.parse(content); const json = JSON.parse(content);
@ -343,6 +348,12 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
// rename item // rename item
ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => {
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`);
const parentDir = path.dirname(oldPath);
const isWindowsOSAndNotWSLAndItemHasSubDirectories = isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath);
let parentDirUnwatched = false;
let parentDirRewatched = false;
try { try {
// Normalize paths if they are WSL paths // Normalize paths if they are WSL paths
oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath); oldPath = isWSLPath(oldPath) ? normalizeWslPath(oldPath) : normalizeAndResolvePath(oldPath);
@ -365,15 +376,29 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
moveRequestUid(bruFile, newBruFilePath); moveRequestUid(bruFile, newBruFilePath);
} }
if (isWindowsOS() && !isWSLPath(oldPath)) { watcher.unlinkItemPathInWatcher(parentDir);
const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); parentDirUnwatched = true;
/**
* If it is windows OS
* And it is not WSL path (meaning its not linux running on windows using WSL)
* And it has sub directories
* Only then we need to use the temp dir approach to rename the folder
*
* Windows OS would sometimes throw error when renaming a folder with sub directories
* This is a alternative approach to avoid that error
*/
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
await fsExtra.copy(oldPath, tempDir); await fsExtra.copy(oldPath, tempDir);
await fsExtra.move(tempDir, newPath, { overwrite: true });
await fsExtra.remove(oldPath); await fsExtra.remove(oldPath);
await fsExtra.move(tempDir, newPath, { overwrite: true });
await fsExtra.remove(tempDir);
} else { } else {
await fs.renameSync(oldPath, newPath); await fs.renameSync(oldPath, newPath);
} }
watcher.addItemPathInWatcher(parentDir);
parentDirRewatched = true;
return newPath; return newPath;
} }
@ -397,12 +422,33 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return newPath; return newPath;
} catch (error) { } catch (error) {
// in case an error occurs during the rename file operations after unlinking the parent dir
// and the rewatch fails, we need to add it back to watcher
if (parentDirUnwatched && !parentDirRewatched) {
watcher.addItemPathInWatcher(parentDir);
}
// in case the rename file operations fails, and we see that the temp dir exists
// and the old path does not exist, we need to restore the data from the temp dir to the old path
if (isWindowsOSAndNotWSLAndItemHasSubDirectories) {
if (fsExtra.pathExistsSync(tempDir) && !fsExtra.pathExistsSync(oldPath)) {
try {
await fsExtra.copy(tempDir, oldPath);
await fsExtra.remove(tempDir);
} catch (err) {
console.error("Failed to restore data to the old path:", err);
}
}
}
return Promise.reject(error); return Promise.reject(error);
} }
}); });
// new folder // new folder
ipcMain.handle('renderer:new-folder', async (event, pathname) => { ipcMain.handle('renderer:new-folder', async (event, pathname) => {
const resolvedFolderName = sanitizeDirectoryName(path.basename(pathname));
pathname = path.join(path.dirname(pathname), resolvedFolderName);
try { try {
if (!fs.existsSync(pathname)) { if (!fs.existsSync(pathname)) {
fs.mkdirSync(pathname); fs.mkdirSync(pathname);
@ -461,7 +507,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => { ipcMain.handle('renderer:import-collection', async (event, collection, collectionLocation) => {
try { try {
let collectionName = sanitizeDirectoryName(collection.name); let collectionName = sanitizeCollectionName(collection.name);
let collectionPath = path.join(collectionLocation, collectionName); let collectionPath = path.join(collectionLocation, collectionName);
if (fs.existsSync(collectionPath)) { if (fs.existsSync(collectionPath)) {
@ -477,6 +523,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
} }
if (item.type === 'folder') { if (item.type === 'folder') {
item.name = sanitizeDirectoryName(item.name);
const folderPath = path.join(currentPath, item.name); const folderPath = path.join(currentPath, item.name);
fs.mkdirSync(folderPath); fs.mkdirSync(folderPath);

View File

@ -38,7 +38,7 @@ const {
const Oauth2Store = require('../../store/oauth2'); const Oauth2Store = require('../../store/oauth2');
const iconv = require('iconv-lite'); const iconv = require('iconv-lite');
const FormData = require('form-data'); const FormData = require('form-data');
const { createFormData } = prepareRequest; const { createFormData } = require('../../utils/form-data');
const safeStringifyJSON = (data) => { const safeStringifyJSON = (data) => {
try { try {
@ -397,7 +397,7 @@ const registerNetworkIpc = (mainWindow) => {
) => { ) => {
// run pre-request script // run pre-request script
let scriptResult; let scriptResult;
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL); const requestScript = get(request, 'script.req');
if (requestScript?.length) { if (requestScript?.length) {
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
scriptResult = await scriptRuntime.runRequestScript( scriptResult = await scriptRuntime.runRequestScript(
@ -493,12 +493,7 @@ const registerNetworkIpc = (mainWindow) => {
} }
// run post-response script // run post-response script
const responseScript = compact(scriptingConfig.flow === 'sequential' ? [ const responseScript = get(request, 'script.res');
get(collectionRoot, 'request.script.res'), get(request, 'script.res')
] : [
get(request, 'script.res'), get(collectionRoot, 'request.script.res')
]).join(os.EOL);
let scriptResult; let scriptResult;
if (responseScript?.length) { if (responseScript?.length) {
const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime }); const scriptRuntime = new ScriptRuntime({ runtime: scriptingConfig?.runtime });
@ -675,14 +670,7 @@ const registerNetworkIpc = (mainWindow) => {
}); });
} }
// run tests const testFile = get(request, 'tests');
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.tests'), testScript,
] : [
testScript, get(collectionRoot, 'request.tests')
]).join(os.EOL);
if (typeof testFile === 'string') { if (typeof testFile === 'string') {
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests( const testResults = await testRuntime.runTests(
@ -1147,14 +1135,7 @@ const registerNetworkIpc = (mainWindow) => {
}); });
} }
// run tests const testFile = get(request, 'tests');
const testScript = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
const testFile = compact(scriptingConfig.flow === 'sequential' ? [
get(collectionRoot, 'request.tests'), testScript
] : [
testScript, get(collectionRoot, 'request.tests')
]).join(os.EOL);
if (typeof testFile === 'string') { if (typeof testFile === 'string') {
const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime }); const testRuntime = new TestRuntime({ runtime: scriptingConfig?.runtime });
const testResults = await testRuntime.runTests( const testResults = await testRuntime.runTests(

View File

@ -1,194 +1,8 @@
const os = require('os'); const { get, each, filter } = require('lodash');
const { get, each, filter, compact, forOwn } = require('lodash');
const decomment = require('decomment'); const decomment = require('decomment');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
const crypto = require('node:crypto'); const crypto = require('node:crypto');
const { getTreePathFromCollectionToItem } = require('../../utils/collection'); const { getTreePathFromCollectionToItem, mergeHeaders, mergeScripts, mergeVars } = require('../../utils/collection');
const { buildFormUrlEncodedPayload } = require('../../utils/common'); const { buildFormUrlEncodedPayload, createFormData } = require('../../utils/form-data');
const mergeFolderLevelHeaders = (request, requestTreePath) => {
let folderHeaders = new Map();
for (let i of requestTreePath) {
if (i.type === 'folder') {
let headers = get(i, 'root.request.headers', []);
headers.forEach((header) => {
if (header.enabled) {
folderHeaders.set(header.name, header.value);
}
});
} else if (i.uid === request.uid) {
const headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
headers.forEach((header) => {
if (header.enabled) {
folderHeaders.set(header.name, header.value);
}
});
}
}
let mergedFolderHeaders = Array.from(folderHeaders, ([name, value]) => ({ name, value, enabled: true }));
let requestHeaders = request.headers || [];
let requestHeadersMap = new Map();
for (let header of requestHeaders) {
if (header.enabled) {
requestHeadersMap.set(header.name, header.value);
}
}
mergedFolderHeaders.forEach((header) => {
requestHeadersMap.set(header.name, header.value);
});
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
};
const mergeFolderLevelScripts = (request, requestTreePath, scriptFlow) => {
let folderCombinedPreReqScript = [];
let folderCombinedPostResScript = [];
let folderCombinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
folderCombinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
folderCombinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
folderCombinedTests.push(tests);
}
}
}
if (folderCombinedPreReqScript.length) {
request.script.req = compact([...folderCombinedPreReqScript, request?.script?.req || '']).join(os.EOL);
}
if (folderCombinedPostResScript.length) {
if (scriptFlow === 'sequential') {
request.script.res = compact([...folderCombinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...folderCombinedPostResScript.reverse()]).join(os.EOL);
}
}
if (folderCombinedTests.length) {
if (scriptFlow === 'sequential') {
request.tests = compact([...folderCombinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...folderCombinedTests.reverse()]).join(os.EOL);
}
}
};
const createFormData = (datas, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forOwn(datas, (value, key) => {
if (typeof value == 'string') {
form.append(key, value);
return;
}
const filePaths = value || [];
filePaths?.forEach?.((filePath) => {
let trimmedFilePath = filePath.trim();
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
});
});
return form;
};
const setAuthHeaders = (axiosRequest, request, collectionRoot) => { const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
const collectionAuth = get(collectionRoot, 'request.auth'); const collectionAuth = get(collectionRoot, 'request.auth');
@ -353,28 +167,24 @@ const prepareRequest = (item, collection) => {
let contentTypeDefined = false; let contentTypeDefined = false;
let url = request.url; let url = request.url;
// Collection level headers
each(get(collectionRoot, 'request.headers', []), (h) => { each(get(collectionRoot, 'request.headers', []), (h) => {
if (h.enabled && h.name.length > 0) { if (h.enabled && h.name?.toLowerCase() === 'content-type') {
headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') {
contentTypeDefined = true; contentTypeDefined = true;
} return false;
} }
}); });
// scriptFlow is either "sandwich" or "sequential"
const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich'; const scriptFlow = collection.brunoConfig?.scripts?.flow ?? 'sandwich';
const requestTreePath = getTreePathFromCollectionToItem(collection, item); const requestTreePath = getTreePathFromCollectionToItem(collection, item);
if (requestTreePath && requestTreePath.length > 0) { if (requestTreePath && requestTreePath.length > 0) {
mergeFolderLevelHeaders(request, requestTreePath); mergeHeaders(collection, request, requestTreePath);
mergeFolderLevelScripts(request, requestTreePath, scriptFlow); mergeScripts(collection, request, requestTreePath, scriptFlow);
mergeVars(collection, request, requestTreePath); mergeVars(collection, request, requestTreePath);
request.globalEnvironmentVariables = collection?.globalEnvironmentVariables; request.globalEnvironmentVariables = collection?.globalEnvironmentVariables;
} }
// Request level headers
each(request.headers, (h) => { each(get(request, 'headers', []), (h) => {
if (h.enabled && h.name.length > 0) { if (h.enabled && h.name.length > 0) {
headers[h.name] = h.value; headers[h.name] = h.value;
if (h.name.toLowerCase() === 'content-type') { if (h.name.toLowerCase() === 'content-type') {
@ -414,7 +224,7 @@ const prepareRequest = (item, collection) => {
if (request.body.mode === 'xml') { if (request.body.mode === 'xml') {
if (!contentTypeDefined) { if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'text/xml'; axiosRequest.headers['content-type'] = 'application/xml';
} }
axiosRequest.data = request.body.xml; axiosRequest.data = request.body.xml;
} }
@ -435,11 +245,11 @@ const prepareRequest = (item, collection) => {
} }
if (request.body.mode === 'multipartForm') { if (request.body.mode === 'multipartForm') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'multipart/form-data'; axiosRequest.headers['content-type'] = 'multipart/form-data';
const params = {}; }
const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); const enabledParams = filter(request.body.multipartForm, (p) => p.enabled);
each(enabledParams, (p) => (params[p.name] = p.value)); axiosRequest.data = createFormData(enabledParams);
axiosRequest.data = params;
} }
if (request.body.mode === 'graphql') { if (request.body.mode === 'graphql') {
@ -470,4 +280,3 @@ const prepareRequest = (item, collection) => {
module.exports = prepareRequest; module.exports = prepareRequest;
module.exports.setAuthHeaders = setAuthHeaders; module.exports.setAuthHeaders = setAuthHeaders;
module.exports.createFormData = createFormData;

View File

@ -1,5 +1,161 @@
const each = require('lodash/each'); const { get, each, find, compact } = require('lodash');
const find = require('lodash/find'); const os = require('os');
const mergeHeaders = (collection, request, requestTreePath) => {
let headers = new Map();
let collectionHeaders = get(collection, 'root.request.headers', []);
collectionHeaders.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
if (header?.name?.toLowerCase() === 'content-type') {
contentTypeDefined = true;
}
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let _headers = get(i, 'root.request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
} else {
const _headers = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'request.headers', []);
_headers.forEach((header) => {
if (header.enabled) {
headers.set(header.name, header.value);
}
});
}
}
request.headers = Array.from(headers, ([name, value]) => ({ name, value, enabled: true }));
};
const mergeVars = (collection, request, requestTreePath) => {
let reqVars = new Map();
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
let collectionVariables = {};
collectionRequestVars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
collectionVariables[_var.name] = _var.value;
}
});
let folderVariables = {};
let requestVariables = {};
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
folderVariables[_var.name] = _var.value;
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
vars.forEach((_var) => {
if (_var.enabled) {
reqVars.set(_var.name, _var.value);
requestVariables[_var.name] = _var.value;
}
});
}
}
request.collectionVariables = collectionVariables;
request.folderVariables = folderVariables;
request.requestVariables = requestVariables;
if(request?.vars) {
request.vars.req = Array.from(reqVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'request'
}));
}
let resVars = new Map();
let collectionResponseVars = get(collection, 'root.request.vars.res', []);
collectionResponseVars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
for (let i of requestTreePath) {
if (i.type === 'folder') {
let vars = get(i, 'root.request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
} else {
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
vars.forEach((_var) => {
if (_var.enabled) {
resVars.set(_var.name, _var.value);
}
});
}
}
if(request?.vars) {
request.vars.res = Array.from(resVars, ([name, value]) => ({
name,
value,
enabled: true,
type: 'response'
}));
}
};
const mergeScripts = (collection, request, requestTreePath, scriptFlow) => {
let collectionPreReqScript = get(collection, 'root.request.script.req', '');
let collectionPostResScript = get(collection, 'root.request.script.res', '');
let collectionTests = get(collection, 'root.request.tests', '');
let combinedPreReqScript = [];
let combinedPostResScript = [];
let combinedTests = [];
for (let i of requestTreePath) {
if (i.type === 'folder') {
let preReqScript = get(i, 'root.request.script.req', '');
if (preReqScript && preReqScript.trim() !== '') {
combinedPreReqScript.push(preReqScript);
}
let postResScript = get(i, 'root.request.script.res', '');
if (postResScript && postResScript.trim() !== '') {
combinedPostResScript.push(postResScript);
}
let tests = get(i, 'root.request.tests', '');
if (tests && tests?.trim?.() !== '') {
combinedTests.push(tests);
}
}
}
request.script.req = compact([collectionPreReqScript, ...combinedPreReqScript, request?.script?.req || '']).join(os.EOL);
if (scriptFlow === 'sequential') {
request.script.res = compact([collectionPostResScript, ...combinedPostResScript, request?.script?.res || '']).join(os.EOL);
} else {
request.script.res = compact([request?.script?.res || '', ...combinedPostResScript.reverse(), collectionPostResScript]).join(os.EOL);
}
if (scriptFlow === 'sequential') {
request.tests = compact([collectionTests, ...combinedTests, request?.tests || '']).join(os.EOL);
} else {
request.tests = compact([request?.tests || '', ...combinedTests.reverse(), collectionTests]).join(os.EOL);
}
};
const flattenItems = (items = []) => { const flattenItems = (items = []) => {
const flattenedItems = []; const flattenedItems = [];
@ -44,14 +200,12 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
path.unshift(item); path.unshift(item);
item = findParentItemInCollection(collection, item.uid); item = findParentItemInCollection(collection, item.uid);
} }
return path; return path;
}; };
module.exports = { module.exports = {
flattenItems, mergeHeaders,
findItem, mergeVars,
findItemInCollection, mergeScripts,
findParentItemInCollection,
getTreePathFromCollectionToItem getTreePathFromCollectionToItem
}; }

View File

@ -85,24 +85,6 @@ const flattenDataForDotNotation = (data) => {
return result; return result;
}; };
/**
* @param {Array.<object>} params The request body Array
* @returns {object} Returns an obj with repeating key as a array of values
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
*/
const buildFormUrlEncodedPayload = (params) => {
return params.reduce((acc, p) => {
if (!acc[p.name]) {
acc[p.name] = p.value;
} else if (Array.isArray(acc[p.name])) {
acc[p.name].push(p.value);
} else {
acc[p.name] = [acc[p.name], p.value];
}
return acc;
}, {});
};
module.exports = { module.exports = {
uuid, uuid,
stringifyJson, stringifyJson,
@ -111,6 +93,5 @@ module.exports = {
safeParseJSON, safeParseJSON,
simpleHash, simpleHash,
generateUidBasedOnHash, generateUidBasedOnHash,
flattenDataForDotNotation, flattenDataForDotNotation
buildFormUrlEncodedPayload
}; };

View File

@ -38,6 +38,11 @@ const isDirectory = (dirPath) => {
} }
}; };
const hasSubDirectories = (dir) => {
const files = fs.readdirSync(dir);
return files.some(file => fs.statSync(path.join(dir, file)).isDirectory());
};
const normalizeAndResolvePath = (pathname) => { const normalizeAndResolvePath = (pathname) => {
if (isSymbolicLink(pathname)) { if (isSymbolicLink(pathname)) {
const absPath = path.dirname(pathname); const absPath = path.dirname(pathname);
@ -156,8 +161,12 @@ const searchForBruFiles = (dir) => {
return searchForFiles(dir, '.bru'); return searchForFiles(dir, '.bru');
}; };
const sanitizeCollectionName = (name) => {
return name.trim();
}
const sanitizeDirectoryName = (name) => { const sanitizeDirectoryName = (name) => {
return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-'); return name.replace(/[<>:"/\\|?*\x00-\x1F]+/g, '-').trim();
}; };
const isWindowsOS = () => { const isWindowsOS = () => {
@ -222,7 +231,9 @@ module.exports = {
searchForFiles, searchForFiles,
searchForBruFiles, searchForBruFiles,
sanitizeDirectoryName, sanitizeDirectoryName,
sanitizeCollectionName,
isWindowsOS, isWindowsOS,
safeToRename, safeToRename,
isValidFilename isValidFilename,
hasSubDirectories
}; };

View File

@ -0,0 +1,60 @@
const { forOwn } = require('lodash');
const FormData = require('form-data');
const fs = require('fs');
const path = require('path');
/**
* @param {Array.<object>} params The request body Array
* @returns {object} Returns an obj with repeating key as a array of values
* {item: 2, item: 3, item1: 4} becomes {item: [2,3], item1: 4}
*/
const buildFormUrlEncodedPayload = (params) => {
return params.reduce((acc, p) => {
if (!acc[p.name]) {
acc[p.name] = p.value;
} else if (Array.isArray(acc[p.name])) {
acc[p.name].push(p.value);
} else {
acc[p.name] = [acc[p.name], p.value];
}
return acc;
}, {});
};
const createFormData = (data, collectionPath) => {
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
const form = new FormData();
forEach(data, (datum) => {
const { name, type, value } = datum;
if (type === 'text') {
if (Array.isArray(value)) {
value.forEach((val) => form.append(name, val));
} else {
form.append(name, value);
}
return;
}
if (type === 'file') {
const filePaths = value || [];
filePaths.forEach((filePath) => {
let trimmedFilePath = filePath.trim();
if (!path.isAbsolute(trimmedFilePath)) {
trimmedFilePath = path.join(collectionPath, trimmedFilePath);
}
form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath));
});
}
});
return form;
};
module.exports = {
buildFormUrlEncodedPayload,
createFormData
};

View File

@ -1,7 +1,7 @@
const { describe, it, expect } = require('@jest/globals'); const { describe, it, expect } = require('@jest/globals');
const prepareRequest = require('../../src/ipc/network/prepare-request'); const prepareRequest = require('../../src/ipc/network/prepare-request');
const { buildFormUrlEncodedPayload } = require('../../src/utils/common'); const { buildFormUrlEncodedPayload } = require('../../src/utils/form-data');
describe('prepare-request: prepareRequest', () => { describe('prepare-request: prepareRequest', () => {
describe('Decomments request body', () => { describe('Decomments request body', () => {

View File

@ -20,7 +20,7 @@
"graphql": "^16.6.0", "graphql": "^16.6.0",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"postcss": "8.4.47", "postcss": "8.4.47",
"react": "18.2.0", "react": "19.0.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"rollup":"3.29.5", "rollup":"3.29.5",
"rollup-plugin-dts": "^5.0.0", "rollup-plugin-dts": "^5.0.0",

View File

@ -58,6 +58,7 @@ chai.use(function (chai, utils) {
* endsWith : ends with * endsWith : ends with
* between : between * between : between
* isEmpty : is empty * isEmpty : is empty
* isNotEmpty : is not empty
* isNull : is null * isNull : is null
* isUndefined : is undefined * isUndefined : is undefined
* isDefined : is defined * isDefined : is defined
@ -95,6 +96,7 @@ const parseAssertionOperator = (str = '') => {
'endsWith', 'endsWith',
'between', 'between',
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -109,6 +111,7 @@ const parseAssertionOperator = (str = '') => {
const unaryOperators = [ const unaryOperators = [
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -147,6 +150,7 @@ const parseAssertionOperator = (str = '') => {
const isUnaryOperator = (operator) => { const isUnaryOperator = (operator) => {
const unaryOperators = [ const unaryOperators = [
'isEmpty', 'isEmpty',
'isNotEmpty',
'isNull', 'isNull',
'isUndefined', 'isUndefined',
'isDefined', 'isDefined',
@ -345,6 +349,9 @@ class AssertRuntime {
case 'isEmpty': case 'isEmpty':
expect(lhs).to.be.empty; expect(lhs).to.be.empty;
break; break;
case 'isNotEmpty':
expect(lhs).to.not.be.empty;
break;
case 'isNull': case 'isNull':
expect(lhs).to.be.null; expect(lhs).to.be.null;
break; break;

View File

@ -10,11 +10,11 @@ rm -rf packages/bruno-electron/web
mkdir packages/bruno-electron/web mkdir packages/bruno-electron/web
# Copy build # Copy build
cp -r packages/bruno-app/out/* packages/bruno-electron/web cp -r packages/bruno-app/dist/* packages/bruno-electron/web
# Change paths in next # Change paths in next
sed -i'' -e 's@/_next/@_next/@g' packages/bruno-electron/web/**.html sed -i'' -e 's@/static/@static/@g' packages/bruno-electron/web/**.html
# Remove sourcemaps # Remove sourcemaps
find packages/bruno-electron/web -name '*.map' -type f -delete find packages/bruno-electron/web -name '*.map' -type f -delete