mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
1829 lines
57 KiB
JavaScript
1829 lines
57 KiB
JavaScript
import { cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash';
|
|
import { uuid } from 'utils/common';
|
|
import { buildPersistedEnvVariables } from 'utils/environments';
|
|
import { sortByNameThenSequence } from 'utils/common/index';
|
|
import path from 'utils/common/path';
|
|
import { isRequestTagsIncluded } from '@usebruno/common';
|
|
|
|
const replaceTabsWithSpaces = (str, numSpaces = 2) => {
|
|
if (!str || !str.length || !isString(str)) {
|
|
return '';
|
|
}
|
|
|
|
return str.replaceAll('\t', ' '.repeat(numSpaces));
|
|
};
|
|
|
|
export const addDepth = (items = []) => {
|
|
const depth = (itms, initialDepth) => {
|
|
each(itms, (i) => {
|
|
i.depth = initialDepth;
|
|
|
|
if (i.items && i.items.length) {
|
|
depth(i.items, initialDepth + 1);
|
|
}
|
|
});
|
|
};
|
|
|
|
depth(items, 1);
|
|
};
|
|
|
|
export const collapseAllItemsInCollection = (collection) => {
|
|
collection.collapsed = true;
|
|
|
|
const collapseItem = (items) => {
|
|
each(items, (i) => {
|
|
i.collapsed = true;
|
|
|
|
if (i.items && i.items.length) {
|
|
collapseItem(i.items);
|
|
}
|
|
});
|
|
};
|
|
|
|
collapseItem(collection.items);
|
|
};
|
|
|
|
export const sortItems = (collection) => {
|
|
const sort = (obj) => {
|
|
if (obj.items && obj.items.length) {
|
|
obj.items = sortBy(obj.items, 'type');
|
|
}
|
|
|
|
each(obj.items, (i) => sort(i));
|
|
};
|
|
|
|
sort(collection);
|
|
};
|
|
|
|
export const flattenItems = (items = []) => {
|
|
const flattenedItems = [];
|
|
|
|
const flatten = (itms, flattened) => {
|
|
each(itms, (i) => {
|
|
flattened.push(i);
|
|
|
|
if (i.items && i.items.length) {
|
|
flatten(i.items, flattened);
|
|
}
|
|
});
|
|
};
|
|
|
|
flatten(items, flattenedItems);
|
|
|
|
return flattenedItems;
|
|
};
|
|
|
|
export const findItem = (items = [], itemUid) => {
|
|
return find(items, (i) => i.uid === itemUid);
|
|
};
|
|
|
|
export const findCollectionByUid = (collections, collectionUid) => {
|
|
return find(collections, (c) => c.uid === collectionUid);
|
|
};
|
|
|
|
export const findCollectionByPathname = (collections, pathname) => {
|
|
return find(collections, (c) => c.pathname === pathname);
|
|
};
|
|
|
|
export const findCollectionByItemUid = (collections, itemUid) => {
|
|
return find(collections, (c) => {
|
|
return findItemInCollection(c, itemUid);
|
|
});
|
|
};
|
|
|
|
export const findItemByPathname = (items = [], pathname) => {
|
|
return find(items, (i) => i.pathname === pathname);
|
|
};
|
|
|
|
export const findItemInCollectionByPathname = (collection, pathname) => {
|
|
let flattenedItems = flattenItems(collection.items);
|
|
|
|
return findItemByPathname(flattenedItems, pathname);
|
|
};
|
|
|
|
export const findItemInCollectionByItemUid = (collection, itemUid) => {
|
|
let flattenedItems = flattenItems(collection.items);
|
|
return findItem(flattenedItems, itemUid);
|
|
};
|
|
|
|
export const findParentItemInCollectionByPathname = (collection, pathname) => {
|
|
let flattenedItems = flattenItems(collection.items);
|
|
|
|
return find(flattenedItems, (item) => {
|
|
return item.items && find(item.items, (i) => i.pathname === pathname);
|
|
});
|
|
};
|
|
|
|
export const findItemInCollection = (collection, itemUid) => {
|
|
if (!collection || !collection.items) {
|
|
return null;
|
|
}
|
|
let flattenedItems = flattenItems(collection.items);
|
|
|
|
return findItem(flattenedItems, itemUid);
|
|
};
|
|
|
|
export const findParentItemInCollection = (collection, itemUid) => {
|
|
let flattenedItems = flattenItems(collection.items);
|
|
|
|
return find(flattenedItems, (item) => {
|
|
return item.items && find(item.items, (i) => i.uid === itemUid);
|
|
});
|
|
};
|
|
|
|
export const recursivelyGetAllItemUids = (items = []) => {
|
|
let flattenedItems = flattenItems(items);
|
|
|
|
return map(flattenedItems, (i) => i.uid);
|
|
};
|
|
|
|
export const findEnvironmentInCollection = (collection, envUid) => {
|
|
return find(collection.environments, (e) => e.uid === envUid);
|
|
};
|
|
|
|
export const findEnvironmentInCollectionByName = (collection, name) => {
|
|
return find(collection.environments, (e) => e.name === name);
|
|
};
|
|
|
|
export const areItemsLoading = (folder) => {
|
|
if (!folder || folder.isLoading) {
|
|
return true;
|
|
}
|
|
|
|
let flattenedItems = flattenItems(folder.items);
|
|
return flattenedItems?.reduce((isLoading, i) => {
|
|
if (i?.loading) {
|
|
isLoading = true;
|
|
}
|
|
return isLoading;
|
|
}, false);
|
|
};
|
|
|
|
export const getItemsLoadStats = (folder) => {
|
|
let loadingCount = 0;
|
|
let flattenedItems = flattenItems(folder.items);
|
|
flattenedItems?.forEach((i) => {
|
|
if (i?.loading) {
|
|
loadingCount += 1;
|
|
}
|
|
});
|
|
return {
|
|
loading: loadingCount,
|
|
total: flattenedItems?.length
|
|
};
|
|
};
|
|
|
|
export const transformCollectionToSaveToExportAsFile = (collection, options = {}) => {
|
|
const copyHeaders = (headers) => {
|
|
return map(headers, (header) => {
|
|
return {
|
|
uid: header.uid,
|
|
name: header.name,
|
|
value: header.value,
|
|
description: header.description,
|
|
annotations: header.annotations,
|
|
enabled: header.enabled
|
|
};
|
|
});
|
|
};
|
|
|
|
const copyParams = (params) => {
|
|
return map(params, (param) => {
|
|
return {
|
|
uid: param.uid,
|
|
name: param.name,
|
|
value: param.value,
|
|
description: param.description,
|
|
annotations: param.annotations,
|
|
type: param.type,
|
|
enabled: param.enabled
|
|
};
|
|
});
|
|
};
|
|
|
|
const copyFormUrlEncodedParams = (params = []) => {
|
|
return map(params, (param) => {
|
|
return {
|
|
uid: param.uid,
|
|
name: param.name,
|
|
value: param.value,
|
|
description: param.description,
|
|
enabled: param.enabled
|
|
};
|
|
});
|
|
};
|
|
|
|
const copyMultipartFormParams = (params = []) => {
|
|
return map(params, (param) => {
|
|
return {
|
|
uid: param.uid,
|
|
type: param.type,
|
|
name: param.name,
|
|
value: param.value,
|
|
description: param.description,
|
|
enabled: param.enabled
|
|
};
|
|
});
|
|
};
|
|
|
|
const copyFileParams = (params = []) => {
|
|
return map(params, (param) => {
|
|
return {
|
|
uid: param.uid,
|
|
filePath: param.filePath,
|
|
contentType: param.contentType,
|
|
selected: param.selected
|
|
};
|
|
});
|
|
};
|
|
|
|
const copyExamples = (examples = []) => {
|
|
return map(examples, (example) => {
|
|
const copiedExample = {
|
|
uid: example.uid,
|
|
itemUid: example.itemUid,
|
|
name: example.name,
|
|
description: example.description,
|
|
type: example.type,
|
|
request: {
|
|
url: example.request.url,
|
|
method: example.request.method,
|
|
headers: copyHeaders(example.request.headers),
|
|
params: copyParams(example.request.params),
|
|
body: {
|
|
mode: example.request.body.mode,
|
|
json: example.request.body.json,
|
|
text: example.request.body.text,
|
|
xml: example.request.body.xml,
|
|
graphql: example.request.body.graphql,
|
|
sparql: example.request.body.sparql,
|
|
formUrlEncoded: copyFormUrlEncodedParams(example.request.body.formUrlEncoded),
|
|
multipartForm: copyMultipartFormParams(example.request.body.multipartForm),
|
|
file: copyFileParams(example.request.body.file),
|
|
grpc: example.request.body.grpc,
|
|
ws: example.request.body.ws
|
|
},
|
|
auth: example.request.auth
|
|
},
|
|
response: {
|
|
status: example.response.status,
|
|
statusText: example.response.statusText,
|
|
headers: copyHeaders(example.response.headers),
|
|
body: example.response.body
|
|
}
|
|
};
|
|
|
|
// Handle gRPC-specific fields if present
|
|
if (example.request.methodType) {
|
|
copiedExample.request.methodType = example.request.methodType;
|
|
}
|
|
if (example.request.protoPath) {
|
|
copiedExample.request.protoPath = example.request.protoPath;
|
|
}
|
|
|
|
return copiedExample;
|
|
});
|
|
};
|
|
|
|
const normalizeFilenameToBru = (filename) => {
|
|
if (!filename) return filename;
|
|
return filename.replace(/\.(yml|yaml)$/i, '.bru');
|
|
};
|
|
|
|
const copyItems = (sourceItems, destItems) => {
|
|
each(sourceItems, (si) => {
|
|
if (!isItemAFolder(si) && !isItemARequest(si) && si.type !== 'js') {
|
|
return;
|
|
}
|
|
|
|
// Skip transient requests
|
|
if (si.isTransient) {
|
|
return;
|
|
}
|
|
|
|
const isGrpcRequest = si.type === 'grpc-request';
|
|
|
|
const di = {
|
|
uid: si.uid,
|
|
type: si.type,
|
|
name: si.name,
|
|
filename: isItemARequest(si) ? normalizeFilenameToBru(si.filename) : si.filename,
|
|
seq: si.seq,
|
|
settings: si.settings,
|
|
tags: si.tags,
|
|
examples: copyExamples(si.examples || [])
|
|
};
|
|
|
|
if (si.request) {
|
|
di.request = {
|
|
url: si.request.url,
|
|
method: si.request.method,
|
|
headers: copyHeaders(si.request.headers),
|
|
params: copyParams(si.request.params),
|
|
body: {
|
|
mode: si.request.body.mode,
|
|
json: si.request.body.json,
|
|
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),
|
|
file: copyFileParams(si.request.body.file),
|
|
grpc: si.request.body.grpc,
|
|
ws: si.request.body.ws
|
|
},
|
|
script: si.request.script,
|
|
vars: si.request.vars,
|
|
assertions: si.request.assertions,
|
|
tests: si.request.tests,
|
|
docs: si.request.docs
|
|
};
|
|
|
|
if (isGrpcRequest) {
|
|
di.request.methodType = si.request.methodType;
|
|
di.request.protoPath = si.request.protoPath;
|
|
delete di.request.params;
|
|
}
|
|
|
|
// Handle auth object dynamically
|
|
di.request.auth = {
|
|
mode: get(si.request, 'auth.mode', 'none')
|
|
};
|
|
|
|
switch (di.request.auth.mode) {
|
|
case 'awsv4':
|
|
di.request.auth.awsv4 = {
|
|
accessKeyId: get(si.request, 'auth.awsv4.accessKeyId', ''),
|
|
secretAccessKey: get(si.request, 'auth.awsv4.secretAccessKey', ''),
|
|
sessionToken: get(si.request, 'auth.awsv4.sessionToken', ''),
|
|
service: get(si.request, 'auth.awsv4.service', ''),
|
|
region: get(si.request, 'auth.awsv4.region', ''),
|
|
profileName: get(si.request, 'auth.awsv4.profileName', '')
|
|
};
|
|
break;
|
|
case 'basic':
|
|
di.request.auth.basic = {
|
|
username: get(si.request, 'auth.basic.username', ''),
|
|
password: get(si.request, 'auth.basic.password', '')
|
|
};
|
|
break;
|
|
case 'bearer':
|
|
di.request.auth.bearer = {
|
|
token: get(si.request, 'auth.bearer.token', '')
|
|
};
|
|
break;
|
|
case 'digest':
|
|
di.request.auth.digest = {
|
|
username: get(si.request, 'auth.digest.username', ''),
|
|
password: get(si.request, 'auth.digest.password', '')
|
|
};
|
|
break;
|
|
case 'ntlm':
|
|
di.request.auth.ntlm = {
|
|
username: get(si.request, 'auth.ntlm.username', ''),
|
|
password: get(si.request, 'auth.ntlm.password', ''),
|
|
domain: get(si.request, 'auth.ntlm.domain', '')
|
|
};
|
|
break;
|
|
case 'oauth1':
|
|
di.request.auth.oauth1 = {
|
|
consumerKey: get(si.request, 'auth.oauth1.consumerKey', ''),
|
|
consumerSecret: get(si.request, 'auth.oauth1.consumerSecret', ''),
|
|
accessToken: get(si.request, 'auth.oauth1.accessToken', ''),
|
|
accessTokenSecret: get(si.request, 'auth.oauth1.accessTokenSecret', ''),
|
|
callbackUrl: get(si.request, 'auth.oauth1.callbackUrl', ''),
|
|
verifier: get(si.request, 'auth.oauth1.verifier', ''),
|
|
signatureMethod: get(si.request, 'auth.oauth1.signatureMethod', 'HMAC-SHA1'),
|
|
privateKey: get(si.request, 'auth.oauth1.privateKey', ''),
|
|
privateKeyType: get(si.request, 'auth.oauth1.privateKeyType', 'text'),
|
|
timestamp: get(si.request, 'auth.oauth1.timestamp', ''),
|
|
nonce: get(si.request, 'auth.oauth1.nonce', ''),
|
|
version: get(si.request, 'auth.oauth1.version', '1.0'),
|
|
realm: get(si.request, 'auth.oauth1.realm', ''),
|
|
placement: get(si.request, 'auth.oauth1.placement', 'header'),
|
|
includeBodyHash: get(si.request, 'auth.oauth1.includeBodyHash', false)
|
|
};
|
|
break;
|
|
case 'oauth2':
|
|
let grantType = get(si.request, 'auth.oauth2.grantType', '');
|
|
switch (grantType) {
|
|
case 'password':
|
|
di.request.auth.oauth2 = {
|
|
grantType: grantType,
|
|
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
|
refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),
|
|
username: get(si.request, 'auth.oauth2.username', ''),
|
|
password: get(si.request, 'auth.oauth2.password', ''),
|
|
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
|
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
|
scope: get(si.request, 'auth.oauth2.scope', ''),
|
|
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
|
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
|
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
|
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),
|
|
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
|
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
|
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
|
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})
|
|
};
|
|
break;
|
|
case 'authorization_code':
|
|
di.request.auth.oauth2 = {
|
|
grantType: grantType,
|
|
callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),
|
|
authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),
|
|
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
|
refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),
|
|
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
|
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
|
scope: get(si.request, 'auth.oauth2.scope', ''),
|
|
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
|
pkce: get(si.request, 'auth.oauth2.pkce', false),
|
|
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
|
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
|
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),
|
|
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
|
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
|
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
|
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})
|
|
};
|
|
break;
|
|
case 'implicit':
|
|
di.request.auth.oauth2 = {
|
|
grantType: grantType,
|
|
callbackUrl: get(si.request, 'auth.oauth2.callbackUrl', ''),
|
|
authorizationUrl: get(si.request, 'auth.oauth2.authorizationUrl', ''),
|
|
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
|
scope: get(si.request, 'auth.oauth2.scope', ''),
|
|
state: get(si.request, 'auth.oauth2.state', ''),
|
|
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
|
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
|
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', 'Bearer'),
|
|
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
|
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
|
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})
|
|
};
|
|
break;
|
|
case 'client_credentials':
|
|
di.request.auth.oauth2 = {
|
|
grantType: grantType,
|
|
accessTokenUrl: get(si.request, 'auth.oauth2.accessTokenUrl', ''),
|
|
refreshTokenUrl: get(si.request, 'auth.oauth2.refreshTokenUrl', ''),
|
|
clientId: get(si.request, 'auth.oauth2.clientId', ''),
|
|
clientSecret: get(si.request, 'auth.oauth2.clientSecret', ''),
|
|
scope: get(si.request, 'auth.oauth2.scope', ''),
|
|
credentialsPlacement: get(si.request, 'auth.oauth2.credentialsPlacement', 'body'),
|
|
credentialsId: get(si.request, 'auth.oauth2.credentialsId', 'credentials'),
|
|
tokenPlacement: get(si.request, 'auth.oauth2.tokenPlacement', 'header'),
|
|
tokenHeaderPrefix: get(si.request, 'auth.oauth2.tokenHeaderPrefix', ''),
|
|
tokenQueryKey: get(si.request, 'auth.oauth2.tokenQueryKey', ''),
|
|
autoFetchToken: get(si.request, 'auth.oauth2.autoFetchToken', true),
|
|
autoRefreshToken: get(si.request, 'auth.oauth2.autoRefreshToken', true),
|
|
additionalParameters: get(si.request, 'auth.oauth2.additionalParameters', {})
|
|
};
|
|
break;
|
|
}
|
|
break;
|
|
case 'apikey':
|
|
di.request.auth.apikey = {
|
|
key: get(si.request, 'auth.apikey.key', ''),
|
|
value: get(si.request, 'auth.apikey.value', ''),
|
|
placement: get(si.request, 'auth.apikey.placement', 'header')
|
|
};
|
|
break;
|
|
case 'wsse':
|
|
di.request.auth.wsse = {
|
|
username: get(si.request, 'auth.wsse.username', ''),
|
|
password: get(si.request, 'auth.wsse.password', '')
|
|
};
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (di.request.body.mode === 'json') {
|
|
di.request.body.json = replaceTabsWithSpaces(di.request.body.json);
|
|
}
|
|
|
|
if (di.request.body.mode === 'grpc') {
|
|
di.request.body.grpc = di.request.body.grpc.map(({ name, content }, index) => ({
|
|
name: name ? name : `message ${index + 1}`,
|
|
content: replaceTabsWithSpaces(content)
|
|
}));
|
|
}
|
|
|
|
if (di.request.body.mode === 'ws') {
|
|
di.request.body.ws = di.request.body.ws.map(({ name, content, type }, index) => ({
|
|
name: name ? name : `message ${index + 1}`,
|
|
type: type ?? 'json',
|
|
content: replaceTabsWithSpaces(content)
|
|
}));
|
|
}
|
|
}
|
|
|
|
if (si.type == 'folder' && si?.root) {
|
|
di.root = {
|
|
request: {}
|
|
};
|
|
|
|
let { request, meta, docs } = si?.root || {};
|
|
let { auth, headers, script = {}, vars = {}, tests } = request || {};
|
|
|
|
// folder level auth
|
|
if (auth?.mode) {
|
|
di.root.request.auth = auth;
|
|
}
|
|
|
|
// folder level headers
|
|
if (headers?.length) {
|
|
di.root.request.headers = headers;
|
|
}
|
|
// folder level script
|
|
if (Object.keys(script)?.length) {
|
|
di.root.request.script = {};
|
|
if (script?.req?.length) {
|
|
di.root.request.script.req = script?.req;
|
|
}
|
|
if (script?.res?.length) {
|
|
di.root.request.script.res = script?.res;
|
|
}
|
|
}
|
|
// folder level vars
|
|
if (Object.keys(vars)?.length) {
|
|
di.root.request.vars = {};
|
|
if (vars?.req?.length) {
|
|
di.root.request.vars.req = vars?.req;
|
|
}
|
|
if (vars?.res?.length) {
|
|
di.root.request.vars.res = vars?.res;
|
|
}
|
|
}
|
|
// folder level tests
|
|
if (tests?.length) {
|
|
di.root.request.tests = tests;
|
|
}
|
|
|
|
// folder level docs
|
|
if (docs?.length) {
|
|
di.root.docs = docs;
|
|
}
|
|
|
|
if (meta?.name) {
|
|
di.root.meta = {};
|
|
di.root.meta.name = meta?.name;
|
|
di.root.meta.seq = meta?.seq;
|
|
}
|
|
if (!Object.keys(di.root.request)?.length) {
|
|
delete di.root.request;
|
|
}
|
|
if (!Object.keys(di.root)?.length) {
|
|
delete di.root;
|
|
}
|
|
}
|
|
|
|
if (si.type === 'js') {
|
|
di.fileContent = si.raw;
|
|
}
|
|
|
|
destItems.push(di);
|
|
|
|
if (si.items && si.items.length) {
|
|
di.items = [];
|
|
copyItems(si.items, di.items);
|
|
}
|
|
});
|
|
};
|
|
|
|
const collectionToSave = {};
|
|
collectionToSave.name = collection.name;
|
|
collectionToSave.uid = collection.uid;
|
|
|
|
// todo: move this to the place where collection gets created
|
|
collectionToSave.version = '1';
|
|
collectionToSave.items = [];
|
|
collectionToSave.activeEnvironmentUid = collection.activeEnvironmentUid;
|
|
// Save environments without runtime metadata (ephemeral/persistedValue)
|
|
collectionToSave.environments = (collection.environments || []).map((env) => ({
|
|
...env,
|
|
variables: buildPersistedEnvVariables(env?.variables, { mode: 'save' })
|
|
}));
|
|
|
|
collectionToSave.root = {
|
|
request: {}
|
|
};
|
|
|
|
let { request, docs, meta } = collection?.root || {};
|
|
let { auth, headers, script = {}, vars = {}, tests } = request || {};
|
|
|
|
// collection level auth
|
|
if (auth?.mode) {
|
|
collectionToSave.root.request.auth = auth;
|
|
}
|
|
// collection level headers
|
|
if (headers?.length) {
|
|
collectionToSave.root.request.headers = headers;
|
|
}
|
|
// collection level script
|
|
if (Object.keys(script)?.length) {
|
|
collectionToSave.root.request.script = {};
|
|
if (script?.req?.length) {
|
|
collectionToSave.root.request.script.req = script?.req;
|
|
}
|
|
if (script?.res?.length) {
|
|
collectionToSave.root.request.script.res = script?.res;
|
|
}
|
|
}
|
|
// collection level vars
|
|
if (Object.keys(vars)?.length) {
|
|
collectionToSave.root.request.vars = {};
|
|
if (vars?.req?.length) {
|
|
collectionToSave.root.request.vars.req = vars?.req;
|
|
}
|
|
if (vars?.res?.length) {
|
|
collectionToSave.root.request.vars.res = vars?.res;
|
|
}
|
|
}
|
|
// collection level tests
|
|
if (tests?.length) {
|
|
collectionToSave.root.request.tests = tests;
|
|
}
|
|
// collection level docs
|
|
if (docs?.length) {
|
|
collectionToSave.root.docs = docs;
|
|
}
|
|
if (meta?.name) {
|
|
collectionToSave.root.meta = {};
|
|
collectionToSave.root.meta.name = meta?.name;
|
|
}
|
|
if (!Object.keys(collectionToSave.root.request)?.length) {
|
|
delete collectionToSave.root.request;
|
|
}
|
|
if (!Object.keys(collectionToSave.root)?.length) {
|
|
delete collectionToSave.root;
|
|
}
|
|
|
|
collectionToSave.brunoConfig = cloneDeep(collection?.brunoConfig);
|
|
|
|
// delete proxy password if present
|
|
if (collectionToSave?.brunoConfig?.proxy?.auth?.password) {
|
|
delete collectionToSave.brunoConfig.proxy.auth.password;
|
|
}
|
|
|
|
if (collectionToSave?.brunoConfig?.protobuf?.importPaths) {
|
|
collectionToSave.brunoConfig.protobuf.importPaths = collectionToSave.brunoConfig.protobuf.importPaths.map((importPath) => {
|
|
delete importPath.exists;
|
|
return importPath;
|
|
});
|
|
}
|
|
|
|
if (collectionToSave?.brunoConfig?.protobuf?.protoFiles) {
|
|
collectionToSave.brunoConfig.protobuf.protoFiles = collectionToSave.brunoConfig.protobuf.protoFiles.map((protoFile) => {
|
|
delete protoFile.exists;
|
|
return protoFile;
|
|
});
|
|
}
|
|
|
|
copyItems(collection.items, collectionToSave.items);
|
|
return collectionToSave;
|
|
};
|
|
|
|
export const transformRequestToSaveToFilesystem = (item) => {
|
|
const _item = item.draft ? item.draft : item;
|
|
|
|
// Transform examples to ensure status is a number
|
|
const transformExamples = (examples = []) => {
|
|
return map(examples, (example) => ({
|
|
...example,
|
|
response: example.response ? {
|
|
...example.response,
|
|
status: example.response.status !== undefined && example.response.status !== null
|
|
? Number(example.response.status)
|
|
: null
|
|
} : example.response
|
|
}));
|
|
};
|
|
|
|
const itemToSave = {
|
|
uid: _item.uid,
|
|
type: _item.type,
|
|
name: _item.name,
|
|
seq: _item.seq,
|
|
settings: _item.settings,
|
|
tags: _item.tags,
|
|
examples: transformExamples(_item.examples || []),
|
|
request: {
|
|
method: _item.request.method,
|
|
url: _item.request.url,
|
|
params: [],
|
|
headers: [],
|
|
auth: _item.request.auth,
|
|
body: _item.request.body,
|
|
script: _item.request.script,
|
|
vars: _item.request.vars,
|
|
assertions: _item.request.assertions,
|
|
tests: _item.request.tests,
|
|
docs: _item.request.docs
|
|
}
|
|
};
|
|
|
|
if (_item.type === 'grpc-request') {
|
|
itemToSave.request.methodType = _item.request.methodType;
|
|
itemToSave.request.protoPath = _item.request.protoPath;
|
|
delete itemToSave.request.params;
|
|
}
|
|
|
|
if (_item.type === 'ws-request') {
|
|
delete itemToSave.request.method;
|
|
delete itemToSave.request.methodType;
|
|
delete itemToSave.request.params;
|
|
}
|
|
|
|
// Only process params for non-gRPC requests
|
|
if (!['grpc-request', 'ws-request'].includes(_item.type)) {
|
|
each(_item.request.params, (param) => {
|
|
itemToSave.request.params.push({
|
|
uid: param.uid,
|
|
name: param.name,
|
|
value: param.value,
|
|
description: param.description,
|
|
annotations: param.annotations,
|
|
type: param.type,
|
|
enabled: param.enabled
|
|
});
|
|
});
|
|
}
|
|
|
|
each(_item.request.headers, (header) => {
|
|
itemToSave.request.headers.push({
|
|
uid: header.uid,
|
|
name: header.name,
|
|
value: header.value,
|
|
description: header.description,
|
|
annotations: header.annotations,
|
|
enabled: header.enabled
|
|
});
|
|
});
|
|
|
|
if (itemToSave.request.body.mode === 'json') {
|
|
itemToSave.request.body = {
|
|
...itemToSave.request.body,
|
|
json: replaceTabsWithSpaces(itemToSave.request.body.json)
|
|
};
|
|
}
|
|
|
|
if (itemToSave.request.body.mode === 'grpc') {
|
|
itemToSave.request.body = {
|
|
...itemToSave.request.body,
|
|
grpc: itemToSave.request.body.grpc.map(({ name, content }, index) => ({
|
|
name: name ? name : `message ${index + 1}`,
|
|
content: replaceTabsWithSpaces(content)
|
|
}))
|
|
};
|
|
}
|
|
|
|
if (itemToSave.request.body.mode === 'ws') {
|
|
itemToSave.request.body = {
|
|
...itemToSave.request.body,
|
|
ws: itemToSave.request.body.ws.map(({ name, content, type, selected }, index) => ({
|
|
name: name ? name : `message ${index + 1}`,
|
|
type,
|
|
content: replaceTabsWithSpaces(content),
|
|
selected: selected || false
|
|
}))
|
|
};
|
|
}
|
|
|
|
return itemToSave;
|
|
};
|
|
|
|
export const transformCollectionRootToSave = (collection) => {
|
|
const _collection = collection.draft?.root ? collection.draft.root : collection.root;
|
|
|
|
const collectionRootToSave = {
|
|
docs: _collection?.docs,
|
|
meta: _collection?.meta,
|
|
request: {
|
|
auth: _collection?.request?.auth,
|
|
headers: [],
|
|
script: _collection?.request?.script,
|
|
vars: _collection?.request?.vars,
|
|
tests: _collection?.request?.tests
|
|
}
|
|
};
|
|
|
|
each(_collection?.request?.headers, (header) => {
|
|
collectionRootToSave.request.headers.push({
|
|
uid: header.uid,
|
|
name: header.name,
|
|
value: header.value,
|
|
description: header.description,
|
|
annotations: header.annotations,
|
|
enabled: header.enabled
|
|
});
|
|
});
|
|
|
|
return collectionRootToSave;
|
|
};
|
|
|
|
export const transformFolderRootToSave = (folder) => {
|
|
const _folder = folder.draft ? folder.draft : folder.root;
|
|
const folderRootToSave = {
|
|
meta: {
|
|
name: folder.name,
|
|
seq: folder.seq
|
|
},
|
|
docs: _folder.docs,
|
|
request: {
|
|
auth: _folder?.request?.auth,
|
|
headers: [],
|
|
script: _folder?.request?.script,
|
|
vars: _folder?.request?.vars,
|
|
tests: _folder?.request?.tests
|
|
}
|
|
};
|
|
|
|
each(_folder.request.headers, (header) => {
|
|
folderRootToSave.request.headers.push({
|
|
uid: header.uid,
|
|
name: header.name,
|
|
value: header.value,
|
|
description: header.description,
|
|
annotations: header.annotations,
|
|
enabled: header.enabled
|
|
});
|
|
});
|
|
|
|
return folderRootToSave;
|
|
};
|
|
|
|
// todo: optimize this
|
|
export const deleteItemInCollection = (itemUid, collection) => {
|
|
collection.items = filter(collection.items, (i) => i.uid !== itemUid);
|
|
|
|
let flattenedItems = flattenItems(collection.items);
|
|
each(flattenedItems, (i) => {
|
|
if (i.items && i.items.length) {
|
|
i.items = filter(i.items, (i) => i.uid !== itemUid);
|
|
}
|
|
});
|
|
};
|
|
|
|
export const deleteItemInCollectionByPathname = (pathname, collection) => {
|
|
collection.items = filter(collection.items, (i) => i.pathname !== pathname);
|
|
|
|
let flattenedItems = flattenItems(collection.items);
|
|
each(flattenedItems, (i) => {
|
|
if (i.items && i.items.length) {
|
|
i.items = filter(i.items, (i) => i.pathname !== pathname);
|
|
}
|
|
});
|
|
};
|
|
|
|
export const isItemARequest = (item) => {
|
|
return item.hasOwnProperty('request') && ['http-request', 'graphql-request', 'grpc-request', 'ws-request'].includes(item.type) && !item.items;
|
|
};
|
|
|
|
export const isItemAFolder = (item) => {
|
|
return !item.hasOwnProperty('request') && item.type === 'folder';
|
|
};
|
|
|
|
/**
|
|
* Orders a list of collection items exactly the way the Sidebar tree renders them:
|
|
* folders first (via `sortByNameThenSequence`), then requests ordered by `seq`. The
|
|
* same ordering is applied recursively to every nested folder so an exported/serialized
|
|
* tree matches the sidebar at all depths.
|
|
*
|
|
* Items that are neither folders nor requests (e.g. `js` script files) are excluded,
|
|
* mirroring the sidebar, which only renders folders and requests. Transient items are
|
|
* excluded too.
|
|
*/
|
|
export const sortItemsBySidebarOrder = (items = []) => {
|
|
const folderItems = sortByNameThenSequence(filter(items, (i) => isItemAFolder(i) && !i.isTransient));
|
|
const requestItems = filter(items, (i) => isItemARequest(i) && !i.isTransient).sort((a, b) => a.seq - b.seq);
|
|
|
|
return [...folderItems, ...requestItems].map((item) =>
|
|
Array.isArray(item.items) ? { ...item, items: sortItemsBySidebarOrder(item.items) } : item
|
|
);
|
|
};
|
|
|
|
export const humanizeRequestBodyMode = (mode) => {
|
|
let label = 'No Body';
|
|
switch (mode) {
|
|
case 'json': {
|
|
label = 'JSON';
|
|
break;
|
|
}
|
|
case 'text': {
|
|
label = 'TEXT';
|
|
break;
|
|
}
|
|
case 'xml': {
|
|
label = 'XML';
|
|
break;
|
|
}
|
|
case 'sparql': {
|
|
label = 'SPARQL';
|
|
break;
|
|
}
|
|
case 'file': {
|
|
label = 'File / Binary';
|
|
break;
|
|
}
|
|
case 'formUrlEncoded': {
|
|
label = 'Form URL Encoded';
|
|
break;
|
|
}
|
|
case 'multipartForm': {
|
|
label = 'Multipart Form';
|
|
break;
|
|
}
|
|
}
|
|
|
|
return label;
|
|
};
|
|
|
|
export const humanizeRequestAuthMode = (mode) => {
|
|
let label = 'No Auth';
|
|
switch (mode) {
|
|
case 'inherit': {
|
|
label = 'Inherit';
|
|
break;
|
|
}
|
|
case 'awsv4': {
|
|
label = 'AWS Sig V4';
|
|
break;
|
|
}
|
|
case 'basic': {
|
|
label = 'Basic Auth';
|
|
break;
|
|
}
|
|
case 'bearer': {
|
|
label = 'Bearer Token';
|
|
break;
|
|
}
|
|
case 'digest': {
|
|
label = 'Digest Auth';
|
|
break;
|
|
}
|
|
case 'ntlm': {
|
|
label = 'NTLM';
|
|
break;
|
|
}
|
|
case 'oauth1': {
|
|
label = 'OAuth 1.0';
|
|
break;
|
|
}
|
|
case 'oauth2': {
|
|
label = 'OAuth 2.0';
|
|
break;
|
|
}
|
|
case 'wsse': {
|
|
label = 'WSSE Auth';
|
|
break;
|
|
}
|
|
case 'apikey': {
|
|
label = 'API Key';
|
|
break;
|
|
}
|
|
}
|
|
|
|
return label;
|
|
};
|
|
|
|
export const humanizeRequestAPIKeyPlacement = (placement) => {
|
|
let label = 'Header';
|
|
switch (placement) {
|
|
case 'header': {
|
|
label = 'Header';
|
|
break;
|
|
}
|
|
case 'queryparams': {
|
|
label = 'Query Params';
|
|
break;
|
|
}
|
|
}
|
|
|
|
return label;
|
|
};
|
|
|
|
export const humanizeGrantType = (mode) => {
|
|
if (!mode || typeof mode !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
switch (mode) {
|
|
case 'password':
|
|
return 'Password Credentials';
|
|
case 'authorization_code':
|
|
return 'Authorization Code';
|
|
case 'client_credentials':
|
|
return 'Client Credentials';
|
|
case 'implicit':
|
|
return 'Implicit';
|
|
default:
|
|
return mode;
|
|
}
|
|
};
|
|
|
|
export const refreshUidsInItem = (item) => {
|
|
item.uid = uuid();
|
|
|
|
each(get(item, 'request.headers'), (header) => (header.uid = uuid()));
|
|
each(get(item, 'request.params'), (param) => (param.uid = uuid()));
|
|
each(get(item, 'request.body.multipartForm'), (param) => (param.uid = uuid()));
|
|
each(get(item, 'request.body.formUrlEncoded'), (param) => (param.uid = uuid()));
|
|
each(get(item, 'request.body.file'), (param) => (param.uid = uuid()));
|
|
each(get(item, 'request.body.ws'), (msg) => (msg.uid = uuid()));
|
|
each(get(item, 'request.assertions'), (assertion) => (assertion.uid = uuid()));
|
|
|
|
return item;
|
|
};
|
|
|
|
export const deleteUidsInItem = (item) => {
|
|
delete item.uid;
|
|
const params = get(item, 'request.params', []);
|
|
const headers = get(item, 'request.headers', []);
|
|
const bodyFormUrlEncoded = get(item, 'request.body.formUrlEncoded', []);
|
|
const bodyMultipartForm = get(item, 'request.body.multipartForm', []);
|
|
const file = get(item, 'request.body.file', []);
|
|
const assertions = get(item, 'request.assertions', []);
|
|
|
|
params.forEach((param) => delete param.uid);
|
|
headers.forEach((header) => delete header.uid);
|
|
bodyFormUrlEncoded.forEach((param) => delete param.uid);
|
|
bodyMultipartForm.forEach((param) => delete param.uid);
|
|
file.forEach((param) => delete param.uid);
|
|
assertions.forEach((assertion) => delete assertion.uid);
|
|
|
|
return item;
|
|
};
|
|
|
|
export const areItemsTheSameExceptSeqUpdate = (_item1, _item2) => {
|
|
let item1 = cloneDeep(_item1);
|
|
let item2 = cloneDeep(_item2);
|
|
|
|
// remove seq from both items
|
|
delete item1.seq;
|
|
delete item2.seq;
|
|
|
|
// remove draft from both items
|
|
delete item1.draft;
|
|
delete item2.draft;
|
|
|
|
// get projection of both items
|
|
item1 = transformRequestToSaveToFilesystem(item1);
|
|
item2 = transformRequestToSaveToFilesystem(item2);
|
|
|
|
// delete uids from both items
|
|
deleteUidsInItem(item1);
|
|
deleteUidsInItem(item2);
|
|
|
|
return isEqual(item1, item2);
|
|
};
|
|
|
|
/**
|
|
* Check if a request has actual changes (excluding examples)
|
|
* This function compares the request data between the original item and its draft,
|
|
* but excludes examples from the comparison to determine if the save dot should be shown
|
|
*/
|
|
export const hasRequestChanges = (item) => {
|
|
if (!item || !item.draft) {
|
|
return false;
|
|
}
|
|
|
|
// Create copies of the item and draft without examples for comparison
|
|
const originalItem = cloneDeep(item);
|
|
const draftItem = cloneDeep(item.draft);
|
|
|
|
// Remove examples from both items for comparison
|
|
delete originalItem.examples;
|
|
delete originalItem.draft;
|
|
delete draftItem.examples;
|
|
delete draftItem.draft;
|
|
|
|
return !isEqual(originalItem, draftItem);
|
|
};
|
|
|
|
/**
|
|
* Check if a specific example has unsaved changes
|
|
* This function compares the example data between the original item and its draft
|
|
*/
|
|
export const hasExampleChanges = (_item, exampleUid) => {
|
|
if (!_item || !_item.draft || !exampleUid) {
|
|
return false;
|
|
}
|
|
|
|
const item = cloneDeep(_item);
|
|
deleteUidsInItem(item);
|
|
|
|
// Get the original example from the saved item
|
|
const originalExample = item.examples?.find((ex) => ex.uid === exampleUid);
|
|
if (!originalExample) {
|
|
return false;
|
|
}
|
|
|
|
// Get the draft example from the draft item
|
|
const draftExample = item.draft.examples?.find((ex) => ex.uid === exampleUid);
|
|
if (!draftExample) {
|
|
return false;
|
|
}
|
|
|
|
// Compare the examples (excluding any internal metadata)
|
|
return !isEqual(originalExample, draftExample);
|
|
};
|
|
|
|
export const getDefaultRequestPaneTab = (item) => {
|
|
if (item.type === 'http-request') {
|
|
// If no params are enabled and body mode is set, default to 'body' tab
|
|
// This provides better UX for POST/PUT requests with a body
|
|
const request = item.draft?.request || item.request;
|
|
const params = request?.params || [];
|
|
const bodyMode = request?.body?.mode;
|
|
const hasEnabledParams = params.some((p) => p.enabled);
|
|
|
|
if (!hasEnabledParams && bodyMode && bodyMode !== 'none') {
|
|
return 'body';
|
|
}
|
|
return 'params';
|
|
}
|
|
|
|
if (item.type === 'graphql-request') {
|
|
return 'query';
|
|
}
|
|
|
|
if (['ws-request', 'grpc-request'].includes(item.type)) {
|
|
return 'body';
|
|
}
|
|
};
|
|
|
|
export const getGlobalEnvironmentVariables = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
|
|
let variables = {};
|
|
const environment = globalEnvironments?.find((env) => env?.uid === activeGlobalEnvironmentUid);
|
|
if (environment) {
|
|
each(environment.variables, (variable) => {
|
|
if (variable.name && variable.enabled) {
|
|
variables[variable.name] = variable.value;
|
|
}
|
|
});
|
|
}
|
|
return variables;
|
|
};
|
|
|
|
export const getGlobalEnvironmentVariablesMasked = ({ globalEnvironments, activeGlobalEnvironmentUid }) => {
|
|
const environment = globalEnvironments?.find((env) => env?.uid === activeGlobalEnvironmentUid);
|
|
|
|
if (environment && Array.isArray(environment.variables)) {
|
|
return environment.variables
|
|
.filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)
|
|
.map((variable) => variable.name);
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
export const getEnvironmentVariables = (collection) => {
|
|
let variables = {};
|
|
if (collection) {
|
|
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
|
if (environment) {
|
|
each(environment.variables, (variable) => {
|
|
if (variable.name && variable.value && variable.enabled) {
|
|
variables[variable.name] = variable.value;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
return variables;
|
|
};
|
|
|
|
export const getEnvironmentVariablesMasked = (collection) => {
|
|
// Return an empty array if the collection is invalid or not provided
|
|
if (!collection || !collection.activeEnvironmentUid) {
|
|
return [];
|
|
}
|
|
|
|
// Find the active environment in the collection
|
|
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
|
if (!environment || !environment.variables) {
|
|
return [];
|
|
}
|
|
|
|
// Filter the environment variables to get only the masked (secret) ones
|
|
return environment.variables
|
|
.filter((variable) => variable.name && variable.value && variable.enabled && variable.secret)
|
|
.map((variable) => variable.name);
|
|
};
|
|
|
|
const getPathParams = (item) => {
|
|
let pathParams = {};
|
|
if (item && item.request && item.request.params) {
|
|
item.request.params.forEach((param) => {
|
|
if (param.type === 'path' && param.name && param.value) {
|
|
pathParams[param.name] = param.value;
|
|
}
|
|
});
|
|
}
|
|
return pathParams;
|
|
};
|
|
|
|
export const getTotalRequestCountInCollection = (collection) => {
|
|
let count = 0;
|
|
each(collection.items, (item) => {
|
|
if (isItemARequest(item) && !item.isTransient) {
|
|
count++;
|
|
} else if (isItemAFolder(item)) {
|
|
count += getTotalRequestCountInCollection(item);
|
|
}
|
|
});
|
|
|
|
return count;
|
|
};
|
|
|
|
export const getAllVariables = (collection, item) => {
|
|
if (!collection) return {};
|
|
const envVariables = getEnvironmentVariables(collection);
|
|
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
|
let { collectionVariables, folderVariables, requestVariables } = mergeVars(collection, requestTreePath);
|
|
const pathParams = getPathParams(item);
|
|
const { globalEnvironmentVariables = {} } = collection;
|
|
|
|
const { processEnvVariables = {}, runtimeVariables = {}, promptVariables = {}, workspaceProcessEnvVariables = {} } = collection;
|
|
|
|
// Merge workspace and collection processEnvVariables (collection takes priority)
|
|
const mergedProcessEnvVariables = {
|
|
...workspaceProcessEnvVariables,
|
|
...processEnvVariables
|
|
};
|
|
|
|
const mergedVariables = {
|
|
...folderVariables,
|
|
...requestVariables,
|
|
...runtimeVariables,
|
|
...promptVariables
|
|
};
|
|
|
|
const mergedVariablesGlobal = {
|
|
...collectionVariables,
|
|
...envVariables,
|
|
...folderVariables,
|
|
...requestVariables,
|
|
...runtimeVariables,
|
|
...promptVariables
|
|
};
|
|
|
|
const maskedEnvVariables = getEnvironmentVariablesMasked(collection) || [];
|
|
const maskedGlobalEnvVariables = collection?.globalEnvSecrets || [];
|
|
|
|
const filteredMaskedEnvVariables = maskedEnvVariables.filter((key) => !(key in mergedVariables));
|
|
const filteredMaskedGlobalEnvVariables = maskedGlobalEnvVariables.filter((key) => !(key in mergedVariablesGlobal));
|
|
|
|
const uniqueMaskedVariables = [...new Set([...filteredMaskedEnvVariables, ...filteredMaskedGlobalEnvVariables])];
|
|
|
|
const oauth2CredentialVariables = getFormattedCollectionOauth2Credentials({ oauth2Credentials: collection?.oauth2Credentials });
|
|
|
|
return {
|
|
...globalEnvironmentVariables,
|
|
...collectionVariables,
|
|
...envVariables,
|
|
...folderVariables,
|
|
...requestVariables,
|
|
...oauth2CredentialVariables,
|
|
...runtimeVariables,
|
|
...promptVariables,
|
|
pathParams: {
|
|
...pathParams
|
|
},
|
|
maskedEnvVariables: uniqueMaskedVariables,
|
|
process: {
|
|
env: {
|
|
...mergedProcessEnvVariables
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
// Merge headers from collection, folders, and request
|
|
export const mergeHeaders = (collection, request, requestTreePath, options = {}) => {
|
|
const { includeDisabledHeaders = false } = options;
|
|
let headers = new Map();
|
|
let disabledHeaders = new Map();
|
|
|
|
// Add collection headers first
|
|
const collectionHeaders = collection?.draft?.root ? get(collection, 'draft.root.request.headers', []) : get(collection, 'root.request.headers', []);
|
|
collectionHeaders.forEach((header) => {
|
|
if (header.enabled) {
|
|
headers.set(header.name, header);
|
|
} else if (header.name?.length > 0) {
|
|
disabledHeaders.set(header.name, header);
|
|
}
|
|
});
|
|
|
|
// Add folder headers next, traversing from root to leaf
|
|
if (requestTreePath && requestTreePath.length > 0) {
|
|
for (let i of requestTreePath) {
|
|
if (i.type === 'folder') {
|
|
const folderHeaders = i?.draft ? get(i, 'draft.request.headers', []) : get(i, 'root.request.headers', []);
|
|
folderHeaders.forEach((header) => {
|
|
if (header.enabled) {
|
|
headers.set(header.name, header);
|
|
} else if (header.name?.length > 0) {
|
|
disabledHeaders.set(header.name, header);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add request headers last (they take precedence)
|
|
const requestHeaders = request.headers || [];
|
|
requestHeaders.forEach((header) => {
|
|
if (header.enabled) {
|
|
headers.set(header.name, header);
|
|
} else if (header.name?.length > 0) {
|
|
disabledHeaders.set(header.name, header);
|
|
}
|
|
});
|
|
|
|
// Convert Map back to array
|
|
return [
|
|
...Array.from(headers.values()),
|
|
...(includeDisabledHeaders ? Array.from(disabledHeaders.values()) : [])
|
|
];
|
|
};
|
|
|
|
export const maskInputValue = (value) => {
|
|
if (!value || typeof value !== 'string') {
|
|
return '';
|
|
}
|
|
|
|
return value
|
|
.split('')
|
|
.map(() => '*')
|
|
.join('');
|
|
};
|
|
|
|
export const getTreePathFromCollectionToItem = (collection, _item) => {
|
|
let path = [];
|
|
let item = findItemInCollection(collection, _item?.uid);
|
|
while (item) {
|
|
path.unshift(item);
|
|
item = findParentItemInCollection(collection, item?.uid);
|
|
}
|
|
return path;
|
|
};
|
|
|
|
const mergeVars = (collection, requestTreePath = []) => {
|
|
let collectionVariables = {};
|
|
let folderVariables = {};
|
|
let requestVariables = {};
|
|
const collectionRoot = collection?.draft?.root || collection?.root || {};
|
|
let collectionRequestVars = get(collectionRoot, 'request.vars.req', []);
|
|
collectionRequestVars.forEach((_var) => {
|
|
if (_var.enabled) {
|
|
collectionVariables[_var.name] = _var.value;
|
|
}
|
|
});
|
|
for (let i of requestTreePath) {
|
|
if (!i) {
|
|
continue;
|
|
}
|
|
|
|
if (i.type === 'folder') {
|
|
// Check draft first, then fall back to root
|
|
const folderRoot = i.draft || i.root;
|
|
let vars = get(folderRoot, 'request.vars.req', []);
|
|
vars.forEach((_var) => {
|
|
if (_var.enabled) {
|
|
folderVariables[_var.name] = _var.value;
|
|
}
|
|
});
|
|
} else {
|
|
let vars = i.draft ? get(i, 'draft.request.vars.req', []) : get(i, 'request.vars.req', []);
|
|
vars.forEach((_var) => {
|
|
if (_var.enabled) {
|
|
requestVariables[_var.name] = _var.value;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
collectionVariables,
|
|
folderVariables,
|
|
requestVariables
|
|
};
|
|
};
|
|
|
|
export const getEnvVars = (environment = {}) => {
|
|
const variables = environment.variables;
|
|
if (!variables || !variables.length) {
|
|
return {
|
|
__name__: environment.name
|
|
};
|
|
}
|
|
|
|
const envVars = {};
|
|
each(variables, (variable) => {
|
|
if (variable.enabled) {
|
|
envVars[variable.name] = variable.value;
|
|
}
|
|
});
|
|
|
|
return {
|
|
...envVars,
|
|
__name__: environment.name
|
|
};
|
|
};
|
|
|
|
export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] }) => {
|
|
let credentialsVariables = {};
|
|
oauth2Credentials.forEach(({ credentialsId, credentials }) => {
|
|
if (credentials) {
|
|
Object.entries(credentials).forEach(([key, value]) => {
|
|
credentialsVariables[`$oauth2.${credentialsId}.${key}`] = value;
|
|
});
|
|
}
|
|
});
|
|
return credentialsVariables;
|
|
};
|
|
|
|
// item sequence utils - START
|
|
|
|
export const resetSequencesInFolder = (folderItems) => {
|
|
const items = folderItems;
|
|
const sortedItems = sortByNameThenSequence(items);
|
|
return sortedItems.map((item, index) => {
|
|
item.seq = index + 1;
|
|
return item;
|
|
});
|
|
};
|
|
|
|
export const isItemBetweenSequences = (itemSequence, sourceItemSequence, targetItemSequence) => {
|
|
if (targetItemSequence > sourceItemSequence) {
|
|
return itemSequence > sourceItemSequence && itemSequence < targetItemSequence;
|
|
}
|
|
return itemSequence < sourceItemSequence && itemSequence >= targetItemSequence;
|
|
};
|
|
|
|
export const calculateNewSequence = (isDraggedItem, targetSequence, draggedSequence) => {
|
|
if (!isDraggedItem) {
|
|
return null;
|
|
}
|
|
return targetSequence > draggedSequence ? targetSequence - 1 : targetSequence;
|
|
};
|
|
|
|
export const getReorderedItemsInTargetDirectory = ({ items, targetItemUid, draggedItemUid }) => {
|
|
const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));
|
|
const targetItem = findItem(itemsWithFixedSequences, targetItemUid);
|
|
const draggedItem = findItem(itemsWithFixedSequences, draggedItemUid);
|
|
const targetSequence = targetItem?.seq;
|
|
const draggedSequence = draggedItem?.seq;
|
|
itemsWithFixedSequences?.forEach((item) => {
|
|
const isDraggedItem = item?.uid === draggedItemUid;
|
|
const isBetween = isItemBetweenSequences(item?.seq, draggedSequence, targetSequence);
|
|
if (isBetween) {
|
|
item.seq += targetSequence > draggedSequence ? -1 : 1;
|
|
}
|
|
const newSequence = calculateNewSequence(isDraggedItem, targetSequence, draggedSequence);
|
|
if (newSequence !== null) {
|
|
item.seq = newSequence;
|
|
}
|
|
});
|
|
// only return items that have been reordered
|
|
return itemsWithFixedSequences.filter((item) =>
|
|
items?.find((originalItem) => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
|
);
|
|
};
|
|
|
|
export const getReorderedItemsInSourceDirectory = ({ items }) => {
|
|
const itemsWithFixedSequences = resetSequencesInFolder(cloneDeep(items));
|
|
return itemsWithFixedSequences.filter((item) =>
|
|
items?.find((originalItem) => originalItem?.uid === item?.uid)?.seq !== item?.seq
|
|
);
|
|
};
|
|
|
|
export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType, collectionPathname }) => {
|
|
const { pathname: targetItemPathname } = targetItem;
|
|
const { filename: draggedItemFilename } = draggedItem;
|
|
const targetItemDirname = path.dirname(targetItemPathname);
|
|
const isTargetTheCollection = targetItemPathname === collectionPathname;
|
|
const isTargetItemAFolder = isItemAFolder(targetItem);
|
|
|
|
if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) {
|
|
return path.join(targetItemPathname, draggedItemFilename);
|
|
} else if (dropType === 'adjacent') {
|
|
return path.join(targetItemDirname, draggedItemFilename);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// item sequence utils - END
|
|
|
|
export const getUniqueTagsFromItems = (items = []) => {
|
|
const allTags = new Set();
|
|
const getTags = (items) => {
|
|
items.forEach((item) => {
|
|
if (isItemARequest(item)) {
|
|
const tags = item.draft ? get(item, 'draft.tags', []) : get(item, 'tags', []);
|
|
tags.forEach((tag) => allTags.add(tag));
|
|
}
|
|
if (item.items) {
|
|
getTags(item.items);
|
|
}
|
|
});
|
|
};
|
|
getTags(items);
|
|
return Array.from(allTags).sort();
|
|
};
|
|
|
|
export const getRequestItemsForCollectionRun = ({ recursive, items = [], tags }) => {
|
|
let requestItems = [];
|
|
|
|
if (recursive) {
|
|
requestItems = flattenItems(items);
|
|
} else {
|
|
each(items, (item) => {
|
|
if (item.request) {
|
|
requestItems.push(item);
|
|
}
|
|
});
|
|
}
|
|
|
|
const requestTypes = ['http-request', 'graphql-request'];
|
|
requestItems = requestItems.filter((request) => requestTypes.includes(request.type) && !request.isTransient);
|
|
|
|
if (tags && tags.include && tags.exclude) {
|
|
const includeTags = tags.include ? tags.include : [];
|
|
const excludeTags = tags.exclude ? tags.exclude : [];
|
|
requestItems = requestItems.filter(({ tags: requestTags = [], draft }) => {
|
|
requestTags = draft?.tags || requestTags || [];
|
|
return isRequestTagsIncluded(requestTags, includeTags, excludeTags);
|
|
});
|
|
}
|
|
|
|
return requestItems;
|
|
};
|
|
|
|
export const getPropertyFromDraftOrRequest = (item, propertyKey, defaultValue = null) => {
|
|
return item.draft ? get(item, `draft.${propertyKey}`, defaultValue) : get(item, propertyKey, defaultValue);
|
|
};
|
|
|
|
export const transformExampleToDraft = (example, newExample) => {
|
|
const exampleToDraft = cloneDeep(example);
|
|
|
|
if (newExample.name) {
|
|
exampleToDraft.name = newExample.name;
|
|
}
|
|
if (newExample.description) {
|
|
exampleToDraft.description = newExample.description;
|
|
}
|
|
if (newExample.status) {
|
|
exampleToDraft.response.status = Number(newExample.status);
|
|
}
|
|
if (newExample.statusText) {
|
|
exampleToDraft.response.statusText = newExample.statusText;
|
|
}
|
|
if (newExample.headers && newExample.headers.length) {
|
|
exampleToDraft.response.headers = newExample.headers.map((header) => ({
|
|
uid: uuid(),
|
|
name: String(header.name),
|
|
value: String(header.value),
|
|
description: String(header.description),
|
|
enabled: header.enabled
|
|
}));
|
|
}
|
|
if (newExample.body) {
|
|
exampleToDraft.response.body = newExample.body;
|
|
}
|
|
|
|
return exampleToDraft;
|
|
};
|
|
|
|
/**
|
|
* Generate an initial name for a new response example
|
|
* @param {Object} item - The request item that will contain the example
|
|
* @returns {string} - The suggested name for the new example
|
|
*/
|
|
export const getInitialExampleName = (item) => {
|
|
const baseName = 'example';
|
|
const existingExamples = item.draft?.examples || item.examples || [];
|
|
const existingNames = new Set(existingExamples.map((example) => example.name || '').filter(Boolean));
|
|
|
|
if (!existingNames.has(baseName)) {
|
|
return baseName;
|
|
}
|
|
|
|
let counter = 1;
|
|
while (true) {
|
|
const candidateName = `${baseName} (${counter})`;
|
|
if (!existingNames.has(candidateName)) {
|
|
return candidateName;
|
|
}
|
|
counter++;
|
|
}
|
|
};
|
|
|
|
// Get the scope and raw value of a variable by checking all scopes in priority order
|
|
export const getVariableScope = (variableName, collection, item) => {
|
|
if (!variableName || !collection) {
|
|
return null;
|
|
}
|
|
|
|
// 1. Check Request Variables (highest priority)
|
|
if (item) {
|
|
const requestVars = item.draft ? get(item, 'draft.request.vars.req', []) : get(item, 'request.vars.req', []);
|
|
const requestVar = requestVars.find((v) => v.name === variableName && v.enabled);
|
|
if (requestVar) {
|
|
return {
|
|
type: 'request',
|
|
value: requestVar.value,
|
|
data: { item, variable: requestVar }
|
|
};
|
|
}
|
|
}
|
|
|
|
// 2. Check Folder Variables
|
|
const requestTreePath = getTreePathFromCollectionToItem(collection, item);
|
|
for (let i = requestTreePath.length - 1; i >= 0; i--) {
|
|
const pathItem = requestTreePath[i];
|
|
if (!pathItem) {
|
|
continue;
|
|
}
|
|
|
|
if (pathItem.type === 'folder') {
|
|
// Check draft first, then fall back to root
|
|
const folderRoot = pathItem.draft || pathItem.root;
|
|
const folderVars = get(folderRoot, 'request.vars.req', []);
|
|
const folderVar = folderVars.find((v) => v.name === variableName && v.enabled);
|
|
if (folderVar) {
|
|
return {
|
|
type: 'folder',
|
|
value: folderVar.value,
|
|
data: { folder: pathItem, variable: folderVar }
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Check Environment Variables
|
|
if (collection.activeEnvironmentUid) {
|
|
const environment = findEnvironmentInCollection(collection, collection.activeEnvironmentUid);
|
|
if (environment && environment.variables) {
|
|
const envVar = environment.variables.find((v) => v.name === variableName && v.enabled);
|
|
if (envVar) {
|
|
return {
|
|
type: 'environment',
|
|
value: envVar.value,
|
|
data: { environment, variable: envVar }
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Check Collection Variables
|
|
// Check draft first, then fall back to root
|
|
const collectionRoot = (collection.draft && collection.draft.root) || collection.root || {};
|
|
const collectionVars = get(collectionRoot, 'request.vars.req', []);
|
|
const collectionVar = collectionVars.find((v) => v.name === variableName && v.enabled);
|
|
if (collectionVar) {
|
|
return {
|
|
type: 'collection',
|
|
value: collectionVar.value,
|
|
data: { collection, variable: collectionVar }
|
|
};
|
|
}
|
|
|
|
// 5. Check Global Environment Variables
|
|
const { globalEnvironmentVariables = {} } = collection;
|
|
if (globalEnvironmentVariables && globalEnvironmentVariables[variableName]) {
|
|
return {
|
|
type: 'global',
|
|
value: globalEnvironmentVariables[variableName],
|
|
data: { variableName, value: globalEnvironmentVariables[variableName] }
|
|
};
|
|
}
|
|
|
|
// 6. Check Runtime Variables (set during request execution via scripts)
|
|
const { runtimeVariables = {} } = collection;
|
|
if (runtimeVariables && runtimeVariables[variableName]) {
|
|
return {
|
|
type: 'runtime',
|
|
value: runtimeVariables[variableName],
|
|
data: { variableName, value: runtimeVariables[variableName], readonly: true }
|
|
};
|
|
}
|
|
|
|
// Process.env variables are not checked here
|
|
|
|
return null;
|
|
};
|
|
|
|
// Check if a variable is marked as secret
|
|
export const isVariableSecret = (scopeInfo) => {
|
|
if (!scopeInfo) {
|
|
return false;
|
|
}
|
|
|
|
// Only environment variables can be marked as secret
|
|
if (scopeInfo.type === 'environment') {
|
|
return !!scopeInfo.data.variable?.secret;
|
|
}
|
|
|
|
// Global variables are not checked here
|
|
if (scopeInfo.type === 'global') {
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
/**
|
|
* Generate a unique request name by checking existing filenames in the collection and filesystem
|
|
* @param {Object} collection - The collection object
|
|
* @param {string} baseName - The base name (default: 'Untitled')
|
|
* @param {string} itemUid - The parent item UID (null for root level, folder UID for folder level)
|
|
* @returns {Promise<string>} - A unique request name (Untitled, Untitled1, Untitled2, etc.)
|
|
*/
|
|
export const generateUniqueRequestName = async (collection, baseName = 'Untitled', itemUid = null) => {
|
|
if (!collection) {
|
|
return baseName;
|
|
}
|
|
|
|
const trim = require('lodash/trim');
|
|
const parentItem = itemUid ? findItemInCollection(collection, itemUid) : null;
|
|
const parentItems = parentItem ? (parentItem.items || []) : (collection.items || []);
|
|
const baseNamePattern = new RegExp(`^${baseName}(\\d+)?$`);
|
|
// Support .bru, .yml, and .yaml file extensions
|
|
const requestExtensions = /\.(bru|yml|yaml)$/i;
|
|
const matchingItems = parentItems
|
|
.filter((item) => {
|
|
if (item.type === 'folder') return false;
|
|
|
|
const filename = trim(item.filename);
|
|
if (!requestExtensions.test(filename)) return false;
|
|
|
|
const filenameWithoutExt = filename.replace(requestExtensions, '');
|
|
return baseNamePattern.test(filenameWithoutExt);
|
|
})
|
|
.map((item) => {
|
|
const filenameWithoutExt = trim(item.filename).replace(requestExtensions, '');
|
|
const match = filenameWithoutExt.match(baseNamePattern);
|
|
|
|
if (!match) return null;
|
|
|
|
const number = match[1] ? parseInt(match[1], 10) : 0;
|
|
return { name: filenameWithoutExt, number: isNaN(number) ? null : number };
|
|
})
|
|
.filter((item) => item !== null && item.number !== null);
|
|
|
|
if (matchingItems.length === 0) {
|
|
return baseName;
|
|
}
|
|
|
|
const sortedMatches = matchingItems.sort((a, b) => a.number - b.number);
|
|
const lastElement = sortedMatches[sortedMatches.length - 1];
|
|
const nextNumber = lastElement.number + 1;
|
|
|
|
return `${baseName}${nextNumber}`;
|
|
};
|
|
|
|
export const isItemTransientRequest = (item) => {
|
|
return isItemARequest(item) && item?.isTransient;
|
|
};
|
|
|
|
/**
|
|
* Recursively filter out transient items from a collection's items array.
|
|
* Used for collection runner, exports, and other operations that shouldn't include transient requests.
|
|
* @param {Array} items - The items array to filter
|
|
* @returns {Array} A new array with transient items removed
|
|
*/
|
|
export const filterTransientItems = (items) => {
|
|
if (!items || !Array.isArray(items)) {
|
|
return [];
|
|
}
|
|
|
|
return items
|
|
.filter((item) => !item?.isTransient)
|
|
.map((item) => {
|
|
if (item.items && item.items.length > 0) {
|
|
return {
|
|
...item,
|
|
items: filterTransientItems(item.items)
|
|
};
|
|
}
|
|
return item;
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Checks if a collection is a scratch collection for any workspace
|
|
* @param {Object} collection - The collection to check
|
|
* @param {Array} workspaces - Array of workspace objects
|
|
* @returns {boolean} True if the collection is a scratch collection
|
|
*/
|
|
export const isScratchCollection = (collection, workspaces) => {
|
|
if (!collection || !workspaces) return false;
|
|
return workspaces.some((w) => w.scratchCollectionUid === collection.uid);
|
|
};
|