mirror of
https://github.com/usebruno/bruno.git
synced 2024-12-22 14:41:04 +01:00
Feat/improved path params (#2357)
* feat: path parameters (#484) * add path parameters on bruno-app * add path parameters on bruno-cli * fix bruno-schema testing * fix generate request code not replace path parameter value --------- Co-authored-by: game5413 <febryanph10@gmail.com> Co-authored-by: Anoop M D <anoop.md1421@gmail.com> * feat: Refactor request parameter handling - Update prepare-request.js to filter and rename 'paths' to 'params' with type 'path' - Remove 'paths' from export.js and interpolate-vars.js - Update bru.js to use 'params' instead of 'path' - Update requestSchema in index.js to use 'keyValueWithTypeSchema' for 'params' Co-authored-by: game5413 <febryanph10@gmail.com> Co-authored-by: Anoop M D <anoop.md1421@gmail.com> * feat: Refactor request parameter handling * refactor: changes form the review * refactor: Refactor transformItemsInCollection handling * refactor: Refactor improved export/import functionalities * refactor: Remove console.log statement in bruToJson.js --------- Co-authored-by: game5413 <37659721+game5413@users.noreply.github.com> Co-authored-by: game5413 <febryanph10@gmail.com> Co-authored-by: Anoop M D <anoop.md1421@gmail.com>
This commit is contained in:
parent
77b1e6d738
commit
abfd14a306
@ -1,5 +1,5 @@
|
||||
**English** | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
|
||||
| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md)
|
||||
| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | [日本語](docs/contributing/contributing_ja.md) | [हिंदी](docs/contributing/contributing_hi.md)
|
||||
|
||||
## Let's make Bruno better, together !!
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
[English](../../contributing.md) | [Українська](docs/contributing/contributing_ua.md) | [Русский](docs/contributing/contributing_ru.md) | [Türkçe](docs/contributing/contributing_tr.md) | [Deutsch](docs/contributing/contributing_de.md) | [Français](docs/contributing/contributing_fr.md) | [Português (BR)](docs/contributing/contributing_pt_br.md) | [বাংলা](docs/contributing/contributing_bn.md) | [Español](docs/contributing/contributing_es.md) | [Română](docs/contributing/contributing_ro.md) | [Polski](docs/contributing/contributing_pl.md)
|
||||
| [简体中文](docs/contributing/contributing_cn.md) | [正體中文](docs/contributing/contributing_zhtw.md) | **日本語**
|
||||
|
||||
## 一緒にBrunoをよりよいものにしていきましょう!!
|
||||
## 一緒に Bruno をよりよいものにしていきましょう!!
|
||||
|
||||
Brunoを改善していただけるのは歓迎です。以下はあなたの環境でBrunoを起動するためのガイドラインです。
|
||||
Bruno を改善していただけるのは歓迎です。以下はあなたの環境で Bruno を起動するためのガイドラインです。
|
||||
|
||||
### 技術スタック
|
||||
|
||||
BrunoはNext.jsとReactで作られています。デスクトップアプリ(ローカルのコレクションに対応しています)にはelectronも使用しています。
|
||||
Bruno は Next.js と React で作られています。デスクトップアプリ(ローカルのコレクションに対応しています)には electron も使用しています。
|
||||
|
||||
使用ライブラリ
|
||||
|
||||
@ -22,11 +22,11 @@ BrunoはNext.jsとReactで作られています。デスクトップアプリ(
|
||||
|
||||
### 依存関係
|
||||
|
||||
[Node v18.x もしくは最新のLTSバージョン](https://nodejs.org/en/)とnpm 8.xが必要です。プロジェクトにnpmワークスペースを使用しています。
|
||||
[Node v18.x もしくは最新の LTS バージョン](https://nodejs.org/en/)と npm 8.x が必要です。プロジェクトに npm ワークスペースを使用しています。
|
||||
|
||||
## 開発
|
||||
|
||||
Brunoはデスクトップアプリとして開発されています。一つのターミナルでNext.jsアプリを立ち上げ、もう一つのターミナルでelectronアプリを立ち上げてアプリを読み込む必要があります。
|
||||
Bruno はデスクトップアプリとして開発されています。一つのターミナルで Next.js アプリを立ち上げ、もう一つのターミナルで electron アプリを立ち上げてアプリを読み込む必要があります。
|
||||
|
||||
### ローカル環境での開発
|
||||
|
||||
|
@ -33,7 +33,7 @@ Bruno jest rozwijane jako aplikacja desktopowa. Musisz załadować aplikację, u
|
||||
|
||||
### Lokalny Rozwój
|
||||
|
||||
```bash
|
||||
````bash
|
||||
# użyj wersji nodejs 18
|
||||
nvm use
|
||||
|
||||
@ -66,7 +66,7 @@ done
|
||||
# Usuń package-lock w podkatalogach
|
||||
find . -type f -name "package-lock.json" -delete
|
||||
|
||||
```
|
||||
````
|
||||
|
||||
### Testowanie
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
[English](../../publishing.md) | [Português (BR)](docs/publishing/publishing_pt_br.md) | [Română](docs/publishing/publishing_ro.md) | [Polski](docs/publishing/publishing_pl.md) | [বাংলা](docs/publishing/publishing_bn.md) | [Français](docs/publishing/publishing_fr.md) | [正體中文](docs/publishing/publishing_zhtw.md) | **日本語**
|
||||
|
||||
### Brunoを新しいパッケージマネージャに公開する場合の注意
|
||||
### Bruno を新しいパッケージマネージャに公開する場合の注意
|
||||
|
||||
私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私はBrunoの製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャでBrunoを使いたい場合は、GitHubのissueを立ててください。
|
||||
私たちのソースコードはオープンソースで誰でも使用できますが、新しいパッケージマネージャで公開を検討する前に、私たちにご連絡ください。私は Bruno の製作者として、このプロジェクト「Bruno」の商標を保有しており、その配布を管理したいと考えています。もし新しいパッケージマネージャで Bruno を使いたい場合は、GitHub の issue を立ててください。
|
||||
|
||||
私たちの機能の大部分が無料でオープンソース(RESTやGraphQLのAPIも含む)ですが、
|
||||
私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269
|
||||
私たちの機能の大部分が無料でオープンソース(REST や GraphQL の API も含む)ですが、
|
||||
私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269
|
||||
|
@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import find from 'lodash/find';
|
||||
import classnames from 'classnames';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { updateRequestPaneTab } from 'providers/ReduxStore/slices/tabs';
|
||||
@ -14,7 +13,7 @@ import Assertions from 'components/RequestPane/Assertions';
|
||||
import Script from 'components/RequestPane/Script';
|
||||
import Tests from 'components/RequestPane/Tests';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { get } from 'lodash';
|
||||
import { find, get } from 'lodash';
|
||||
import Documentation from 'components/Documentation/index';
|
||||
|
||||
const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
@ -81,6 +80,8 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isMultipleContentTab = ['params', 'script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab);
|
||||
|
||||
// get the length of active params, headers, asserts and vars
|
||||
const params = item.draft ? get(item, 'draft.request.params', []) : get(item, 'request.params', []);
|
||||
const headers = item.draft ? get(item, 'draft.request.headers', []) : get(item, 'request.headers', []);
|
||||
@ -99,7 +100,7 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
<StyledWrapper className="flex flex-col h-full relative">
|
||||
<div className="flex flex-wrap items-center tabs" role="tablist">
|
||||
<div className={getTabClassname('params')} role="tab" onClick={() => selectTab('params')}>
|
||||
Query
|
||||
Params
|
||||
{activeParamsLength > 0 && <sup className="ml-1 font-medium">{activeParamsLength}</sup>}
|
||||
</div>
|
||||
<div className={getTabClassname('body')} role="tab" onClick={() => selectTab('body')}>
|
||||
@ -136,9 +137,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => {
|
||||
) : null}
|
||||
</div>
|
||||
<section
|
||||
className={`flex w-full ${
|
||||
['script', 'vars', 'auth', 'docs'].includes(focusedTab.requestPaneTab) ? '' : 'mt-5'
|
||||
}`}
|
||||
className={classnames('flex w-full', {
|
||||
'mt-5': !isMultipleContentTab
|
||||
})}
|
||||
>
|
||||
{getTabPanel(focusedTab.requestPaneTab)}
|
||||
</section>
|
||||
|
@ -1,6 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
div.title {
|
||||
color: var(--color-tab-inactive);
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
@ -1,12 +1,18 @@
|
||||
import React from 'react';
|
||||
import get from 'lodash/get';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import has from 'lodash/has';
|
||||
import { IconTrash } from '@tabler/icons';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTheme } from 'providers/Theme';
|
||||
import { addQueryParam, updateQueryParam, deleteQueryParam } from 'providers/ReduxStore/slices/collections';
|
||||
import {
|
||||
addQueryParam,
|
||||
deleteQueryParam,
|
||||
updatePathParam,
|
||||
updateQueryParam
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import { saveRequest, sendRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
|
||||
@ -14,8 +20,10 @@ const QueryParams = ({ item, collection }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { storedTheme } = useTheme();
|
||||
const params = item.draft ? get(item, 'draft.request.params') : get(item, 'request.params');
|
||||
const queryParams = params.filter((param) => param.type === 'query');
|
||||
const pathParams = params.filter((param) => param.type === 'path');
|
||||
|
||||
const handleAddParam = () => {
|
||||
const handleAddQueryParam = () => {
|
||||
dispatch(
|
||||
addQueryParam({
|
||||
itemUid: item.uid,
|
||||
@ -26,24 +34,39 @@ const QueryParams = ({ item, collection }) => {
|
||||
|
||||
const onSave = () => dispatch(saveRequest(item.uid, collection.uid));
|
||||
const handleRun = () => dispatch(sendRequest(item, collection.uid));
|
||||
const handleParamChange = (e, _param, type) => {
|
||||
const param = cloneDeep(_param);
|
||||
|
||||
const handleValueChange = (data, type, value) => {
|
||||
const _data = cloneDeep(data);
|
||||
|
||||
if (!has(_data, type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_data[type] = value;
|
||||
|
||||
return _data;
|
||||
};
|
||||
|
||||
const handleQueryParamChange = (e, data, type) => {
|
||||
let value;
|
||||
|
||||
switch (type) {
|
||||
case 'name': {
|
||||
param.name = e.target.value;
|
||||
value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'value': {
|
||||
param.value = e.target.value;
|
||||
value = e.target.value;
|
||||
break;
|
||||
}
|
||||
case 'enabled': {
|
||||
param.enabled = e.target.checked;
|
||||
value = e.target.checked;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const param = handleValueChange(data, type, value);
|
||||
|
||||
dispatch(
|
||||
updateQueryParam({
|
||||
param,
|
||||
@ -53,7 +76,21 @@ const QueryParams = ({ item, collection }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveParam = (param) => {
|
||||
const handlePathParamChange = (e, data) => {
|
||||
let value = e.target.value;
|
||||
|
||||
const path = handleValueChange(data, 'value', value);
|
||||
|
||||
dispatch(
|
||||
updatePathParam({
|
||||
path,
|
||||
itemUid: item.uid,
|
||||
collectionUid: collection.uid
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const handleRemoveQueryParam = (param) => {
|
||||
dispatch(
|
||||
deleteQueryParam({
|
||||
paramUid: param.uid,
|
||||
@ -64,75 +101,128 @@ const QueryParams = ({ item, collection }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledWrapper className="w-full">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{params && params.length
|
||||
? params.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'name')}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center">
|
||||
<StyledWrapper className="w-full flex flex-col">
|
||||
<div className="flex-1 mt-2">
|
||||
<div className="mb-1 title text-xs">Query</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{queryParams && queryParams.length
|
||||
? queryParams.map((param, index) => {
|
||||
return (
|
||||
<tr key={param.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={param.enabled}
|
||||
tabIndex="-1"
|
||||
className="mr-3 mousetrap"
|
||||
onChange={(e) => handleParamChange(e, param, 'enabled')}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={param.name}
|
||||
className="mousetrap"
|
||||
onChange={(e) => handleQueryParamChange(e, param, 'name')}
|
||||
/>
|
||||
<button tabIndex="-1" onClick={() => handleRemoveParam(param)}>
|
||||
<IconTrash strokeWidth={1.5} size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={param.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handleQueryParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
param,
|
||||
'value'
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className="btn-add-param text-link pr-2 py-3 mt-2 select-none" onClick={handleAddQueryParam}>
|
||||
+ <span>Add Param</span>
|
||||
</button>
|
||||
<div className="mb-1 title text-xs">Path</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>Value</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pathParams && pathParams.length
|
||||
? pathParams.map((path, index) => {
|
||||
return (
|
||||
<tr key={path.uid}>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
spellCheck="false"
|
||||
value={path.name}
|
||||
className="mousetrap"
|
||||
readOnly={true}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<SingleLineEditor
|
||||
value={path.value}
|
||||
theme={storedTheme}
|
||||
onSave={onSave}
|
||||
onChange={(newValue) =>
|
||||
handlePathParamChange(
|
||||
{
|
||||
target: {
|
||||
value: newValue
|
||||
}
|
||||
},
|
||||
path
|
||||
)
|
||||
}
|
||||
onRun={handleRun}
|
||||
collection={collection}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -2,8 +2,8 @@ import Modal from 'components/Modal/index';
|
||||
import { useState } from 'react';
|
||||
import CodeView from './CodeView';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import { isValidUrl } from 'utils/url/index';
|
||||
import get from 'lodash/get';
|
||||
import { isValidUrl } from 'utils/url';
|
||||
import { find, get } from 'lodash';
|
||||
import { findEnvironmentInCollection } from 'utils/collections';
|
||||
|
||||
// Todo: Fix this
|
||||
@ -27,6 +27,44 @@ const interpolateUrl = ({ url, envVars, collectionVariables, processEnvVars }) =
|
||||
});
|
||||
};
|
||||
|
||||
const joinPathUrl = (url, params) => {
|
||||
const processPaths = (uri, paths) => {
|
||||
return uri
|
||||
.split('/')
|
||||
.map((segment) => {
|
||||
if (segment.startsWith(':')) {
|
||||
const paramName = segment.slice(1);
|
||||
const param = paths.find((p) => p.name === paramName && p.type === 'path' && p.enabled);
|
||||
return param ? param.value : segment;
|
||||
}
|
||||
return segment;
|
||||
})
|
||||
.join('/');
|
||||
};
|
||||
|
||||
const processQueryParams = (search, params) => {
|
||||
const queryParams = new URLSearchParams(search);
|
||||
params
|
||||
.filter((p) => p.type === 'query' && p.enabled)
|
||||
.forEach((param) => {
|
||||
queryParams.set(param.name, param.value);
|
||||
});
|
||||
return queryParams.toString();
|
||||
};
|
||||
|
||||
let uri;
|
||||
try {
|
||||
uri = new URL(url);
|
||||
} catch (error) {
|
||||
uri = new URL(`http://${url}`);
|
||||
}
|
||||
|
||||
const basePath = processPaths(uri.pathname, params);
|
||||
const queryString = processQueryParams(uri.search, params);
|
||||
|
||||
return `${uri.origin}${basePath}${queryString ? `?${queryString}` : ''}`;
|
||||
};
|
||||
|
||||
const languages = [
|
||||
{
|
||||
name: 'HTTP',
|
||||
@ -76,7 +114,10 @@ const languages = [
|
||||
];
|
||||
|
||||
const GenerateCodeItem = ({ collection, item, onClose }) => {
|
||||
const url = get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url');
|
||||
const url = joinPathUrl(
|
||||
get(item, 'draft.request.url') !== undefined ? get(item, 'draft.request.url') : get(item, 'request.url'),
|
||||
get(item, 'draft.request.params') !== undefined ? get(item, 'draft.request.params') : get(item, 'request.params')
|
||||
);
|
||||
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
||||
let envVars = {};
|
||||
if (environment) {
|
||||
|
@ -1,13 +1,6 @@
|
||||
import { uuid } from 'utils/common';
|
||||
import { find, map, forOwn, concat, filter, each, cloneDeep, get, set, debounce } from 'lodash';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import concat from 'lodash/concat';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
import forOwn from 'lodash/forOwn';
|
||||
import get from 'lodash/get';
|
||||
import map from 'lodash/map';
|
||||
import set from 'lodash/set';
|
||||
import {
|
||||
addDepth,
|
||||
areItemsTheSameExceptSeqUpdate,
|
||||
@ -21,9 +14,9 @@ import {
|
||||
findItemInCollectionByPathname,
|
||||
isItemARequest
|
||||
} from 'utils/collections';
|
||||
import { uuid } from 'utils/common';
|
||||
import { PATH_SEPARATOR, getDirectoryName, getSubdirectoriesFromRoot } from 'utils/common/platform';
|
||||
import { parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
|
||||
import { parsePathParams, parseQueryParams, splitOnFirst, stringifyQueryParams } from 'utils/url';
|
||||
import { getDirectoryName, getSubdirectoriesFromRoot, PATH_SEPARATOR } from 'utils/common/platform';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
const initialState = {
|
||||
collections: [],
|
||||
@ -294,10 +287,35 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
if (collection && collection.items && collection.items.length) {
|
||||
const parts = splitOnFirst(action.payload.requestUrl, '?');
|
||||
const params = parseQueryParams(parts[1]);
|
||||
each(params, (urlParam) => {
|
||||
urlParam.enabled = true;
|
||||
});
|
||||
const queryParams = parseQueryParams(parts[1]);
|
||||
|
||||
let pathParams = [];
|
||||
try {
|
||||
pathParams = parsePathParams(parts[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(err.message);
|
||||
}
|
||||
|
||||
const queryParamObjects = queryParams.map((param) => ({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: '',
|
||||
type: 'query',
|
||||
enabled: true
|
||||
}));
|
||||
|
||||
const pathParamObjects = pathParams.map((param) => ({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
}));
|
||||
|
||||
const params = [...queryParamObjects, ...pathParamObjects];
|
||||
|
||||
const item = {
|
||||
uid: action.payload.uid,
|
||||
@ -351,14 +369,26 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const urlParams = parseQueryParams(parts[1]);
|
||||
let urlPaths = [];
|
||||
|
||||
try {
|
||||
urlPaths = parsePathParams(parts[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.error(err.message);
|
||||
}
|
||||
|
||||
const disabledParams = filter(item.draft.request.params, (p) => !p.enabled);
|
||||
let enabledParams = filter(item.draft.request.params, (p) => p.enabled);
|
||||
let enabledParams = filter(item.draft.request.params, (p) => p.enabled && p.type === 'query');
|
||||
let oldPaths = filter(item.draft.request.params, (p) => p.enabled && p.type === 'path');
|
||||
let newPaths = [];
|
||||
|
||||
// try and connect as much as old params uid's as possible
|
||||
each(urlParams, (urlParam) => {
|
||||
const existingParam = find(enabledParams, (p) => p.name === urlParam.name || p.value === urlParam.value);
|
||||
urlParam.uid = existingParam ? existingParam.uid : uuid();
|
||||
urlParam.enabled = true;
|
||||
urlParam.type = 'query';
|
||||
|
||||
// once found, remove it - trying our best here to accommodate duplicate query params
|
||||
if (existingParam) {
|
||||
@ -366,10 +396,27 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
// filter the newest path param and compare with previous data that already inserted
|
||||
newPaths = filter(urlPaths, (urlPath) => {
|
||||
const existingPath = find(oldPaths, (p) => p.name === urlPath.name);
|
||||
if (existingPath) {
|
||||
return false;
|
||||
}
|
||||
urlPath.uid = uuid();
|
||||
urlPath.enabled = true;
|
||||
urlPath.type = 'path';
|
||||
return true;
|
||||
});
|
||||
|
||||
// remove path param that not used or deleted when typing url
|
||||
oldPaths = filter(oldPaths, (urlPath) => {
|
||||
return find(urlPaths, (p) => p.name === urlPath.name);
|
||||
});
|
||||
|
||||
// ultimately params get replaced with params in url + the disabled ones that existed prior
|
||||
// the query params are the source of truth, the url in the queryurl input gets constructed using these params
|
||||
// we however are also storing the full url (with params) in the url itself
|
||||
item.draft.request.params = concat(urlParams, disabledParams);
|
||||
item.draft.request.params = concat(urlParams, newPaths, disabledParams, oldPaths);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -426,6 +473,7 @@ export const collectionsSlice = createSlice({
|
||||
name: '',
|
||||
value: '',
|
||||
description: '',
|
||||
type: 'query',
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
@ -441,16 +489,20 @@ export const collectionsSlice = createSlice({
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
const param = find(item.draft.request.params, (h) => h.uid === action.payload.param.uid);
|
||||
if (param) {
|
||||
param.name = action.payload.param.name;
|
||||
param.value = action.payload.param.value;
|
||||
param.description = action.payload.param.description;
|
||||
param.enabled = action.payload.param.enabled;
|
||||
const queryParam = find(
|
||||
item.draft.request.params,
|
||||
(h) => h.uid === action.payload.param.uid && h.type === 'query'
|
||||
);
|
||||
if (queryParam) {
|
||||
queryParam.name = action.payload.param.name;
|
||||
queryParam.value = action.payload.param.value;
|
||||
queryParam.enabled = action.payload.param.enabled;
|
||||
|
||||
// update request url
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled));
|
||||
const query = stringifyQueryParams(
|
||||
filter(item.draft.request.params, (p) => p.enabled && p.type === 'query')
|
||||
);
|
||||
|
||||
// if no query is found, then strip the query params in url
|
||||
if (!query || !query.length) {
|
||||
@ -486,7 +538,7 @@ export const collectionsSlice = createSlice({
|
||||
|
||||
// update request url
|
||||
const parts = splitOnFirst(item.draft.request.url, '?');
|
||||
const query = stringifyQueryParams(filter(item.draft.request.params, (p) => p.enabled));
|
||||
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 {
|
||||
@ -495,6 +547,26 @@ export const collectionsSlice = createSlice({
|
||||
}
|
||||
}
|
||||
},
|
||||
updatePathParam: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
if (collection) {
|
||||
const item = findItemInCollection(collection, action.payload.itemUid);
|
||||
|
||||
if (item && isItemARequest(item)) {
|
||||
if (!item.draft) {
|
||||
item.draft = cloneDeep(item);
|
||||
}
|
||||
|
||||
const param = find(item.draft.request.params, (p) => p.uid === action.payload.path.uid && p.type === 'path');
|
||||
|
||||
if (param) {
|
||||
param.name = action.payload.path.name;
|
||||
param.value = action.payload.path.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addRequestHeader: (state, action) => {
|
||||
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
|
||||
|
||||
@ -1418,6 +1490,7 @@ export const {
|
||||
addQueryParam,
|
||||
updateQueryParam,
|
||||
deleteQueryParam,
|
||||
updatePathParam,
|
||||
addRequestHeader,
|
||||
updateRequestHeader,
|
||||
deleteRequestHeader,
|
||||
|
@ -24,7 +24,7 @@ const createHeaders = (headers) => {
|
||||
|
||||
const createQuery = (queryParams = []) => {
|
||||
return queryParams
|
||||
.filter((param) => param.enabled)
|
||||
.filter((param) => param.enabled && param.type === 'query')
|
||||
.map((param) => ({
|
||||
name: param.name,
|
||||
value: param.value
|
||||
|
@ -29,9 +29,6 @@ export const deleteUidsInItems = (items) => {
|
||||
export const transformItem = (items = []) => {
|
||||
each(items, (item) => {
|
||||
if (['http-request', 'graphql-request'].includes(item.type)) {
|
||||
item.request.query = item.request.params;
|
||||
delete item.request.params;
|
||||
|
||||
if (item.type === 'graphql-request') {
|
||||
item.type = 'graphql';
|
||||
}
|
||||
|
@ -228,13 +228,14 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
});
|
||||
};
|
||||
|
||||
const copyQueryParams = (params) => {
|
||||
const copyParams = (params) => {
|
||||
return map(params, (param) => {
|
||||
return {
|
||||
uid: param.uid,
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: param.type,
|
||||
enabled: param.enabled
|
||||
};
|
||||
});
|
||||
@ -283,7 +284,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
|
||||
url: si.request.url,
|
||||
method: si.request.method,
|
||||
headers: copyHeaders(si.request.headers),
|
||||
params: copyQueryParams(si.request.params),
|
||||
params: copyParams(si.request.params),
|
||||
body: {
|
||||
mode: si.request.body.mode,
|
||||
json: si.request.body.json,
|
||||
@ -441,6 +442,7 @@ export const transformRequestToSaveToFilesystem = (item) => {
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: param.type,
|
||||
enabled: param.enabled
|
||||
});
|
||||
});
|
||||
|
@ -171,11 +171,43 @@ export const exportCollection = (collection) => {
|
||||
}
|
||||
};
|
||||
|
||||
const generateHost = (url) => {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
return hostname.split('.');
|
||||
} catch (error) {
|
||||
console.error(`Invalid URL: ${url}`, error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const generatePathParams = (params) => {
|
||||
return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`);
|
||||
};
|
||||
|
||||
const generateQueryParams = (params) => {
|
||||
return params
|
||||
.filter((param) => param.type === 'query')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
};
|
||||
|
||||
const generateVariables = (params) => {
|
||||
return params
|
||||
.filter((param) => param.type === 'path')
|
||||
.map(({ name, value, description }) => ({ key: name, value, description }));
|
||||
};
|
||||
|
||||
const generateRequestSection = (itemRequest) => {
|
||||
const requestObject = {
|
||||
method: itemRequest.method,
|
||||
header: generateHeaders(itemRequest.headers),
|
||||
url: itemRequest.url,
|
||||
url: {
|
||||
raw: itemRequest.url,
|
||||
host: generateHost(itemRequest.url),
|
||||
path: generatePathParams(itemRequest.params),
|
||||
query: generateQueryParams(itemRequest.params),
|
||||
variable: generateVariables(itemRequest.params)
|
||||
},
|
||||
auth: generateAuth(itemRequest.auth)
|
||||
};
|
||||
|
||||
|
@ -29,7 +29,6 @@ export const updateUidsInCollection = (_collection) => {
|
||||
item.uid = uuid();
|
||||
|
||||
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
||||
each(get(item, 'request.query'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
||||
each(get(item, 'request.vars.req'), (v) => (v.uid = uuid()));
|
||||
each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));
|
||||
@ -66,8 +65,13 @@ export const transformItemsInCollection = (collection) => {
|
||||
|
||||
if (['http', 'graphql'].includes(item.type)) {
|
||||
item.type = `${item.type}-request`;
|
||||
|
||||
if (item.request.query) {
|
||||
item.request.params = item.request.query;
|
||||
item.request.params = item.request.query.map((queryItem) => ({
|
||||
...queryItem,
|
||||
type: 'query',
|
||||
uid: queryItem.uid || uuid()
|
||||
}));
|
||||
}
|
||||
|
||||
delete item.request.query;
|
||||
|
@ -112,10 +112,22 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(request.pathParameters, (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
description: '',
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
const authType = get(request, 'authentication.type', '');
|
||||
|
||||
if (authType === 'basic') {
|
||||
|
@ -92,7 +92,17 @@ const transformOpenapiRequestItem = (request) => {
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required
|
||||
enabled: param.required,
|
||||
type: 'query'
|
||||
});
|
||||
} else if (param.in === 'path') {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.name,
|
||||
value: '',
|
||||
description: param.description || '',
|
||||
enabled: param.required,
|
||||
type: 'path'
|
||||
});
|
||||
} else if (param.in === 'header') {
|
||||
brunoRequestItem.request.headers.push({
|
||||
|
@ -275,10 +275,22 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'query',
|
||||
enabled: !param.disabled
|
||||
});
|
||||
});
|
||||
|
||||
each(get(i, 'request.url.variable'), (param) => {
|
||||
brunoRequestItem.request.params.push({
|
||||
uid: uuid(),
|
||||
name: param.key,
|
||||
value: param.value,
|
||||
description: param.description,
|
||||
type: 'path',
|
||||
enabled: true
|
||||
});
|
||||
});
|
||||
|
||||
brunoParent.items.push(brunoRequestItem);
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import isEmpty from 'lodash/isEmpty';
|
||||
import trim from 'lodash/trim';
|
||||
import each from 'lodash/each';
|
||||
import filter from 'lodash/filter';
|
||||
import find from 'lodash/find';
|
||||
|
||||
const hasLength = (str) => {
|
||||
if (!str || !str.length) {
|
||||
@ -26,6 +27,41 @@ export const parseQueryParams = (query) => {
|
||||
return filter(params, (p) => hasLength(p.name));
|
||||
};
|
||||
|
||||
export const parsePathParams = (url) => {
|
||||
let uri = url.slice();
|
||||
|
||||
if (!uri || !uri.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!uri.startsWith('http://') && !uri.startsWith('https://')) {
|
||||
uri = `http://${uri}`;
|
||||
}
|
||||
|
||||
try {
|
||||
uri = new URL(uri);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
let paths = uri.pathname.split('/');
|
||||
|
||||
paths = paths.reduce((acc, path) => {
|
||||
if (path !== '' && path[0] === ':') {
|
||||
let name = path.slice(1, path.length);
|
||||
if (name) {
|
||||
let isExist = find(acc, (path) => path.name === name);
|
||||
if (!isExist) {
|
||||
acc.push({ name: path.slice(1, path.length), value: '' });
|
||||
}
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return paths;
|
||||
};
|
||||
|
||||
export const stringifyQueryParams = (params) => {
|
||||
if (!params || isEmpty(params)) {
|
||||
return '';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { parseQueryParams, splitOnFirst } from './index';
|
||||
import { parseQueryParams, splitOnFirst, parsePathParams } from './index';
|
||||
|
||||
describe('Url Utils - parseQueryParams', () => {
|
||||
it('should parse query - case 1', () => {
|
||||
@ -51,6 +51,51 @@ describe('Url Utils - parseQueryParams', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url Utils - parsePathParams', () => {
|
||||
it('should parse path - case 1', () => {
|
||||
const params = parsePathParams('www.example.com');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse path - case 2', () => {
|
||||
const params = parsePathParams('http://www.example.com');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse path - case 3', () => {
|
||||
const params = parsePathParams('https://www.example.com');
|
||||
expect(params).toEqual([]);
|
||||
});
|
||||
|
||||
it('should parse path - case 4', () => {
|
||||
const params = parsePathParams('https://www.example.com/users/:id');
|
||||
expect(params).toEqual([{ name: 'id', value: '' }]);
|
||||
});
|
||||
|
||||
it('should parse path - case 5', () => {
|
||||
const params = parsePathParams('https://www.example.com/users/:id/');
|
||||
expect(params).toEqual([{ name: 'id', value: '' }]);
|
||||
});
|
||||
|
||||
it('should parse path - case 6', () => {
|
||||
const params = parsePathParams('https://www.example.com/users/:id/:');
|
||||
expect(params).toEqual([{ name: 'id', value: '' }]);
|
||||
});
|
||||
|
||||
it('should parse path - case 7', () => {
|
||||
const params = parsePathParams('https://www.example.com/users/:id/posts/:id');
|
||||
expect(params).toEqual([{ name: 'id', value: '' }]);
|
||||
});
|
||||
|
||||
it('should parse path - case 8', () => {
|
||||
const params = parsePathParams('https://www.example.com/users/:id/posts/:postId');
|
||||
expect(params).toEqual([
|
||||
{ name: 'id', value: '' },
|
||||
{ name: 'postId', value: '' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Url Utils - splitOnFirst', () => {
|
||||
it('should split on first - case 1', () => {
|
||||
const params = splitOnFirst('a', '=');
|
||||
|
@ -1,5 +1,5 @@
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { each, forOwn, cloneDeep } = require('lodash');
|
||||
const { each, forOwn, cloneDeep, find } = require('lodash');
|
||||
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
@ -86,6 +86,36 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
param.value = _interpolate(param.value);
|
||||
});
|
||||
|
||||
if (request.params.length) {
|
||||
let url = request.url;
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = `http://${url}`;
|
||||
}
|
||||
|
||||
try {
|
||||
url = new URL(url);
|
||||
} catch (e) {
|
||||
throw { message: 'Invalid URL format', originalError: e.message };
|
||||
}
|
||||
|
||||
const urlPaths = url.pathname
|
||||
.split('/')
|
||||
.filter((path) => path !== '')
|
||||
.map((path) => {
|
||||
if (path[0] !== ':') {
|
||||
return '/' + path;
|
||||
} else {
|
||||
const name = path.slice(1);
|
||||
const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name);
|
||||
return existingPathParam ? '/' + existingPathParam.value : '';
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
|
||||
request.url = url.origin + urlPaths + url.search;
|
||||
}
|
||||
|
||||
if (request.proxy) {
|
||||
request.proxy.protocol = _interpolate(request.proxy.protocol);
|
||||
request.proxy.hostname = _interpolate(request.proxy.hostname);
|
||||
|
@ -29,7 +29,8 @@ const prepareRequest = (request, collectionRoot) => {
|
||||
let axiosRequest = {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: headers
|
||||
headers: headers,
|
||||
paths: request.paths
|
||||
};
|
||||
|
||||
const collectionAuth = get(collectionRoot, 'request.auth');
|
||||
|
@ -13,7 +13,7 @@ const collectionBruToJson = (bru) => {
|
||||
|
||||
const transformedJson = {
|
||||
request: {
|
||||
params: _.get(json, 'query', []),
|
||||
params: _.get(json, 'params', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
@ -60,7 +60,7 @@ const bruToJson = (bru) => {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
params: _.get(json, 'query', []),
|
||||
params: _.get(json, 'params', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
body: _.get(json, 'body', {}),
|
||||
vars: _.get(json, 'vars', []),
|
||||
|
@ -14,7 +14,7 @@ const collectionBruToJson = (bru) => {
|
||||
|
||||
const transformedJson = {
|
||||
request: {
|
||||
params: _.get(json, 'query', []),
|
||||
params: _.get(json, 'params', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
script: _.get(json, 'script', {}),
|
||||
@ -33,7 +33,7 @@ const collectionBruToJson = (bru) => {
|
||||
const jsonToCollectionBru = (json) => {
|
||||
try {
|
||||
const collectionBruJson = {
|
||||
query: _.get(json, 'request.params', []),
|
||||
params: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
script: {
|
||||
@ -111,7 +111,7 @@ const bruToJson = (bru) => {
|
||||
request: {
|
||||
method: _.upperCase(_.get(json, 'http.method')),
|
||||
url: _.get(json, 'http.url'),
|
||||
params: _.get(json, 'query', []),
|
||||
params: _.get(json, 'params', []),
|
||||
headers: _.get(json, 'headers', []),
|
||||
auth: _.get(json, 'auth', {}),
|
||||
body: _.get(json, 'body', {}),
|
||||
@ -162,7 +162,7 @@ const jsonToBru = (json) => {
|
||||
auth: _.get(json, 'request.auth.mode', 'none'),
|
||||
body: _.get(json, 'request.body.mode', 'none')
|
||||
},
|
||||
query: _.get(json, 'request.params', []),
|
||||
params: _.get(json, 'request.params', []),
|
||||
headers: _.get(json, 'request.headers', []),
|
||||
auth: _.get(json, 'request.auth', {}),
|
||||
body: _.get(json, 'request.body', {}),
|
||||
|
@ -1,5 +1,5 @@
|
||||
const { interpolate } = require('@usebruno/common');
|
||||
const { each, forOwn, cloneDeep } = require('lodash');
|
||||
const { each, forOwn, cloneDeep, find } = require('lodash');
|
||||
|
||||
const getContentType = (headers = {}) => {
|
||||
let contentType = '';
|
||||
@ -86,6 +86,36 @@ const interpolateVars = (request, envVars = {}, collectionVariables = {}, proces
|
||||
param.value = _interpolate(param.value);
|
||||
});
|
||||
|
||||
if (request.params.length) {
|
||||
let url = request.url;
|
||||
|
||||
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||
url = `http://${url}`;
|
||||
}
|
||||
|
||||
try {
|
||||
url = new URL(url);
|
||||
} catch (e) {
|
||||
throw { message: 'Invalid URL format', originalError: e.message };
|
||||
}
|
||||
|
||||
const urlPaths = url.pathname
|
||||
.split('/')
|
||||
.filter((path) => path !== '')
|
||||
.map((path) => {
|
||||
if (path[0] !== ':') {
|
||||
return '/' + path;
|
||||
} else {
|
||||
const name = path.slice(1);
|
||||
const existingPathParam = request.params.find((param) => param.type === 'path' && param.name === name);
|
||||
return existingPathParam ? '/' + existingPathParam.value : '';
|
||||
}
|
||||
})
|
||||
.join('');
|
||||
|
||||
request.url = url.origin + urlPaths + url.search;
|
||||
}
|
||||
|
||||
if (request.proxy) {
|
||||
request.proxy.protocol = _interpolate(request.proxy.protocol);
|
||||
request.proxy.hostname = _interpolate(request.proxy.hostname);
|
||||
|
@ -161,6 +161,7 @@ const prepareRequest = (request, collectionRoot, collectionPath) => {
|
||||
method: request.method,
|
||||
url,
|
||||
headers,
|
||||
params: request.params.filter((param) => param.type === 'path'),
|
||||
responseType: 'arraybuffer'
|
||||
};
|
||||
|
||||
|
@ -22,10 +22,11 @@ const { outdentString } = require('../../v1/src/utils');
|
||||
*
|
||||
*/
|
||||
const grammar = ohm.grammar(`Bru {
|
||||
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||
BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
|
||||
auths = authawsv4 | authbasic | authbearer | authdigest | authOAuth2
|
||||
bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
|
||||
bodyforms = bodyformurlencoded | bodymultipart
|
||||
params = paramspath | paramsquery
|
||||
|
||||
nl = "\\r"? "\\n"
|
||||
st = " " | "\\t"
|
||||
@ -74,6 +75,8 @@ const grammar = ohm.grammar(`Bru {
|
||||
headers = "headers" dictionary
|
||||
|
||||
query = "query" dictionary
|
||||
paramspath = "params:path" dictionary
|
||||
paramsquery = "params:query" dictionary
|
||||
|
||||
varsandassert = varsreq | varsres | assert
|
||||
varsreq = "vars:pre-request" dictionary
|
||||
@ -133,6 +136,28 @@ const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
|
||||
});
|
||||
};
|
||||
|
||||
const mapPairListToKeyValPairsWithType = (pairList = [], type) => {
|
||||
if (!pairList.length) {
|
||||
return [];
|
||||
}
|
||||
return _.map(pairList[0], (pair) => {
|
||||
let name = _.keys(pair)[0];
|
||||
let value = pair[name];
|
||||
let enabled = true;
|
||||
if (name && name.length && name.charAt(0) === '~') {
|
||||
name = name.slice(1);
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
value,
|
||||
enabled,
|
||||
type
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const mapPairListToKeyValPairsMultipart = (pairList = [], parseEnabled = true) => {
|
||||
const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
|
||||
|
||||
@ -321,7 +346,17 @@ const sem = grammar.createSemantics().addAttribute('ast', {
|
||||
},
|
||||
query(_1, dictionary) {
|
||||
return {
|
||||
query: mapPairListToKeyValPairs(dictionary.ast)
|
||||
params: mapPairListToKeyValPairsWithType(dictionary.ast, 'query')
|
||||
};
|
||||
},
|
||||
paramspath(_1, dictionary) {
|
||||
return {
|
||||
params: mapPairListToKeyValPairsWithType(dictionary.ast, 'path')
|
||||
};
|
||||
},
|
||||
paramsquery(_1, dictionary) {
|
||||
return {
|
||||
params: mapPairListToKeyValPairsWithType(dictionary.ast, 'query')
|
||||
};
|
||||
},
|
||||
headers(_1, dictionary) {
|
||||
|
@ -30,7 +30,7 @@ const getValueString = (value) => {
|
||||
};
|
||||
|
||||
const jsonToBru = (json) => {
|
||||
const { meta, http, query, headers, auth, body, script, tests, vars, assertions, docs } = json;
|
||||
const { meta, http, params, headers, auth, body, script, tests, vars, assertions, docs } = json;
|
||||
|
||||
let bru = '';
|
||||
|
||||
@ -62,25 +62,38 @@ const jsonToBru = (json) => {
|
||||
`;
|
||||
}
|
||||
|
||||
if (query && query.length) {
|
||||
bru += 'query {';
|
||||
if (enabled(query).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(query)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
if (params && params.length) {
|
||||
const queryParams = params.filter((param) => param.type === 'query');
|
||||
const pathParams = params.filter((param) => param.type === 'path');
|
||||
|
||||
if (queryParams.length) {
|
||||
bru += 'params:query {';
|
||||
if (enabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
enabled(queryParams)
|
||||
.map((item) => `${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
|
||||
if (disabled(queryParams).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(queryParams)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
|
||||
if (disabled(query).length) {
|
||||
bru += `\n${indentString(
|
||||
disabled(query)
|
||||
.map((item) => `~${item.name}: ${item.value}`)
|
||||
.join('\n')
|
||||
)}`;
|
||||
}
|
||||
if (pathParams.length) {
|
||||
bru += 'params:path {';
|
||||
|
||||
bru += '\n}\n\n';
|
||||
bru += `\n${indentString(pathParams.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
|
||||
|
||||
bru += '\n}\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (headers && headers.length) {
|
||||
|
@ -185,6 +185,17 @@ const authSchema = Yup.object({
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
const keyValueWithTypeSchema = Yup.object({
|
||||
uid: uidSchema,
|
||||
name: Yup.string().nullable(),
|
||||
value: Yup.string().nullable(),
|
||||
description: Yup.string().nullable(),
|
||||
type: Yup.string().oneOf(['query', 'path']).required('type is required'),
|
||||
enabled: Yup.boolean()
|
||||
})
|
||||
.noUnknown(true)
|
||||
.strict();
|
||||
|
||||
// Right now, the request schema is very tightly coupled with http request
|
||||
// As we introduce more request types in the future, we will improve the definition to support
|
||||
// schema structure based on other request type
|
||||
@ -192,7 +203,7 @@ const requestSchema = Yup.object({
|
||||
url: requestUrlSchema,
|
||||
method: requestMethodSchema,
|
||||
headers: Yup.array().of(keyValueSchema).required('headers are required'),
|
||||
params: Yup.array().of(keyValueSchema).required('params are required'),
|
||||
params: Yup.array().of(keyValueWithTypeSchema).required('params are required'),
|
||||
auth: authSchema,
|
||||
body: requestBodySchema,
|
||||
script: Yup.object({
|
||||
|
@ -59,6 +59,7 @@ describe('Collection Schema Validation', () => {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
paths: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
}
|
||||
@ -116,6 +117,7 @@ describe('Collection Schema Validation', () => {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
paths: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ describe('Request Schema Validation', () => {
|
||||
method: 'GET',
|
||||
headers: [],
|
||||
params: [],
|
||||
paths: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
}
|
||||
@ -24,6 +25,7 @@ describe('Request Schema Validation', () => {
|
||||
method: 'GET-junk',
|
||||
headers: [],
|
||||
params: [],
|
||||
paths: [],
|
||||
body: {
|
||||
mode: 'none'
|
||||
}
|
||||
|
@ -43,3 +43,48 @@ describe('bruno toml', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('joinPathUrl', () => {
|
||||
it('should join path and query params correctly', () => {
|
||||
const url = 'https://example.com/api/:id';
|
||||
const params = [
|
||||
{ name: 'id', type: 'path', enabled: true, value: '123' },
|
||||
{ name: 'sort', type: 'query', enabled: true, value: 'asc' },
|
||||
{ name: 'filter', type: 'query', enabled: true, value: 'active' }
|
||||
];
|
||||
const expectedUrl = 'https://example.com/api/123?sort=asc&filter=active';
|
||||
|
||||
const result = joinPathUrl(url, params);
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should handle empty path and query params', () => {
|
||||
const url = 'https://example.com/api';
|
||||
const params = [];
|
||||
const expectedUrl = 'https://example.com/api';
|
||||
|
||||
const result = joinPathUrl(url, params);
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should handle empty query params', () => {
|
||||
const url = 'https://example.com/api/:id';
|
||||
const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];
|
||||
const expectedUrl = 'https://example.com/api/123';
|
||||
|
||||
const result = joinPathUrl(url, params);
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
});
|
||||
|
||||
it('should handle invalid URL', () => {
|
||||
const url = 'example.com/api/:id';
|
||||
const params = [{ name: 'id', type: 'path', enabled: true, value: '123' }];
|
||||
const expectedUrl = 'http://example.com/api/123';
|
||||
|
||||
const result = joinPathUrl(url, params);
|
||||
|
||||
expect(result).toEqual(expectedUrl);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user