diff --git a/contributing.md b/contributing.md index c6b3b632c..81fc27a22 100644 --- a/contributing.md +++ b/contributing.md @@ -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 !! diff --git a/docs/contributing/contributing_ja.md b/docs/contributing/contributing_ja.md index 7a4f195f3..c40e01681 100644 --- a/docs/contributing/contributing_ja.md +++ b/docs/contributing/contributing_ja.md @@ -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 アプリを立ち上げてアプリを読み込む必要があります。 ### ローカル環境での開発 diff --git a/docs/contributing/contributing_pl.md b/docs/contributing/contributing_pl.md index 3c10af8de..0d67045c4 100644 --- a/docs/contributing/contributing_pl.md +++ b/docs/contributing/contributing_pl.md @@ -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 diff --git a/docs/publishing/publishing_ja.md b/docs/publishing/publishing_ja.md index 681a806b1..f5d60b00e 100644 --- a/docs/publishing/publishing_ja.md +++ b/docs/publishing/publishing_ja.md @@ -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 \ No newline at end of file +私たちの機能の大部分が無料でオープンソース(REST や GraphQL の API も含む)ですが、 +私たちはオープンソースの原則と長期的な維持の間でよいバランスをとれるように努力しています- https://github.com/usebruno/bruno/discussions/269 diff --git a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js index 834751848..df90082c6 100644 --- a/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js +++ b/packages/bruno-app/src/components/RequestPane/HttpRequestPane/index.js @@ -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 }) => {
selectTab('params')}> - Query + Params {activeParamsLength > 0 && {activeParamsLength}}
selectTab('body')}> @@ -136,9 +137,9 @@ const HttpRequestPane = ({ item, collection, leftPaneWidth }) => { ) : null}
{getTabPanel(focusedTab.requestPaneTab)}
diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js index d3dc58d5c..5c3e1d537 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/StyledWrapper.js @@ -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; diff --git a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js index 54e3ee0b3..ecb84b6a9 100644 --- a/packages/bruno-app/src/components/RequestPane/QueryParams/index.js +++ b/packages/bruno-app/src/components/RequestPane/QueryParams/index.js @@ -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 ( - - - - - - - - - - - {params && params.length - ? params.map((param, index) => { - return ( - - - - + + + + ); + }) + : null} + +
NameValue
- handleParamChange(e, param, 'name')} - /> - - - handleParamChange( - { - target: { - value: newValue - } - }, - param, - 'value' - ) - } - onRun={handleRun} - collection={collection} - /> - -
+ +
+
Query
+ + + + + + + + + + {queryParams && queryParams.length + ? queryParams.map((param, index) => { + return ( + + - - ); - }) - : null} - -
NameValue
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')} /> - - -
- +
+ + handleQueryParamChange( + { + target: { + value: newValue + } + }, + param, + 'value' + ) + } + onRun={handleRun} + collection={collection} + /> + +
+ handleQueryParamChange(e, param, 'enabled')} + /> + +
+
+ +
Path
+ + + + + + + + + {pathParams && pathParams.length + ? pathParams.map((path, index) => { + return ( + + + + + ); + }) + : null} + +
NameValue
+ + + + handlePathParamChange( + { + target: { + value: newValue + } + }, + path + ) + } + onRun={handleRun} + collection={collection} + /> +
+
); }; diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js index ed1bc3f64..a768db8ce 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/GenerateCodeItem/index.js @@ -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) { diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 15afa72f5..8cba29c5a 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -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, diff --git a/packages/bruno-app/src/utils/codegenerator/har.js b/packages/bruno-app/src/utils/codegenerator/har.js index 0f2656370..0e3476256 100644 --- a/packages/bruno-app/src/utils/codegenerator/har.js +++ b/packages/bruno-app/src/utils/codegenerator/har.js @@ -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 diff --git a/packages/bruno-app/src/utils/collections/export.js b/packages/bruno-app/src/utils/collections/export.js index 17c979fe6..5ef7b1b49 100644 --- a/packages/bruno-app/src/utils/collections/export.js +++ b/packages/bruno-app/src/utils/collections/export.js @@ -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'; } diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 615d81952..8b08174e2 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -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 }); }); diff --git a/packages/bruno-app/src/utils/exporters/postman-collection.js b/packages/bruno-app/src/utils/exporters/postman-collection.js index 309a77d05..719391f0a 100644 --- a/packages/bruno-app/src/utils/exporters/postman-collection.js +++ b/packages/bruno-app/src/utils/exporters/postman-collection.js @@ -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) }; diff --git a/packages/bruno-app/src/utils/importers/common.js b/packages/bruno-app/src/utils/importers/common.js index f1e17ac00..c99048419 100644 --- a/packages/bruno-app/src/utils/importers/common.js +++ b/packages/bruno-app/src/utils/importers/common.js @@ -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; diff --git a/packages/bruno-app/src/utils/importers/insomnia-collection.js b/packages/bruno-app/src/utils/importers/insomnia-collection.js index 0a38a85e5..0fec995ca 100644 --- a/packages/bruno-app/src/utils/importers/insomnia-collection.js +++ b/packages/bruno-app/src/utils/importers/insomnia-collection.js @@ -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') { diff --git a/packages/bruno-app/src/utils/importers/openapi-collection.js b/packages/bruno-app/src/utils/importers/openapi-collection.js index 1900f6e47..01fb66c01 100644 --- a/packages/bruno-app/src/utils/importers/openapi-collection.js +++ b/packages/bruno-app/src/utils/importers/openapi-collection.js @@ -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({ diff --git a/packages/bruno-app/src/utils/importers/postman-collection.js b/packages/bruno-app/src/utils/importers/postman-collection.js index 902b1a2dc..e34530a19 100644 --- a/packages/bruno-app/src/utils/importers/postman-collection.js +++ b/packages/bruno-app/src/utils/importers/postman-collection.js @@ -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); } } diff --git a/packages/bruno-app/src/utils/url/index.js b/packages/bruno-app/src/utils/url/index.js index 328b22cdc..e1aea7316 100644 --- a/packages/bruno-app/src/utils/url/index.js +++ b/packages/bruno-app/src/utils/url/index.js @@ -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 ''; diff --git a/packages/bruno-app/src/utils/url/index.spec.js b/packages/bruno-app/src/utils/url/index.spec.js index 02112cdf2..1f43affaf 100644 --- a/packages/bruno-app/src/utils/url/index.spec.js +++ b/packages/bruno-app/src/utils/url/index.spec.js @@ -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', '='); diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 63ebdd4ca..e84fc7f0a 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -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); diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 8b1b249db..1e7e083cd 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -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'); diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index 34fb09c6b..262cca650 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -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', []), diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index de1080ac0..41c35d53d 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -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', {}), diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 2139194a2..886ead69b 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -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); diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index e8c88275f..94fe834b1 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -161,6 +161,7 @@ const prepareRequest = (request, collectionRoot, collectionPath) => { method: request.method, url, headers, + params: request.params.filter((param) => param.type === 'path'), responseType: 'arraybuffer' }; diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 6f12a6ce5..d62888d2c 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -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) { diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 3357e5d09..45d7bba0f 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -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) { diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js index 6bf8dd2e4..b1bf21d3e 100644 --- a/packages/bruno-schema/src/collections/index.js +++ b/packages/bruno-schema/src/collections/index.js @@ -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({ diff --git a/packages/bruno-schema/src/collections/index.spec.js b/packages/bruno-schema/src/collections/index.spec.js index 16b683d08..0c6b69156 100644 --- a/packages/bruno-schema/src/collections/index.spec.js +++ b/packages/bruno-schema/src/collections/index.spec.js @@ -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' } diff --git a/packages/bruno-schema/src/collections/requestSchema.spec.js b/packages/bruno-schema/src/collections/requestSchema.spec.js index 87399c690..2430b2862 100644 --- a/packages/bruno-schema/src/collections/requestSchema.spec.js +++ b/packages/bruno-schema/src/collections/requestSchema.spec.js @@ -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' } diff --git a/packages/bruno-toml/tests/index.spec.js b/packages/bruno-toml/tests/index.spec.js index bcd00b77f..cad9d2e17 100644 --- a/packages/bruno-toml/tests/index.spec.js +++ b/packages/bruno-toml/tests/index.spec.js @@ -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); + }); +});