UNPKG

@usebruno/converters

Version:

The converters package is responsible for converting collections from one format to a Bruno collection. It can be used as a standalone package or as a part of the Bruno framework.

672 lines (590 loc) 22 kB
import get from 'lodash/get'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common'; import each from 'lodash/each'; import postmanTranslation from './postman-translations'; import { invalidVariableCharacterRegex } from '../constants/index'; const parseGraphQLRequest = (graphqlSource) => { try { let queryResultObject = { query: '', variables: '' }; if (typeof graphqlSource === 'string') { graphqlSource = JSON.parse(graphqlSource); } if (graphqlSource.hasOwnProperty('variables') && graphqlSource.variables !== '') { queryResultObject.variables = graphqlSource.variables; } if (graphqlSource.hasOwnProperty('query') && graphqlSource.query !== '') { queryResultObject.query = graphqlSource.query; } return queryResultObject; } catch (e) { return { query: '', variables: '' }; } }; const isItemAFolder = (item) => { return !item.request; }; const convertV21Auth = (array) => { return array.reduce((accumulator, currentValue) => { accumulator[currentValue.key] = currentValue.value; return accumulator; }, {}); }; const constructUrlFromParts = (url) => { if (!url) return ''; const { protocol = 'http', host, path, port, query, hash } = url || {}; const hostStr = Array.isArray(host) ? host.filter(Boolean).join('.') : host || ''; const pathStr = Array.isArray(path) ? path.filter(Boolean).join('/') : path || ''; const portStr = port ? `:${port}` : ''; const queryStr = query && Array.isArray(query) && query.length > 0 ? `?${query .filter((q) => q && q.key) .map((q) => `${q.key}=${q.value || ''}`) .join('&')}` : ''; const urlStr = `${protocol}://${hostStr}${portStr}${pathStr ? `/${pathStr}` : ''}${queryStr}`; return urlStr; }; const constructUrl = (url) => { if (!url) return ''; if (typeof url === 'string') { return url; } if (typeof url === 'object') { const { raw } = url; if (raw && typeof raw === 'string') { // If the raw URL contains url-fragments remove it if (raw.includes('#')) { return raw.split('#')[0]; // Returns the part of raw URL without the url-fragment part. } return raw; } // If no raw value exists, construct the URL from parts return constructUrlFromParts(url); } return ''; }; const importScriptsFromEvents = (events, requestObject) => { events.forEach((event) => { if (event.script && event.script.exec) { if (event.listen === 'prerequest') { if (!requestObject.script) { requestObject.script = {}; } if (event.script.exec && event.script.exec.length > 0) { requestObject.script.req = postmanTranslation(event.script.exec) } else { requestObject.script.req = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } if (event.listen === 'test') { if (!requestObject.tests) { requestObject.tests = {}; } if (event.script.exec && event.script.exec.length > 0) { requestObject.tests = postmanTranslation(event.script.exec) } else { requestObject.tests = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } } }); }; const importCollectionLevelVariables = (variables, requestObject) => { const vars = variables.map((v) => ({ uid: uuid(), name: v.key.replace(invalidVariableCharacterRegex, '_'), value: v.value, enabled: true })); requestObject.vars.req = vars; }; const processAuth = (auth, requestObject) => { if (!auth || !auth.type || auth.type === 'noauth') { return; } let authValues = auth[auth.type]; if (Array.isArray(authValues)) { authValues = convertV21Auth(authValues); } if (auth.type === 'basic') { requestObject.auth.mode = 'basic'; requestObject.auth.basic = { username: authValues.username || '', password: authValues.password || '' }; } else if (auth.type === 'bearer') { requestObject.auth.mode = 'bearer'; requestObject.auth.bearer = { token: authValues.token || '' }; } else if (auth.type === 'awsv4') { requestObject.auth.mode = 'awsv4'; requestObject.auth.awsv4 = { accessKeyId: authValues.accessKey || '', secretAccessKey: authValues.secretKey || '', sessionToken: authValues.sessionToken || '', service: authValues.service || '', region: authValues.region || '', profileName: '' }; } else if (auth.type === 'apikey') { requestObject.auth.mode = 'apikey'; requestObject.auth.apikey = { key: authValues.key || '', value: authValues.value?.toString() || '', // Convert the value to a string as Postman's schema does not rigidly define the type of it, placement: 'header' //By default we are placing the apikey values in headers! }; } else if (auth.type === 'digest') { requestObject.auth.mode = 'digest'; requestObject.auth.digest = { username: authValues.username || '', password: authValues.password || '' }; } else if (auth.type === 'oauth2') { const findValueUsingKey = (key) => { return authValues[key] || ''; }; const oauth2GrantTypeMaps = { authorization_code_with_pkce: 'authorization_code', authorization_code: 'authorization_code', client_credentials: 'client_credentials', password_credentials: 'password_credentials' }; const grantType = oauth2GrantTypeMaps[findValueUsingKey('grant_type')] || 'authorization_code'; requestObject.auth.mode = 'oauth2'; if (grantType === 'authorization_code') { requestObject.auth.oauth2 = { grantType: 'authorization_code', authorizationUrl: findValueUsingKey('authUrl'), callbackUrl: findValueUsingKey('redirect_uri'), accessTokenUrl: findValueUsingKey('accessTokenUrl'), refreshTokenUrl: findValueUsingKey('refreshTokenUrl'), clientId: findValueUsingKey('clientId'), clientSecret: findValueUsingKey('clientSecret'), scope: findValueUsingKey('scope'), state: findValueUsingKey('state'), pkce: Boolean(findValueUsingKey('grant_type') == 'authorization_code_with_pkce'), tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url', credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header' }; } else if (grantType === 'password_credentials') { requestObject.auth.oauth2 = { grantType: 'password', accessTokenUrl: findValueUsingKey('accessTokenUrl'), refreshTokenUrl: findValueUsingKey('refreshTokenUrl'), username: findValueUsingKey('username'), password: findValueUsingKey('password'), clientId: findValueUsingKey('clientId'), clientSecret: findValueUsingKey('clientSecret'), scope: findValueUsingKey('scope'), state: findValueUsingKey('state'), tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url', credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header' }; } else if (grantType === 'client_credentials') { requestObject.auth.oauth2 = { grantType: 'client_credentials', accessTokenUrl: findValueUsingKey('accessTokenUrl'), refreshTokenUrl: findValueUsingKey('refreshTokenUrl'), clientId: findValueUsingKey('clientId'), clientSecret: findValueUsingKey('clientSecret'), scope: findValueUsingKey('scope'), state: findValueUsingKey('state'), tokenPlacement: findValueUsingKey('addTokenTo') == 'header' ? 'header' : 'url', credentialsPlacement: findValueUsingKey('client_authentication') == 'body' ? 'body' : 'basic_auth_header' }; } } else { console.warn('Unexpected auth.type', auth.type); } }; const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, { useWorkers = false } = {}, scriptMap)=> { brunoParent.items = brunoParent.items || []; const folderMap = {}; const requestMap = {}; const requestMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE'] item.forEach((i, index) => { if (isItemAFolder(i)) { const baseFolderName = i.name || 'Untitled Folder'; let folderName = baseFolderName; let count = 1; while (folderMap[folderName]) { folderName = `${baseFolderName}_${count}`; count++; } const brunoFolderItem = { uid: uuid(), name: folderName, type: 'folder', items: [], seq: index + 1, root: { docs: i.description || '', meta: { name: folderName }, request: { auth: { mode: 'none', basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null }, headers: [], script: {}, tests: '', vars: {} } } }; brunoParent.items.push(brunoFolderItem); // Folder level auth if (i.auth) { processAuth(i.auth, brunoFolderItem.root.request); } else if (parentAuth) { // Inherit parent auth if folder doesn't define its own processAuth(parentAuth, brunoFolderItem.root.request); } if (i.item && i.item.length) { importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, { useWorkers }, scriptMap); } if (i.event) { if(useWorkers) { scriptMap.set(brunoFolderItem.uid, { events: i.event, request: brunoFolderItem.root.request }); } else { importScriptsFromEvents(i.event, brunoFolderItem.root.request); } } folderMap[folderName] = brunoFolderItem; } else if (i.request) { if (!requestMethods.includes(i?.request?.method.toUpperCase())) { console.warn('Unexpected request.method', i?.request?.method); return; } const baseRequestName = i.name || 'Untitled Request'; let requestName = baseRequestName; let count = 1; while (requestMap[requestName]) { requestName = `${baseRequestName}_${count}`; count++; } const url = constructUrl(i.request.url); const brunoRequestItem = { uid: uuid(), name: requestName, type: 'http-request', seq: index + 1, request: { url: url, method: i?.request?.method?.toUpperCase(), auth: { mode: 'none', basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null }, headers: [], params: [], body: { mode: 'none', json: null, text: null, xml: null, formUrlEncoded: [], multipartForm: [] }, docs: i.request.description || '' } }; brunoParent.items.push(brunoRequestItem); if (i.event) { if(useWorkers) { scriptMap.set(brunoRequestItem.uid, { events: i.event, request: brunoRequestItem.request }); } else { i.event.forEach((event) => { if (event.listen === 'prerequest' && event.script && event.script.exec) { if (!brunoRequestItem.request?.script) { brunoRequestItem.request.script = {}; } if (event.script.exec && event.script.exec.length > 0) { brunoRequestItem.request.script.req = postmanTranslation(event.script.exec) } else { brunoRequestItem.request.script.req = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } if (event.listen === 'test' && event.script && event.script.exec) { if (!brunoRequestItem.request?.tests) { brunoRequestItem.request.tests = {}; } if (event.script.exec && event.script.exec.length > 0) { brunoRequestItem.request.tests = postmanTranslation(event.script.exec) } else { brunoRequestItem.request.tests = ''; console.warn('Unexpected event.script.exec type', typeof event.script.exec); } } }); } } const bodyMode = get(i, 'request.body.mode'); if (bodyMode) { if (bodyMode === 'formdata') { brunoRequestItem.request.body.mode = 'multipartForm'; each(i.request.body.formdata, (param) => { const isFile = param.type === 'file'; let value; let type; if (isFile) { // If param.src is an array, keep it as it is. // If param.src is a string, convert it into an array with a single element. value = Array.isArray(param.src) ? param.src : typeof param.src === 'string' ? [param.src] : null; type = 'file'; } else { value = param.value; type = 'text'; } brunoRequestItem.request.body.multipartForm.push({ uid: uuid(), type: type, name: param.key, value: value, description: param.description, enabled: !param.disabled }); }); } if (bodyMode === 'urlencoded') { brunoRequestItem.request.body.mode = 'formUrlEncoded'; each(i.request.body.urlencoded, (param) => { brunoRequestItem.request.body.formUrlEncoded.push({ uid: uuid(), name: param.key, value: param.value, description: param.description, enabled: !param.disabled }); }); } if (bodyMode === 'raw') { let language = get(i, 'request.body.options.raw.language'); if (!language) { language = searchLanguageByHeader(i.request.header); } if (language === 'json') { brunoRequestItem.request.body.mode = 'json'; brunoRequestItem.request.body.json = i.request.body.raw; } else if (language === 'xml') { brunoRequestItem.request.body.mode = 'xml'; brunoRequestItem.request.body.xml = i.request.body.raw; } else { brunoRequestItem.request.body.mode = 'text'; brunoRequestItem.request.body.text = i.request.body.raw; } } } if (bodyMode === 'graphql') { brunoRequestItem.type = 'graphql-request'; brunoRequestItem.request.body.mode = 'graphql'; brunoRequestItem.request.body.graphql = parseGraphQLRequest(i.request.body.graphql); } each(i.request.header, (header) => { brunoRequestItem.request.headers.push({ uid: uuid(), name: header.key, value: header.value, description: header.description, enabled: !header.disabled }); }); // Handle request-level auth or inherit from parent const auth = i.request.auth ?? parentAuth; processAuth(auth, brunoRequestItem.request); each(get(i, 'request.url.query'), (param) => { brunoRequestItem.request.params.push({ uid: uuid(), name: param.key, value: param.value, description: param.description, type: 'query', enabled: !param.disabled }); }); each(get(i, 'request.url.variable', []), (param) => { if (!param.key) { // If no key, skip this iteration and discard the param return; } brunoRequestItem.request.params.push({ uid: uuid(), name: param.key, value: param.value ?? '', description: param.description ?? '', type: 'path', enabled: true }); }); requestMap[requestName] = brunoRequestItem; } }); }; const searchLanguageByHeader = (headers) => { let contentType; each(headers, (header) => { if (header.key.toLowerCase() === 'content-type' && !header.disabled) { if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?json/.test(header.value)) { contentType = 'json'; } else if (typeof header.value == 'string' && /^[\w\-]+\/([\w\-]+\+)?xml/.test(header.value)) { contentType = 'xml'; } return false; } }); return contentType; }; const importPostmanV2Collection = async (collection, { useWorkers = false }) => { const brunoCollection = { name: collection.info.name || 'Untitled Collection', uid: uuid(), version: '1', items: [], environments: [], root: { docs: collection.info.description || '', meta: { name: collection.info.name || 'Untitled Collection' }, request: { auth: { mode: 'none', basic: null, bearer: null, awsv4: null, apikey: null, oauth2: null, digest: null }, headers: [], script: {}, tests: '', vars: {} } } }; if (collection.event) { importScriptsFromEvents(collection.event, brunoCollection.root.request); } if (collection?.variable) { importCollectionLevelVariables(collection.variable, brunoCollection.root.request); } // Collection level auth processAuth(collection.auth, brunoCollection.root.request); // Create a single scriptMap for all items const scriptMap = useWorkers ? new Map() : null; importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, { useWorkers }, scriptMap); // Process all scripts in a single call at the top level if (useWorkers && scriptMap && scriptMap.size > 0) { try { const { default: scriptTranslationWorker } = await import('../workers/postman-translator-worker'); const translatedScripts = await scriptTranslationWorker(scriptMap); // Apply translated scripts to all items in the collection const applyScriptsToItems = (items) => { items.forEach(item => { if (item.type === 'folder') { // Apply scripts to the folder if (translatedScripts.has(item.uid)) { if (!item.root.request.script) { item.root.request.script = {}; } if (!item.root.request.tests) { item.root.request.tests = ''; } const script = translatedScripts.get(item.uid).request?.script?.req; const tests = translatedScripts.get(item.uid).request?.tests; item.root.request.script.req = script && script.length > 0 ? script : ''; item.root.request.tests = tests && tests.length > 0 ? tests : ''; } // Recursively apply to nested items if (item.items && item.items.length > 0) { applyScriptsToItems(item.items); } } else { if (translatedScripts.has(item.uid)) { if (!item.request.script) { item.request.script = {}; } if (!item.request.tests) { item.request.tests = ''; } const script = translatedScripts.get(item.uid).request?.script?.req; const tests = translatedScripts.get(item.uid).request?.tests; item.request.script.req = script && script.length > 0 ? script : ''; item.request.tests = tests && tests.length > 0 ? tests : ''; } } }); }; applyScriptsToItems(brunoCollection.items); } catch (error) { console.error('Error in script translation worker:', error); } finally { scriptMap.clear(); } } return brunoCollection; }; const parsePostmanCollection = async (collection, { useWorkers = false }) => { try { let schema = get(collection, 'info.schema'); let v2Schemas = [ 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json', 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json', 'https://schema.postman.com/json/collection/v2.0.0/collection.json', 'https://schema.postman.com/json/collection/v2.1.0/collection.json' ]; if (v2Schemas.includes(schema)) { return await importPostmanV2Collection(collection, { useWorkers }); } throw new Error('Unsupported Postman schema version. Only Postman Collection v2.0 and v2.1 are supported.'); } catch (err) { console.log(err); if (err instanceof Error) { throw err; } throw new Error('Invalid Postman collection format. Please check your JSON file.'); } }; const postmanToBruno = async (postmanCollection, { useWorkers = false } = {}) => { try { const parsedPostmanCollection = await parsePostmanCollection(postmanCollection, { useWorkers }); const transformedCollection = transformItemsInCollection(parsedPostmanCollection); const hydratedCollection = hydrateSeqInCollection(transformedCollection); const validatedCollection = validateSchema(hydratedCollection); return validatedCollection; } catch (err) { console.log(err); throw new Error(`Import collection failed: ${err.message}`); } }; export default postmanToBruno;