mirror of
https://github.com/usebruno/bruno.git
synced 2024-11-21 23:43:15 +01:00
Merge branch 'main' of github.com:usebruno/bruno into feature/cli-specific-collection-path
This commit is contained in:
commit
0279381649
55
package-lock.json
generated
55
package-lock.json
generated
@ -50,6 +50,7 @@
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
"version": "2.3.0",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
@ -670,6 +671,7 @@
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -677,6 +679,7 @@
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
@ -740,6 +743,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.25.2",
|
||||
@ -754,6 +758,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^3.0.2"
|
||||
@ -761,6 +766,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
@ -839,6 +845,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.25.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.24.7",
|
||||
@ -905,6 +912,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-simple-access": {
|
||||
"version": "7.24.7",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.24.7",
|
||||
@ -942,6 +950,7 @@
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.24.8",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -962,6 +971,7 @@
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.25.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.25.0",
|
||||
@ -3644,37 +3654,6 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/vm2": {
|
||||
"version": "3.9.25",
|
||||
"resolved": "https://registry.npmjs.org/@n8n/vm2/-/vm2-3.9.25.tgz",
|
||||
"integrity": "sha512-qoGLFzyHBW7HKpwXkl05QKsIh3GkDw6lOiTOWYlUDnOIQ1b7EgM+O5EMjrMGy7r+kz52+Q7o6GLxBIcxVI8rEg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"acorn": "^8.7.0",
|
||||
"acorn-walk": "^8.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"vm2": "bin/vm2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.10",
|
||||
"pnpm": ">=9.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@n8n/vm2/node_modules/acorn": {
|
||||
"version": "8.12.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
|
||||
"integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "12.3.3",
|
||||
"license": "MIT"
|
||||
@ -4751,6 +4730,7 @@
|
||||
},
|
||||
"node_modules/@types/linkify-it": {
|
||||
"version": "5.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/lodash": {
|
||||
@ -4759,6 +4739,7 @@
|
||||
},
|
||||
"node_modules/@types/markdown-it": {
|
||||
"version": "12.2.3",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/linkify-it": "*",
|
||||
@ -4767,6 +4748,7 @@
|
||||
},
|
||||
"node_modules/@types/mdurl": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
@ -6240,6 +6222,7 @@
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.23.3",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -7238,6 +7221,7 @@
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
@ -8477,6 +8461,7 @@
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.11",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/electron-util": {
|
||||
@ -9438,6 +9423,7 @@
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@ -13186,6 +13172,7 @@
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.18",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-vault": {
|
||||
@ -16201,6 +16188,7 @@
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -17663,6 +17651,7 @@
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.0",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -18578,7 +18567,6 @@
|
||||
"inquirer": "^9.1.4",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"vm2": "^3.9.13",
|
||||
@ -18633,7 +18621,7 @@
|
||||
},
|
||||
"packages/bruno-electron": {
|
||||
"name": "bruno",
|
||||
"version": "v1.26.1",
|
||||
"version": "v1.28.0",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "3.525.0",
|
||||
"@usebruno/common": "0.1.0",
|
||||
@ -18664,7 +18652,6 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "3.3.4",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
|
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
@ -0,0 +1,56 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-weight: 600;
|
||||
table-layout: fixed;
|
||||
|
||||
thead,
|
||||
td {
|
||||
border: 1px solid ${(props) => props.theme.table.border};
|
||||
}
|
||||
|
||||
thead {
|
||||
color: ${(props) => props.theme.table.thead.color};
|
||||
font-size: 0.8125rem;
|
||||
user-select: none;
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-add-var {
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
border: solid 1px transparent;
|
||||
outline: none !important;
|
||||
background-color: inherit;
|
||||
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: solid 1px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
@ -0,0 +1,162 @@
|
||||
import React from 'react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import InfoTip from 'components/InfoTip';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import toast from 'react-hot-toast';
|
||||
import { variableNameRegex } from 'utils/common/regex';
|
||||
import {
|
||||
addCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionVar
|
||||
} from 'providers/ReduxStore/slices/collections/index';
|
||||
|
||||
const VarsTable = ({ collection, vars, varType }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
|
||||
const addVar = () => {
|
||||
dispatch(
|
||||
addCollectionVar({
|
||||
collectionUid: collection.uid,
|
||||
type: varType
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const onSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
const handleVarChange = (e, v, type) => {
|
||||
const _var = cloneDeep(v);
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
const value = e.target.value;
|
||||
|
||||
if (variableNameRegex.test(value) === false) {
|
||||
toast.error(
|
||||
'Variable contains invalid characters! Variables must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_var.name = value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
_var.value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
_var.enabled = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
dispatch(
|
||||
updateCollectionVar({
|
||||
type: varType,
|
||||
var: _var,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveVar = (_var) => {
|
||||
dispatch(
|
||||
deleteCollectionVar({
|
||||
type: varType,
|
||||
varUid: _var.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
{varType === 'request' ? (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Value</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
) : (
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<span>Expr</span>
|
||||
<InfoTip text="You can write any valid JS Template Literal here" infotipId="request-var" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{vars && vars.length
|
||||
? vars.map((_var) => {
|
||||
return (
|
||||
<tr key={_var.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={_var.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={_var.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleVarChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
_var,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={_var.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleVarChange(e, _var, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveVar(_var)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-var text-link pr-2 py-3 mt-2 select-none" onClick={addVar}>
|
||||
+ Add
|
||||
</button>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
export default VarsTable;
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import VarsTable from './VarsTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { saveCollectionRoot } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
const Vars = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const requestVars = get(collection, 'root.request.vars.req', []);
|
||||
const responseVars = get(collection, 'root.request.vars.res', []);
|
||||
const handleSave = () => dispatch(saveCollectionRoot(collection.uid));
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Pre Request</div>
|
||||
<VarsTable collection={collection} vars={requestVars} varType="request" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="mt-1 mb-1 title text-xs">Post Response</div>
|
||||
<VarsTable collection={collection} vars={responseVars} varType="response" />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<button type="submit" className="submit btn btn-sm btn-secondary" onClick={handleSave}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Vars;
|
@ -16,6 +16,7 @@ import Docs from './Docs';
|
||||
import Presets from './Presets';
|
||||
import Info from './Info';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Vars from './Vars/index';
|
||||
|
||||
const CollectionSettings = ({ collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -77,6 +78,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
case 'headers': {
|
||||
return <Headers collection={collection} />;
|
||||
}
|
||||
case 'vars': {
|
||||
return <Vars collection={collection} />;
|
||||
}
|
||||
case 'auth': {
|
||||
return <Auth collection={collection} />;
|
||||
}
|
||||
@ -123,6 +127,9 @@ const CollectionSettings = ({ collection }) => {
|
||||
<div className={getTabClassname('headers')} role="tab" onClick={() => setTab('headers')}>
|
||||
Headers
|
||||
</div>
|
||||
<div className={getTabClassname('vars')} role="tab" onClick={() => setTab('vars')}>
|
||||
Vars
|
||||
</div>
|
||||
<div className={getTabClassname('auth')} role="tab" onClick={() => setTab('auth')}>
|
||||
Auth
|
||||
</div>
|
||||
|
@ -116,6 +116,7 @@ const Headers = ({ collection, folder }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -130,6 +130,7 @@ const VarsTable = ({ folder, collection, vars, varType }) => {
|
||||
)
|
||||
}
|
||||
collection={collection}
|
||||
item={folder}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
112
packages/bruno-app/src/components/ReorderTable/index.js
Normal file
112
packages/bruno-app/src/components/ReorderTable/index.js
Normal file
@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { IconGripVertical, IconMinusVertical } from '@tabler/icons';
|
||||
|
||||
/**
|
||||
* ReorderTable Component
|
||||
*
|
||||
* A table component that allows rows to be reordered via drag-and-drop.
|
||||
*
|
||||
* @param {Object} props - The component props
|
||||
* @param {React.ReactNode[]} props.children - The table rows as children
|
||||
* @param {function} props.updateReorderedItem - Callback function to handle reordered rows
|
||||
*/
|
||||
|
||||
const ReorderTable = ({ children, updateReorderedItem }) => {
|
||||
const tbodyRef = useRef();
|
||||
const [rowsOrder, setRowsOrder] = useState(React.Children.toArray(children));
|
||||
const [hoveredRow, setHoveredRow] = useState(null);
|
||||
const [dragStart, setDragStart] = useState(null);
|
||||
|
||||
/**
|
||||
* useEffect hook to update the rows order and handle row hover states
|
||||
*/
|
||||
useEffect(() => {
|
||||
setRowsOrder(React.Children.toArray(children));
|
||||
handleRowHover(null, false);
|
||||
}, [children, dragStart]);
|
||||
|
||||
const handleRowHover = (index, hoverstatus = true) => {
|
||||
setHoveredRow(hoverstatus ? index : null);
|
||||
};
|
||||
|
||||
const handleDragStart = (e, index) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', index);
|
||||
setDragStart(index);
|
||||
};
|
||||
|
||||
const handleDragOver = (e, index) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
handleRowHover(index);
|
||||
};
|
||||
|
||||
const handleDrop = (e, toIndex) => {
|
||||
e.preventDefault();
|
||||
const fromIndex = parseInt(e.dataTransfer.getData('text/plain'), 10);
|
||||
if (fromIndex !== toIndex) {
|
||||
const updatedRowsOrder = [...rowsOrder];
|
||||
const [movedRow] = updatedRowsOrder.splice(fromIndex, 1);
|
||||
updatedRowsOrder.splice(toIndex, 0, movedRow);
|
||||
setRowsOrder(updatedRowsOrder);
|
||||
|
||||
updateReorderedItem({
|
||||
updateReorderedItem: updatedRowsOrder.map((row) => row.props['data-uid'])
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
handleRowHover(toIndex);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<tbody ref={tbodyRef}>
|
||||
{rowsOrder.map((row, index) => (
|
||||
<tr
|
||||
key={row.props['data-uid']}
|
||||
data-uid={row.props['data-uid']}
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, index)}
|
||||
onDragOver={(e) => handleDragOver(e, index)}
|
||||
onDrop={(e) => handleDrop(e, index)}
|
||||
onMouseEnter={() => handleRowHover(index)}
|
||||
onMouseLeave={() => handleRowHover(index, false)}
|
||||
>
|
||||
{React.Children.map(row.props.children, (child, childIndex) => {
|
||||
if (childIndex === 0) {
|
||||
return React.cloneElement(child, {
|
||||
children: (
|
||||
<>
|
||||
<div
|
||||
draggable
|
||||
className="group drag-handle absolute z-10 left-[-17px] p-3.5 py-3.5 px-2.5 top-[3px] cursor-grab"
|
||||
>
|
||||
{hoveredRow === index && (
|
||||
<>
|
||||
<IconGripVertical
|
||||
size={14}
|
||||
className="z-10 icon-grip rounded-md absolute hidden group-hover:block"
|
||||
/>
|
||||
<IconMinusVertical
|
||||
size={14}
|
||||
className="z-10 icon-minus rounded-md absolute block group-hover:hidden"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{child.props.children}
|
||||
</>
|
||||
)
|
||||
});
|
||||
} else {
|
||||
return child;
|
||||
}
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReorderTable;
|
@ -191,6 +191,7 @@ const AssertionRow = ({
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
) : (
|
||||
<input type="text" className="cursor-default" disabled />
|
||||
|
@ -6,6 +6,9 @@ import { updateRequestGraphqlVariables } from 'providers/ReduxStore/slices/colle
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { format, applyEdits } from 'jsonc-parser';
|
||||
import { IconWand } from '@tabler/icons';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -13,6 +16,25 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const { displayedTheme } = useTheme();
|
||||
const preferences = useSelector((state) => state.app.preferences);
|
||||
|
||||
const onPrettify = () => {
|
||||
if (!variables) return;
|
||||
try {
|
||||
const edits = format(variables, undefined, { tabSize: 2, insertSpaces: true });
|
||||
const prettyVariables = applyEdits(variables, edits);
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
variables: prettyVariables,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
toast.success('Variables prettified');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error('Error occurred while prettifying GraphQL variables');
|
||||
}
|
||||
};
|
||||
|
||||
const onEdit = (value) => {
|
||||
dispatch(
|
||||
updateRequestGraphqlVariables({
|
||||
@ -27,7 +49,14 @@ const GraphQLVariables = ({ variables, item, collection }) => {
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<StyledWrapper className="w-full relative">
|
||||
<button
|
||||
className="btn-add-param text-link px-4 py-4 select-none absolute top-0 right-0 z-10"
|
||||
onClick={onPrettify}
|
||||
title={'Prettify'}
|
||||
>
|
||||
<IconWand size={20} strokeWidth={1.5} />
|
||||
</button>
|
||||
<CodeEditor
|
||||
collection={collection}
|
||||
value={variables || ''}
|
||||
|
@ -97,6 +97,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
const script = getPropertyFromDraftOrRequest('request.script');
|
||||
const assertions = getPropertyFromDraftOrRequest('request.assertions');
|
||||
const tests = getPropertyFromDraftOrRequest('request.tests');
|
||||
const docs = getPropertyFromDraftOrRequest('request.docs');
|
||||
const requestVars = getPropertyFromDraftOrRequest('request.vars.req');
|
||||
const responseVars = getPropertyFromDraftOrRequest('request.vars.res');
|
||||
|
||||
@ -139,10 +140,11 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
</div>
|
||||
<div className={getTabClassname('tests')} role="tab" onClick={() => selectTab('tests')}>
|
||||
Tests
|
||||
{tests && <ContentIndicator />}
|
||||
{tests && tests.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
<div className={getTabClassname('docs')} role="tab" onClick={() => selectTab('docs')}>
|
||||
Docs
|
||||
{docs && docs.length > 0 && <ContentIndicator />}
|
||||
</div>
|
||||
{focusedTab.requestPaneTab === 'body' ? (
|
||||
<div className="flex flex-grow justify-end items-center">
|
||||
|
@ -22,14 +22,12 @@ const Wrapper = styled.div`
|
||||
}
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 70px;
|
||||
}
|
||||
td {
|
||||
&:nth-child(1) {
|
||||
padding: 0 0 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,14 +7,17 @@ import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import {
|
||||
addQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
updatePathParam,
|
||||
updateQueryParam
|
||||
moveQueryParam,
|
||||
updatePathParam
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import Table from 'components/Table/index';
|
||||
import ReorderTable from 'components/ReorderTable';
|
||||
|
||||
const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
@ -100,76 +103,75 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleParamDrag = ({ updateReorderedItem }) => {
|
||||
dispatch(
|
||||
moveQueryParam({
|
||||
collectionUid: collection.uid,
|
||||
itemUid: item.uid,
|
||||
updateReorderedItem
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<StyledWrapper className="w-full flex flex-col absolute">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-2 title text-xs">Query</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div className="mb-1 title text-xs">Query</div>
|
||||
|
||||
<Table
|
||||
headers={[
|
||||
{ name: 'Name', accessor: 'name', width: '31%' },
|
||||
{ name: 'Path', accessor: 'path', width: '56%' },
|
||||
{ name: '', accessor: '', width: '13%' }
|
||||
]}
|
||||
>
|
||||
<ReorderTable updateReorderedItem={handleParamDrag}>
|
||||
{queryParams && queryParams.length
|
||||
? queryParams.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
? queryParams.map((param, index) => (
|
||||
<tr key={param.uid} data-uid={param.uid}>
|
||||
<td className="flex relative">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) => handleQueryParamChange({ target: { value: newValue } }, param, 'value')}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
variablesAutocomplete={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleQueryParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'enabled')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
<button tabIndex="-1" onClick={() => handleRemoveQueryParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</ReorderTable>
|
||||
</Table>
|
||||
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
|
@ -132,6 +132,7 @@ const VarsTable = ({ item, collection, vars, varType }) => {
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
item={item}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -17,6 +17,8 @@ const Timeline = ({ request, response }) => {
|
||||
});
|
||||
});
|
||||
|
||||
let requestData = typeof request?.data === "string" ? request?.data : safeStringifyJSON(request?.data, true);
|
||||
|
||||
return (
|
||||
<StyledWrapper className="pb-4 w-full">
|
||||
<div>
|
||||
@ -31,10 +33,10 @@ const Timeline = ({ request, response }) => {
|
||||
);
|
||||
})}
|
||||
|
||||
{request.data ? (
|
||||
{requestData ? (
|
||||
<pre className="line request">
|
||||
<span className="arrow">{'>'}</span> data{' '}
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{request.data}</pre>
|
||||
<pre className="text-sm flex flex-wrap whitespace-break-spaces">{requestData}</pre>
|
||||
</pre>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -132,7 +132,7 @@ const Sidebar = () => {
|
||||
Star
|
||||
</GitHubButton> */}
|
||||
</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.27.0</div>
|
||||
<div className="flex flex-grow items-center justify-end text-xs mr-2">v1.28.0</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
63
packages/bruno-app/src/components/Table/StyledWrapper.js
Normal file
63
packages/bruno-app/src/components/Table/StyledWrapper.js
Normal file
@ -0,0 +1,63 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
table {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
overflow-y: hidden;
|
||||
overflow-x: auto;
|
||||
|
||||
// for icon hover
|
||||
position: inherit;
|
||||
left: -4px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
|
||||
grid-template-columns: ${({ columns }) =>
|
||||
columns?.[0]?.width
|
||||
? columns.map((col) => `${col?.width}`).join(' ')
|
||||
: columns.map((col) => `${100 / columns.length}%`).join(' ')};
|
||||
}
|
||||
|
||||
table thead,
|
||||
table tbody,
|
||||
table tr {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
table th {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
table tr td {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
|
||||
tr {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
tr.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
tr.hovered {
|
||||
transform: translateY(10px); /* Adjust the value as needed for the animation effect */
|
||||
}
|
||||
|
||||
table tr th {
|
||||
padding: 0.5rem;
|
||||
text-align: left;
|
||||
border-top: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
border-right: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
|
||||
&:nth-child(1) {
|
||||
border-left: 1px solid ${(props) => props.theme.collection.environment.settings.gridBorder}77;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default StyledWrapper;
|
110
packages/bruno-app/src/components/Table/index.js
Normal file
110
packages/bruno-app/src/components/Table/index.js
Normal file
@ -0,0 +1,110 @@
|
||||
import { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
const Table = ({ minColumnWidth = 1, headers = [], children }) => {
|
||||
const [activeColumnIndex, setActiveColumnIndex] = useState(null);
|
||||
const tableRef = useRef(null);
|
||||
|
||||
const columns = headers?.map((item) => ({
|
||||
...item,
|
||||
ref: useRef()
|
||||
}));
|
||||
|
||||
const updateDivHeights = () => {
|
||||
if (tableRef.current) {
|
||||
const height = tableRef.current.offsetHeight;
|
||||
columns.forEach((col) => {
|
||||
if (col.ref.current) {
|
||||
col.ref.current.querySelector('.resizer').style.height = `${height}px`;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateDivHeights();
|
||||
window.addEventListener('resize', updateDivHeights);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateDivHeights);
|
||||
};
|
||||
}, [columns]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tableRef.current) {
|
||||
const observer = new MutationObserver(updateDivHeights);
|
||||
observer.observe(tableRef.current, { childList: true, subtree: true });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
const handleMouseDown = (index) => (e) => {
|
||||
setActiveColumnIndex(index);
|
||||
};
|
||||
|
||||
const handleMouseMove = useCallback(
|
||||
(e) => {
|
||||
const gridColumns = columns.map((col, i) => {
|
||||
if (i === activeColumnIndex) {
|
||||
const width = e.clientX - col.ref?.current?.getBoundingClientRect()?.left;
|
||||
|
||||
if (width >= minColumnWidth) {
|
||||
return `${width}px`;
|
||||
}
|
||||
}
|
||||
return `${col.ref.current.offsetWidth}px`;
|
||||
});
|
||||
|
||||
tableRef.current.style.gridTemplateColumns = `${gridColumns.join(' ')}`;
|
||||
},
|
||||
[activeColumnIndex, columns, minColumnWidth]
|
||||
);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setActiveColumnIndex(null);
|
||||
removeListeners();
|
||||
}, [removeListeners]);
|
||||
|
||||
const removeListeners = useCallback(() => {
|
||||
window.removeEventListener('mousemove', handleMouseMove);
|
||||
window.removeEventListener('mouseup', removeListeners);
|
||||
}, [handleMouseMove]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeColumnIndex !== null) {
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
}
|
||||
return () => {
|
||||
removeListeners();
|
||||
};
|
||||
}, [activeColumnIndex, handleMouseMove, handleMouseUp, removeListeners]);
|
||||
|
||||
return (
|
||||
<StyledWrapper columns={columns}>
|
||||
<div className="relative">
|
||||
<table ref={tableRef} className="px-4 inherit left-[4px]">
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(({ ref, name }, i) => (
|
||||
<th ref={ref} key={name} title={name}>
|
||||
<span>{name}</span>
|
||||
<div
|
||||
className="resizer absolute cursor-col-resize w-[4px] right-[-2px] top-0 z-10 opacity-50 hover:bg-blue-500 active:bg-blue-500"
|
||||
onMouseDown={handleMouseDown(i)}
|
||||
></div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
{children}
|
||||
</table>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
@ -60,7 +60,7 @@ const trackStart = () => {
|
||||
event: 'start',
|
||||
properties: {
|
||||
os: platformLib.os.family,
|
||||
version: '1.27.0'
|
||||
version: '1.28.0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -503,6 +503,39 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
moveQueryParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
// Ensure item.draft is a deep clone of item if not already present
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
// Extract payload data
|
||||
const { updateReorderedItem } = action.payload;
|
||||
const params = item.draft.request.params;
|
||||
|
||||
item.draft.request.params = updateReorderedItem.map((uid) => {
|
||||
return params.find((param) => param.uid === uid);
|
||||
});
|
||||
|
||||
// update request url
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled && p.type === 'query'));
|
||||
if (query && query.length) {
|
||||
item.draft.request.url = parts[0] + '?' + query;
|
||||
} else {
|
||||
item.draft.request.url = parts[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
updateQueryParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1302,6 +1335,71 @@ export const collectionsSlice = createSlice({
|
||||
set(collection, 'root.request.headers', headers);
|
||||
}
|
||||
},
|
||||
addCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (type === 'request') {
|
||||
const vars = get(collection, 'root.request.vars.req', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
const vars = get(collection, 'root.request.vars.res', []);
|
||||
vars.push({
|
||||
uid: uuid(),
|
||||
name: '',
|
||||
value: '',
|
||||
enabled: true
|
||||
});
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
const _var = find(vars, (h) => h.uid === action.payload.var.uid);
|
||||
if (_var) {
|
||||
_var.name = action.payload.var.name;
|
||||
_var.value = action.payload.var.value;
|
||||
_var.description = action.payload.var.description;
|
||||
_var.enabled = action.payload.var.enabled;
|
||||
}
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
},
|
||||
deleteCollectionVar: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
const type = action.payload.type;
|
||||
if (collection) {
|
||||
if (type === 'request') {
|
||||
let vars = get(collection, 'root.request.vars.req', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.req', vars);
|
||||
} else if (type === 'response') {
|
||||
let vars = get(collection, 'root.request.vars.res', []);
|
||||
vars = filter(vars, (h) => h.uid !== action.payload.varUid);
|
||||
set(collection, 'root.request.vars.res', vars);
|
||||
}
|
||||
}
|
||||
},
|
||||
collectionAddFileEvent: (state, action) => {
|
||||
const file = action.payload.file;
|
||||
const isCollectionRoot = file.meta.collectionRoot ? true : false;
|
||||
@ -1655,6 +1753,7 @@ export const {
|
||||
requestUrlChanged,
|
||||
updateAuth,
|
||||
addQueryParam,
|
||||
moveQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
updatePathParam,
|
||||
@ -1694,6 +1793,9 @@ export const {
|
||||
addCollectionHeader,
|
||||
updateCollectionHeader,
|
||||
deleteCollectionHeader,
|
||||
addCollectionVar,
|
||||
updateCollectionVar,
|
||||
deleteCollectionVar,
|
||||
updateCollectionAuthMode,
|
||||
updateCollectionAuth,
|
||||
updateCollectionRequestScript,
|
||||
|
@ -787,24 +787,25 @@ export const getTotalRequestCountInCollection = (collection) => {
|
||||
};
|
||||
|
||||
export const getAllVariables = (collection, item) => {
|
||||
const environmentVariables = getEnvironmentVariables(collection);
|
||||
let requestVariables = {};
|
||||
if (item?.request) {
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
requestVariables = mergeFolderLevelVars(item?.request, requestTreePath);
|
||||
}
|
||||
const envVariables = getEnvironmentVariables(collection);
|
||||
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
||||
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
|
||||
const pathParams = getPathParams(item);
|
||||
|
||||
const { processEnvVariables = {}, runtimeVariables = {} } = collection;
|
||||
|
||||
return {
|
||||
...environmentVariables,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...collection.runtimeVariables,
|
||||
...runtimeVariables,
|
||||
pathParams: {
|
||||
...pathParams
|
||||
},
|
||||
process: {
|
||||
env: {
|
||||
...collection.processEnvVariables
|
||||
...processEnvVariables
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -831,14 +832,22 @@ const getTreePathFromCollectionToItem = (collection, _item) => {
|
||||
return path;
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
const mergeVars = (collection, requestTreePath = []) => {
|
||||
let collectionVariables = {};
|
||||
let folderVariables = {};
|
||||
let requestVariables = {};
|
||||
let collectionRequestVars = get(collection, 'root.request.vars.req', []);
|
||||
collectionRequestVars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
collectionVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
for (let i of requestTreePath) {
|
||||
if (i.type === 'folder') {
|
||||
let vars = get(i, 'root.request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
requestVariables[_var.name] = _var.value;
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -850,6 +859,9 @@ const mergeFolderLevelVars = (request, requestTreePath = []) => {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return requestVariables;
|
||||
return {
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables
|
||||
};
|
||||
};
|
||||
|
@ -31,9 +31,8 @@ const readFile = (files) => {
|
||||
};
|
||||
|
||||
const ensureUrl = (url) => {
|
||||
let protUrl = url.startsWith('http') ? url : `http://${url}`;
|
||||
// replace any double or triple slashes
|
||||
return protUrl.replace(/([^:]\/)\/+/g, '$1');
|
||||
// emoving multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
|
||||
return url.replace(/(^\w+:|^)\/{2,}/, '$1/');
|
||||
};
|
||||
|
||||
const buildEmptyJsonBody = (bodySchema) => {
|
||||
@ -41,9 +40,12 @@ const buildEmptyJsonBody = (bodySchema) => {
|
||||
each(bodySchema.properties || {}, (prop, name) => {
|
||||
if (prop.type === 'object') {
|
||||
_jsonBody[name] = buildEmptyJsonBody(prop);
|
||||
// handle arrays
|
||||
} else if (prop.type === 'array') {
|
||||
_jsonBody[name] = [];
|
||||
if (prop.items && prop.items.type === 'object') {
|
||||
_jsonBody[name] = [buildEmptyJsonBody(prop.items)];
|
||||
} else {
|
||||
_jsonBody[name] = [];
|
||||
}
|
||||
} else {
|
||||
_jsonBody[name] = '';
|
||||
}
|
||||
@ -67,7 +69,7 @@ const transformOpenapiRequestItem = (request) => {
|
||||
name: operationName,
|
||||
type: 'http-request',
|
||||
request: {
|
||||
url: ensureUrl(request.global.server + '/' + path),
|
||||
url: ensureUrl(request.global.server + path),
|
||||
method: request.method.toUpperCase(),
|
||||
auth: {
|
||||
mode: 'none',
|
||||
@ -165,6 +167,9 @@ const transformOpenapiRequestItem = (request) => {
|
||||
let _jsonBody = buildEmptyJsonBody(bodySchema);
|
||||
brunoRequestItem.request.body.json = JSON.stringify(_jsonBody, null, 2);
|
||||
}
|
||||
if (bodySchema && bodySchema.type === 'array') {
|
||||
brunoRequestItem.request.body.json = JSON.stringify([buildEmptyJsonBody(bodySchema.items)], null, 2);
|
||||
}
|
||||
} else if (mimeType === 'application/x-www-form-urlencoded') {
|
||||
brunoRequestItem.request.body.mode = 'formUrlEncoded';
|
||||
if (bodySchema && bodySchema.type === 'object') {
|
||||
@ -224,7 +229,7 @@ const transformOpenapiRequestItem = (request) => {
|
||||
return brunoRequestItem;
|
||||
};
|
||||
|
||||
const resolveRefs = (spec, components = spec.components, visitedItems = new Set()) => {
|
||||
const resolveRefs = (spec, components = spec?.components, visitedItems = new Set()) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return spec;
|
||||
}
|
||||
@ -248,7 +253,7 @@ const resolveRefs = (spec, components = spec.components, visitedItems = new Set(
|
||||
let ref = components;
|
||||
|
||||
for (const key of refKeys) {
|
||||
if (ref[key]) {
|
||||
if (ref && ref[key]) {
|
||||
ref = ref[key];
|
||||
} else {
|
||||
// Handle invalid references gracefully?
|
||||
@ -306,7 +311,7 @@ const getDefaultUrl = (serverObject) => {
|
||||
url = url.replace(`{${variableName}}`, sub);
|
||||
});
|
||||
}
|
||||
return url;
|
||||
return url.endsWith('/') ? url : `${url}/`;
|
||||
};
|
||||
|
||||
const getSecurity = (apiSpec) => {
|
||||
|
@ -40,7 +40,6 @@
|
||||
"inquirer": "^9.1.4",
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mustache": "^4.2.0",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
"vm2": "^3.9.13",
|
||||
|
@ -60,20 +60,6 @@ const runSingleRequest = async function (
|
||||
request.data = form;
|
||||
}
|
||||
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(bruJson, 'request.vars.req');
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run pre request script
|
||||
const requestScriptFile = compact([
|
||||
get(collectionRoot, 'request.script.req'),
|
||||
@ -276,7 +262,7 @@ const runSingleRequest = async function (
|
||||
|
||||
console.log(
|
||||
chalk.green(stripExtension(filename)) +
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
chalk.dim(` (${response.status} ${response.statusText}) - ${responseTime} ms`)
|
||||
);
|
||||
|
||||
// run post-response vars
|
||||
|
@ -1,12 +1,6 @@
|
||||
const _ = require('lodash');
|
||||
const Mustache = require('mustache');
|
||||
const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
const collectionBruToJson = (bru) => {
|
||||
try {
|
||||
const json = _collectionBruToJson(bru);
|
||||
@ -95,7 +89,7 @@ const getEnvVars = (environment = {}) => {
|
||||
const envVars = {};
|
||||
_.each(variables, (variable) => {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = Mustache.escape(variable.value);
|
||||
envVars[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "v1.27.0",
|
||||
"version": "v1.28.0",
|
||||
"name": "bruno",
|
||||
"description": "Opensource API Client for Exploring and Testing APIs",
|
||||
"homepage": "https://www.usebruno.com",
|
||||
@ -51,7 +51,6 @@
|
||||
"json-bigint": "^1.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^2.1.35",
|
||||
"mustache": "^4.2.0",
|
||||
"nanoid": "3.3.4",
|
||||
"qs": "^6.11.0",
|
||||
"socks-proxy-agent": "^8.0.2",
|
||||
|
@ -50,6 +50,19 @@ const checkConnection = (host, port) =>
|
||||
function makeAxiosInstance() {
|
||||
/** @type {axios.AxiosInstance} */
|
||||
const instance = axios.create({
|
||||
transformRequest: function transformRequest(data, headers) {
|
||||
// doesn't apply the default transformRequest if the data is a string, so that axios doesn't add quotes see :
|
||||
// https://github.com/usebruno/bruno/issues/2043
|
||||
// https://github.com/axios/axios/issues/4034
|
||||
const contentType = headers?.['Content-Type'] || headers?.['content-type'] || '';
|
||||
const hasJSONContentType = contentType.includes('json');
|
||||
if (typeof data === 'string' && hasJSONContentType) {
|
||||
return data;
|
||||
}
|
||||
|
||||
axios.defaults.transformRequest.forEach((tr) => data = tr(data, headers));
|
||||
return data;
|
||||
},
|
||||
proxy: false
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,6 @@ const tls = require('tls');
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
const decomment = require('decomment');
|
||||
const Mustache = require('mustache');
|
||||
const contentDispositionParser = require('content-disposition');
|
||||
const mime = require('mime-types');
|
||||
const { ipcMain } = require('electron');
|
||||
@ -39,11 +38,6 @@ const {
|
||||
const Oauth2Store = require('../../store/oauth2');
|
||||
const iconv = require('iconv-lite');
|
||||
|
||||
// override the default escape function to prevent escaping
|
||||
Mustache.escape = function (value) {
|
||||
return value;
|
||||
};
|
||||
|
||||
const safeStringifyJSON = (data) => {
|
||||
try {
|
||||
return JSON.stringify(data);
|
||||
@ -71,7 +65,7 @@ const getEnvVars = (environment = {}) => {
|
||||
const envVars = {};
|
||||
each(variables, (variable) => {
|
||||
if (variable.enabled) {
|
||||
envVars[variable.name] = Mustache.escape(variable.value);
|
||||
envVars[variable.name] = variable.value;
|
||||
}
|
||||
});
|
||||
|
||||
@ -88,6 +82,22 @@ const getJsSandboxRuntime = (collection) => {
|
||||
|
||||
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
|
||||
|
||||
const saveCookies = (url, headers) => {
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
let setCookieHeaders = [];
|
||||
if (headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(headers['set-cookie'])
|
||||
? headers['set-cookie']
|
||||
: [headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const configureRequest = async (
|
||||
collectionUid,
|
||||
request,
|
||||
@ -331,10 +341,10 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) =>
|
||||
// Filter out ZWNBSP character
|
||||
// https://gist.github.com/antic183/619f42b559b78028d1fe9e7ae8a1352d
|
||||
data = data.replace(/^\uFEFF/, '');
|
||||
if(!disableParsingResponseJson) {
|
||||
if (!disableParsingResponseJson) {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
|
||||
return { data, dataBuffer };
|
||||
};
|
||||
@ -360,20 +370,6 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) => {
|
||||
// run pre-request vars
|
||||
const preRequestVars = get(request, 'vars.req', []);
|
||||
if (preRequestVars?.length) {
|
||||
const varsRuntime = new VarsRuntime({ runtime: scriptingConfig?.runtime });
|
||||
varsRuntime.runPreRequestVars(
|
||||
preRequestVars,
|
||||
request,
|
||||
envVars,
|
||||
runtimeVariables,
|
||||
collectionPath,
|
||||
processEnvVars
|
||||
);
|
||||
}
|
||||
|
||||
// run pre-request script
|
||||
let scriptResult;
|
||||
const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(os.EOL);
|
||||
@ -590,17 +586,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
|
||||
// save cookies
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
let setCookieHeaders = [];
|
||||
if (response.headers['set-cookie']) {
|
||||
setCookieHeaders = Array.isArray(response.headers['set-cookie'])
|
||||
? response.headers['set-cookie']
|
||||
: [response.headers['set-cookie']];
|
||||
for (let setCookieHeader of setCookieHeaders) {
|
||||
if (typeof setCookieHeader === 'string' && setCookieHeader.length) {
|
||||
addCookieToJar(setCookieHeader, request.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
saveCookies(request.url, response.headers);
|
||||
}
|
||||
|
||||
// send domain cookies to renderer
|
||||
@ -1016,6 +1002,16 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
response.data = data;
|
||||
response.responseTime = response.headers.get('request-duration');
|
||||
|
||||
// save cookies
|
||||
if (preferencesUtil.shouldStoreCookies()) {
|
||||
saveCookies(request.url, response.headers);
|
||||
}
|
||||
|
||||
// send domain cookies to renderer
|
||||
const domainsWithCookies = await getDomainsWithCookies();
|
||||
|
||||
mainWindow.webContents.send('main:cookies-update', safeParseJSON(safeStringifyJSON(domainsWithCookies)));
|
||||
|
||||
mainWindow.webContents.send('main:run-folder-event', {
|
||||
type: 'response-received',
|
||||
responseReceived: {
|
||||
@ -1201,7 +1197,7 @@ const registerNetworkIpc = (mainWindow) => {
|
||||
try {
|
||||
const disposition = contentDispositionParser.parse(contentDisposition);
|
||||
return disposition && disposition.parameters['filename'];
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
};
|
||||
|
||||
const getFileNameFromUrlPath = () => {
|
||||
|
@ -12,15 +12,17 @@ const getContentType = (headers = {}) => {
|
||||
return contentType;
|
||||
};
|
||||
|
||||
const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, processEnvVars = {}) => {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
// we clone envVars because we don't want to modify the original object
|
||||
envVars = cloneDeep(envVars);
|
||||
envVariables = cloneDeep(envVariables);
|
||||
|
||||
// envVars can inturn have values as {{process.env.VAR_NAME}}
|
||||
// so we need to interpolate envVars first with processEnvVars
|
||||
forOwn(envVars, (value, key) => {
|
||||
envVars[key] = interpolate(value, {
|
||||
forOwn(envVariables, (value, key) => {
|
||||
envVariables[key] = interpolate(value, {
|
||||
process: {
|
||||
env: {
|
||||
...processEnvVars
|
||||
@ -36,7 +38,9 @@ const interpolateVars = (request, envVars = {}, runtimeVariables = {}, processEn
|
||||
|
||||
// runtimeVariables take precedence over envVars
|
||||
const combinedVars = {
|
||||
...envVars,
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
|
@ -44,73 +44,75 @@ const mergeFolderLevelHeaders = (request, requestTreePath) => {
|
||||
request.headers = Array.from(requestHeadersMap, ([name, value]) => ({ name, value, enabled: true }));
|
||||
};
|
||||
|
||||
const mergeFolderLevelVars = (request, requestTreePath) => {
|
||||
let folderReqVars = new Map();
|
||||
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) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
reqVars.set(_var.name, _var.value);
|
||||
folderVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
} else if (i.uid === request.uid) {
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderReqVars.set(_var.name, _var.value);
|
||||
reqVars.set(_var.name, _var.value);
|
||||
requestVariables[_var.name] = _var.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mergedFolderReqVars = Array.from(folderReqVars, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestReqVars = request?.vars?.req || [];
|
||||
let requestReqVarsMap = new Map();
|
||||
for (let _var of requestReqVars) {
|
||||
if (_var.enabled) {
|
||||
requestReqVarsMap.set(_var.name, _var.value);
|
||||
}
|
||||
}
|
||||
mergedFolderReqVars.forEach((_var) => {
|
||||
requestReqVarsMap.set(_var.name, _var.value);
|
||||
});
|
||||
request.vars.req = Array.from(requestReqVarsMap, ([name, 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 folderResVars = new Map();
|
||||
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) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
} else if (i.uid === request.uid) {
|
||||
} else {
|
||||
const vars = i?.draft ? get(i, 'draft.request.vars.res', []) : get(i, 'request.vars.res', []);
|
||||
vars.forEach((_var) => {
|
||||
if (_var.enabled) {
|
||||
folderResVars.set(_var.name, _var.value);
|
||||
resVars.set(_var.name, _var.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
let mergedFolderResVars = Array.from(folderResVars, ([name, value]) => ({ name, value, enabled: true }));
|
||||
let requestResVars = request?.vars?.res || [];
|
||||
let requestResVarsMap = new Map();
|
||||
for (let _var of requestResVars) {
|
||||
if (_var.enabled) {
|
||||
requestResVarsMap.set(_var.name, _var.value);
|
||||
}
|
||||
}
|
||||
mergedFolderResVars.forEach((_var) => {
|
||||
requestResVarsMap.set(_var.name, _var.value);
|
||||
});
|
||||
request.vars.res = Array.from(requestResVarsMap, ([name, value]) => ({
|
||||
|
||||
request.vars.res = Array.from(resVars, ([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
enabled: true,
|
||||
@ -314,7 +316,7 @@ const prepareRequest = (item, collection) => {
|
||||
if (requestTreePath && requestTreePath.length > 0) {
|
||||
mergeFolderLevelHeaders(request, requestTreePath);
|
||||
mergeFolderLevelScripts(request, requestTreePath, scriptFlow);
|
||||
mergeFolderLevelVars(request, requestTreePath);
|
||||
mergeVars(collection, request, requestTreePath);
|
||||
}
|
||||
|
||||
each(request.headers, (h) => {
|
||||
@ -401,6 +403,9 @@ const prepareRequest = (item, collection) => {
|
||||
}
|
||||
|
||||
axiosRequest.vars = request.vars;
|
||||
axiosRequest.collectionVariables = request.collectionVariables;
|
||||
axiosRequest.folderVariables = request.folderVariables;
|
||||
axiosRequest.requestVariables = request.requestVariables;
|
||||
axiosRequest.assertions = request.assertions;
|
||||
|
||||
return axiosRequest;
|
||||
|
@ -4,10 +4,12 @@ const { interpolate } = require('@usebruno/common');
|
||||
const variableNameRegex = /^[\w-.]*$/;
|
||||
|
||||
class Bru {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables) {
|
||||
constructor(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables) {
|
||||
this.envVariables = envVariables || {};
|
||||
this.runtimeVariables = runtimeVariables || {};
|
||||
this.processEnvVars = cloneDeep(processEnvVars || {});
|
||||
this.collectionVariables = collectionVariables || {};
|
||||
this.folderVariables = folderVariables || {};
|
||||
this.requestVariables = requestVariables || {};
|
||||
this.collectionPath = collectionPath;
|
||||
}
|
||||
@ -18,7 +20,9 @@ class Bru {
|
||||
}
|
||||
|
||||
const combinedVars = {
|
||||
...this.collectionVariables,
|
||||
...this.envVariables,
|
||||
...this.folderVariables,
|
||||
...this.requestVariables,
|
||||
...this.runtimeVariables,
|
||||
process: {
|
||||
@ -71,7 +75,7 @@ class Bru {
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters!` +
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
}
|
||||
|
||||
@ -82,7 +86,7 @@ class Bru {
|
||||
if (variableNameRegex.test(key) === false) {
|
||||
throw new Error(
|
||||
`Variable name: "${key}" contains invalid characters!` +
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
' Names must only contain alpha-numeric characters, "-", "_", "."'
|
||||
);
|
||||
}
|
||||
|
||||
@ -93,6 +97,14 @@ class Bru {
|
||||
delete this.runtimeVariables[key];
|
||||
}
|
||||
|
||||
getCollectionVar(key) {
|
||||
return this._interpolate(this.collectionVariables[key]);
|
||||
}
|
||||
|
||||
getFolderVar(key) {
|
||||
return this._interpolate(this.folderVariables[key]);
|
||||
}
|
||||
|
||||
getRequestVar(key) {
|
||||
return this._interpolate(this.requestVariables[key]);
|
||||
}
|
||||
|
@ -2,14 +2,16 @@ const { interpolate } = require('@usebruno/common');
|
||||
|
||||
const interpolateString = (
|
||||
str,
|
||||
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, requestVariables = {} }
|
||||
{ envVariables = {}, runtimeVariables = {}, processEnvVars = {}, collectionVariables = {}, folderVariables = {}, requestVariables = {} }
|
||||
) => {
|
||||
if (!str || !str.length || typeof str !== 'string') {
|
||||
return str;
|
||||
}
|
||||
|
||||
const combinedVars = {
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
process: {
|
||||
|
@ -192,6 +192,8 @@ const evaluateRhsOperand = (rhsOperand, operator, context, runtime) => {
|
||||
}
|
||||
|
||||
const interpolationContext = {
|
||||
collectionVariables: context.bru.collectionVariables,
|
||||
folderVariables: context.bru.folderVariables,
|
||||
requestVariables: context.bru.requestVariables,
|
||||
runtimeVariables: context.bru.runtimeVariables,
|
||||
envVariables: context.bru.envVariables,
|
||||
@ -238,13 +240,23 @@ class AssertRuntime {
|
||||
}
|
||||
|
||||
runAssertions(assertions, request, response, envVariables, runtimeVariables, processEnvVars) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const enabledAssertions = _.filter(assertions, (a) => a.enabled);
|
||||
if (!enabledAssertions.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, undefined, requestVariables);
|
||||
const bru = new Bru(
|
||||
envVariables,
|
||||
runtimeVariables,
|
||||
processEnvVars,
|
||||
undefined,
|
||||
collectionVariables,
|
||||
folderVariables,
|
||||
requestVariables
|
||||
);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = createResponseParser(response);
|
||||
|
||||
@ -255,7 +267,9 @@ class AssertRuntime {
|
||||
};
|
||||
|
||||
const context = {
|
||||
...collectionVariables,
|
||||
...envVariables,
|
||||
...folderVariables,
|
||||
...requestVariables,
|
||||
...runtimeVariables,
|
||||
...processEnvVars,
|
||||
|
@ -47,8 +47,10 @@ class ScriptRuntime {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
|
||||
@ -162,8 +164,10 @@ class ScriptRuntime {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
@ -48,8 +48,10 @@ class TestRuntime {
|
||||
processEnvVars,
|
||||
scriptingConfig
|
||||
) {
|
||||
const collectionVariables = request?.collectionVariables || {};
|
||||
const folderVariables = request?.folderVariables || {};
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, requestVariables);
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars, collectionPath, collectionVariables, folderVariables, requestVariables);
|
||||
const req = new BrunoRequest(request);
|
||||
const res = new BrunoResponse(response);
|
||||
const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
|
||||
|
@ -1,22 +1,10 @@
|
||||
const _ = require('lodash');
|
||||
const Bru = require('../bru');
|
||||
const BrunoRequest = require('../bruno-request');
|
||||
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
const { evaluateJsExpression, createResponseParser } = require('../utils');
|
||||
|
||||
const { executeQuickJsVm } = require('../sandbox/quickjs');
|
||||
|
||||
const evaluateJsTemplateLiteralBasedOnRuntime = (literal, context, runtime) => {
|
||||
if (runtime === 'quickjs') {
|
||||
return executeQuickJsVm({
|
||||
script: literal,
|
||||
context,
|
||||
scriptType: 'template-literal'
|
||||
});
|
||||
}
|
||||
|
||||
return evaluateJsTemplateLiteral(literal, context);
|
||||
};
|
||||
|
||||
const evaluateJsExpressionBasedOnRuntime = (expr, context, runtime, mode) => {
|
||||
if (runtime === 'quickjs') {
|
||||
return executeQuickJsVm({
|
||||
@ -35,35 +23,6 @@ class VarsRuntime {
|
||||
this.mode = props?.mode || 'developer';
|
||||
}
|
||||
|
||||
runPreRequestVars(vars, request, envVariables, runtimeVariables, collectionPath, processEnvVars) {
|
||||
if (!request?.requestVariables) {
|
||||
request.requestVariables = {};
|
||||
}
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
if (!enabledVars.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bru = new Bru(envVariables, runtimeVariables, processEnvVars);
|
||||
const req = new BrunoRequest(request);
|
||||
|
||||
const bruContext = {
|
||||
bru,
|
||||
req
|
||||
};
|
||||
|
||||
const context = {
|
||||
...envVariables,
|
||||
...runtimeVariables,
|
||||
...bruContext
|
||||
};
|
||||
|
||||
_.each(enabledVars, (v) => {
|
||||
const value = evaluateJsTemplateLiteralBasedOnRuntime(v.value, context, this.runtime);
|
||||
request?.requestVariables && (request.requestVariables[v.name] = value);
|
||||
});
|
||||
}
|
||||
|
||||
runPostResponseVars(vars, request, response, envVariables, runtimeVariables, collectionPath, processEnvVars) {
|
||||
const requestVariables = request?.requestVariables || {};
|
||||
const enabledVars = _.filter(vars, (v) => v.enabled);
|
||||
|
@ -69,6 +69,18 @@ const addBruShimToContext = (vm, bru) => {
|
||||
vm.setProp(bruObject, 'getRequestVar', getRequestVar);
|
||||
getRequestVar.dispose();
|
||||
|
||||
let getFolderVar = vm.newFunction('getFolderVar', function (key) {
|
||||
return marshallToVm(bru.getFolderVar(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getFolderVar', getFolderVar);
|
||||
getFolderVar.dispose();
|
||||
|
||||
let getCollectionVar = vm.newFunction('getCollectionVar', function (key) {
|
||||
return marshallToVm(bru.getCollectionVar(vm.dump(key)), vm);
|
||||
});
|
||||
vm.setProp(bruObject, 'getCollectionVar', getCollectionVar);
|
||||
getCollectionVar.dispose();
|
||||
|
||||
const sleep = vm.newFunction('sleep', (timer) => {
|
||||
const t = vm.getString(timer);
|
||||
const promise = vm.newPromise();
|
||||
|
@ -1,5 +1,6 @@
|
||||
headers {
|
||||
check: again
|
||||
token: {{collection_pre_var_token}}
|
||||
}
|
||||
|
||||
auth {
|
||||
@ -10,6 +11,11 @@ auth:bearer {
|
||||
token: {{bearer_auth_token}}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
collection_pre_var: collection_pre_var_value
|
||||
collection_pre_var_token: {{request_pre_var_token}}
|
||||
}
|
||||
|
||||
docs {
|
||||
# bruno-testbench 🐶
|
||||
|
||||
|
@ -25,16 +25,6 @@ body:json {
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
boolean: false
|
||||
undefined: undefined
|
||||
null: null
|
||||
string: foo
|
||||
number_1: 1
|
||||
number_2: 0
|
||||
number_3: -1
|
||||
}
|
||||
|
||||
assert {
|
||||
req.body.boolean: isBoolean false
|
||||
req.body.number_1: isNumber 1
|
||||
@ -51,35 +41,4 @@ assert {
|
||||
req.body.number_3: eq -1
|
||||
req.body.number_2: isNumber
|
||||
req.body.number_3: isNumber
|
||||
boolean: eq false
|
||||
undefined: eq undefined
|
||||
null: eq null
|
||||
string: eq foo
|
||||
number_1: eq 1
|
||||
number_2: eq 0
|
||||
number_3: eq -1
|
||||
}
|
||||
|
||||
tests {
|
||||
test("boolean pre var", function() {
|
||||
expect(bru.getRequestVar('boolean')).to.eql(false);
|
||||
});
|
||||
|
||||
test("number pre var", function() {
|
||||
expect(bru.getRequestVar('number_1')).to.eql(1);
|
||||
expect(bru.getRequestVar('number_2')).to.eql(0);
|
||||
expect(bru.getRequestVar('number_3')).to.eql(-1);
|
||||
});
|
||||
|
||||
test("null pre var", function() {
|
||||
expect(bru.getRequestVar('null')).to.eql(null);
|
||||
});
|
||||
|
||||
test("undefined pre var", function() {
|
||||
expect(bru.getRequestVar('undefined')).to.eql(undefined);
|
||||
});
|
||||
|
||||
test("string pre var", function() {
|
||||
expect(bru.getRequestVar('string')).to.eql('foo');
|
||||
});
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
meta {
|
||||
name: vars asserts
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/api/echo/json
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"boolean": false,
|
||||
"number": 1,
|
||||
"string": "bruno",
|
||||
"array": [1, 2, 3, 4, 5],
|
||||
"object": {
|
||||
"hello": "bruno"
|
||||
},
|
||||
"null": null
|
||||
}
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
vars_asserts__request_var: vars_asserts__request_var__value
|
||||
}
|
||||
|
||||
assert {
|
||||
vars_asserts__request_var: eq vars_asserts__request_var__value
|
||||
vars_asserts__runtime_var: eq vars_asserts__runtime_var__value
|
||||
vars_asserts__env_var: eq vars_asserts__env_var__value
|
||||
}
|
||||
|
||||
script:pre-request {
|
||||
bru.setVar('vars_asserts__runtime_var', 'vars_asserts__runtime_var__value');
|
||||
bru.setEnvVar('vars_asserts__env_var', 'vars_asserts__env_var__value');
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: string interpolation
|
||||
}
|
||||
|
||||
vars:pre-request {
|
||||
folder_pre_var: folder_pre_var_value
|
||||
folder_pre_var_2: {{env.var1}}
|
||||
}
|
Loading…
Reference in New Issue
Block a user