diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js index c9ce0b95..04744b6d 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/RenameCollectionItem/index.js @@ -34,7 +34,7 @@ const RenameCollectionItem = ({ collection, item, onClose }) => { } dispatch(renameItem(values.name, item.uid, collection.uid)) .then(() => { - dispatch(closeTabs({ tabUids: [item.uid] })); + isFolder && dispatch(closeTabs({ tabUids: [item.uid] })); toast.success(isFolder ? 'Folder renamed' : 'Request renamed'); onClose(); }) diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index 97d65990..8e0ff300 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -86,11 +86,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc } catch (err) {} } } else if (contentType === 'multipart/form-data') { - if (typeof request.data === 'object' && !(request?.data instanceof FormData)) { + if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { try { - forOwn(request?.data, (value, key) => { - request.data[key] = _interpolate(value); - }); + request.data = request?.data?.map(d => ({ + ...d, + value: _interpolate(d?.value) + })); } catch (err) {} } } else { diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js index 567087f3..21cf17bd 100644 --- a/packages/bruno-cli/src/runner/prepare-request.js +++ b/packages/bruno-cli/src/runner/prepare-request.js @@ -2,35 +2,7 @@ const { get, each, filter } = require('lodash'); const decomment = require('decomment'); const crypto = require('node:crypto'); const { mergeHeaders, mergeScripts, mergeVars, getTreePathFromCollectionToItem } = require('../utils/collection'); - -const createFormData = (datas, collectionPath) => { - // make axios work in node using form data - // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 - const form = new FormData(); - datas.forEach((item) => { - const value = item.value; - const name = item.name; - let options = {}; - if (item.contentType) { - options.contentType = item.contentType; - } - if (item.type === 'file') { - const filePaths = value || []; - filePaths.forEach((filePath) => { - let trimmedFilePath = filePath.trim(); - - if (!path.isAbsolute(trimmedFilePath)) { - trimmedFilePath = path.join(collectionPath, trimmedFilePath); - } - options.filename = path.basename(trimmedFilePath); - form.append(name, fs.createReadStream(trimmedFilePath), options); - }); - } else { - form.append(name, value, options); - } - }); - return form; -}; +const { createFormData } = require('../utils/form-data'); const prepareRequest = (item = {}, collection = {}) => { const request = item?.request; @@ -164,7 +136,8 @@ const prepareRequest = (item = {}, collection = {}) => { if (request.body.mode === 'multipartForm') { axiosRequest.headers['content-type'] = 'multipart/form-data'; const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - axiosRequest.data = createFormData(enabledParams); + const collectionPath = process.cwd(); + axiosRequest.data = createFormData(enabledParams, collectionPath); } if (request.body.mode === 'graphql') { diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index dd9d450f..07af15c2 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -19,8 +19,9 @@ const { makeAxiosInstance } = require('../utils/axios-instance'); const { addAwsV4Interceptor, resolveAwsV4Credentials } = require('./awsv4auth-helper'); const { shouldUseProxy, PatchedHttpsProxyAgent } = require('../utils/proxy-util'); const path = require('path'); -const { createFormData, parseDataFromResponse } = require('../utils/common'); +const { parseDataFromResponse } = require('../utils/common'); const { getCookieStringForUrl, saveCookies, shouldUseCookies } = require('../utils/cookies'); +const { createFormData } = require('../utils/form-data'); const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/; const onConsoleLog = (type, args) => { diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js index dd45be50..2505225f 100644 --- a/packages/bruno-cli/src/utils/common.js +++ b/packages/bruno-cli/src/utils/common.js @@ -20,30 +20,6 @@ const rpad = (str, width) => { return paddedStr; }; -const createFormData = (datas, collectionPath) => { - // make axios work in node using form data - // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 - const form = new FormData(); - forOwn(datas, (value, key) => { - if (typeof value == 'string') { - form.append(key, value); - return; - } - - const filePaths = value || []; - filePaths?.forEach?.((filePath) => { - let trimmedFilePath = filePath.trim(); - - if (!path.isAbsolute(trimmedFilePath)) { - trimmedFilePath = path.join(collectionPath, trimmedFilePath); - } - - form.append(key, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); - }); - }); - return form; -}; - const parseDataFromResponse = (response, disableParsingResponseJson = false) => { // Parse the charset from content type: https://stackoverflow.com/a/33192813 const charsetMatch = /charset=([^()<>@,;:"/[\]?.=\s]*)/i.exec(response.headers['content-type'] || ''); @@ -73,6 +49,5 @@ const parseDataFromResponse = (response, disableParsingResponseJson = false) => module.exports = { lpad, rpad, - createFormData, parseDataFromResponse }; diff --git a/packages/bruno-cli/src/utils/form-data.js b/packages/bruno-cli/src/utils/form-data.js new file mode 100644 index 00000000..eab5d582 --- /dev/null +++ b/packages/bruno-cli/src/utils/form-data.js @@ -0,0 +1,42 @@ +const { forEach } = require('lodash'); +const FormData = require('form-data'); +const fs = require('fs'); +const path = require('path'); + +const createFormData = (data, collectionPath) => { + // make axios work in node using form data + // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 + const form = new FormData(); + forEach(data, (datum) => { + const { name, type, value, contentType } = datum; + let options = {}; + if (contentType) { + options.contentType = contentType; + } + if (type === 'text') { + if (Array.isArray(value)) { + value.forEach((val) => form.append(name, val, options)); + } else { + form.append(name, value, options); + } + return; + } + + if (type === 'file') { + const filePaths = value || []; + filePaths.forEach((filePath) => { + let trimmedFilePath = filePath.trim(); + if (!path.isAbsolute(trimmedFilePath)) { + trimmedFilePath = path.join(collectionPath, trimmedFilePath); + } + options.filename = path.basename(trimmedFilePath); + form.append(name, fs.createReadStream(trimmedFilePath), options); + }); + } + }); + return form; +}; + +module.exports = { + createFormData +} \ No newline at end of file diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 519da8b3..89832489 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -350,7 +350,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection ipcMain.handle('renderer:rename-item', async (event, oldPath, newPath, newName) => { const tempDir = path.join(os.tmpdir(), `temp-folder-${Date.now()}`); // const parentDir = path.dirname(oldPath); - const isWindowsOSAndNotWSLAndItemHasSubDirectories = isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath); + const isWindowsOSAndNotWSLAndItemHasSubDirectories = isDirectory(oldPath) && isWindowsOS() && !isWSLPath(oldPath) && hasSubDirectories(oldPath); // let parentDirUnwatched = false; // let parentDirRewatched = false; diff --git a/packages/bruno-electron/src/ipc/network/interpolate-vars.js b/packages/bruno-electron/src/ipc/network/interpolate-vars.js index 59f49441..d9833f59 100644 --- a/packages/bruno-electron/src/ipc/network/interpolate-vars.js +++ b/packages/bruno-electron/src/ipc/network/interpolate-vars.js @@ -86,11 +86,12 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc } catch (err) {} } } else if (contentType === 'multipart/form-data') { - if (typeof request.data === 'object' && !(request.data instanceof FormData)) { + if (Array.isArray(request?.data) && !(request.data instanceof FormData)) { try { - forOwn(request?.data, (value, key) => { - request.data[key] = _interpolate(value); - }); + request.data = request?.data?.map(d => ({ + ...d, + value: _interpolate(d?.value) + })); } catch (err) {} } } else { diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js index de6ed163..69825bc4 100644 --- a/packages/bruno-electron/src/ipc/network/prepare-request.js +++ b/packages/bruno-electron/src/ipc/network/prepare-request.js @@ -249,7 +249,7 @@ const prepareRequest = (item, collection) => { axiosRequest.headers['content-type'] = 'multipart/form-data'; } const enabledParams = filter(request.body.multipartForm, (p) => p.enabled); - axiosRequest.data = createFormData(enabledParams); + axiosRequest.data = enabledParams; } if (request.body.mode === 'graphql') { @@ -268,6 +268,10 @@ const prepareRequest = (item, collection) => { axiosRequest.script = request.script; } + if (request.tests) { + axiosRequest.tests = request.tests; + } + axiosRequest.vars = request.vars; axiosRequest.collectionVariables = request.collectionVariables; axiosRequest.folderVariables = request.folderVariables; diff --git a/packages/bruno-electron/src/utils/form-data.js b/packages/bruno-electron/src/utils/form-data.js index cb799682..f2037112 100644 --- a/packages/bruno-electron/src/utils/form-data.js +++ b/packages/bruno-electron/src/utils/form-data.js @@ -1,4 +1,4 @@ -const { forOwn } = require('lodash'); +const { forEach } = require('lodash'); const FormData = require('form-data'); const fs = require('fs'); const path = require('path'); @@ -27,13 +27,16 @@ const createFormData = (data, collectionPath) => { // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 const form = new FormData(); forEach(data, (datum) => { - const { name, type, value } = datum; - + const { name, type, value, contentType } = datum; + let options = {}; + if (contentType) { + options.contentType = contentType; + } if (type === 'text') { if (Array.isArray(value)) { - value.forEach((val) => form.append(name, val)); + value.forEach((val) => form.append(name, val, options)); } else { - form.append(name, value); + form.append(name, value, options); } return; } @@ -42,12 +45,11 @@ const createFormData = (data, collectionPath) => { const filePaths = value || []; filePaths.forEach((filePath) => { let trimmedFilePath = filePath.trim(); - if (!path.isAbsolute(trimmedFilePath)) { trimmedFilePath = path.join(collectionPath, trimmedFilePath); } - - form.append(name, fs.createReadStream(trimmedFilePath), path.basename(trimmedFilePath)); + options.filename = path.basename(trimmedFilePath); + form.append(name, fs.createReadStream(trimmedFilePath), options); }); } }); diff --git a/packages/bruno-js/src/utils.js b/packages/bruno-js/src/utils.js index e15ec09a..55b454d0 100644 --- a/packages/bruno-js/src/utils.js +++ b/packages/bruno-js/src/utils.js @@ -18,8 +18,8 @@ const JS_KEYWORDS = ` * ```js * res.data.pets.map(pet => pet.name.toUpperCase()) * - * function(context) { - * const { res, pet } = context; + * function(__bruno__functionInnerContext) { + * const { res, pet } = __bruno__functionInnerContext; * return res.data.pets.map(pet => pet.name.toUpperCase()) * } * ``` @@ -45,9 +45,11 @@ const compileJsExpression = (expr) => { globals: globals.map((name) => ` ${name} = ${name} ?? globalThis.${name};`).join('') }; - const body = `let { ${code.vars} } = context; ${code.globals}; return ${expr}`; + // param name that is unlikely to show up as a var in an expression + const param = `__bruno__functionInnerContext`; + const body = `let { ${code.vars} } = ${param}; ${code.globals}; return ${expr}`; - return new Function('context', body); + return new Function(param, body); }; const internalExpressionCache = new Map(); diff --git a/packages/bruno-js/tests/utils.spec.js b/packages/bruno-js/tests/utils.spec.js index 6ac687f0..b1ecc7db 100644 --- a/packages/bruno-js/tests/utils.spec.js +++ b/packages/bruno-js/tests/utils.spec.js @@ -5,7 +5,9 @@ describe('utils', () => { describe('expression evaluation', () => { const context = { res: { - data: { pets: ['bruno', 'max'] } + data: { pets: ['bruno', 'max'] }, + context: 'testContext', + __bruno__functionInnerContext: 0 } }; @@ -45,32 +47,32 @@ describe('utils', () => { it('should identify top level variables', () => { const expr = 'res.data.pets[0].toUpperCase()'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should not duplicate variables', () => { const expr = 'res.data.pets[0] + res.data.pets[1]'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should exclude js keywords like true false from vars', () => { const expr = 'res.data.pets.length > 0 ? true : false'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should exclude numbers from vars', () => { const expr = 'res.data.pets.length + 10'; evaluateJsExpression(expr, context); - expect(cache.get(expr).toString()).toContain('let { res } = context;'); + expect(cache.get(expr).toString()).toContain('let { res } = __bruno__functionInnerContext;'); }); it('should pick variables from complex expressions', () => { const expr = 'res.data.pets.map(pet => pet.length)'; const result = evaluateJsExpression(expr, context); expect(result).toEqual([5, 3]); - expect(cache.get(expr).toString()).toContain('let { res, pet } = context;'); + expect(cache.get(expr).toString()).toContain('let { res, pet } = __bruno__functionInnerContext;'); }); it('should be ok picking extra vars from strings', () => { @@ -78,7 +80,7 @@ describe('utils', () => { const result = evaluateJsExpression(expr, context); expect(result).toBe('hello bruno'); // extra var hello is harmless - expect(cache.get(expr).toString()).toContain('let { hello, res } = context;'); + expect(cache.get(expr).toString()).toContain('let { hello, res } = __bruno__functionInnerContext;'); }); it('should evaluate expressions referencing globals', () => { @@ -112,6 +114,20 @@ describe('utils', () => { expect(result).toBe(startTime); }); + + it('should allow "context" as a var name', () => { + const expr = 'res["context"].toUpperCase()'; + evaluateJsExpression(expr, context); + expect(cache.get(expr).toString()).toContain('let { res, context } = __bruno__functionInnerContext;'); + }); + + it('should throw an error when we use "__bruno__functionInnerContext" as a var name', () => { + const expr = 'res["__bruno__functionInnerContext"].toUpperCase()'; + expect(() => evaluateJsExpression(expr, context)).toThrow(SyntaxError); + expect(() => evaluateJsExpression(expr, context)).toThrow( + "Identifier '__bruno__functionInnerContext' has already been declared" + ); + }); }); describe('response parser', () => { diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js index 13244389..228691c1 100644 --- a/packages/bruno-lang/v2/src/bruToJson.js +++ b/packages/bruno-lang/v2/src/bruToJson.js @@ -162,7 +162,7 @@ const mapRequestParams = (pairList = [], type) => { const multipartExtractContentType = (pair) => { if (_.isString(pair.value)) { - const match = pair.value.match(/^(.*?)\s*\(Content-Type=(.*?)\)\s*$/); + const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/); if (match != null && match.length > 2) { pair.value = match[1]; pair.contentType = match[2]; diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js index 03e36c71..5c8a573b 100644 --- a/packages/bruno-lang/v2/src/jsonToBru.js +++ b/packages/bruno-lang/v2/src/jsonToBru.js @@ -281,7 +281,7 @@ ${indentString(body.sparql)} .map((item) => { const enabled = item.enabled ? '' : '~'; const contentType = - item.contentType && item.contentType !== '' ? ' (Content-Type=' + item.contentType + ')' : ''; + item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : ''; if (item.type === 'text') { return `${enabled}${item.name}: ${getValueString(item.value)}${contentType}`; diff --git a/packages/bruno-tests/collection/multipart/mixed-content-types.bru b/packages/bruno-tests/collection/multipart/mixed-content-types.bru index 45a1cdd1..29ed04ba 100644 --- a/packages/bruno-tests/collection/multipart/mixed-content-types.bru +++ b/packages/bruno-tests/collection/multipart/mixed-content-types.bru @@ -12,13 +12,13 @@ post { body:multipart-form { param1: test - param2: {"test":"i am json"} (Content-Type=application/json) + param2: {"test":"i am json"} @contentType(application/json) param3: @file(multipart/small.png) } assert { res.status: eq 200 - res.body.find(p=>p.name === 'param1').contentType: isUndefined + res.body.find(p=>p.name === 'param1').contentType: isUndefined res.body.find(p=>p.name === 'param2').contentType: eq application/json res.body.find(p=>p.name === 'param3').contentType: eq image/png } diff --git a/packages/bruno-tests/src/index.js b/packages/bruno-tests/src/index.js index 0bff6ab7..a09cb434 100644 --- a/packages/bruno-tests/src/index.js +++ b/packages/bruno-tests/src/index.js @@ -5,6 +5,7 @@ const formDataParser = require('./multipart/form-data-parser'); const authRouter = require('./auth'); const echoRouter = require('./echo'); const xmlParser = require('./utils/xmlParser'); +const multipartRouter = require('./multipart'); const app = new express(); const port = process.env.PORT || 8080;