handleCloseClick(e)}>
+
{
+ if (!item.draft) return handleCloseClick(e);
+
+ setShowConfirmClose(true);
+ }}
+ >
{!item.draft ? (
diff --git a/packages/bruno-app/src/globalStyles.js b/packages/bruno-app/src/globalStyles.js
index 7cf517bdf..34e5e70aa 100644
--- a/packages/bruno-app/src/globalStyles.js
+++ b/packages/bruno-app/src/globalStyles.js
@@ -129,6 +129,24 @@ const GlobalStyle = createGlobalStyle`
}
}
+ @keyframes rotateClockwise {
+ 0% {
+ transform: scaleY(-1) rotate(0deg);
+ }
+ 100% {
+ transform: scaleY(-1) rotate(360deg);
+ }
+ }
+
+ @keyframes rotateCounterClockwise {
+ 0% {
+ transform: scaleY(-1) rotate(360deg);
+ }
+ 100% {
+ transform: scaleY(-1) rotate(0deg);
+ }
+ }
+
// codemirror
.CodeMirror {
.cm-variable-valid {
diff --git a/packages/bruno-app/src/pageComponents/Index/index.js b/packages/bruno-app/src/pageComponents/Index/index.js
index f2620052d..9f4de2434 100644
--- a/packages/bruno-app/src/pageComponents/Index/index.js
+++ b/packages/bruno-app/src/pageComponents/Index/index.js
@@ -14,24 +14,26 @@ const SERVER_RENDERED = typeof navigator === 'undefined' || global['PREVENT_CODE
if (!SERVER_RENDERED) {
require('codemirror/mode/javascript/javascript');
require('codemirror/mode/xml/xml');
- require('codemirror/addon/scroll/simplescrollbars');
+ require('codemirror/mode/sparql/sparql');
+ require('codemirror/addon/comment/comment');
+ require('codemirror/addon/dialog/dialog');
+ require('codemirror/addon/edit/closebrackets');
require('codemirror/addon/edit/matchbrackets');
require('codemirror/addon/fold/brace-fold');
require('codemirror/addon/fold/foldgutter');
- require('codemirror/addon/mode/overlay');
require('codemirror/addon/hint/show-hint');
- require('codemirror/keymap/sublime');
- require('codemirror/addon/comment/comment');
- require('codemirror/addon/edit/closebrackets');
+ require('codemirror/addon/lint/lint');
+ require('codemirror/addon/mode/overlay');
+ require('codemirror/addon/scroll/simplescrollbars');
+ require('codemirror/addon/search/jump-to-line');
require('codemirror/addon/search/search');
require('codemirror/addon/search/searchcursor');
- require('codemirror/addon/search/jump-to-line');
- require('codemirror/addon/dialog/dialog');
+ require('codemirror/keymap/sublime');
require('codemirror-graphql/hint');
- require('codemirror-graphql/lint');
require('codemirror-graphql/info');
require('codemirror-graphql/jump');
+ require('codemirror-graphql/lint');
require('codemirror-graphql/mode');
require('utils/codemirror/brunoVarInfo');
diff --git a/packages/bruno-app/src/providers/Hotkeys/index.js b/packages/bruno-app/src/providers/Hotkeys/index.js
index 522fa0d46..468061305 100644
--- a/packages/bruno-app/src/providers/Hotkeys/index.js
+++ b/packages/bruno-app/src/providers/Hotkeys/index.js
@@ -51,7 +51,8 @@ export const HotkeysProvider = (props) => {
if (item && item.uid) {
dispatch(saveRequest(activeTab.uid, activeTab.collectionUid));
} else {
- setShowSaveRequestModal(true);
+ // todo: when ephermal requests go live
+ // setShowSaveRequestModal(true);
}
}
}
diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
index c1ec2ff3b..fbe404584 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js
@@ -45,6 +45,8 @@ import {
import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
+import { parseQueryParams, splitOnFirst } from 'utils/url/index';
+import { each } from 'lodash';
const PATH_SEPARATOR = path.sep;
@@ -82,8 +84,35 @@ export const saveRequest = (itemUid, collectionUid) => (dispatch, getState) => {
itemSchema
.validate(itemToSave)
.then(() => ipcRenderer.invoke('renderer:save-request', item.pathname, itemToSave))
+ .then(() => toast.success('Request saved successfully'))
.then(resolve)
- .catch(reject);
+ .catch((err) => {
+ toast.error('Failed to save request!');
+ reject(err);
+ });
+ });
+};
+
+export const saveCollectionRoot = (collectionUid) => (dispatch, getState) => {
+ const state = getState();
+ const collection = findCollectionByUid(state.collections.collections, collectionUid);
+ console.log(collection.root);
+
+ return new Promise((resolve, reject) => {
+ if (!collection) {
+ return reject(new Error('Collection not found'));
+ }
+
+ const { ipcRenderer } = window;
+
+ ipcRenderer
+ .invoke('renderer:save-collection-root', collection.pathname, collection.root)
+ .then(() => toast.success('Collection Settings saved successfully'))
+ .then(resolve)
+ .catch((err) => {
+ toast.error('Failed to save collection settings!');
+ reject(err);
+ });
});
};
@@ -561,6 +590,12 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
return reject(new Error('Collection not found'));
}
+ const parts = splitOnFirst(requestUrl, '?');
+ const params = parseQueryParams(parts[1]);
+ each(params, (urlParam) => {
+ urlParam.enabled = true;
+ });
+
const collectionCopy = cloneDeep(collection);
const item = {
uid: uuid(),
@@ -570,11 +605,13 @@ export const newHttpRequest = (params) => (dispatch, getState) => {
method: requestMethod,
url: requestUrl,
headers: [],
+ params,
body: {
mode: 'none',
json: null,
text: null,
xml: null,
+ sparql: null,
multipartForm: null,
formUrlEncoded: null
}
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 e91d2a0e0..f8595daa1 100644
--- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
+++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js
@@ -7,6 +7,8 @@ import concat from 'lodash/concat';
import filter from 'lodash/filter';
import each from 'lodash/each';
import cloneDeep from 'lodash/cloneDeep';
+import get from 'lodash/get';
+import set from 'lodash/set';
import { createSlice } from '@reduxjs/toolkit';
import { splitOnFirst } from 'utils/url';
import {
@@ -40,6 +42,8 @@ export const collectionsSlice = createSlice({
const collectionUids = map(state.collections, (c) => c.uid);
const collection = action.payload;
+ collection.settingsSelectedTab = 'headers';
+
// TODO: move this to use the nextAction approach
// last action is used to track the last action performed on the collection
// this is optional
@@ -107,6 +111,15 @@ export const collectionsSlice = createSlice({
collection.nextAction = nextAction;
}
},
+ updateSettingsSelectedTab: (state, action) => {
+ const { collectionUid, tab } = action.payload;
+
+ const collection = findCollectionByUid(state.collections, collectionUid);
+
+ if (collection) {
+ collection.settingsSelectedTab = tab;
+ }
+ },
collectionUnlinkEnvFileEvent: (state, action) => {
const { data: environment, meta } = action.payload;
const collection = findCollectionByUid(state.collections, meta.collectionUid);
@@ -258,10 +271,27 @@ export const collectionsSlice = createSlice({
}
}
},
+ deleteRequestDraft: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const item = findItemInCollection(collection, action.payload.itemUid);
+
+ if (item && item.draft) {
+ item.draft = null;
+ }
+ }
+ },
newEphemeralHttpRequest: (state, action) => {
const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
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 item = {
uid: action.payload.uid,
name: action.payload.requestName,
@@ -269,7 +299,7 @@ export const collectionsSlice = createSlice({
request: {
url: action.payload.requestUrl,
method: action.payload.requestMethod,
- params: [],
+ params,
headers: [],
body: {
mode: null,
@@ -672,6 +702,10 @@ export const collectionsSlice = createSlice({
item.draft.request.body.xml = action.payload.content;
break;
}
+ case 'sparql': {
+ item.draft.request.body.sparql = action.payload.content;
+ break;
+ }
case 'formUrlEncoded': {
item.draft.request.body.formUrlEncoded = action.payload.content;
break;
@@ -923,10 +957,100 @@ export const collectionsSlice = createSlice({
}
}
},
+ updateCollectionAuthMode: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ set(collection, 'root.request.auth.mode', action.payload.mode);
+ }
+ },
+ updateCollectionAuth: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ switch (action.payload.mode) {
+ case 'bearer':
+ set(collection, 'root.request.auth.bearer', action.payload.content);
+ break;
+ case 'basic':
+ set(collection, 'root.request.auth.basic', action.payload.content);
+ break;
+ }
+ }
+ },
+ updateCollectionRequestScript: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ set(collection, 'root.request.script.req', action.payload.script);
+ }
+ },
+ updateCollectionResponseScript: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ set(collection, 'root.request.script.res', action.payload.script);
+ }
+ },
+
+ updateCollectionTests: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ set(collection, 'root.request.tests', action.payload.tests);
+ }
+ },
+ addCollectionHeader: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const headers = get(collection, 'root.request.headers', []);
+ headers.push({
+ uid: uuid(),
+ name: '',
+ value: '',
+ description: '',
+ enabled: true
+ });
+ set(collection, 'root.request.headers', headers);
+ }
+ },
+ updateCollectionHeader: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ const headers = get(collection, 'root.request.headers', []);
+ const header = find(headers, (h) => h.uid === action.payload.header.uid);
+ if (header) {
+ header.name = action.payload.header.name;
+ header.value = action.payload.header.value;
+ header.description = action.payload.header.description;
+ header.enabled = action.payload.header.enabled;
+ }
+ }
+ },
+ deleteCollectionHeader: (state, action) => {
+ const collection = findCollectionByUid(state.collections, action.payload.collectionUid);
+
+ if (collection) {
+ let headers = get(collection, 'root.request.headers', []);
+ headers = filter(headers, (h) => h.uid !== action.payload.headerUid);
+ set(collection, 'root.request.headers', headers);
+ }
+ },
collectionAddFileEvent: (state, action) => {
const file = action.payload.file;
+ const isCollectionRoot = file.meta.collectionRoot ? true : false;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
+ if (isCollectionRoot) {
+ if (collection) {
+ collection.root = file.data;
+ }
+ console.log('collectionAddFileEvent', file);
+ return;
+ }
+
if (collection) {
const dirname = getDirectoryName(file.meta.pathname);
const subDirectories = getSubdirectoriesFromRoot(collection.pathname, dirname);
@@ -1011,6 +1135,12 @@ export const collectionsSlice = createSlice({
const { file } = action.payload;
const collection = findCollectionByUid(state.collections, file.meta.collectionUid);
+ // check and update collection root
+ if (collection && file.meta.collectionRoot) {
+ collection.root = file.data;
+ return;
+ }
+
if (collection) {
const item = findItemInCollection(collection, file.data.uid);
@@ -1107,7 +1237,6 @@ export const collectionsSlice = createSlice({
const { cancelTokenUid } = action.payload;
item.requestUid = requestUid;
item.requestState = 'queued';
- item.response = null;
item.cancelTokenUid = cancelTokenUid;
}
@@ -1216,6 +1345,7 @@ export const {
sortCollections,
updateLastAction,
updateNextAction,
+ updateSettingsSelectedTab,
collectionUnlinkEnvFileEvent,
saveEnvironment,
selectEnvironment,
@@ -1228,6 +1358,7 @@ export const {
requestCancelled,
responseReceived,
saveRequest,
+ deleteRequestDraft,
newEphemeralHttpRequest,
collectionClicked,
collectionFolderClicked,
@@ -1260,6 +1391,14 @@ export const {
addVar,
updateVar,
deleteVar,
+ addCollectionHeader,
+ updateCollectionHeader,
+ deleteCollectionHeader,
+ updateCollectionAuthMode,
+ updateCollectionAuth,
+ updateCollectionRequestScript,
+ updateCollectionResponseScript,
+ updateCollectionTests,
collectionAddFileEvent,
collectionAddDirectoryEvent,
collectionChangeFileEvent,
diff --git a/packages/bruno-app/src/styles/globals.css b/packages/bruno-app/src/styles/globals.css
index fb8eb5b5f..c820ff134 100644
--- a/packages/bruno-app/src/styles/globals.css
+++ b/packages/bruno-app/src/styles/globals.css
@@ -53,3 +53,12 @@ body::-webkit-scrollbar-thumb,
background-color: #cdcdcd;
border-radius: 5rem;
}
+
+/*
+ * todo: this will be supported in the future to be changed via applying a theme
+ * making all the checkboxes and radios bigger
+ * input[type='checkbox'],
+ * input[type='radio'] {
+ * transform: scale(1.1);
+ * }
+ */
diff --git a/packages/bruno-app/src/themes/dark.js b/packages/bruno-app/src/themes/dark.js
index 7a4ad64de..3dda2505d 100644
--- a/packages/bruno-app/src/themes/dark.js
+++ b/packages/bruno-app/src/themes/dark.js
@@ -86,7 +86,11 @@ const darkTheme = {
get: '#8cd656',
post: '#cd56d6',
put: '#d69956',
- delete: '#f06f57'
+ delete: '#f06f57',
+ // customize these colors if needed
+ patch: '#d69956',
+ options: '#d69956',
+ head: '#d69956'
}
},
@@ -105,7 +109,8 @@ const darkTheme = {
responseSendIcon: '#555',
responseStatus: '#ccc',
responseOk: '#8cd656',
- responseError: '#f06f57'
+ responseError: '#f06f57',
+ responseOverlayBg: 'rgba(30, 30, 30, 0.6)'
},
collection: {
diff --git a/packages/bruno-app/src/themes/light.js b/packages/bruno-app/src/themes/light.js
index b014c2f38..5b9f1a8bc 100644
--- a/packages/bruno-app/src/themes/light.js
+++ b/packages/bruno-app/src/themes/light.js
@@ -86,7 +86,11 @@ const lightTheme = {
get: 'rgb(5, 150, 105)',
post: '#8e44ad',
put: '#ca7811',
- delete: 'rgb(185, 28, 28)'
+ delete: 'rgb(185, 28, 28)',
+ // customize these colors if needed
+ patch: '#ca7811',
+ options: '#ca7811',
+ head: '#ca7811'
}
},
@@ -105,7 +109,8 @@ const lightTheme = {
responseSendIcon: 'rgb(209, 213, 219)',
responseStatus: 'rgb(117 117 117)',
responseOk: '#047857',
- responseError: 'rgb(185, 28, 28)'
+ responseError: 'rgb(185, 28, 28)',
+ responseOverlayBg: 'rgba(255, 255, 255, 0.6)'
},
collection: {
diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js
index 9abe1a66f..e4532b7e0 100644
--- a/packages/bruno-app/src/utils/collections/index.js
+++ b/packages/bruno-app/src/utils/collections/index.js
@@ -138,7 +138,7 @@ export const moveCollectionItem = (collection, draggedItem, targetItem) => {
}
if (targetItem.type === 'folder') {
- targetItem.items = targetItem.items || [];
+ targetItem.items = sortBy(targetItem.items || [], (item) => item.seq);
targetItem.items.push(draggedItem);
draggedItem.pathname = path.join(targetItem.pathname, draggedItem.filename);
} else {
@@ -166,7 +166,9 @@ export const moveCollectionItemToRootOfCollection = (collection, draggedItem) =>
return;
}
+ draggedItemParent.items = sortBy(draggedItemParent.items, (item) => item.seq);
draggedItemParent.items = filter(draggedItemParent.items, (i) => i.uid !== draggedItem.uid);
+ collection.items = sortBy(collection.items, (item) => item.seq);
collection.items.push(draggedItem);
if (draggedItem.type == 'folder') {
draggedItem.pathname = path.join(collection.pathname, draggedItem.name);
@@ -282,6 +284,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
text: si.draft.request.body.text,
xml: si.draft.request.body.xml,
graphql: si.draft.request.body.graphql,
+ sparql: si.draft.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.draft.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.draft.request.body.multipartForm)
},
@@ -314,6 +317,7 @@ export const transformCollectionToSaveToExportAsFile = (collection, options = {}
text: si.request.body.text,
xml: si.request.body.xml,
graphql: si.request.body.graphql,
+ sparql: si.request.body.sparql,
formUrlEncoded: copyFormUrlEncodedParams(si.request.body.formUrlEncoded),
multipartForm: copyMultipartFormParams(si.request.body.multipartForm)
},
@@ -457,6 +461,10 @@ export const humanizeRequestBodyMode = (mode) => {
label = 'XML';
break;
}
+ case 'sparql': {
+ label = 'SPARQL';
+ break;
+ }
case 'formUrlEncoded': {
label = 'Form URL Encoded';
break;
diff --git a/packages/bruno-app/src/utils/common/codemirror.js b/packages/bruno-app/src/utils/common/codemirror.js
index ce2f8be17..78016de6f 100644
--- a/packages/bruno-app/src/utils/common/codemirror.js
+++ b/packages/bruno-app/src/utils/common/codemirror.js
@@ -60,6 +60,8 @@ export const getCodeMirrorModeBasedOnContentType = (contentType) => {
return 'application/xml';
} else if (contentType.includes('yaml')) {
return 'application/yaml';
+ } else if (contentType.includes('image')) {
+ return 'application/image';
} else {
return 'application/text';
}
diff --git a/packages/bruno-app/src/utils/common/platform.js b/packages/bruno-app/src/utils/common/platform.js
index e49a66ec9..771daaf14 100644
--- a/packages/bruno-app/src/utils/common/platform.js
+++ b/packages/bruno-app/src/utils/common/platform.js
@@ -41,3 +41,10 @@ export const isWindowsOS = () => {
return osFamily.includes('windows');
};
+
+export const isMacOS = () => {
+ const os = platform.os;
+ const osFamily = os.family.toLowerCase();
+
+ return osFamily.includes('os x');
+};
diff --git a/packages/bruno-app/src/utils/network/index.js b/packages/bruno-app/src/utils/network/index.js
index 2c5c3a1d3..8952e1986 100644
--- a/packages/bruno-app/src/utils/network/index.js
+++ b/packages/bruno-app/src/utils/network/index.js
@@ -1,3 +1,5 @@
+import { safeStringifyJSON } from 'utils/common';
+
export const sendNetworkRequest = async (item, collection, environment, collectionVariables) => {
return new Promise((resolve, reject) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
@@ -7,7 +9,7 @@ export const sendNetworkRequest = async (item, collection, environment, collecti
state: 'success',
data: response.data,
headers: Object.entries(response.headers),
- size: response.headers['content-length'] || 0,
+ size: getResponseSize(response),
status: response.status,
statusText: response.statusText,
duration: response.duration
@@ -23,20 +25,21 @@ const sendHttpRequest = async (item, collection, environment, collectionVariable
const { ipcRenderer } = window;
ipcRenderer
- .invoke('send-http-request', item, collection.uid, collection.pathname, environment, collectionVariables)
+ .invoke('send-http-request', item, collection, environment, collectionVariables)
.then(resolve)
.catch(reject);
});
};
-export const fetchGqlSchema = async (endpoint, environment, request, collectionVariables) => {
+const getResponseSize = (response) => {
+ return response.headers['content-length'] || Buffer.byteLength(safeStringifyJSON(response.data)) || 0;
+};
+
+export const fetchGqlSchema = async (endpoint, environment, request, collection) => {
return new Promise((resolve, reject) => {
const { ipcRenderer } = window;
- ipcRenderer
- .invoke('fetch-gql-schema', endpoint, environment, request, collectionVariables)
- .then(resolve)
- .catch(reject);
+ ipcRenderer.invoke('fetch-gql-schema', endpoint, environment, request, collection).then(resolve).catch(reject);
});
};
diff --git a/packages/bruno-cli/changelog.md b/packages/bruno-cli/changelog.md
index 1b83060f4..44130498a 100644
--- a/packages/bruno-cli/changelog.md
+++ b/packages/bruno-cli/changelog.md
@@ -1,5 +1,9 @@
# Changelog
+## 0.13.0
+
+- feat(#306) Module whitelisting and filesystem access support
+
## 0.12.0
- show response time in milliseconds per request and total
diff --git a/packages/bruno-cli/package.json b/packages/bruno-cli/package.json
index c5e213509..f36459c78 100644
--- a/packages/bruno-cli/package.json
+++ b/packages/bruno-cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@usebruno/cli",
- "version": "0.12.0",
+ "version": "0.14.0",
"license": "MIT",
"main": "src/index.js",
"bin": {
@@ -24,8 +24,8 @@
"package.json"
],
"dependencies": {
- "@usebruno/js": "0.6.0",
- "@usebruno/lang": "0.5.0",
+ "@usebruno/js": "0.8.0",
+ "@usebruno/lang": "0.8.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
"chalk": "^3.0.0",
@@ -39,6 +39,7 @@
"lodash": "^4.17.21",
"mustache": "^4.2.0",
"qs": "^6.11.0",
+ "socks-proxy-agent": "^8.0.2",
"yargs": "^17.6.2"
}
}
diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js
index 7866425ec..78b7226ca 100644
--- a/packages/bruno-cli/src/commands/run.js
+++ b/packages/bruno-cli/src/commands/run.js
@@ -6,7 +6,7 @@ const { exists, isFile, isDirectory } = require('../utils/filesystem');
const { runSingleRequest } = require('../runner/run-single-request');
const { bruToEnvJson, getEnvVars } = require('../utils/bru');
const { rpad } = require('../utils/common');
-const { bruToJson, getOptions } = require('../utils/bru');
+const { bruToJson, getOptions, collectionBruToJson } = require('../utils/bru');
const { dotenvToJson } = require('@usebruno/lang');
const command = 'run [filename]';
@@ -121,6 +121,9 @@ const getBruFilesRecursively = (dir) => {
const currentDirBruJsons = [];
for (const file of filesInCurrentDir) {
+ if (['collection.bru', 'folder.bru'].includes(file)) {
+ continue;
+ }
const filePath = path.join(currentPath, file);
const stats = fs.lstatSync(filePath);
@@ -151,6 +154,19 @@ const getBruFilesRecursively = (dir) => {
return getFilesInOrder(dir);
};
+const getCollectionRoot = (dir) => {
+ const collectionRootPath = path.join(dir, 'collection.bru');
+ const exists = fs.existsSync(collectionRootPath);
+ if (!exists) {
+ return {};
+ }
+
+ const content = fs.readFileSync(collectionRootPath, 'utf8');
+ const json = collectionBruToJson(content);
+
+ return json;
+};
+
const builder = async (yargs) => {
yargs
.option('r', {
@@ -210,6 +226,7 @@ const handler = async function (argv) {
const brunoConfigFile = fs.readFileSync(brunoJsonPath, 'utf8');
const brunoConfig = JSON.parse(brunoConfigFile);
+ const collectionRoot = getCollectionRoot(collectionPath);
if (filename && filename.length) {
const pathExists = await exists(filename);
@@ -349,7 +366,8 @@ const handler = async function (argv) {
collectionVariables,
envVars,
processEnvVars,
- brunoConfig
+ brunoConfig,
+ collectionRoot
);
results.push(result);
diff --git a/packages/bruno-cli/src/runner/interpolate-string.js b/packages/bruno-cli/src/runner/interpolate-string.js
new file mode 100644
index 000000000..33701dd0b
--- /dev/null
+++ b/packages/bruno-cli/src/runner/interpolate-string.js
@@ -0,0 +1,55 @@
+const Handlebars = require('handlebars');
+const { forOwn, cloneDeep } = require('lodash');
+
+const interpolateEnvVars = (str, processEnvVars) => {
+ if (!str || !str.length || typeof str !== 'string') {
+ return str;
+ }
+
+ const template = Handlebars.compile(str, { noEscape: true });
+
+ return template({
+ process: {
+ env: {
+ ...processEnvVars
+ }
+ }
+ });
+};
+
+const interpolateString = (str, { envVars, collectionVariables, processEnvVars }) => {
+ if (!str || !str.length || typeof str !== 'string') {
+ return str;
+ }
+
+ processEnvVars = processEnvVars || {};
+ collectionVariables = collectionVariables || {};
+
+ // we clone envVars because we don't want to modify the original object
+ envVars = envVars ? cloneDeep(envVars) : {};
+
+ // envVars can inturn have values as {{process.env.VAR_NAME}}
+ // so we need to interpolate envVars first with processEnvVars
+ forOwn(envVars, (value, key) => {
+ envVars[key] = interpolateEnvVars(value, processEnvVars);
+ });
+
+ const template = Handlebars.compile(str, { noEscape: true });
+
+ // collectionVariables take precedence over envVars
+ const combinedVars = {
+ ...envVars,
+ ...collectionVariables,
+ process: {
+ env: {
+ ...processEnvVars
+ }
+ }
+ };
+
+ return template(combinedVars);
+};
+
+module.exports = {
+ interpolateString
+};
diff --git a/packages/bruno-cli/src/runner/prepare-request.js b/packages/bruno-cli/src/runner/prepare-request.js
index e52cb5418..5568ae311 100644
--- a/packages/bruno-cli/src/runner/prepare-request.js
+++ b/packages/bruno-cli/src/runner/prepare-request.js
@@ -1,9 +1,20 @@
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
-const prepareRequest = (request) => {
+const prepareRequest = (request, collectionRoot) => {
const headers = {};
let contentTypeDefined = false;
+
+ // collection headers
+ each(get(collectionRoot, 'request.headers', []), (h) => {
+ if (h.enabled) {
+ headers[h.name] = h.value;
+ if (h.name.toLowerCase() === 'content-type') {
+ contentTypeDefined = true;
+ }
+ }
+ });
+
each(request.headers, (h) => {
if (h.enabled) {
headers[h.name] = h.value;
@@ -20,6 +31,23 @@ const prepareRequest = (request) => {
};
// Authentication
+ // A request can override the collection auth with another auth
+ // But it cannot override the collection auth with no auth
+ // We will provide support for disabling the auth via scripting in the future
+ const collectionAuth = get(collectionRoot, 'request.auth');
+ if (collectionAuth) {
+ if (collectionAuth.mode === 'basic') {
+ axiosRequest.auth = {
+ username: get(collectionAuth, 'basic.username'),
+ password: get(collectionAuth, 'basic.password')
+ };
+ }
+
+ if (collectionAuth.mode === 'bearer') {
+ axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
+ }
+ }
+
if (request.auth) {
if (request.auth.mode === 'basic') {
axiosRequest.auth = {
diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js
index 7497a53c4..082c3d416 100644
--- a/packages/bruno-cli/src/runner/run-single-request.js
+++ b/packages/bruno-cli/src/runner/run-single-request.js
@@ -1,17 +1,20 @@
+const os = require('os');
const qs = require('qs');
const chalk = require('chalk');
const decomment = require('decomment');
const fs = require('fs');
-const { forOwn, each, extend, get } = require('lodash');
+const { forOwn, each, extend, get, compact } = require('lodash');
const FormData = require('form-data');
const prepareRequest = require('./prepare-request');
const interpolateVars = require('./interpolate-vars');
+const { interpolateString } = require('./interpolate-string');
const { ScriptRuntime, TestRuntime, VarsRuntime, AssertRuntime } = require('@usebruno/js');
const { stripExtension } = require('../utils/filesystem');
const { getOptions } = require('../utils/bru');
const https = require('https');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent');
+const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('../utils/axios-instance');
const runSingleRequest = async function (
@@ -21,13 +24,16 @@ const runSingleRequest = async function (
collectionVariables,
envVariables,
processEnvVars,
- brunoConfig
+ brunoConfig,
+ collectionRoot
) {
try {
let request;
request = prepareRequest(bruJson.request);
+ const scriptingConfig = get(brunoConfig, 'scripts', {});
+
// make axios work in node using form data
// reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
@@ -54,7 +60,10 @@ const runSingleRequest = async function (
}
// run pre request script
- const requestScriptFile = get(bruJson, 'request.script.req');
+ const requestScriptFile = compact([
+ get(collectionRoot, 'request.script.req'),
+ get(bruJson, 'request.script.req')
+ ]).join(os.EOL);
if (requestScriptFile && requestScriptFile.length) {
const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runRequestScript(
@@ -64,7 +73,8 @@ const runSingleRequest = async function (
collectionVariables,
collectionPath,
null,
- processEnvVars
+ processEnvVars,
+ scriptingConfig
);
}
@@ -92,28 +102,44 @@ const runSingleRequest = async function (
// set proxy if enabled
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
- const proxyProtocol = get(brunoConfig, 'proxy.protocol');
- const proxyHostname = get(brunoConfig, 'proxy.hostname');
- const proxyPort = get(brunoConfig, 'proxy.port');
- const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
+ let proxyUri;
+ const interpolationOptions = {
+ envVars: envVariables,
+ collectionVariables,
+ processEnvVars
+ };
- let proxy;
+ const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
+ const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
+ const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
+ const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
+ const socksEnabled = proxyProtocol.includes('socks');
+
+ interpolateString;
if (proxyAuthEnabled) {
- const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
- const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
+ const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
+ const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
- proxy = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
+ proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
} else {
- proxy = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
+ proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
}
- request.httpsAgent = new HttpsProxyAgent(
- proxy,
- Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
- );
+ if (socksEnabled) {
+ const socksProxyAgent = new SocksProxyAgent(proxyUri);
- request.httpAgent = new HttpProxyAgent(proxy);
+ request.httpsAgent = socksProxyAgent;
+
+ request.httpAgent = socksProxyAgent;
+ } else {
+ request.httpsAgent = new HttpsProxyAgent(
+ proxyUri,
+ Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
+ );
+
+ request.httpAgent = new HttpProxyAgent(proxyUri);
+ }
} else if (Object.keys(httpsAgentRequestFields).length > 0) {
request.httpsAgent = new https.Agent({
...httpsAgentRequestFields
@@ -187,7 +213,10 @@ const runSingleRequest = async function (
}
// run post response script
- const responseScriptFile = get(bruJson, 'request.script.res');
+ const responseScriptFile = compact([
+ get(collectionRoot, 'request.script.res'),
+ get(bruJson, 'request.script.res')
+ ]).join(os.EOL);
if (responseScriptFile && responseScriptFile.length) {
const scriptRuntime = new ScriptRuntime();
await scriptRuntime.runResponseScript(
@@ -198,7 +227,8 @@ const runSingleRequest = async function (
collectionVariables,
collectionPath,
null,
- processEnvVars
+ processEnvVars,
+ scriptingConfig
);
}
@@ -228,7 +258,7 @@ const runSingleRequest = async function (
// run tests
let testResults = [];
- const testFile = get(bruJson, 'request.tests');
+ const testFile = compact([get(collectionRoot, 'request.tests'), get(bruJson, 'request.tests')]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const result = await testRuntime.runTests(
@@ -239,7 +269,8 @@ const runSingleRequest = async function (
collectionVariables,
collectionPath,
null,
- processEnvVars
+ processEnvVars,
+ scriptingConfig
);
testResults = get(result, 'results', []);
}
@@ -273,6 +304,7 @@ const runSingleRequest = async function (
testResults
};
} catch (err) {
+ console.log(chalk.red(stripExtension(filename)) + chalk.dim(` (${err.message})`));
return {
request: {
method: null,
diff --git a/packages/bruno-cli/src/utils/axios-instance.js b/packages/bruno-cli/src/utils/axios-instance.js
index f4810becd..286ffc0f5 100644
--- a/packages/bruno-cli/src/utils/axios-instance.js
+++ b/packages/bruno-cli/src/utils/axios-instance.js
@@ -26,7 +26,9 @@ function makeAxiosInstance() {
if (error.response) {
const end = Date.now();
const start = error.config.headers['request-start-time'];
- error.response.headers['request-duration'] = end - start;
+ if (error.response) {
+ error.response.headers['request-duration'] = end - start;
+ }
}
return Promise.reject(error);
}
diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js
index 684106399..34fb09c6b 100644
--- a/packages/bruno-cli/src/utils/bru.js
+++ b/packages/bruno-cli/src/utils/bru.js
@@ -1,12 +1,33 @@
const _ = require('lodash');
const Mustache = require('mustache');
-const { bruToEnvJsonV2, bruToJsonV2 } = require('@usebruno/lang');
+const { bruToEnvJsonV2, bruToJsonV2, collectionBruToJson: _collectionBruToJson } = require('@usebruno/lang');
// override the default escape function to prevent escaping
Mustache.escape = function (value) {
return value;
};
+const collectionBruToJson = (bru) => {
+ try {
+ const json = _collectionBruToJson(bru);
+
+ const transformedJson = {
+ request: {
+ params: _.get(json, 'query', []),
+ headers: _.get(json, 'headers', []),
+ auth: _.get(json, 'auth', {}),
+ script: _.get(json, 'script', {}),
+ vars: _.get(json, 'vars', {}),
+ tests: _.get(json, 'tests', '')
+ }
+ };
+
+ return transformedJson;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+};
+
/**
* The transformer function for converting a BRU file to JSON.
*
@@ -91,5 +112,6 @@ module.exports = {
bruToJson,
bruToEnvJson,
getEnvVars,
- getOptions
+ getOptions,
+ collectionBruToJson
};
diff --git a/packages/bruno-electron/package.json b/packages/bruno-electron/package.json
index 050e294bc..5a9f27df5 100644
--- a/packages/bruno-electron/package.json
+++ b/packages/bruno-electron/package.json
@@ -1,5 +1,5 @@
{
- "version": "v0.20.0",
+ "version": "v0.23.0",
"name": "bruno",
"description": "Opensource API Client for Exploring and Testing APIs",
"homepage": "https://www.usebruno.com",
@@ -15,13 +15,14 @@
},
"dependencies": {
"@aws-sdk/credential-providers": "^3.425.0",
- "@usebruno/js": "0.6.0",
- "@usebruno/lang": "0.5.0",
+ "@usebruno/js": "0.8.0",
+ "@usebruno/lang": "0.8.0",
"@usebruno/schema": "0.5.0",
"about-window": "^1.15.2",
"aws4-axios": "^3.3.0",
"axios": "^1.5.1",
"chai": "^4.3.7",
+ "chai-string": "^1.5.0",
"chokidar": "^3.5.3",
"decomment": "^0.9.5",
"dotenv": "^16.0.3",
@@ -41,6 +42,7 @@
"nanoid": "3.3.4",
"node-machine-id": "^1.1.12",
"qs": "^6.11.0",
+ "socks-proxy-agent": "^8.0.2",
"uuid": "^9.0.0",
"vm2": "^3.9.13",
"yup": "^0.32.11"
diff --git a/packages/bruno-electron/src/app/menu-template.js b/packages/bruno-electron/src/app/menu-template.js
index 2efd93cde..6b4707729 100644
--- a/packages/bruno-electron/src/app/menu-template.js
+++ b/packages/bruno-electron/src/app/menu-template.js
@@ -24,7 +24,10 @@ const template = [
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
- { role: 'selectAll' }
+ { role: 'selectAll' },
+ { type: 'separator' },
+ { role: 'hide' },
+ { role: 'hideOthers' }
]
},
{
diff --git a/packages/bruno-electron/src/app/watcher.js b/packages/bruno-electron/src/app/watcher.js
index b4162db90..51fa79f6f 100644
--- a/packages/bruno-electron/src/app/watcher.js
+++ b/packages/bruno-electron/src/app/watcher.js
@@ -2,12 +2,10 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const chokidar = require('chokidar');
-const { hasJsonExtension, hasBruExtension, writeFile } = require('../utils/filesystem');
-const { bruToEnvJson, envJsonToBru, bruToJson, jsonToBru } = require('../bru');
+const { hasBruExtension } = require('../utils/filesystem');
+const { bruToEnvJson, bruToJson, collectionBruToJson } = require('../bru');
const { dotenvToJson } = require('@usebruno/lang');
-const { isLegacyEnvFile, migrateLegacyEnvFile, isLegacyBruFile, migrateLegacyBruFile } = require('../bru/migrate');
-const { itemSchema } = require('@usebruno/schema');
const { uuid } = require('../utils/common');
const { getRequestUid } = require('../cache/requestUids');
const { decryptString } = require('../utils/encryption');
@@ -17,13 +15,6 @@ const EnvironmentSecretsStore = require('../store/env-secrets');
const environmentSecretsStore = new EnvironmentSecretsStore();
-const isJsonEnvironmentConfig = (pathname, collectionPath) => {
- const dirname = path.dirname(pathname);
- const basename = path.basename(pathname);
-
- return dirname === collectionPath && basename === 'environments.json';
-};
-
const isDotEnvFile = (pathname, collectionPath) => {
const dirname = path.dirname(pathname);
const basename = path.basename(pathname);
@@ -46,6 +37,13 @@ const isBruEnvironmentConfig = (pathname, collectionPath) => {
return dirname === envDirectory && hasBruExtension(basename);
};
+const isCollectionRootBruFile = (pathname, collectionPath) => {
+ const dirname = path.dirname(pathname);
+ const basename = path.basename(pathname);
+
+ return dirname === collectionPath && basename === 'collection.bru';
+};
+
const hydrateRequestWithUuid = (request, pathname) => {
request.uid = getRequestUid(pathname);
@@ -68,6 +66,20 @@ const hydrateRequestWithUuid = (request, pathname) => {
return request;
};
+const hydrateBruCollectionFileWithUuid = (collectionRoot) => {
+ const params = _.get(collectionRoot, 'request.params', []);
+ const headers = _.get(collectionRoot, 'request.headers', []);
+ const requestVars = _.get(collectionRoot, 'request.vars.req', []);
+ const responseVars = _.get(collectionRoot, 'request.vars.res', []);
+
+ params.forEach((param) => (param.uid = uuid()));
+ headers.forEach((header) => (header.uid = uuid()));
+ requestVars.forEach((variable) => (variable.uid = uuid()));
+ responseVars.forEach((variable) => (variable.uid = uuid()));
+
+ return collectionRoot;
+};
+
const envHasSecrets = (environment = {}) => {
const secrets = _.filter(environment.variables, (v) => v.secret);
@@ -87,11 +99,6 @@ const addEnvironmentFile = async (win, pathname, collectionUid, collectionPath)
let bruContent = fs.readFileSync(pathname, 'utf8');
- // migrate old env json to bru file
- if (isLegacyEnvFile(bruContent)) {
- bruContent = await migrateLegacyEnvFile(bruContent, pathname);
- }
-
file.data = bruToEnvJson(bruContent);
file.data.name = basename.substring(0, basename.length - 4);
file.data.uid = getRequestUid(pathname);
@@ -205,54 +212,31 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
}
}
- if (isJsonEnvironmentConfig(pathname, collectionPath)) {
- try {
- const dirname = path.dirname(pathname);
- const bruContent = fs.readFileSync(pathname, 'utf8');
-
- const jsonData = JSON.parse(bruContent);
-
- const envDirectory = path.join(dirname, 'environments');
- if (!fs.existsSync(envDirectory)) {
- fs.mkdirSync(envDirectory);
- }
-
- for (const env of jsonData) {
- const bruEnvFilename = path.join(envDirectory, `${env.name}.bru`);
- const bruContent = envJsonToBru(env);
- await writeFile(bruEnvFilename, bruContent);
- }
-
- await fs.unlinkSync(pathname);
- } catch (err) {
- // do nothing
- }
-
- return;
- }
-
if (isBruEnvironmentConfig(pathname, collectionPath)) {
return addEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
- // migrate old json files to bru
- if (hasJsonExtension(pathname)) {
+ if (isCollectionRootBruFile(pathname, collectionPath)) {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname),
+ collectionRoot: true
+ }
+ };
+
try {
- const json = fs.readFileSync(pathname, 'utf8');
- const jsonData = JSON.parse(json);
+ let bruContent = fs.readFileSync(pathname, 'utf8');
- await itemSchema.validate(jsonData);
+ file.data = collectionBruToJson(bruContent);
- const content = jsonToBru(jsonData);
-
- const re = /(.*)\.json$/;
- const subst = `$1.bru`;
- const bruFilename = pathname.replace(re, subst);
-
- await writeFile(bruFilename, content);
- await fs.unlinkSync(pathname);
+ hydrateBruCollectionFileWithUuid(file.data);
+ win.webContents.send('main:collection-tree-updated', 'addFile', file);
+ return;
} catch (err) {
- // do nothing
+ console.error(err);
+ return;
}
}
@@ -268,12 +252,8 @@ const add = async (win, pathname, collectionUid, collectionPath) => {
try {
let bruContent = fs.readFileSync(pathname, 'utf8');
- // migrate old bru format to new bru format
- if (isLegacyBruFile(bruContent)) {
- bruContent = await migrateLegacyBruFile(bruContent, pathname);
- }
-
file.data = bruToJson(bruContent);
+
hydrateRequestWithUuid(file.data, pathname);
win.webContents.send('main:collection-tree-updated', 'addFile', file);
} catch (err) {
@@ -340,6 +320,30 @@ const change = async (win, pathname, collectionUid, collectionPath) => {
return changeEnvironmentFile(win, pathname, collectionUid, collectionPath);
}
+ if (isCollectionRootBruFile(pathname, collectionPath)) {
+ const file = {
+ meta: {
+ collectionUid,
+ pathname,
+ name: path.basename(pathname),
+ collectionRoot: true
+ }
+ };
+
+ try {
+ let bruContent = fs.readFileSync(pathname, 'utf8');
+
+ file.data = collectionBruToJson(bruContent);
+
+ hydrateBruCollectionFileWithUuid(file.data);
+ win.webContents.send('main:collection-tree-updated', 'change', file);
+ return;
+ } catch (err) {
+ console.error(err);
+ return;
+ }
+ }
+
if (hasBruExtension(pathname)) {
try {
const file = {
@@ -404,11 +408,6 @@ class Watcher {
this.watchers[watchPath].close();
}
- // todo
- // enable this in a future release
- // once we can confirm all older json based files have been auto migrated to .bru format
- // watchPath = path.join(watchPath, '**/*.bru');
-
const self = this;
setTimeout(() => {
const watcher = chokidar.watch(watchPath, {
diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js
index a28c04a7b..18f739f5c 100644
--- a/packages/bruno-electron/src/bru/index.js
+++ b/packages/bruno-electron/src/bru/index.js
@@ -1,6 +1,56 @@
const _ = require('lodash');
-const { bruToJsonV2, jsonToBruV2, bruToEnvJsonV2, envJsonToBruV2 } = require('@usebruno/lang');
-const { each } = require('lodash');
+const {
+ bruToJsonV2,
+ jsonToBruV2,
+ bruToEnvJsonV2,
+ envJsonToBruV2,
+ collectionBruToJson: _collectionBruToJson,
+ jsonToCollectionBru: _jsonToCollectionBru
+} = require('@usebruno/lang');
+
+const collectionBruToJson = (bru) => {
+ try {
+ const json = _collectionBruToJson(bru);
+
+ const transformedJson = {
+ request: {
+ params: _.get(json, 'query', []),
+ headers: _.get(json, 'headers', []),
+ auth: _.get(json, 'auth', {}),
+ script: _.get(json, 'script', {}),
+ vars: _.get(json, 'vars', {}),
+ tests: _.get(json, 'tests', '')
+ }
+ };
+
+ return transformedJson;
+ } catch (error) {
+ return Promise.reject(error);
+ }
+};
+
+const jsonToCollectionBru = (json) => {
+ try {
+ const collectionBruJson = {
+ query: _.get(json, 'request.params', []),
+ headers: _.get(json, 'request.headers', []),
+ auth: _.get(json, 'request.auth', {}),
+ script: {
+ req: _.get(json, 'request.script.req', ''),
+ res: _.get(json, 'request.script.res', '')
+ },
+ vars: {
+ req: _.get(json, 'request.vars.req', []),
+ res: _.get(json, 'request.vars.req', [])
+ },
+ tests: _.get(json, 'request.tests', '')
+ };
+
+ return _jsonToCollectionBru(collectionBruJson);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+};
const bruToEnvJson = (bru) => {
try {
@@ -10,7 +60,7 @@ const bruToEnvJson = (bru) => {
// this need to be evaluated and safely removed
// i don't see it being used in schema validation
if (json && json.variables && json.variables.length) {
- each(json.variables, (v) => (v.type = 'text'));
+ _.each(json.variables, (v) => (v.type = 'text'));
}
return json;
@@ -129,5 +179,7 @@ module.exports = {
bruToJson,
jsonToBru,
bruToEnvJson,
- envJsonToBru
+ envJsonToBru,
+ collectionBruToJson,
+ jsonToCollectionBru
};
diff --git a/packages/bruno-electron/src/bru/migrate.js b/packages/bruno-electron/src/bru/migrate.js
deleted file mode 100644
index a74dc2fd0..000000000
--- a/packages/bruno-electron/src/bru/migrate.js
+++ /dev/null
@@ -1,99 +0,0 @@
-const {
- bruToEnvJson: bruToEnvJsonV1,
- bruToJson: bruToJsonV1,
-
- jsonToBruV2,
- envJsonToBruV2
-} = require('@usebruno/lang');
-const _ = require('lodash');
-
-const { writeFile } = require('../utils/filesystem');
-
-const isLegacyEnvFile = (bruContent = '') => {
- bruContent = bruContent.trim();
- let regex = /^vars[\s\S]*\/vars$/;
-
- return regex.test(bruContent);
-};
-
-const migrateLegacyEnvFile = async (bruContent, pathname) => {
- const envJson = bruToEnvJsonV1(bruContent);
- const newBruContent = envJsonToBruV2(envJson);
-
- await writeFile(pathname, newBruContent);
-
- return newBruContent;
-};
-
-const isLegacyBruFile = (bruContent = '') => {
- bruContent = bruContent.trim();
- let lines = bruContent.split(/\r?\n/);
- let hasName = false;
- let hasMethod = false;
- let hasUrl = false;
-
- for (let line of lines) {
- line = line.trim();
- if (line.startsWith('name')) {
- hasName = true;
- } else if (line.startsWith('method')) {
- hasMethod = true;
- } else if (line.startsWith('url')) {
- hasUrl = true;
- }
- }
-
- return hasName && hasMethod && hasUrl;
-};
-
-const migrateLegacyBruFile = async (bruContent, pathname) => {
- const json = bruToJsonV1(bruContent);
-
- let type = _.get(json, 'type');
- if (type === 'http-request') {
- type = 'http';
- } else if (type === 'graphql-request') {
- type = 'graphql';
- } else {
- type = 'http';
- }
-
- let script = {};
- let legacyScript = _.get(json, 'request.script');
- if (legacyScript && legacyScript.trim().length > 0) {
- script = {
- res: legacyScript
- };
- }
-
- const bruJson = {
- meta: {
- name: _.get(json, 'name'),
- type: type,
- seq: _.get(json, 'seq')
- },
- http: {
- method: _.lowerCase(_.get(json, 'request.method')),
- url: _.get(json, 'request.url'),
- body: _.get(json, 'request.body.mode', 'none')
- },
- query: _.get(json, 'request.params', []),
- headers: _.get(json, 'request.headers', []),
- body: _.get(json, 'request.body', {}),
- script: script,
- tests: _.get(json, 'request.tests', '')
- };
-
- const newBruContent = jsonToBruV2(bruJson);
-
- await writeFile(pathname, newBruContent);
-
- return newBruContent;
-};
-
-module.exports = {
- isLegacyEnvFile,
- migrateLegacyEnvFile,
- isLegacyBruFile,
- migrateLegacyBruFile
-};
diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js
index 3908373e9..3a38e21ae 100644
--- a/packages/bruno-electron/src/index.js
+++ b/packages/bruno-electron/src/index.js
@@ -9,15 +9,20 @@ const LastOpenedCollections = require('./store/last-opened-collections');
const registerNetworkIpc = require('./ipc/network');
const registerCollectionsIpc = require('./ipc/collection');
const Watcher = require('./app/watcher');
+const { loadWindowState, saveWindowState } = require('./utils/window');
const lastOpenedCollections = new LastOpenedCollections();
-setContentSecurityPolicy(`
- default-src * 'unsafe-inline' 'unsafe-eval';
- script-src * 'unsafe-inline' 'unsafe-eval';
- connect-src * 'unsafe-inline';
- form-action 'none';
-`);
+const contentSecurityPolicy = [
+ isDev ? "default-src 'self' 'unsafe-inline' 'unsafe-eval'" : "default-src 'self'",
+ "connect-src 'self' https://api.github.com/repos/usebruno/bruno",
+ "font-src 'self' https://fonts.gstatic.com",
+ "form-action 'none'",
+ "img-src 'self' blob: data:",
+ "style-src 'self' https://fonts.googleapis.com"
+];
+
+setContentSecurityPolicy(contentSecurityPolicy.join(';'));
const menu = Menu.buildFromTemplate(menuTemplate);
Menu.setApplicationMenu(menu);
@@ -27,9 +32,13 @@ let watcher;
// Prepare the renderer once the app is ready
app.on('ready', async () => {
+ const { x, y, width, height } = loadWindowState();
+
mainWindow = new BrowserWindow({
- width: 1280,
- height: 768,
+ x,
+ y,
+ width,
+ height,
webPreferences: {
nodeIntegration: true,
contextIsolation: true,
@@ -37,8 +46,10 @@ app.on('ready', async () => {
webviewTag: true
},
title: 'Bruno',
- icon: path.join(__dirname, 'about/256x256.png'),
- autoHideMenuBar: true
+ icon: path.join(__dirname, 'about/256x256.png')
+ // we will bring this back
+ // see https://github.com/usebruno/bruno/issues/440
+ // autoHideMenuBar: true
});
const url = isDev
@@ -52,6 +63,9 @@ app.on('ready', async () => {
mainWindow.loadURL(url);
watcher = new Watcher();
+ mainWindow.on('resize', () => saveWindowState(mainWindow));
+ mainWindow.on('move', () => saveWindowState(mainWindow));
+
mainWindow.webContents.on('new-window', function (e, url) {
e.preventDefault();
require('electron').shell.openExternal(url);
diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js
index 864aff82e..944a04f01 100644
--- a/packages/bruno-electron/src/ipc/collection.js
+++ b/packages/bruno-electron/src/ipc/collection.js
@@ -2,7 +2,7 @@ const _ = require('lodash');
const fs = require('fs');
const path = require('path');
const { ipcMain, shell } = require('electron');
-const { envJsonToBru, bruToJson, jsonToBru } = require('../bru');
+const { envJsonToBru, bruToJson, jsonToBru, jsonToCollectionBru } = require('../bru');
const {
isValidPathname,
@@ -101,6 +101,17 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
}
});
+ ipcMain.handle('renderer:save-collection-root', async (event, collectionPathname, collectionRoot) => {
+ try {
+ const collectionBruFilePath = path.join(collectionPathname, 'collection.bru');
+
+ const content = jsonToCollectionBru(collectionRoot);
+ await writeFile(collectionBruFilePath, content);
+ } catch (error) {
+ return Promise.reject(error);
+ }
+ });
+
// new request
ipcMain.handle('renderer:new-request', async (event, pathname, request) => {
try {
@@ -486,6 +497,10 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
return Promise.reject(error);
}
});
+
+ ipcMain.handle('renderer:open-devtools', async () => {
+ mainWindow.webContents.openDevTools();
+ });
};
const registerMainEventHandlers = (mainWindow, watcher, lastOpenedCollections) => {
diff --git a/packages/bruno-electron/src/ipc/network/axios-instance.js b/packages/bruno-electron/src/ipc/network/axios-instance.js
index f4abd839a..f4810becd 100644
--- a/packages/bruno-electron/src/ipc/network/axios-instance.js
+++ b/packages/bruno-electron/src/ipc/network/axios-instance.js
@@ -23,9 +23,11 @@ function makeAxiosInstance() {
return response;
},
(error) => {
- const end = Date.now();
- const start = error.config.headers['request-start-time'];
- error.response.headers['request-duration'] = end - start;
+ if (error.response) {
+ const end = Date.now();
+ const start = error.config.headers['request-start-time'];
+ error.response.headers['request-duration'] = end - start;
+ }
return Promise.reject(error);
}
);
diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js
index 882776866..1def8d964 100644
--- a/packages/bruno-electron/src/ipc/network/index.js
+++ b/packages/bruno-electron/src/ipc/network/index.js
@@ -1,3 +1,4 @@
+const os = require('os');
const qs = require('qs');
const https = require('https');
const axios = require('axios');
@@ -5,19 +6,21 @@ const decomment = require('decomment');
const Mustache = require('mustache');
const FormData = require('form-data');
const { ipcMain } = require('electron');
-const { forOwn, extend, each, get } = require('lodash');
+const { forOwn, extend, each, get, compact } = require('lodash');
const { VarsRuntime, AssertRuntime, ScriptRuntime, TestRuntime } = require('@usebruno/js');
const prepareRequest = require('./prepare-request');
const prepareGqlIntrospectionRequest = require('./prepare-gql-introspection-request');
const { cancelTokens, saveCancelToken, deleteCancelToken } = require('../../utils/cancel-token');
const { uuid } = require('../../utils/common');
const interpolateVars = require('./interpolate-vars');
+const { interpolateString } = require('./interpolate-string');
const { sortFolder, getAllRequestsInFolderRecursively } = require('./helper');
const { getPreferences } = require('../../store/preferences');
const { getProcessEnvVars } = require('../../store/process-env');
const { getBrunoConfig } = require('../../store/bruno-config');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { HttpProxyAgent } = require('http-proxy-agent');
+const { SocksProxyAgent } = require('socks-proxy-agent');
const { makeAxiosInstance } = require('./axios-instance');
const { addAwsV4Interceptor, resolveCredentials } = require('./awsv4auth-helper');
@@ -73,7 +76,7 @@ const getSize = (data) => {
}
if (typeof data === 'object') {
- return Buffer.byteLength(JSON.stringify(data), 'utf8');
+ return Buffer.byteLength(safeStringifyJSON(data), 'utf8');
}
return 0;
@@ -81,224 +84,217 @@ const getSize = (data) => {
const registerNetworkIpc = (mainWindow) => {
// handler for sending http request
- ipcMain.handle(
- 'send-http-request',
- async (event, item, collectionUid, collectionPath, environment, collectionVariables) => {
- const cancelTokenUid = uuid();
- const requestUid = uuid();
+ ipcMain.handle('send-http-request', async (event, item, collection, environment, collectionVariables) => {
+ const collectionUid = collection.uid;
+ const collectionPath = collection.pathname;
+ const cancelTokenUid = uuid();
+ const requestUid = uuid();
- const onConsoleLog = (type, args) => {
- console[type](...args);
+ const onConsoleLog = (type, args) => {
+ console[type](...args);
- mainWindow.webContents.send('main:console-log', {
- type,
- args
+ mainWindow.webContents.send('main:console-log', {
+ type,
+ args
+ });
+ };
+
+ mainWindow.webContents.send('main:run-request-event', {
+ type: 'request-queued',
+ requestUid,
+ collectionUid,
+ itemUid: item.uid,
+ cancelTokenUid
+ });
+
+ const collectionRoot = get(collection, 'root', {});
+ const _request = item.draft ? item.draft.request : item.request;
+ const request = prepareRequest(_request, collectionRoot);
+ const envVars = getEnvVars(environment);
+ const processEnvVars = getProcessEnvVars(collectionUid);
+ const brunoConfig = getBrunoConfig(collectionUid);
+ const scriptingConfig = get(brunoConfig, 'scripts', {});
+
+ try {
+ // make axios work in node using form data
+ // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
+ if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
+ const form = new FormData();
+ forOwn(request.data, (value, key) => {
+ form.append(key, value);
});
- };
+ extend(request.headers, form.getHeaders());
+ request.data = form;
+ }
+ const cancelToken = axios.CancelToken.source();
+ request.cancelToken = cancelToken.token;
+ saveCancelToken(cancelTokenUid, cancelToken);
+
+ // run pre-request vars
+ const preRequestVars = get(request, 'vars.req', []);
+ if (preRequestVars && preRequestVars.length) {
+ const varsRuntime = new VarsRuntime();
+ const result = varsRuntime.runPreRequestVars(
+ preRequestVars,
+ request,
+ envVars,
+ collectionVariables,
+ collectionPath,
+ processEnvVars
+ );
+
+ if (result) {
+ mainWindow.webContents.send('main:script-environment-update', {
+ envVariables: result.envVariables,
+ collectionVariables: result.collectionVariables,
+ requestUid,
+ collectionUid
+ });
+ }
+ }
+
+ // run pre-request script
+ const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
+ os.EOL
+ );
+ if (requestScript && requestScript.length) {
+ const scriptRuntime = new ScriptRuntime();
+ const result = await scriptRuntime.runRequestScript(
+ decomment(requestScript),
+ request,
+ envVars,
+ collectionVariables,
+ collectionPath,
+ onConsoleLog,
+ processEnvVars,
+ scriptingConfig
+ );
+
+ mainWindow.webContents.send('main:script-environment-update', {
+ envVariables: result.envVariables,
+ collectionVariables: result.collectionVariables,
+ requestUid,
+ collectionUid
+ });
+ }
+
+ interpolateVars(request, envVars, collectionVariables, processEnvVars);
+
+ // stringify the request url encoded params
+ if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
+ request.data = qs.stringify(request.data);
+ }
+
+ // todo:
+ // i have no clue why electron can't send the request object
+ // without safeParseJSON(safeStringifyJSON(request.data))
mainWindow.webContents.send('main:run-request-event', {
- type: 'request-queued',
- requestUid,
+ type: 'request-sent',
+ requestSent: {
+ url: request.url,
+ method: request.method,
+ headers: request.headers,
+ data: safeParseJSON(safeStringifyJSON(request.data))
+ },
collectionUid,
itemUid: item.uid,
+ requestUid,
cancelTokenUid
});
- const _request = item.draft ? item.draft.request : item.request;
- const request = prepareRequest(_request);
- const envVars = getEnvVars(environment);
- const processEnvVars = getProcessEnvVars(collectionUid);
+ const preferences = getPreferences();
+ const sslVerification = get(preferences, 'request.sslVerification', true);
+ const httpsAgentRequestFields = {};
+ if (!sslVerification) {
+ httpsAgentRequestFields['rejectUnauthorized'] = false;
+ } else {
+ const cacertArray = [preferences['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
+ cacertFile = cacertArray.find((el) => el);
+ if (cacertFile && cacertFile.length > 1) {
+ try {
+ const fs = require('fs');
+ caCrt = fs.readFileSync(cacertFile);
+ httpsAgentRequestFields['ca'] = caCrt;
+ } catch (err) {
+ console.log('Error reading CA cert file:' + cacertFile, err);
+ }
+ }
+ }
+
+ // proxy configuration
const brunoConfig = getBrunoConfig(collectionUid);
- const allowScriptFilesystemAccess = get(brunoConfig, 'filesystemAccess.allow', false);
+ const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
+ if (proxyEnabled) {
+ let proxyUri;
- try {
- // make axios work in node using form data
- // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427
- if (request.headers && request.headers['content-type'] === 'multipart/form-data') {
- const form = new FormData();
- forOwn(request.data, (value, key) => {
- form.append(key, value);
- });
- extend(request.headers, form.getHeaders());
- request.data = form;
- }
+ const interpolationOptions = {
+ envVars,
+ collectionVariables,
+ processEnvVars
+ };
- const cancelToken = axios.CancelToken.source();
- request.cancelToken = cancelToken.token;
- saveCancelToken(cancelTokenUid, cancelToken);
+ const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
+ const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
+ const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
+ const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
+ const socksEnabled = proxyProtocol.includes('socks');
- // run pre-request vars
- const preRequestVars = get(request, 'vars.req', []);
- if (preRequestVars && preRequestVars.length) {
- const varsRuntime = new VarsRuntime();
- const result = varsRuntime.runPreRequestVars(
- preRequestVars,
- request,
- envVars,
- collectionVariables,
- collectionPath,
- processEnvVars
- );
+ if (proxyAuthEnabled) {
+ const proxyAuthUsername = interpolateString(get(brunoConfig, 'proxy.auth.username'), interpolationOptions);
+ const proxyAuthPassword = interpolateString(get(brunoConfig, 'proxy.auth.password'), interpolationOptions);
- if (result) {
- mainWindow.webContents.send('main:script-environment-update', {
- envVariables: result.envVariables,
- collectionVariables: result.collectionVariables,
- requestUid,
- collectionUid
- });
- }
- }
-
- // run pre-request script
- const requestScript = get(request, 'script.req');
- if (requestScript && requestScript.length) {
- const scriptRuntime = new ScriptRuntime();
- const result = await scriptRuntime.runRequestScript(
- decomment(requestScript),
- request,
- envVars,
- collectionVariables,
- collectionPath,
- onConsoleLog,
- processEnvVars,
- allowScriptFilesystemAccess
- );
-
- mainWindow.webContents.send('main:script-environment-update', {
- envVariables: result.envVariables,
- collectionVariables: result.collectionVariables,
- requestUid,
- collectionUid
- });
- }
-
- interpolateVars(request, envVars, collectionVariables, processEnvVars);
-
- // stringify the request url encoded params
- if (request.headers['content-type'] === 'application/x-www-form-urlencoded') {
- request.data = qs.stringify(request.data);
- }
-
- // todo:
- // i have no clue why electron can't send the request object
- // without safeParseJSON(safeStringifyJSON(request.data))
- mainWindow.webContents.send('main:run-request-event', {
- type: 'request-sent',
- requestSent: {
- url: request.url,
- method: request.method,
- headers: request.headers,
- data: safeParseJSON(safeStringifyJSON(request.data))
- },
- collectionUid,
- itemUid: item.uid,
- requestUid,
- cancelTokenUid
- });
-
- const preferences = getPreferences();
- const sslVerification = get(preferences, 'request.sslVerification', true);
- const httpsAgentRequestFields = {};
- if (!sslVerification) {
- httpsAgentRequestFields['rejectUnauthorized'] = false;
+ proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
} else {
- const cacertArray = [preferences['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS];
- cacertFile = cacertArray.find((el) => el);
- if (cacertFile && cacertFile.length > 1) {
- try {
- const fs = require('fs');
- caCrt = fs.readFileSync(cacertFile);
- httpsAgentRequestFields['ca'] = caCrt;
- } catch (err) {
- console.log('Error reading CA cert file:' + cacertFile, err);
- }
- }
+ proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
}
- // proxy configuration
- const brunoConfig = getBrunoConfig(collectionUid);
- const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
- if (proxyEnabled) {
- const proxyProtocol = get(brunoConfig, 'proxy.protocol');
- const proxyHostname = get(brunoConfig, 'proxy.hostname');
- const proxyPort = get(brunoConfig, 'proxy.port');
- const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
+ if (socksEnabled) {
+ const socksProxyAgent = new SocksProxyAgent(proxyUri);
- let proxy;
-
- if (proxyAuthEnabled) {
- const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
- const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
-
- proxy = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
- } else {
- proxy = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
- }
+ request.httpsAgent = socksProxyAgent;
+ request.httpAgent = socksProxyAgent;
+ } else {
request.httpsAgent = new HttpsProxyAgent(
- proxy,
+ proxyUri,
Object.keys(httpsAgentRequestFields).length > 0 ? { ...httpsAgentRequestFields } : undefined
);
- request.httpAgent = new HttpProxyAgent(proxy);
- } else if (Object.keys(httpsAgentRequestFields).length > 0) {
- request.httpsAgent = new https.Agent({
- ...httpsAgentRequestFields
- });
+ request.httpAgent = new HttpProxyAgent(proxyUri);
}
+ } else if (Object.keys(httpsAgentRequestFields).length > 0) {
+ request.httpsAgent = new https.Agent({
+ ...httpsAgentRequestFields
+ });
+ }
- const axiosInstance = makeAxiosInstance();
+ const axiosInstance = makeAxiosInstance();
- if (request.awsv4config) {
- request.awsv4config = await resolveCredentials(request);
- addAwsV4Interceptor(axiosInstance, request);
- delete request.awsv4config;
- }
+ if (request.awsv4config) {
+ request.awsv4config = await resolveCredentials(request);
+ addAwsV4Interceptor(axiosInstance, request);
+ delete request.awsv4config;
+ }
- /** @type {import('axios').AxiosResponse} */
- const response = await axiosInstance(request);
+ /** @type {import('axios').AxiosResponse} */
+ const response = await axiosInstance(request);
- // run post-response vars
- const postResponseVars = get(request, 'vars.res', []);
- if (postResponseVars && postResponseVars.length) {
- const varsRuntime = new VarsRuntime();
- const result = varsRuntime.runPostResponseVars(
- postResponseVars,
- request,
- response,
- envVars,
- collectionVariables,
- collectionPath,
- processEnvVars
- );
-
- if (result) {
- mainWindow.webContents.send('main:script-environment-update', {
- envVariables: result.envVariables,
- collectionVariables: result.collectionVariables,
- requestUid,
- collectionUid
- });
- }
- }
-
- // run post-response script
- const responseScript = get(request, 'script.res');
- if (responseScript && responseScript.length) {
- const scriptRuntime = new ScriptRuntime();
- const result = await scriptRuntime.runResponseScript(
- decomment(responseScript),
- request,
- response,
- envVars,
- collectionVariables,
- collectionPath,
- onConsoleLog,
- processEnvVars,
- allowScriptFilesystemAccess
- );
+ // run post-response vars
+ const postResponseVars = get(request, 'vars.res', []);
+ if (postResponseVars && postResponseVars.length) {
+ const varsRuntime = new VarsRuntime();
+ const result = varsRuntime.runPostResponseVars(
+ postResponseVars,
+ request,
+ response,
+ envVars,
+ collectionVariables,
+ collectionPath,
+ processEnvVars
+ );
+ if (result) {
mainWindow.webContents.send('main:script-environment-update', {
envVariables: result.envVariables,
collectionVariables: result.collectionVariables,
@@ -306,7 +302,116 @@ const registerNetworkIpc = (mainWindow) => {
collectionUid
});
}
+ }
+ // run post-response script
+ const responseScript = compact([get(collectionRoot, 'request.script.res'), get(request, 'script.res')]).join(
+ os.EOL
+ );
+ if (responseScript && responseScript.length) {
+ const scriptRuntime = new ScriptRuntime();
+ const result = await scriptRuntime.runResponseScript(
+ decomment(responseScript),
+ request,
+ response,
+ envVars,
+ collectionVariables,
+ collectionPath,
+ onConsoleLog,
+ processEnvVars,
+ scriptingConfig
+ );
+
+ mainWindow.webContents.send('main:script-environment-update', {
+ envVariables: result.envVariables,
+ collectionVariables: result.collectionVariables,
+ requestUid,
+ collectionUid
+ });
+ }
+
+ // run assertions
+ const assertions = get(request, 'assertions');
+ if (assertions) {
+ const assertRuntime = new AssertRuntime();
+ const results = assertRuntime.runAssertions(
+ assertions,
+ request,
+ response,
+ envVars,
+ collectionVariables,
+ collectionPath
+ );
+
+ mainWindow.webContents.send('main:run-request-event', {
+ type: 'assertion-results',
+ results: results,
+ itemUid: item.uid,
+ requestUid,
+ collectionUid
+ });
+ }
+
+ // run tests
+ const testFile = compact([
+ get(collectionRoot, 'request.tests'),
+ item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
+ ]).join(os.EOL);
+ if (typeof testFile === 'string') {
+ const testRuntime = new TestRuntime();
+ const testResults = await testRuntime.runTests(
+ decomment(testFile),
+ request,
+ response,
+ envVars,
+ collectionVariables,
+ collectionPath,
+ onConsoleLog,
+ processEnvVars,
+ scriptingConfig
+ );
+
+ mainWindow.webContents.send('main:run-request-event', {
+ type: 'test-results',
+ results: testResults.results,
+ itemUid: item.uid,
+ requestUid,
+ collectionUid
+ });
+
+ mainWindow.webContents.send('main:script-environment-update', {
+ envVariables: testResults.envVariables,
+ collectionVariables: testResults.collectionVariables,
+ requestUid,
+ collectionUid
+ });
+ }
+
+ deleteCancelToken(cancelTokenUid);
+ // Prevents the duration on leaking to the actual result
+ const requestDuration = response.headers.get('request-duration');
+ response.headers.delete('request-duration');
+
+ return {
+ status: response.status,
+ statusText: response.statusText,
+ headers: response.headers,
+ data: response.data,
+ duration: requestDuration
+ };
+ } catch (error) {
+ // todo: better error handling
+ // need to convey the error to the UI
+ // and need not be always a network error
+ deleteCancelToken(cancelTokenUid);
+
+ if (axios.isCancel(error)) {
+ let error = new Error('Request cancelled');
+ error.isCancel = true;
+ return Promise.reject(error);
+ }
+
+ if (error && error.response) {
// run assertions
const assertions = get(request, 'assertions');
if (assertions) {
@@ -314,7 +419,7 @@ const registerNetworkIpc = (mainWindow) => {
const results = assertRuntime.runAssertions(
assertions,
request,
- response,
+ error.response,
envVars,
collectionVariables,
collectionPath
@@ -330,19 +435,22 @@ const registerNetworkIpc = (mainWindow) => {
}
// run tests
- const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
+ const testFile = compact([
+ get(collectionRoot, 'request.tests'),
+ item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
+ ]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
decomment(testFile),
request,
- response,
+ error.response,
envVars,
collectionVariables,
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
);
mainWindow.webContents.send('main:run-request-event', {
@@ -361,101 +469,21 @@ const registerNetworkIpc = (mainWindow) => {
});
}
- deleteCancelToken(cancelTokenUid);
- // Prevents the duration on leaking to the actual result
- const requestDuration = response.headers.get('request-duration');
- response.headers.delete('request-duration');
-
+ // Prevents the duration from leaking to the actual result
+ const requestDuration = error.response.headers.get('request-duration');
+ error.response.headers.delete('request-duration');
return {
- status: response.status,
- statusText: response.statusText,
- headers: response.headers,
- data: response.data,
- duration: requestDuration
+ status: error.response.status,
+ statusText: error.response.statusText,
+ headers: error.response.headers,
+ data: error.response.data,
+ duration: requestDuration ?? 0
};
- } catch (error) {
- // todo: better error handling
- // need to convey the error to the UI
- // and need not be always a network error
- deleteCancelToken(cancelTokenUid);
-
- if (axios.isCancel(error)) {
- let error = new Error('Request cancelled');
- error.isCancel = true;
- return Promise.reject(error);
- }
-
- if (error && error.response) {
- // run assertions
- const assertions = get(request, 'assertions');
- if (assertions) {
- const assertRuntime = new AssertRuntime();
- const results = assertRuntime.runAssertions(
- assertions,
- request,
- error.response,
- envVars,
- collectionVariables,
- collectionPath
- );
-
- mainWindow.webContents.send('main:run-request-event', {
- type: 'assertion-results',
- results: results,
- itemUid: item.uid,
- requestUid,
- collectionUid
- });
- }
-
- // run tests
- const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
- if (typeof testFile === 'string') {
- const testRuntime = new TestRuntime();
- const testResults = await testRuntime.runTests(
- decomment(testFile),
- request,
- error.response,
- envVars,
- collectionVariables,
- collectionPath,
- onConsoleLog,
- processEnvVars,
- allowScriptFilesystemAccess
- );
-
- mainWindow.webContents.send('main:run-request-event', {
- type: 'test-results',
- results: testResults.results,
- itemUid: item.uid,
- requestUid,
- collectionUid
- });
-
- mainWindow.webContents.send('main:script-environment-update', {
- envVariables: testResults.envVariables,
- collectionVariables: testResults.collectionVariables,
- requestUid,
- collectionUid
- });
- }
-
- // Prevents the duration from leaking to the actual result
- const requestDuration = error.response.headers.get('request-duration');
- error.response.headers.delete('request-duration');
- return {
- status: error.response.status,
- statusText: error.response.statusText,
- headers: error.response.headers,
- data: error.response.data,
- duration: requestDuration ?? 0
- };
- }
-
- return Promise.reject(error);
}
+
+ return Promise.reject(error);
}
- );
+ });
ipcMain.handle('cancel-http-request', async (event, cancelTokenUid) => {
return new Promise((resolve, reject) => {
@@ -469,10 +497,11 @@ const registerNetworkIpc = (mainWindow) => {
});
});
- ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, request, collectionVariables) => {
+ ipcMain.handle('fetch-gql-schema', async (event, endpoint, environment, request, collection) => {
try {
const envVars = getEnvVars(environment);
- const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request);
+ const collectionRoot = get(collection, 'root', {});
+ const preparedRequest = prepareGqlIntrospectionRequest(endpoint, envVars, request, collectionRoot);
const preferences = getPreferences();
const sslVerification = get(preferences, 'request.sslVerification', true);
@@ -483,7 +512,8 @@ const registerNetworkIpc = (mainWindow) => {
});
}
- interpolateVars(preparedRequest, envVars, collectionVariables);
+ const processEnvVars = getProcessEnvVars(collection.uid);
+ interpolateVars(preparedRequest, envVars, collection.collectionVariables, processEnvVars);
const response = await axios(preparedRequest);
@@ -514,7 +544,8 @@ const registerNetworkIpc = (mainWindow) => {
const collectionPath = collection.pathname;
const folderUid = folder ? folder.uid : null;
const brunoConfig = getBrunoConfig(collectionUid);
- const allowScriptFilesystemAccess = get(brunoConfig, 'filesystemAccess.allow', false);
+ const scriptingConfig = get(brunoConfig, 'scripts', {});
+ const collectionRoot = get(collection, 'root', {});
const onConsoleLog = (type, args) => {
console[type](...args);
@@ -573,7 +604,7 @@ const registerNetworkIpc = (mainWindow) => {
});
const _request = item.draft ? item.draft.request : item.request;
- const request = prepareRequest(_request);
+ const request = prepareRequest(_request, collectionRoot);
const processEnvVars = getProcessEnvVars(collectionUid);
try {
@@ -610,7 +641,9 @@ const registerNetworkIpc = (mainWindow) => {
}
// run pre-request script
- const requestScript = get(request, 'script.req');
+ const requestScript = compact([get(collectionRoot, 'request.script.req'), get(request, 'script.req')]).join(
+ os.EOL
+ );
if (requestScript && requestScript.length) {
const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runRequestScript(
@@ -621,7 +654,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -655,27 +688,47 @@ const registerNetworkIpc = (mainWindow) => {
const brunoConfig = getBrunoConfig(collectionUid);
const proxyEnabled = get(brunoConfig, 'proxy.enabled', false);
if (proxyEnabled) {
- const proxyProtocol = get(brunoConfig, 'proxy.protocol');
- const proxyHostname = get(brunoConfig, 'proxy.hostname');
- const proxyPort = get(brunoConfig, 'proxy.port');
- const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
+ let proxyUri;
+ const interpolationOptions = {
+ envVars,
+ collectionVariables,
+ processEnvVars
+ };
- let proxy;
+ const proxyProtocol = interpolateString(get(brunoConfig, 'proxy.protocol'), interpolationOptions);
+ const proxyHostname = interpolateString(get(brunoConfig, 'proxy.hostname'), interpolationOptions);
+ const proxyPort = interpolateString(get(brunoConfig, 'proxy.port'), interpolationOptions);
+ const proxyAuthEnabled = get(brunoConfig, 'proxy.auth.enabled', false);
+ const socksEnabled = proxyProtocol.includes('socks');
if (proxyAuthEnabled) {
- const proxyAuthUsername = get(brunoConfig, 'proxy.auth.username');
- const proxyAuthPassword = get(brunoConfig, 'proxy.auth.password');
+ const proxyAuthUsername = interpolateString(
+ get(brunoConfig, 'proxy.auth.username'),
+ interpolationOptions
+ );
- proxy = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
+ const proxyAuthPassword = interpolateString(
+ get(brunoConfig, 'proxy.auth.password'),
+ interpolationOptions
+ );
+
+ proxyUri = `${proxyProtocol}://${proxyAuthUsername}:${proxyAuthPassword}@${proxyHostname}:${proxyPort}`;
} else {
- proxy = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
+ proxyUri = `${proxyProtocol}://${proxyHostname}:${proxyPort}`;
}
- request.httpsAgent = new HttpsProxyAgent(proxy, {
- rejectUnauthorized: sslVerification
- });
+ if (socksEnabled) {
+ const socksProxyAgent = new SocksProxyAgent(proxyUri);
- request.httpAgent = new HttpProxyAgent(proxy);
+ request.httpsAgent = socksProxyAgent;
+ request.httpAgent = socksProxyAgent;
+ } else {
+ request.httpsAgent = new HttpsProxyAgent(proxyUri, {
+ rejectUnauthorized: sslVerification
+ });
+
+ request.httpAgent = new HttpProxyAgent(proxyUri);
+ }
} else if (!sslVerification) {
request.httpsAgent = new https.Agent({
rejectUnauthorized: false
@@ -711,7 +764,10 @@ const registerNetworkIpc = (mainWindow) => {
}
// run response script
- const responseScript = get(request, 'script.res');
+ const responseScript = compact([
+ get(collectionRoot, 'request.script.res'),
+ get(request, 'script.res')
+ ]).join(os.EOL);
if (responseScript && responseScript.length) {
const scriptRuntime = new ScriptRuntime();
const result = await scriptRuntime.runResponseScript(
@@ -723,7 +779,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
);
mainWindow.webContents.send('main:script-environment-update', {
@@ -755,7 +811,10 @@ const registerNetworkIpc = (mainWindow) => {
}
// run tests
- const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
+ const testFile = compact([
+ get(collectionRoot, 'request.tests'),
+ item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
+ ]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
@@ -767,7 +826,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
);
mainWindow.webContents.send('main:run-folder-event', {
@@ -835,7 +894,10 @@ const registerNetworkIpc = (mainWindow) => {
}
// run tests
- const testFile = item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests');
+ const testFile = compact([
+ get(collectionRoot, 'request.tests'),
+ item.draft ? get(item.draft, 'request.tests') : get(item, 'request.tests')
+ ]).join(os.EOL);
if (typeof testFile === 'string') {
const testRuntime = new TestRuntime();
const testResults = await testRuntime.runTests(
@@ -847,7 +909,7 @@ const registerNetworkIpc = (mainWindow) => {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
);
mainWindow.webContents.send('main:run-folder-event', {
diff --git a/packages/bruno-electron/src/ipc/network/interpolate-string.js b/packages/bruno-electron/src/ipc/network/interpolate-string.js
new file mode 100644
index 000000000..33701dd0b
--- /dev/null
+++ b/packages/bruno-electron/src/ipc/network/interpolate-string.js
@@ -0,0 +1,55 @@
+const Handlebars = require('handlebars');
+const { forOwn, cloneDeep } = require('lodash');
+
+const interpolateEnvVars = (str, processEnvVars) => {
+ if (!str || !str.length || typeof str !== 'string') {
+ return str;
+ }
+
+ const template = Handlebars.compile(str, { noEscape: true });
+
+ return template({
+ process: {
+ env: {
+ ...processEnvVars
+ }
+ }
+ });
+};
+
+const interpolateString = (str, { envVars, collectionVariables, processEnvVars }) => {
+ if (!str || !str.length || typeof str !== 'string') {
+ return str;
+ }
+
+ processEnvVars = processEnvVars || {};
+ collectionVariables = collectionVariables || {};
+
+ // we clone envVars because we don't want to modify the original object
+ envVars = envVars ? cloneDeep(envVars) : {};
+
+ // envVars can inturn have values as {{process.env.VAR_NAME}}
+ // so we need to interpolate envVars first with processEnvVars
+ forOwn(envVars, (value, key) => {
+ envVars[key] = interpolateEnvVars(value, processEnvVars);
+ });
+
+ const template = Handlebars.compile(str, { noEscape: true });
+
+ // collectionVariables take precedence over envVars
+ const combinedVars = {
+ ...envVars,
+ ...collectionVariables,
+ process: {
+ env: {
+ ...processEnvVars
+ }
+ }
+ };
+
+ return template(combinedVars);
+};
+
+module.exports = {
+ interpolateString
+};
diff --git a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js
index d41be8f4c..a448d9106 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-gql-introspection-request.js
@@ -1,45 +1,34 @@
-const Mustache = require('mustache');
+const Handlebars = require('handlebars');
const { getIntrospectionQuery } = require('graphql');
-const { get } = require('lodash');
+const { setAuthHeaders } = require('./prepare-request');
-// override the default escape function to prevent escaping
-Mustache.escape = function (value) {
- return value;
-};
-
-const prepareGqlIntrospectionRequest = (endpoint, envVars, request) => {
+const prepareGqlIntrospectionRequest = (endpoint, envVars, request, collectionRoot) => {
if (endpoint && endpoint.length) {
- endpoint = Mustache.render(endpoint, envVars);
+ endpoint = Handlebars.compile(endpoint, { noEscape: true })(envVars);
}
- const introspectionQuery = getIntrospectionQuery();
+
const queryParams = {
- query: introspectionQuery
+ query: getIntrospectionQuery()
};
let axiosRequest = {
method: 'POST',
url: endpoint,
headers: {
+ ...mapHeaders(request.headers),
Accept: 'application/json',
'Content-Type': 'application/json'
},
data: JSON.stringify(queryParams)
};
- if (request.auth) {
- if (request.auth.mode === 'basic') {
- axiosRequest.auth = {
- username: get(request, 'auth.basic.username'),
- password: get(request, 'auth.basic.password')
- };
- }
+ return setAuthHeaders(axiosRequest, request, collectionRoot);
+};
- if (request.auth.mode === 'bearer') {
- axiosRequest.headers.authorization = `Bearer ${get(request, 'auth.bearer.token')}`;
- }
- }
+const mapHeaders = (headers) => {
+ const entries = headers.filter((header) => header.enabled).map(({ name, value }) => [name, value]);
- return axiosRequest;
+ return Object.fromEntries(entries);
};
module.exports = prepareGqlIntrospectionRequest;
diff --git a/packages/bruno-electron/src/ipc/network/prepare-request.js b/packages/bruno-electron/src/ipc/network/prepare-request.js
index 226cb86bd..f08536209 100644
--- a/packages/bruno-electron/src/ipc/network/prepare-request.js
+++ b/packages/bruno-electron/src/ipc/network/prepare-request.js
@@ -1,25 +1,25 @@
const { get, each, filter } = require('lodash');
const decomment = require('decomment');
-const prepareRequest = (request) => {
- const headers = {};
- let contentTypeDefined = false;
- each(request.headers, (h) => {
- if (h.enabled) {
- headers[h.name] = h.value;
- if (h.name.toLowerCase() === 'content-type') {
- contentTypeDefined = true;
- }
+// Authentication
+// A request can override the collection auth with another auth
+// But it cannot override the collection auth with no auth
+// We will provide support for disabling the auth via scripting in the future
+const setAuthHeaders = (axiosRequest, request, collectionRoot) => {
+ const collectionAuth = get(collectionRoot, 'request.auth');
+ if (collectionAuth) {
+ if (collectionAuth.mode === 'basic') {
+ axiosRequest.auth = {
+ username: get(collectionAuth, 'basic.username'),
+ password: get(collectionAuth, 'basic.password')
+ };
}
- });
- let axiosRequest = {
- method: request.method,
- url: request.url,
- headers: headers
- };
+ if (collectionAuth.mode === 'bearer') {
+ axiosRequest.headers['authorization'] = `Bearer ${get(collectionAuth, 'bearer.token')}`;
+ }
+ }
- // Authentication
if (request.auth) {
switch (request.auth.mode) {
case 'awsv4':
@@ -44,6 +44,40 @@ const prepareRequest = (request) => {
}
}
+ return axiosRequest;
+};
+
+const prepareRequest = (request, collectionRoot) => {
+ const headers = {};
+ let contentTypeDefined = false;
+
+ // collection headers
+ each(get(collectionRoot, 'request.headers', []), (h) => {
+ if (h.enabled) {
+ headers[h.name] = h.value;
+ if (h.name.toLowerCase() === 'content-type') {
+ contentTypeDefined = true;
+ }
+ }
+ });
+
+ each(request.headers, (h) => {
+ if (h.enabled) {
+ headers[h.name] = h.value;
+ if (h.name.toLowerCase() === 'content-type') {
+ contentTypeDefined = true;
+ }
+ }
+ });
+
+ let axiosRequest = {
+ method: request.method,
+ url: request.url,
+ headers: headers
+ };
+
+ axiosRequest = setAuthHeaders(axiosRequest, request, collectionRoot);
+
if (request.body.mode === 'json') {
if (!contentTypeDefined) {
axiosRequest.headers['content-type'] = 'application/json';
@@ -70,6 +104,13 @@ const prepareRequest = (request) => {
axiosRequest.data = request.body.xml;
}
+ if (request.body.mode === 'sparql') {
+ if (!contentTypeDefined) {
+ axiosRequest.headers['content-type'] = 'application/sparql-query';
+ }
+ axiosRequest.data = request.body.sparql;
+ }
+
if (request.body.mode === 'formUrlEncoded') {
axiosRequest.headers['content-type'] = 'application/x-www-form-urlencoded';
const params = {};
@@ -108,3 +149,4 @@ const prepareRequest = (request) => {
};
module.exports = prepareRequest;
+module.exports.setAuthHeaders = setAuthHeaders;
diff --git a/packages/bruno-electron/src/store/window-state.js b/packages/bruno-electron/src/store/window-state.js
new file mode 100644
index 000000000..bb0a61b64
--- /dev/null
+++ b/packages/bruno-electron/src/store/window-state.js
@@ -0,0 +1,31 @@
+const _ = require('lodash');
+const Store = require('electron-store');
+
+const DEFAULT_WINDOW_WIDTH = 1280;
+const DEFAULT_WINDOW_HEIGHT = 768;
+
+class WindowStateStore {
+ constructor() {
+ this.store = new Store({
+ name: 'preferences',
+ clearInvalidConfig: true
+ });
+ }
+
+ getBounds() {
+ return (
+ this.store.get('window-bounds') || {
+ x: 0,
+ y: 0,
+ width: DEFAULT_WINDOW_WIDTH,
+ height: DEFAULT_WINDOW_HEIGHT
+ }
+ );
+ }
+
+ setBounds(bounds) {
+ this.store.set('window-bounds', bounds);
+ }
+}
+
+module.exports = WindowStateStore;
diff --git a/packages/bruno-electron/src/utils/window.js b/packages/bruno-electron/src/utils/window.js
new file mode 100644
index 000000000..d824141d3
--- /dev/null
+++ b/packages/bruno-electron/src/utils/window.js
@@ -0,0 +1,53 @@
+const { screen } = require('electron');
+const WindowStateStore = require('../store/window-state');
+
+const windowStateStore = new WindowStateStore();
+
+const DEFAULT_WINDOW_WIDTH = 1280;
+const DEFAULT_WINDOW_HEIGHT = 768;
+
+const loadWindowState = () => {
+ const bounds = windowStateStore.getBounds();
+
+ const positionValid = isPositionValid(bounds);
+ const sizeValid = isSizeValid(bounds);
+
+ return {
+ x: bounds.x && positionValid ? bounds.x : undefined,
+ y: bounds.y && positionValid ? bounds.y : undefined,
+ width: bounds.width && sizeValid ? bounds.width : DEFAULT_WINDOW_WIDTH,
+ height: bounds.height && sizeValid ? bounds.height : DEFAULT_WINDOW_HEIGHT
+ };
+};
+
+const saveWindowState = (window) => {
+ const bounds = window.getBounds();
+
+ windowStateStore.setBounds(bounds);
+};
+
+const isPositionValid = (bounds) => {
+ const area = getArea(bounds);
+
+ return (
+ bounds.x >= area.x &&
+ bounds.y >= area.y &&
+ bounds.x + bounds.width <= area.x + area.width &&
+ bounds.y + bounds.height <= area.y + area.height
+ );
+};
+
+const isSizeValid = (bounds) => {
+ const area = getArea(bounds);
+
+ return bounds.width <= area.width && bounds.height <= area.height;
+};
+
+const getArea = (bounds) => {
+ return screen.getDisplayMatching(bounds).workArea;
+};
+
+module.exports = {
+ loadWindowState,
+ saveWindowState
+};
diff --git a/packages/bruno-js/package.json b/packages/bruno-js/package.json
index 00c66c254..00ccdcb15 100644
--- a/packages/bruno-js/package.json
+++ b/packages/bruno-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@usebruno/js",
- "version": "0.6.0",
+ "version": "0.8.0",
"license": "MIT",
"main": "src/index.js",
"files": [
diff --git a/packages/bruno-js/src/runtime/assert-runtime.js b/packages/bruno-js/src/runtime/assert-runtime.js
index f0f84fbc0..06f6adab4 100644
--- a/packages/bruno-js/src/runtime/assert-runtime.js
+++ b/packages/bruno-js/src/runtime/assert-runtime.js
@@ -6,6 +6,7 @@ const BrunoRequest = require('../bruno-request');
const { evaluateJsTemplateLiteral, evaluateJsExpression, createResponseParser } = require('../utils');
const { expect } = chai;
+chai.use(require('chai-string'));
chai.use(function (chai, utils) {
// Custom assertion for checking if a variable is JSON
chai.Assertion.addProperty('json', function () {
@@ -268,7 +269,7 @@ class AssertRuntime {
expect(lhs).to.endWith(rhs);
break;
case 'between':
- const [min, max] = value.split(',');
+ const [min, max] = rhs;
expect(lhs).to.be.within(min, max);
break;
case 'isEmpty':
diff --git a/packages/bruno-js/src/runtime/script-runtime.js b/packages/bruno-js/src/runtime/script-runtime.js
index 46c88b0c1..910c7df43 100644
--- a/packages/bruno-js/src/runtime/script-runtime.js
+++ b/packages/bruno-js/src/runtime/script-runtime.js
@@ -8,12 +8,14 @@ const zlib = require('zlib');
const url = require('url');
const punycode = require('punycode');
const fs = require('fs');
+const { get } = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
const { cleanJson } = require('../utils');
// Inbuilt Library Support
+const ajv = require('ajv');
const atob = require('atob');
const btoa = require('btoa');
const lodash = require('lodash');
@@ -38,10 +40,23 @@ class ScriptRuntime {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
) {
const bru = new Bru(envVariables, collectionVariables, processEnvVars, collectionPath);
const req = new BrunoRequest(request);
+ const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
+ const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
+
+ const whitelistedModules = {};
+
+ for (let module of moduleWhitelist) {
+ try {
+ whitelistedModules[module] = require(module);
+ } catch (e) {
+ // Ignore
+ console.warn(e);
+ }
+ }
const context = {
bru,
@@ -79,6 +94,7 @@ class ScriptRuntime {
punycode,
zlib,
// 3rd party libs
+ ajv,
atob,
btoa,
lodash,
@@ -89,6 +105,7 @@ class ScriptRuntime {
chai,
'node-fetch': fetch,
'crypto-js': CryptoJS,
+ ...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined
}
}
@@ -111,11 +128,24 @@ class ScriptRuntime {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
) {
const bru = new Bru(envVariables, collectionVariables, processEnvVars, collectionPath);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
+ const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
+ const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
+
+ const whitelistedModules = {};
+
+ for (let module of moduleWhitelist) {
+ try {
+ whitelistedModules[module] = require(module);
+ } catch (e) {
+ // Ignore
+ console.warn(e);
+ }
+ }
const context = {
bru,
@@ -154,6 +184,7 @@ class ScriptRuntime {
punycode,
zlib,
// 3rd party libs
+ ajv,
atob,
btoa,
lodash,
@@ -163,6 +194,7 @@ class ScriptRuntime {
axios,
'node-fetch': fetch,
'crypto-js': CryptoJS,
+ ...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined
}
}
diff --git a/packages/bruno-js/src/runtime/test-runtime.js b/packages/bruno-js/src/runtime/test-runtime.js
index efefb451f..9da2cb287 100644
--- a/packages/bruno-js/src/runtime/test-runtime.js
+++ b/packages/bruno-js/src/runtime/test-runtime.js
@@ -9,6 +9,7 @@ const zlib = require('zlib');
const url = require('url');
const punycode = require('punycode');
const fs = require('fs');
+const { get } = require('lodash');
const Bru = require('../bru');
const BrunoRequest = require('../bruno-request');
const BrunoResponse = require('../bruno-response');
@@ -17,13 +18,15 @@ const TestResults = require('../test-results');
const { cleanJson } = require('../utils');
// Inbuilt Library Support
+const ajv = require('ajv');
const atob = require('atob');
-const axios = require('axios');
const btoa = require('btoa');
const lodash = require('lodash');
const moment = require('moment');
const uuid = require('uuid');
const nanoid = require('nanoid');
+const axios = require('axios');
+const fetch = require('node-fetch');
const CryptoJS = require('crypto-js');
class TestRuntime {
@@ -38,11 +41,24 @@ class TestRuntime {
collectionPath,
onConsoleLog,
processEnvVars,
- allowScriptFilesystemAccess
+ scriptingConfig
) {
const bru = new Bru(envVariables, collectionVariables, processEnvVars, collectionPath);
const req = new BrunoRequest(request);
const res = new BrunoResponse(response);
+ const allowScriptFilesystemAccess = get(scriptingConfig, 'filesystemAccess.allow', false);
+ const moduleWhitelist = get(scriptingConfig, 'moduleWhitelist', []);
+
+ const whitelistedModules = {};
+
+ for (let module of moduleWhitelist) {
+ try {
+ whitelistedModules[module] = require(module);
+ } catch (e) {
+ // Ignore
+ console.warn(e);
+ }
+ }
const __brunoTestResults = new TestResults();
const test = Test(__brunoTestResults, chai);
@@ -97,15 +113,18 @@ class TestRuntime {
punycode,
zlib,
// 3rd party libs
- atob,
- axios,
+ ajv,
btoa,
+ atob,
lodash,
moment,
uuid,
nanoid,
+ axios,
chai,
+ 'node-fetch': fetch,
'crypto-js': CryptoJS,
+ ...whitelistedModules,
fs: allowScriptFilesystemAccess ? fs : undefined
}
}
diff --git a/packages/bruno-lang/package.json b/packages/bruno-lang/package.json
index fffde571f..2e4b3df03 100644
--- a/packages/bruno-lang/package.json
+++ b/packages/bruno-lang/package.json
@@ -1,6 +1,6 @@
{
"name": "@usebruno/lang",
- "version": "0.5.0",
+ "version": "0.8.0",
"license": "MIT",
"main": "src/index.js",
"files": [
@@ -14,6 +14,7 @@
},
"dependencies": {
"arcsecond": "^5.0.0",
+ "dotenv": "^16.3.1",
"lodash": "^4.17.21",
"ohm-js": "^16.6.0"
}
diff --git a/packages/bruno-lang/src/index.js b/packages/bruno-lang/src/index.js
index f27179c45..55a9569d7 100644
--- a/packages/bruno-lang/src/index.js
+++ b/packages/bruno-lang/src/index.js
@@ -1,21 +1,23 @@
-const { bruToJson, jsonToBru, bruToEnvJson, envJsonToBru } = require('../v1/src');
-
const bruToJsonV2 = require('../v2/src/bruToJson');
const jsonToBruV2 = require('../v2/src/jsonToBru');
const bruToEnvJsonV2 = require('../v2/src/envToJson');
const envJsonToBruV2 = require('../v2/src/jsonToEnv');
const dotenvToJson = require('../v2/src/dotenvToJson');
-module.exports = {
- bruToJson,
- jsonToBru,
- bruToEnvJson,
- envJsonToBru,
+const collectionBruToJson = require('../v2/src/collectionBruToJson');
+const jsonToCollectionBru = require('../v2/src/jsonToCollectionBru');
+// Todo: remove V2 suffixes
+// Changes will have to be made to the CLI and GUI
+
+module.exports = {
bruToJsonV2,
jsonToBruV2,
bruToEnvJsonV2,
envJsonToBruV2,
+ collectionBruToJson,
+ jsonToCollectionBru,
+
dotenvToJson
};
diff --git a/packages/bruno-lang/v2/src/bruToJson.js b/packages/bruno-lang/v2/src/bruToJson.js
index 8df77f1cd..5ce65eba0 100644
--- a/packages/bruno-lang/v2/src/bruToJson.js
+++ b/packages/bruno-lang/v2/src/bruToJson.js
@@ -23,8 +23,8 @@ const { outdentString } = require('../../v1/src/utils');
*/
const grammar = ohm.grammar(`Bru {
BruFile = (meta | http | query | headers | auths | bodies | varsandassert | script | tests | docs)*
- auths = authawsv4 | authbasic | authbearer
- bodies = bodyjson | bodytext | bodyxml | bodygraphql | bodygraphqlvars | bodyforms | body
+ auths = authawsv4 | authbasic | authbearer
+ bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
bodyforms = bodyformurlencoded | bodymultipart
nl = "\\r"? "\\n"
@@ -84,6 +84,7 @@ const grammar = ohm.grammar(`Bru {
bodyjson = "body:json" st* "{" nl* textblock tagend
bodytext = "body:text" st* "{" nl* textblock tagend
bodyxml = "body:xml" st* "{" nl* textblock tagend
+ bodysparql = "body:sparql" st* "{" nl* textblock tagend
bodygraphql = "body:graphql" st* "{" nl* textblock tagend
bodygraphqlvars = "body:graphql:vars" st* "{" nl* textblock tagend
@@ -394,6 +395,13 @@ const sem = grammar.createSemantics().addAttribute('ast', {
}
};
},
+ bodysparql(_1, _2, _3, _4, textblock, _5) {
+ return {
+ body: {
+ sparql: outdentString(textblock.sourceString)
+ }
+ };
+ },
bodygraphql(_1, _2, _3, _4, textblock, _5) {
return {
body: {
diff --git a/packages/bruno-lang/v2/src/collectionBruToJson.js b/packages/bruno-lang/v2/src/collectionBruToJson.js
new file mode 100644
index 000000000..d78f752c0
--- /dev/null
+++ b/packages/bruno-lang/v2/src/collectionBruToJson.js
@@ -0,0 +1,273 @@
+const ohm = require('ohm-js');
+const _ = require('lodash');
+const { outdentString } = require('../../v1/src/utils');
+
+const grammar = ohm.grammar(`Bru {
+ BruFile = (meta | query | headers | auth | auths | vars | script | tests | docs)*
+ auths = authbasic | authbearer
+
+ nl = "\\r"? "\\n"
+ st = " " | "\\t"
+ stnl = st | nl
+ tagend = nl "}"
+ optionalnl = ~tagend nl
+ keychar = ~(tagend | st | nl | ":") any
+ valuechar = ~(nl | tagend) any
+
+ // Dictionary Blocks
+ dictionary = st* "{" pairlist? tagend
+ pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
+ pair = st* key st* ":" st* value st*
+ key = keychar*
+ value = valuechar*
+
+ // Text Blocks
+ textblock = textline (~tagend nl textline)*
+ textline = textchar*
+ textchar = ~nl any
+
+ meta = "meta" dictionary
+
+ auth = "auth" dictionary
+
+ headers = "headers" dictionary
+
+ query = "query" dictionary
+
+ vars = varsreq | varsres
+ varsreq = "vars:pre-request" dictionary
+ varsres = "vars:post-response" dictionary
+
+ authbasic = "auth:basic" dictionary
+ authbearer = "auth:bearer" dictionary
+
+ script = scriptreq | scriptres
+ scriptreq = "script:pre-request" st* "{" nl* textblock tagend
+ scriptres = "script:post-response" st* "{" nl* textblock tagend
+ tests = "tests" st* "{" nl* textblock tagend
+ docs = "docs" st* "{" nl* textblock tagend
+}`);
+
+const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
+ if (!pairList.length) {
+ return [];
+ }
+ return _.map(pairList[0], (pair) => {
+ let name = _.keys(pair)[0];
+ let value = pair[name];
+
+ if (!parseEnabled) {
+ return {
+ name,
+ value
+ };
+ }
+
+ let enabled = true;
+ if (name && name.length && name.charAt(0) === '~') {
+ name = name.slice(1);
+ enabled = false;
+ }
+
+ return {
+ name,
+ value,
+ enabled
+ };
+ });
+};
+
+const concatArrays = (objValue, srcValue) => {
+ if (_.isArray(objValue) && _.isArray(srcValue)) {
+ return objValue.concat(srcValue);
+ }
+};
+
+const mapPairListToKeyValPair = (pairList = []) => {
+ if (!pairList || !pairList.length) {
+ return {};
+ }
+
+ return _.merge({}, ...pairList[0]);
+};
+
+const sem = grammar.createSemantics().addAttribute('ast', {
+ BruFile(tags) {
+ if (!tags || !tags.ast || !tags.ast.length) {
+ return {};
+ }
+
+ return _.reduce(
+ tags.ast,
+ (result, item) => {
+ return _.mergeWith(result, item, concatArrays);
+ },
+ {}
+ );
+ },
+ dictionary(_1, _2, pairlist, _3) {
+ return pairlist.ast;
+ },
+ pairlist(_1, pair, _2, rest, _3) {
+ return [pair.ast, ...rest.ast];
+ },
+ pair(_1, key, _2, _3, _4, value, _5) {
+ let res = {};
+ res[key.ast] = value.ast ? value.ast.trim() : '';
+ return res;
+ },
+ key(chars) {
+ return chars.sourceString ? chars.sourceString.trim() : '';
+ },
+ value(chars) {
+ return chars.sourceString ? chars.sourceString.trim() : '';
+ },
+ textblock(line, _1, rest) {
+ return [line.ast, ...rest.ast].join('\n');
+ },
+ textline(chars) {
+ return chars.sourceString;
+ },
+ textchar(char) {
+ return char.sourceString;
+ },
+ nl(_1, _2) {
+ return '';
+ },
+ st(_) {
+ return '';
+ },
+ tagend(_1, _2) {
+ return '';
+ },
+ _iter(...elements) {
+ return elements.map((e) => e.ast);
+ },
+ meta(_1, dictionary) {
+ let meta = mapPairListToKeyValPair(dictionary.ast) || {};
+
+ meta.type = 'collection';
+
+ return {
+ meta
+ };
+ },
+ auth(_1, dictionary) {
+ let auth = mapPairListToKeyValPair(dictionary.ast) || {};
+
+ return {
+ auth: {
+ mode: auth ? auth.mode : 'none'
+ }
+ };
+ },
+ query(_1, dictionary) {
+ return {
+ query: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ headers(_1, dictionary) {
+ return {
+ headers: mapPairListToKeyValPairs(dictionary.ast)
+ };
+ },
+ authbasic(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+ const usernameKey = _.find(auth, { name: 'username' });
+ const passwordKey = _.find(auth, { name: 'password' });
+ const username = usernameKey ? usernameKey.value : '';
+ const password = passwordKey ? passwordKey.value : '';
+ return {
+ auth: {
+ basic: {
+ username,
+ password
+ }
+ }
+ };
+ },
+ authbearer(_1, dictionary) {
+ const auth = mapPairListToKeyValPairs(dictionary.ast, false);
+ const tokenKey = _.find(auth, { name: 'token' });
+ const token = tokenKey ? tokenKey.value : '';
+ return {
+ auth: {
+ bearer: {
+ token
+ }
+ }
+ };
+ },
+ varsreq(_1, dictionary) {
+ const vars = mapPairListToKeyValPairs(dictionary.ast);
+ _.each(vars, (v) => {
+ let name = v.name;
+ if (name && name.length && name.charAt(0) === '@') {
+ v.name = name.slice(1);
+ v.local = true;
+ } else {
+ v.local = false;
+ }
+ });
+
+ return {
+ vars: {
+ req: vars
+ }
+ };
+ },
+ varsres(_1, dictionary) {
+ const vars = mapPairListToKeyValPairs(dictionary.ast);
+ _.each(vars, (v) => {
+ let name = v.name;
+ if (name && name.length && name.charAt(0) === '@') {
+ v.name = name.slice(1);
+ v.local = true;
+ } else {
+ v.local = false;
+ }
+ });
+
+ return {
+ vars: {
+ res: vars
+ }
+ };
+ },
+ scriptreq(_1, _2, _3, _4, textblock, _5) {
+ return {
+ script: {
+ req: outdentString(textblock.sourceString)
+ }
+ };
+ },
+ scriptres(_1, _2, _3, _4, textblock, _5) {
+ return {
+ script: {
+ res: outdentString(textblock.sourceString)
+ }
+ };
+ },
+ tests(_1, _2, _3, _4, textblock, _5) {
+ return {
+ tests: outdentString(textblock.sourceString)
+ };
+ },
+ docs(_1, _2, _3, _4, textblock, _5) {
+ return {
+ docs: outdentString(textblock.sourceString)
+ };
+ }
+});
+
+const parser = (input) => {
+ const match = grammar.match(input);
+
+ if (match.succeeded()) {
+ return sem(match).ast;
+ } else {
+ throw new Error(match.message);
+ }
+};
+
+module.exports = parser;
diff --git a/packages/bruno-lang/v2/src/dotenvToJson.js b/packages/bruno-lang/v2/src/dotenvToJson.js
index e83911a3d..2c1794ee4 100644
--- a/packages/bruno-lang/v2/src/dotenvToJson.js
+++ b/packages/bruno-lang/v2/src/dotenvToJson.js
@@ -1,80 +1,9 @@
-const ohm = require('ohm-js');
-const _ = require('lodash');
-
-const grammar = ohm.grammar(`Env {
- EnvFile = (entry)*
- entry = st* key st* "=" st* value st* nl*
- key = keychar*
- value = valuechar*
- keychar = ~(nl | st | nl | "=") any
- valuechar = ~nl any
- nl = "\\r"? "\\n"
- st = " " | "\\t"
-}`);
-
-const concatArrays = (objValue, srcValue) => {
- if (_.isArray(objValue) && _.isArray(srcValue)) {
- return objValue.concat(srcValue);
- }
-};
-
-const sem = grammar.createSemantics().addAttribute('ast', {
- EnvFile(entries) {
- return _.reduce(
- entries.ast,
- (result, item) => {
- return _.mergeWith(result, item, concatArrays);
- },
- {}
- );
- },
- entry(_1, key, _2, _3, _4, value, _5, _6) {
- return { [key.ast.trim()]: value.ast.trim() };
- },
- key(chars) {
- return chars.sourceString;
- },
- value(chars) {
- return chars.sourceString;
- },
- nl(_1, _2) {
- return '';
- },
- st(_) {
- return '';
- },
- _iter(...elements) {
- return elements.map((e) => e.ast);
- }
-});
+const dotenv = require('dotenv');
const parser = (input) => {
- const match = grammar.match(input);
-
- if (match.succeeded()) {
- const ast = sem(match).ast;
- return postProcessEntries(ast);
- } else {
- throw new Error(match.message);
- }
+ const buf = Buffer.from(input);
+ const parsed = dotenv.parse(buf);
+ return parsed;
};
-function postProcessEntries(ast) {
- const processed = {};
-
- for (const key in ast) {
- const value = ast[key];
-
- if (!isNaN(value)) {
- processed[key] = parseFloat(value); // Convert to number if it's a valid number
- } else if (value.toLowerCase() === 'true' || value.toLowerCase() === 'false') {
- processed[key] = value.toLowerCase() === 'true'; // Convert to boolean if it's 'true' or 'false'
- } else {
- processed[key] = value; // Otherwise, keep it as a string
- }
- }
-
- return processed;
-}
-
module.exports = parser;
diff --git a/packages/bruno-lang/v2/src/jsonToBru.js b/packages/bruno-lang/v2/src/jsonToBru.js
index 0c4debb63..757406b70 100644
--- a/packages/bruno-lang/v2/src/jsonToBru.js
+++ b/packages/bruno-lang/v2/src/jsonToBru.js
@@ -138,6 +138,14 @@ ${indentString(body.text)}
${indentString(body.xml)}
}
+`;
+ }
+
+ if (body && body.sparql && body.sparql.length) {
+ bru += `body:sparql {
+${indentString(body.sparql)}
+}
+
`;
}
diff --git a/packages/bruno-lang/v2/src/jsonToCollectionBru.js b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
new file mode 100644
index 000000000..57a5ea7bf
--- /dev/null
+++ b/packages/bruno-lang/v2/src/jsonToCollectionBru.js
@@ -0,0 +1,185 @@
+const _ = require('lodash');
+
+const { indentString } = require('../../v1/src/utils');
+
+const enabled = (items = []) => items.filter((item) => item.enabled);
+const disabled = (items = []) => items.filter((item) => !item.enabled);
+
+// remove the last line if two new lines are found
+const stripLastLine = (text) => {
+ if (!text || !text.length) return text;
+
+ return text.replace(/(\r?\n)$/, '');
+};
+
+const jsonToCollectionBru = (json) => {
+ const { meta, query, headers, auth, script, tests, vars, docs } = json;
+
+ let bru = '';
+
+ if (meta) {
+ bru += 'meta {\n';
+ for (const key in meta) {
+ bru += ` ${key}: ${meta[key]}\n`;
+ }
+ bru += '}\n\n';
+ }
+
+ if (query && query.length) {
+ bru += 'query {';
+ if (enabled(query).length) {
+ bru += `\n${indentString(
+ enabled(query)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}`;
+ }
+
+ if (disabled(query).length) {
+ bru += `\n${indentString(
+ disabled(query)
+ .map((item) => `~${item.name}: ${item.value}`)
+ .join('\n')
+ )}`;
+ }
+
+ bru += '\n}\n\n';
+ }
+
+ if (headers && headers.length) {
+ bru += 'headers {';
+ if (enabled(headers).length) {
+ bru += `\n${indentString(
+ enabled(headers)
+ .map((item) => `${item.name}: ${item.value}`)
+ .join('\n')
+ )}`;
+ }
+
+ if (disabled(headers).length) {
+ bru += `\n${indentString(
+ disabled(headers)
+ .map((item) => `~${item.name}: ${item.value}`)
+ .join('\n')
+ )}`;
+ }
+
+ bru += '\n}\n\n';
+ }
+
+ if (auth && auth.mode) {
+ bru += `auth {
+${indentString(`mode: ${auth.mode}`)}
+}
+
+`;
+ }
+
+ if (auth && auth.basic) {
+ bru += `auth:basic {
+${indentString(`username: ${auth.basic.username}`)}
+${indentString(`password: ${auth.basic.password}`)}
+}
+
+`;
+ }
+
+ if (auth && auth.bearer) {
+ bru += `auth:bearer {
+${indentString(`token: ${auth.bearer.token}`)}
+}
+
+`;
+ }
+
+ let reqvars = _.get(vars, 'req');
+ let resvars = _.get(vars, 'res');
+ if (reqvars && reqvars.length) {
+ const varsEnabled = _.filter(reqvars, (v) => v.enabled && !v.local);
+ const varsDisabled = _.filter(reqvars, (v) => !v.enabled && !v.local);
+ const varsLocalEnabled = _.filter(reqvars, (v) => v.enabled && v.local);
+ const varsLocalDisabled = _.filter(reqvars, (v) => !v.enabled && v.local);
+
+ bru += `vars:pre-request {`;
+
+ if (varsEnabled.length) {
+ bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ if (varsLocalEnabled.length) {
+ bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ if (varsDisabled.length) {
+ bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ if (varsLocalDisabled.length) {
+ bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ bru += '\n}\n\n';
+ }
+ if (resvars && resvars.length) {
+ const varsEnabled = _.filter(resvars, (v) => v.enabled && !v.local);
+ const varsDisabled = _.filter(resvars, (v) => !v.enabled && !v.local);
+ const varsLocalEnabled = _.filter(resvars, (v) => v.enabled && v.local);
+ const varsLocalDisabled = _.filter(resvars, (v) => !v.enabled && v.local);
+
+ bru += `vars:post-response {`;
+
+ if (varsEnabled.length) {
+ bru += `\n${indentString(varsEnabled.map((item) => `${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ if (varsLocalEnabled.length) {
+ bru += `\n${indentString(varsLocalEnabled.map((item) => `@${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ if (varsDisabled.length) {
+ bru += `\n${indentString(varsDisabled.map((item) => `~${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ if (varsLocalDisabled.length) {
+ bru += `\n${indentString(varsLocalDisabled.map((item) => `~@${item.name}: ${item.value}`).join('\n'))}`;
+ }
+
+ bru += '\n}\n\n';
+ }
+
+ if (script && script.req && script.req.length) {
+ bru += `script:pre-request {
+${indentString(script.req)}
+}
+
+`;
+ }
+
+ if (script && script.res && script.res.length) {
+ bru += `script:post-response {
+${indentString(script.res)}
+}
+
+`;
+ }
+
+ if (tests && tests.length) {
+ bru += `tests {
+${indentString(tests)}
+}
+
+`;
+ }
+
+ if (docs && docs.length) {
+ bru += `docs {
+${indentString(docs)}
+}
+
+`;
+ }
+
+ return stripLastLine(bru);
+};
+
+module.exports = jsonToCollectionBru;
diff --git a/packages/bruno-lang/v2/tests/collection.spec.js b/packages/bruno-lang/v2/tests/collection.spec.js
new file mode 100644
index 000000000..4bdb7f9dc
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/collection.spec.js
@@ -0,0 +1,24 @@
+const fs = require('fs');
+const path = require('path');
+const collectionBruToJson = require('../src/collectionBruToJson');
+const jsonToCollectionBru = require('../src/jsonToCollectionBru');
+
+describe('collectionBruToJson', () => {
+ it('should parse the collection bru file', () => {
+ const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8');
+ const expected = require('./fixtures/collection.json');
+ const output = collectionBruToJson(input);
+
+ expect(output).toEqual(expected);
+ });
+});
+
+describe('jsonToCollectionBru', () => {
+ it('should convert the collection json to bru', () => {
+ const input = require('./fixtures/collection.json');
+ const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'collection.bru'), 'utf8');
+ const output = jsonToCollectionBru(input);
+
+ expect(output).toEqual(expected);
+ });
+});
diff --git a/packages/bruno-lang/v2/tests/dotenvToJson.spec.js b/packages/bruno-lang/v2/tests/dotenvToJson.spec.js
index 81f07fe87..4afa55647 100644
--- a/packages/bruno-lang/v2/tests/dotenvToJson.spec.js
+++ b/packages/bruno-lang/v2/tests/dotenvToJson.spec.js
@@ -26,16 +26,25 @@ BEEP=false
`;
const expected = {
FOO: 'bar',
- BAZ: 2,
- BEEP: false
+ BAZ: '2',
+ BEEP: 'false'
};
const output = parser(input);
expect(output).toEqual(expected);
});
- test('it should handle leading and trailing whitespace', () => {
+ test('it should not strip leading and trailing whitespace when using quotes', () => {
const input = `
- SPACE = value
+SPACE=" value "
+`;
+ const expected = { SPACE: ' value ' };
+ const output = parser(input);
+ expect(output).toEqual(expected);
+ });
+
+ test('it should strip leading and trailing whitespace when NOT using quotes', () => {
+ const input = `
+SPACE= value
`;
const expected = { SPACE: 'value' };
const output = parser(input);
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.bru b/packages/bruno-lang/v2/tests/fixtures/collection.bru
new file mode 100644
index 000000000..a02be30cb
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.bru
@@ -0,0 +1,43 @@
+meta {
+ type: collection
+}
+
+headers {
+ content-type: application/json
+ Authorization: Bearer 123
+ ~transaction-id: {{transactionId}}
+}
+
+auth {
+ mode: none
+}
+
+auth:basic {
+ username: john
+ password: secret
+}
+
+auth:bearer {
+ token: 123
+}
+
+vars:pre-request {
+ departingDate: 2020-01-01
+ ~returningDate: 2020-01-02
+}
+
+vars:post-response {
+ ~transactionId: $res.body.transactionId
+}
+
+script:pre-request {
+ console.log("In Collection pre Request Script");
+}
+
+script:post-response {
+ console.log("In Collection post Request Script");
+}
+
+docs {
+ This request needs auth token to be set in the headers.
+}
diff --git a/packages/bruno-lang/v2/tests/fixtures/collection.json b/packages/bruno-lang/v2/tests/fixtures/collection.json
new file mode 100644
index 000000000..de827d11e
--- /dev/null
+++ b/packages/bruno-lang/v2/tests/fixtures/collection.json
@@ -0,0 +1,61 @@
+{
+ "meta": {
+ "type": "collection"
+ },
+ "headers": [
+ {
+ "name": "content-type",
+ "value": "application/json",
+ "enabled": true
+ },
+ {
+ "name": "Authorization",
+ "value": "Bearer 123",
+ "enabled": true
+ },
+ {
+ "name": "transaction-id",
+ "value": "{{transactionId}}",
+ "enabled": false
+ }
+ ],
+ "auth": {
+ "mode": "none",
+ "basic": {
+ "username": "john",
+ "password": "secret"
+ },
+ "bearer": {
+ "token": "123"
+ }
+ },
+ "vars": {
+ "req": [
+ {
+ "name": "departingDate",
+ "value": "2020-01-01",
+ "enabled": true,
+ "local": false
+ },
+ {
+ "name": "returningDate",
+ "value": "2020-01-02",
+ "enabled": false,
+ "local": false
+ }
+ ],
+ "res": [
+ {
+ "name": "transactionId",
+ "value": "$res.body.transactionId",
+ "enabled": false,
+ "local": false
+ }
+ ]
+ },
+ "script": {
+ "req": "console.log(\"In Collection pre Request Script\");",
+ "res": "console.log(\"In Collection post Request Script\");"
+ },
+ "docs": "This request needs auth token to be set in the headers."
+}
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.bru b/packages/bruno-lang/v2/tests/fixtures/request.bru
index 56b35b81b..22168b194 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.bru
+++ b/packages/bruno-lang/v2/tests/fixtures/request.bru
@@ -57,6 +57,13 @@ body:xml {
}
+body:sparql {
+ SELECT * WHERE {
+ ?subject ?predicate ?object .
+ }
+ LIMIT 10
+}
+
body:form-urlencoded {
apikey: secret
numbers: +91998877665
diff --git a/packages/bruno-lang/v2/tests/fixtures/request.json b/packages/bruno-lang/v2/tests/fixtures/request.json
index 151ba4fee..def7b5f08 100644
--- a/packages/bruno-lang/v2/tests/fixtures/request.json
+++ b/packages/bruno-lang/v2/tests/fixtures/request.json
@@ -65,6 +65,7 @@
"json": "{\n \"hello\": \"world\"\n}",
"text": "This is a text body",
"xml": "
\n John\n 30\n",
+ "sparql": "SELECT * WHERE {\n ?subject ?predicate ?object .\n}\nLIMIT 10",
"graphql": {
"query": "{\n launchesPast {\n launch_site {\n site_name\n }\n launch_success\n }\n}",
"variables": "{\n \"limit\": 5\n}"
diff --git a/packages/bruno-schema/src/collections/index.js b/packages/bruno-schema/src/collections/index.js
index 6c7c22230..1721d386a 100644
--- a/packages/bruno-schema/src/collections/index.js
+++ b/packages/bruno-schema/src/collections/index.js
@@ -57,11 +57,12 @@ const graphqlBodySchema = Yup.object({
const requestBodySchema = Yup.object({
mode: Yup.string()
- .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql'])
+ .oneOf(['none', 'json', 'text', 'xml', 'formUrlEncoded', 'multipartForm', 'graphql', 'sparql'])
.required('mode is required'),
json: Yup.string().nullable(),
text: Yup.string().nullable(),
xml: Yup.string().nullable(),
+ sparql: Yup.string().nullable(),
formUrlEncoded: Yup.array().of(keyValueSchema).nullable(),
multipartForm: Yup.array().of(keyValueSchema).nullable(),
graphql: graphqlBodySchema.nullable()
@@ -135,7 +136,7 @@ const itemSchema = Yup.object({
uid: uidSchema,
type: Yup.string().oneOf(['http-request', 'graphql-request', 'folder']).required('type is required'),
seq: Yup.number().min(1),
- name: Yup.string().min(1, 'name must be atleast 1 characters').required('name is required'),
+ name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
request: requestSchema.when('type', {
is: (type) => ['http-request', 'graphql-request'].includes(type),
then: (schema) => schema.required('request is required when item-type is request')
@@ -150,7 +151,7 @@ const itemSchema = Yup.object({
const collectionSchema = Yup.object({
version: Yup.string().oneOf(['1']).required('version is required'),
uid: uidSchema,
- name: Yup.string().min(1, 'name must be atleast 1 characters').required('name is required'),
+ name: Yup.string().min(1, 'name must be at least 1 character').required('name is required'),
items: Yup.array().of(itemSchema),
activeEnvironmentUid: Yup.string()
.length(21, 'activeEnvironmentUid must be 21 characters in length')
diff --git a/packages/bruno-schema/src/collections/itemSchema.spec.js b/packages/bruno-schema/src/collections/itemSchema.spec.js
index a949f3c2b..8c46bed2c 100644
--- a/packages/bruno-schema/src/collections/itemSchema.spec.js
+++ b/packages/bruno-schema/src/collections/itemSchema.spec.js
@@ -34,7 +34,7 @@ describe('Item Schema Validation', () => {
return Promise.all([
expect(itemSchema.validate(item)).rejects.toEqual(
- validationErrorWithMessages('name must be atleast 1 characters')
+ validationErrorWithMessages('name must be at least 1 character')
)
]);
});
diff --git a/readme.md b/readme.md
index 29cb5ad93..8021a5b7a 100644
--- a/readme.md
+++ b/readme.md
@@ -73,6 +73,7 @@ Even if you are not able to make contributions via code, please don't hesitate t
[Twitter](https://twitter.com/use_bruno)
[Website](https://www.usebruno.com)
[Discord](https://discord.com/invite/KgcZUncpjq)
+[LinkedIn](https://www.linkedin.com/company/usebruno)
### License 📄