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} - 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); };