Feat: Standalone Package to convert to Bruno collection(Part 2)

This contains the bulk of the changes apart from renaming files.
This is a continuation of #2341.

Co-authored-by: lohit <lohit@usebruno.com>
Co-authored-by: pooja-bruno <pooja@usebruno.com>
This commit is contained in:
Thim 2025-04-07 20:15:50 +05:30 committed by Anoop M D
parent 1a6fa7a799
commit 9845363349
39 changed files with 2029 additions and 614 deletions

147
package-lock.json generated
View File

@ -10,6 +10,7 @@
"packages/bruno-electron",
"packages/bruno-cli",
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-query",
"packages/bruno-js",
@ -6343,6 +6344,24 @@
}
}
},
"node_modules/@rollup/plugin-alias": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.1.tgz",
"integrity": "sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-commonjs": {
"version": "23.0.7",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-23.0.7.tgz",
@ -7937,6 +7956,10 @@
"resolved": "packages/bruno-common",
"link": true
},
"node_modules/@usebruno/converters": {
"resolved": "packages/bruno-converters",
"link": true
},
"node_modules/@usebruno/crypto-js": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/@usebruno/crypto-js/-/crypto-js-3.1.9.tgz",
@ -26274,6 +26297,130 @@
"typescript": "^4.8.4"
}
},
"packages/bruno-converters": {
"name": "@usebruno/converters",
"version": "0.1.0",
"license": "MIT",
"dependencies": {
"@usebruno/schema": "^0.7.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"nanoid": "3.3.8"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"babel-jest": "^29.7.0",
"rimraf": "^5.0.7",
"rollup": "3.2.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
}
},
"packages/bruno-converters/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.4",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^1.11.1"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/bruno-converters/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/bruno-converters/node_modules/minipass": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"packages/bruno-converters/node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"packages/bruno-converters/node_modules/rimraf": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
"integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^10.3.7"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"packages/bruno-converters/node_modules/rollup": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.2.5.tgz",
"integrity": "sha512-/Ha7HhVVofduy+RKWOQJrxe4Qb3xyZo+chcpYiD8SoQa4AG7llhupUtyfKSSrdBM2mWJjhM8wZwmbY23NmlIYw==",
"dev": true,
"license": "MIT",
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=14.18.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"packages/bruno-electron": {
"name": "bruno",
"version": "2.0.0",

View File

@ -6,6 +6,7 @@
"packages/bruno-electron",
"packages/bruno-cli",
"packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema",
"packages/bruno-query",
"packages/bruno-js",
@ -38,6 +39,7 @@
"dev:electron": "npm run dev --workspace=packages/bruno-electron",
"dev:electron:debug": "npm run debug --workspace=packages/bruno-electron",
"build:bruno-common": "npm run build --workspace=packages/bruno-common",
"build:bruno-converters": "npm run build --workspace=packages/bruno-converters",
"build:bruno-query": "npm run build --workspace=packages/bruno-query",
"build:graphql-docs": "npm run build --workspace=packages/bruno-graphql-docs",
"build:electron": "node ./scripts/build-electron.js",

View File

@ -1,7 +1,6 @@
import React from 'react';
import exportBrunoCollection from 'utils/collections/export';
import exportPostmanCollection from 'utils/exporters/postman-collection';
import { toastError } from 'utils/common/error';
import cloneDeep from 'lodash/cloneDeep';
import Modal from 'components/Modal';
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React from 'react';
import importBrunoCollection from 'utils/importers/bruno-collection';
import importPostmanCollection from 'utils/importers/postman-collection';
import importInsomniaCollection from 'utils/importers/insomnia-collection';
@ -7,14 +7,6 @@ import { toastError } from 'utils/common/error';
import Modal from 'components/Modal';
const ImportCollection = ({ onClose, handleSubmit }) => {
const [options, setOptions] = useState({
enablePostmanTranslations: {
enabled: true,
label: 'Auto translate postman scripts',
subLabel:
"When enabled, Bruno will try as best to translate the scripts from the imported collection to Bruno's format."
}
});
const handleImportBrunoCollection = () => {
importBrunoCollection()
.then(({ collection }) => {
@ -24,9 +16,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
};
const handleImportPostmanCollection = () => {
importPostmanCollection(options)
.then(({ collection, translationLog }) => {
handleSubmit({ collection, translationLog });
importPostmanCollection()
.then(({ collection }) => {
handleSubmit({ collection });
})
.catch((err) => toastError(err, 'Postman Import collection failed'));
};
@ -46,15 +38,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
})
.catch((err) => toastError(err, 'OpenAPI v3 Import collection failed'));
};
const toggleOptions = (event, optionKey) => {
setOptions({
...options,
[optionKey]: {
...options[optionKey],
enabled: !options[optionKey].enabled
}
});
};
const CollectionButton = ({ children, className, onClick }) => {
return (
<button
@ -77,31 +60,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
</div>
<div className="flex justify-start w-full mt-4 max-w-[450px]">
{Object.entries(options || {}).map(([key, option]) => (
<div key={key} className="relative flex items-start">
<div className="flex h-6 items-center">
<input
id="comments"
aria-describedby="comments-description"
name="comments"
type="checkbox"
checked={option.enabled}
onChange={(e) => toggleOptions(e, key)}
className="h-3.5 w-3.5 rounded border-zinc-300 dark:ring-offset-zinc-800 bg-transparent text-indigo-600 dark:text-indigo-500 focus:ring-indigo-600 dark:focus:ring-indigo-500"
/>
</div>
<div className="ml-2 text-sm leading-6">
<label htmlFor="comments" className="font-medium text-gray-900 dark:text-zinc-50">
{option.label}
</label>
<p id="comments-description" className="text-zinc-500 text-xs dark:text-zinc-400">
{option.subLabel}
</p>
</div>
</div>
))}
</div>
</div>
</Modal>
);

View File

@ -4,105 +4,8 @@ import { useFormik } from 'formik';
import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import Modal from 'components/Modal';
import { IconAlertTriangle, IconArrowRight, IconCaretDown, IconCaretRight, IconCopy } from '@tabler/icons';
import toast from 'react-hot-toast';
const TranslationLog = ({ translationLog }) => {
const [showDetails, setShowDetails] = useState(false);
const preventSetShowDetails = (e) => {
e.stopPropagation();
e.preventDefault();
setShowDetails(!showDetails);
};
const copyClipboard = (e, value) => {
e.stopPropagation();
e.preventDefault();
navigator.clipboard.writeText(value);
toast.success('Copied to clipboard');
};
return (
<div className="flex flex-col mt-2">
<div className="border-l-2 border-amber-500 dark:border-amber-300 bg-amber-50 dark:bg-amber-50/10 p-1.5 rounded-r">
<div className="flex items-center">
<div className="flex-shrink-0">
<IconAlertTriangle className="h-4 w-4 text-amber-500 dark:text-amber-300" aria-hidden="true" />
</div>
<div className="ml-2">
<p className="text-xs text-amber-700 dark:text-amber-300">
<span className="font-semibold">Warning:</span> Some commands were not translated.{' '}
</p>
</div>
</div>
</div>
<button
onClick={(e) => preventSetShowDetails(e)}
className="flex w-fit items-center rounded px-2.5 py-1 mt-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
>
See details
{showDetails ? <IconCaretDown size={16} className="ml-1" /> : <IconCaretRight size={16} className="ml-1" />}
</button>
{showDetails && (
<div className="flex relative flex-col text-xs max-w-[364px] max-h-[300px] overflow-scroll mt-2 p-2 bg-slate-50 dark:bg-slate-400/10 ring-1 ring-inset rounded text-slate-700 dark:text-slate-300 ring-slate-600/20 dark:ring-slate-400/20">
<span className="font-semibold flex items-center">
Impacted Collections: {Object.keys(translationLog || {}).length}
</span>
<span className="font-semibold flex items-center">
Impacted Lines:{' '}
{Object.values(translationLog || {}).reduce(
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
0
)}
</span>
<span className="my-1">
The numbers after 'script' and 'test' indicate the line numbers of incomplete translations.
</span>
<ul>
{Object.entries(translationLog || {}).map(([name, value]) => (
<li key={name} className="list-none text-xs font-semibold">
<div className="font-semibold flex items-center text-xs whitespace-nowrap">
<IconCaretRight className="min-w-4 max-w-4 -ml-1" />
{name}
</div>
<div className="flex flex-col">
{value.script && (
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
<span className="mr-2">script :</span>
{value.script.map((scriptValue, index) => (
<span className="flex items-center" key={`script_${name}_${index}`}>
<span className="text-xs font-light">{scriptValue}</span>
{index < value.script.length - 1 && <> - </>}
</span>
))}
</div>
)}
{value.test && (
<div className="flex items-center text-xs font-light mb-1 flex-wrap">
<span className="mr-2">test :</span>
{value.test.map((testValue, index) => (
<div className="flex items-center" key={`test_${name}_${index}`}>
<span className="text-xs font-light">{testValue}</span>
{index < value.test.length - 1 && <> - </>}
</div>
))}
</div>
)}
</div>
</li>
))}
</ul>
<button
className="absolute top-1 right-1 flex w-fit items-center rounded p-2 text-xs font-semibold ring-1 ring-inset bg-slate-50 dark:bg-slate-400/10 text-slate-700 dark:text-slate-300 ring-slate-600/10 dark:ring-slate-400/20"
onClick={(e) => copyClipboard(e, JSON.stringify(translationLog))}
>
<IconCopy size={16} />
</button>
</div>
)}
</div>
);
};
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, translationLog }) => {
const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
const inputRef = useRef();
const dispatch = useDispatch();
@ -150,9 +53,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
Name
</label>
<div className="mt-2">{collectionName}</div>
{translationLog && Object.keys(translationLog).length > 0 && (
<TranslationLog translationLog={translationLog} />
)}
<>
<label htmlFor="collectionLocation" className="block font-semibold mt-3">
Location

View File

@ -14,18 +14,14 @@ import StyledWrapper from './StyledWrapper';
const TitleBar = () => {
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
const dispatch = useDispatch();
const { ipcRenderer } = window;
const handleImportCollection = ({ collection, translationLog }) => {
const handleImportCollection = ({ collection }) => {
setImportedCollection(collection);
if (translationLog) {
setImportedTranslationLog(translationLog);
}
setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true);
};
@ -75,7 +71,6 @@ const TitleBar = () => {
{importCollectionLocationModalOpen ? (
<ImportCollectionLocation
collectionName={importedCollection.name}
translationLog={importedTranslationLog}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}
/>

View File

@ -15,7 +15,6 @@ const Welcome = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = useState(false);
@ -24,11 +23,8 @@ const Welcome = () => {
dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
};
const handleImportCollection = ({ collection, translationLog }) => {
const handleImportCollection = ({ collection }) => {
setImportedCollection(collection);
if (translationLog) {
setImportedTranslationLog(translationLog);
}
setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true);
};
@ -55,7 +51,6 @@ const Welcome = () => {
) : null}
{importCollectionLocationModalOpen ? (
<ImportCollectionLocation
translationLog={importedTranslationLog}
collectionName={importedCollection.name}
onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation}

View File

@ -0,0 +1,15 @@
import * as FileSaver from 'file-saver';
import brunoConverters from '@usebruno/converters';
const { brunoToPostman } = brunoConverters;
export const exportCollection = (collection) => {
const collectionToExport = brunoToPostman(collection);
const fileName = `${collection.name}.json`;
const fileBlob = new Blob([JSON.stringify(collectionToExport, null, 2)], { type: 'application/json' });
FileSaver.saveAs(fileBlob, fileName);
};
export default exportCollection;

View File

@ -0,0 +1,119 @@
import each from 'lodash/each';
import get from 'lodash/get';
import cloneDeep from 'lodash/cloneDeep';
import { uuid } from 'utils/common';
import { isItemARequest } from 'utils/collections';
import { collectionSchema } from '@usebruno/schema';
import { BrunoError } from 'utils/common/error';
export const validateSchema = (collection = {}) => {
return new Promise((resolve, reject) => {
collectionSchema
.validate(collection)
.then(() => resolve(collection))
.catch((err) => {
console.log(err);
reject(new BrunoError('The Collection file is corrupted'));
});
});
};
export const updateUidsInCollection = (_collection) => {
const collection = cloneDeep(_collection);
collection.uid = uuid();
const updateItemUids = (items = []) => {
each(items, (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.vars.req'), (v) => (v.uid = uuid()));
each(get(item, 'request.vars.res'), (v) => (v.uid = uuid()));
each(get(item, 'request.assertions'), (a) => (a.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()));
if (item.items && item.items.length) {
updateItemUids(item.items);
}
});
};
updateItemUids(collection.items);
const updateEnvUids = (envs = []) => {
each(envs, (env) => {
env.uid = uuid();
each(env.variables, (variable) => (variable.uid = uuid()));
});
};
updateEnvUids(collection.environments);
return collection;
};
// todo
// need to eventually get rid of supporting old collection app models
// 1. start with making request type a constant fetched from a single place
// 2. move references of param and replace it with query inside the app
export const transformItemsInCollection = (collection) => {
const transformItems = (items = []) => {
each(items, (item) => {
if (['http', 'graphql'].includes(item.type)) {
item.type = `${item.type}-request`;
if (item.request.query) {
item.request.params = item.request.query.map((queryItem) => ({
...queryItem,
type: 'query',
uid: queryItem.uid || uuid()
}));
}
delete item.request.query;
// from 5 feb 2024, multipartFormData needs to have a type
// this was introduced when we added support for file uploads
// below logic is to make older collection exports backward compatible
let multipartFormData = get(item, 'request.body.multipartForm');
if (multipartFormData) {
each(multipartFormData, (form) => {
if (!form.type) {
form.type = 'text';
}
});
}
}
if (item.items && item.items.length) {
transformItems(item.items);
}
});
};
transformItems(collection.items);
return collection;
};
export const hydrateSeqInCollection = (collection) => {
const hydrateSeq = (items = []) => {
let index = 1;
each(items, (item) => {
if (isItemARequest(item) && !item.seq) {
item.seq = index;
index++;
}
if (item.items && item.items.length) {
hydrateSeq(item.items);
}
});
};
hydrateSeq(collection.items);
return collection;
};

View File

@ -0,0 +1,44 @@
import jsyaml from 'js-yaml';
import fileDialog from 'file-dialog';
import { BrunoError } from 'utils/common/error';
import brunoConverters from '@usebruno/converters';
const { insomniaToBruno } = brunoConverters;
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
// try to load JSON
const parsedData = JSON.parse(e.target.result);
resolve(parsedData);
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
const parsedData = jsyaml.load(e.target.result, { schema: jsyaml.CORE_SCHEMA });
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
reject(new BrunoError('Import collection failed'));
}
}
};
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then((collection) => insomniaToBruno(collection))
.then((collection) => resolve({ collection }))
.catch((err) => {
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
});
});
};
export default importCollection;

View File

@ -0,0 +1,44 @@
import jsyaml from 'js-yaml';
import fileDialog from 'file-dialog';
import { BrunoError } from 'utils/common/error';
import brunoConverters from '@usebruno/converters';
const { openApiToBruno } = brunoConverters;
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
// try to load JSON
const parsedData = JSON.parse(e.target.result);
resolve(parsedData);
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
const parsedData = jsyaml.load(e.target.result);
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
reject(new BrunoError('Import collection failed'));
}
}
};
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then((collection) => openApiToBruno(collection))
.then((collection) => resolve({ collection }))
.catch((err) => {
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
});
});
};
export default importCollection;

View File

@ -1,67 +0,0 @@
import { parseOpenApiCollection } from './openapi-collection';
import { uuid } from 'utils/common';
jest.mock('utils/common');
describe('openapi importer util functions', () => {
afterEach(jest.clearAllMocks);
it('should convert openapi object to bruno collection correctly', async () => {
const input = {
openapi: '3.0.3',
info: {
title: 'Sample API with Multiple Servers',
description: 'API spec with multiple servers.',
version: '1.0.0'
},
servers: [
{ url: 'https://api.example.com/v1', description: 'Production Server' },
{ url: 'https://staging-api.example.com/v1', description: 'Staging Server' },
{ url: 'http://localhost:3000/v1', description: 'Local Server' }
],
paths: {
'/users': {
get: {
summary: 'Get a list of users',
parameters: [
{ name: 'page', in: 'query', required: false, schema: { type: 'integer' } },
{ name: 'limit', in: 'query', required: false, schema: { type: 'integer' } }
],
responses: {
'200': { description: 'A list of users' }
}
}
}
}
};
const expectedOutput = {
name: 'Sample API with Multiple Servers',
version: '1',
items: [
{
name: 'Get a list of users',
type: 'http-request',
request: {
url: '{{baseUrl}}/users',
method: 'GET',
params: [
{ name: 'page', value: '', enabled: false, type: 'query' },
{ name: 'limit', value: '', enabled: false, type: 'query' }
]
}
}
],
environments: [
{ name: 'Production Server', variables: [{ name: 'baseUrl', value: 'https://api.example.com/v1' }] },
{ name: 'Staging Server', variables: [{ name: 'baseUrl', value: 'https://staging-api.example.com/v1' }] },
{ name: 'Local Server', variables: [{ name: 'baseUrl', value: 'http://localhost:3000/v1' }] }
]
};
const result = await parseOpenApiCollection(input);
expect(result).toMatchObject(expectedOutput);
expect(uuid).toHaveBeenCalledTimes(10);
});
});

View File

@ -0,0 +1,30 @@
import fileDialog from 'file-dialog';
import { BrunoError } from 'utils/common/error';
import brunoConverters from '@usebruno/converters';
import { safeParseJSON } from 'utils/common/index';
const { postmanToBruno } = brunoConverters;
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => resolve(safeParseJSON(e.target.result));
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' })
.then(readFile)
.then((collection) => postmanToBruno(collection))
.then(({ collection }) => resolve({ collection }))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import collection failed'));
})
});
};
export default importCollection;

View File

@ -0,0 +1,38 @@
import fileDialog from 'file-dialog';
import { BrunoError } from 'utils/common/error';
import brunoConverters from '@usebruno/converters';
const { postmanToBrunoEnvironment } = brunoConverters;
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => resolve(e.target.result);
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
const importEnvironment = () => {
return new Promise((resolve, reject) => {
fileDialog({ multiple: true, accept: 'application/json' })
.then((files) => {
return Promise.all(
Object.values(files ?? {}).map((file) =>
readFile([file])
.then((environment) => postmanToBrunoEnvironment(environment))
.catch((err) => {
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
throw err;
})
)
);
})
.then((environments) => resolve(environments))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import Environment failed'));
});
});
};
export default importEnvironment;

22
packages/bruno-converters/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# dependencies
node_modules
yarn.lock
pnpm-lock.yaml
package-lock.json
.pnp
.pnp.js
# testing
coverage
# production
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
};

View File

@ -0,0 +1,13 @@
module.exports = {
transform: {
'^.+\\.js$': 'babel-jest',
},
setupFiles: ['<rootDir>/jest.setup.js'],
transformIgnorePatterns: [
'node_modules/(?!(nanoid)/)'
],
testEnvironment: 'node',
moduleNameMapper: {
'^nanoid(/(.*)|$)': 'nanoid$1'
}
};

View File

@ -0,0 +1,11 @@
// Mock the uuid function
jest.mock('./src/common', () => {
// Import the original module to keep other functions intact
const originalModule = jest.requireActual('./src/common');
return {
__esModule: true, // Use this property to indicate it's an ES module
...originalModule,
uuid: jest.fn(() => 'mockeduuidvalue123456'), // Mock uuid to return a fixed value
};
});

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Anoop M D, Anusree P S and Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,45 @@
{
"name": "@usebruno/converters",
"version": "0.1.0",
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src",
"package.json"
],
"scripts": {
"clean": "rimraf dist",
"test": "node --experimental-vm-modules $(npx which jest) --colors --collectCoverage",
"prebuild": "npm run clean",
"build": "rollup -c",
"watch": "rollup -c -w",
"prepack": "npm run test && npm run build"
},
"dependencies": {
"@usebruno/schema": "^0.7.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"nanoid": "3.3.8"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",
"@rollup/plugin-typescript": "^9.0.2",
"babel-jest": "^29.7.0",
"rimraf": "^5.0.7",
"rollup": "3.2.5",
"rollup-plugin-dts": "^5.0.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.8.4"
},
"overrides": {
"rollup": "3.2.5"
}
}

View File

@ -0,0 +1,45 @@
# bruno-converters
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.
## Installation
```bash
npm install @usebruno/converters
```
## Usage
### Convert Postman collection to Bruno collection
```javascript
const { postmanToBruno } = require('@usebruno/converters');
// Convert Postman collection to Bruno collection
const brunoCollection = postmanToBruno(postmanCollection);
```
### Convert Postman Environment to Bruno Environment
```javascript
const { postmanToBrunoEnvironment } = require('@usebruno/converters');
const brunoEnvironment = postmanToBrunoEnvironment(postmanEnvironment);
```
### Convert Insomnia collection to Bruno collection
```javascript
import { insomniaToBruno } from '@usebruno/converters';
const brunoCollection = insomniaToBruno(insomniaCollection);
```
### Convert OpenAPI specification to Bruno collection
```javascript
import { openApiToBruno } from '@usebruno/converters';
const brunoCollection = openApiToBruno(openApiSpecification);
```

View File

@ -0,0 +1,38 @@
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const { terser } = require('rollup-plugin-terser');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const packageJson = require('./package.json');
const alias = require('@rollup/plugin-alias');
const path = require('path');
module.exports = [
{
input: 'src/index.js',
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: true
},
{
file: packageJson.module,
format: 'esm',
sourcemap: true
}
],
plugins: [
peerDepsExternal(),
nodeResolve({
preferBuiltins: true,
extensions: ['.js', '.css'] // Resolve .js files
}),
commonjs(),
terser(),
alias({
entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }]
})
]
}
];

View File

@ -1,22 +1,54 @@
import each from 'lodash/each';
import get from 'lodash/get';
import { customAlphabet } from 'nanoid';
import cloneDeep from 'lodash/cloneDeep';
import { uuid, normalizeFileName } from 'utils/common';
import { isItemARequest } from 'utils/collections';
import { collectionSchema } from '@usebruno/schema';
import { BrunoError } from 'utils/common/error';
export const safeParseJSON = (str) => {
if (!str || !str.length || typeof str !== 'string') {
return str;
}
try {
return JSON.parse(str);
} catch (e) {
return str;
}
};
export const safeStringifyJSON = (obj, indent = false) => {
if (obj === undefined) {
return obj;
}
try {
if (indent) {
return JSON.stringify(obj, null, 2);
}
return JSON.stringify(obj);
} catch (e) {
return obj;
}
};
export const isItemARequest = (item) => {
return item.hasOwnProperty('request') && ['http-request', 'graphql-request'].includes(item.type) && !item.items;
};
// a customized version of nanoid without using _ and -
export const uuid = () => {
// https://github.com/ai/nanoid/blob/main/url-alphabet/index.js
const urlAlphabet = 'useandom26T198340PX75pxJACKVERYMINDBUSHWOLFGQZbfghjklqvwyzrict';
const customNanoId = customAlphabet(urlAlphabet, 21);
return customNanoId();
};
export const validateSchema = (collection = {}) => {
return new Promise((resolve, reject) => {
collectionSchema
.validate(collection)
.then(() => resolve(collection))
.catch((err) => {
console.log(err);
reject(new BrunoError('The Collection file is corrupted'));
});
});
try {
collectionSchema.validateSync(collection);
return collection;
} catch (err) {
throw new Error('The Collection has an invalid schema');
}
};
export const updateUidsInCollection = (_collection) => {
@ -117,3 +149,63 @@ export const hydrateSeqInCollection = (collection) => {
return collection;
};
export const deleteUidsInItems = (items) => {
each(items, (item) => {
delete item.uid;
if (['http-request', 'graphql-request'].includes(item.type)) {
each(get(item, 'request.headers'), (header) => delete header.uid);
each(get(item, 'request.params'), (param) => delete param.uid);
each(get(item, 'request.vars.req'), (v) => delete v.uid);
each(get(item, 'request.vars.res'), (v) => delete v.uid);
each(get(item, 'request.vars.assertions'), (a) => delete a.uid);
each(get(item, 'request.body.multipartForm'), (param) => delete param.uid);
each(get(item, 'request.body.formUrlEncoded'), (param) => delete param.uid);
each(get(item, 'request.body.file'), (param) => delete param.uid);
}
if (item.items && item.items.length) {
deleteUidsInItems(item.items);
}
});
};
/**
* Some of the models in the app are not consistent with the Collection Json format
* This function is used to transform the models to the Collection Json format
*/
export const transformItem = (items = []) => {
each(items, (item) => {
if (['http-request', 'graphql-request'].includes(item.type)) {
if (item.type === 'graphql-request') {
item.type = 'graphql';
}
if (item.type === 'http-request') {
item.type = 'http';
}
}
if (item.items && item.items.length) {
transformItem(item.items);
}
});
};
export const deleteUidsInEnvs = (envs) => {
each(envs, (env) => {
delete env.uid;
each(env.variables, (variable) => delete variable.uid);
});
};
export const deleteSecretsInEnvs = (envs) => {
each(envs, (env) => {
each(env.variables, (variable) => {
if (variable.secret) {
variable.value = '';
}
});
});
};

View File

@ -0,0 +1,16 @@
import postmanToBruno from './postman/postman-to-bruno.js';
import postmanToBrunoEnvironment from './postman/postman-env-to-bruno-env.js';
import brunoToPostman from './postman/bruno-to-postman.js';
import openApiToBruno from './openapi/openapi-to-bruno.js';
import insomniaToBruno from './insomnia/insomnia-to-bruno.js';
export default {
postmanToBruno,
postmanToBrunoEnvironment,
brunoToPostman,
openApiToBruno,
insomniaToBruno
};

View File

@ -1,34 +1,6 @@
import jsyaml from 'js-yaml';
import each from 'lodash/each';
import get from 'lodash/get';
import fileDialog from 'file-dialog';
import { uuid } from 'utils/common';
import { BrunoError } from 'utils/common/error';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
// try to load JSON
const parsedData = JSON.parse(e.target.result);
resolve(parsedData);
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
const parsedData = jsyaml.load(e.target.result, { schema: jsyaml.CORE_SCHEMA });
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
reject(new BrunoError('Import collection failed'));
}
}
};
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
const parseGraphQL = (text) => {
try {
@ -187,7 +159,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
return brunoRequestItem;
};
const parseInsomniaCollection = (data) => {
const parseInsomniaCollection = (_insomniaCollection) => {
const brunoCollection = {
name: '',
uid: uuid(),
@ -196,66 +168,61 @@ const parseInsomniaCollection = (data) => {
environments: []
};
return new Promise((resolve, reject) => {
try {
const insomniaExport = data;
const insomniaResources = get(insomniaExport, 'resources', []);
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
try {
const insomniaExport = _insomniaCollection;
const insomniaResources = get(insomniaExport, 'resources', []);
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
if (!insomniaCollection) {
reject(new BrunoError('Collection not found inside Insomnia export'));
}
brunoCollection.name = insomniaCollection.name;
const requestsAndFolders =
insomniaResources.filter((resource) => resource._type === 'request' || resource._type === 'request_group') ||
[];
function createFolderStructure(resources, parentId = null) {
const requestGroups =
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
const folders = requestGroups.map((folder, index, allFolder) => {
const name = addSuffixToDuplicateName(folder, index, allFolder);
const requests = resources.filter(
(resource) => resource._type === 'request' && resource.parentId === folder._id
);
return {
uid: uuid(),
name,
type: 'folder',
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
};
});
return folders.concat(requests.map(transformInsomniaRequestItem));
}
(brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id)),
resolve(brunoCollection);
} catch (err) {
reject(new BrunoError('An error occurred while parsing the Insomnia collection'));
if (!insomniaCollection) {
throw new Error('Collection not found inside Insomnia export');
}
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then(parseInsomniaCollection)
.then(transformItemsInCollection)
.then(hydrateSeqInCollection)
.then(validateSchema)
.then((collection) => resolve({ collection }))
.catch((err) => {
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
brunoCollection.name = insomniaCollection.name;
const requestsAndFolders =
insomniaResources.filter((resource) => resource._type === 'request' || resource._type === 'request_group') ||
[];
function createFolderStructure(resources, parentId = null) {
const requestGroups =
resources.filter((resource) => resource._type === 'request_group' && resource.parentId === parentId) || [];
const requests = resources.filter((resource) => resource._type === 'request' && resource.parentId === parentId);
const folders = requestGroups.map((folder, index, allFolder) => {
const name = addSuffixToDuplicateName(folder, index, allFolder);
const requests = resources.filter(
(resource) => resource._type === 'request' && resource.parentId === folder._id
);
return {
uid: uuid(),
name,
type: 'folder',
items: createFolderStructure(resources, folder._id).concat(requests.map(transformInsomniaRequestItem))
};
});
});
return folders.concat(requests.map(transformInsomniaRequestItem));
}
brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);
return brunoCollection;
} catch (err) {
throw new Error('An error occurred while parsing the Insomnia collection');
}
};
export default importCollection;
export const insomniaToBruno = (insomniaCollection) => {
try {
const collection = parseInsomniaCollection(insomniaCollection);
const transformedCollection = transformItemsInCollection(collection);
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
const validatedCollection = validateSchema(hydratedCollection);
return validatedCollection;
} catch (err) {
console.error(err);
throw new Error('Import collection failed');
}
};
export default insomniaToBruno;

View File

@ -1,34 +1,6 @@
import jsyaml from 'js-yaml';
import each from 'lodash/each';
import get from 'lodash/get';
import fileDialog from 'file-dialog';
import { uuid } from 'utils/common';
import { BrunoError } from 'utils/common/error';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
try {
// try to load JSON
const parsedData = JSON.parse(e.target.result);
resolve(parsedData);
} catch (jsonError) {
// not a valid JSOn, try yaml
try {
const parsedData = jsyaml.load(e.target.result);
resolve(parsedData);
} catch (yamlError) {
console.error('Error parsing the file :', jsonError, yamlError);
reject(new BrunoError('Import collection failed'));
}
}
};
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
const ensureUrl = (url) => {
// removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise
@ -363,12 +335,10 @@ export const parseOpenApiCollection = (data) => {
items: [],
environments: []
};
return new Promise((resolve, reject) => {
try {
const collectionData = resolveRefs(data);
if (!collectionData) {
reject(new BrunoError('Invalid OpenAPI collection. Failed to resolve refs.'));
throw new Error('Invalid OpenAPI collection. Failed to resolve refs.');
return;
}
@ -377,7 +347,7 @@ export const parseOpenApiCollection = (data) => {
// Assumes v3 if not defined. v2 is not supported yet
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) {
reject(new BrunoError('Only OpenAPI v3 is supported currently.'));
throw new Error('Only OpenAPI v3 is supported currently.');
return;
}
@ -443,28 +413,24 @@ export const parseOpenApiCollection = (data) => {
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
brunoCollection.items = brunoCollectionItems;
resolve(brunoCollection);
return brunoCollection;
} catch (err) {
console.error(err);
reject(new BrunoError('An error occurred while parsing the OpenAPI collection'));
throw new Error('An error occurred while parsing the OpenAPI collection');
}
});
};
const importCollection = () => {
return new Promise((resolve, reject) => {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' })
.then(readFile)
.then(parseOpenApiCollection)
.then(transformItemsInCollection)
.then(hydrateSeqInCollection)
.then(validateSchema)
.then((collection) => resolve({ collection }))
.catch((err) => {
console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message));
});
});
export const openApiToBruno = (openApiSpecification) => {
try {
const collection = parseOpenApiCollection(openApiSpecification);
const transformedCollection = transformItemsInCollection(collection);
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
const validatedCollection = validateSchema(hydratedCollection);
return validatedCollection
} catch (err) {
console.error(err);
throw new Error('Import collection failed');
}
};
export default importCollection;
export default openApiToBruno;

View File

@ -1,6 +1,5 @@
import map from 'lodash/map';
import * as FileSaver from 'file-saver';
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../collections/export';
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../common';
/**
* Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
@ -102,7 +101,7 @@ export const sanitizeUrl = (url) => {
return sanitizedUrl;
};
export const exportCollection = (collection) => {
export const brunoToPostman = (collection) => {
delete collection.uid;
delete collection.processEnvVariables;
deleteUidsInItems(collection.items);
@ -335,11 +334,7 @@ export const exportCollection = (collection) => {
collectionToExport.info = generateInfoSection();
collectionToExport.item = generateItemSection(collection.items);
collectionToExport.variable = generateCollectionVars(collection);
const fileName = `${collection.name}.json`;
const fileBlob = new Blob([JSON.stringify(collectionToExport, null, 2)], { type: 'application/json' });
FileSaver.saveAs(fileBlob, fileName);
return collectionToExport;
};
export default exportCollection;
export default brunoToPostman;

View File

@ -1,16 +1,4 @@
import each from 'lodash/each';
import fileDialog from 'file-dialog';
import { BrunoError } from 'utils/common/error';
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => resolve(e.target.result);
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
const isSecret = (type) => {
return type === 'secret';
};
@ -40,42 +28,13 @@ const importPostmanEnvironment = (environment) => {
return brunoEnvironment;
};
const parsePostmanEnvironment = (str) => {
return new Promise((resolve, reject) => {
try {
let environment = JSON.parse(str);
return resolve(importPostmanEnvironment(environment));
} catch (err) {
console.log(err);
if (err instanceof BrunoError) {
return reject(err);
}
return reject(new BrunoError('Unable to parse the postman environment json file'));
}
});
export const postmanToBrunoEnvironment = (postmanEnvironment) => {
try {
return importPostmanEnvironment(postmanEnvironment);
} catch (err) {
console.log(err);
throw new Error('Unable to parse the postman environment json file');
}
};
const importEnvironment = () => {
return new Promise((resolve, reject) => {
fileDialog({ multiple: true, accept: 'application/json' })
.then((files) => {
return Promise.all(
Object.values(files ?? {}).map((file) =>
readFile([file])
.then(parsePostmanEnvironment)
.catch((err) => {
console.error(`Error processing file: ${file.name || 'undefined'}`, err);
throw err;
})
)
);
})
.then((environments) => resolve(environments))
.catch((err) => {
console.log(err);
reject(new BrunoError('Import Environment failed'));
});
});
};
export default importEnvironment;
export default postmanToBrunoEnvironment;

View File

@ -1,19 +1,7 @@
import get from 'lodash/get';
import fileDialog from 'file-dialog';
import { uuid } from 'utils/common';
import { BrunoError } from 'utils/common/error';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection } from './common';
import { postmanTranslation } from 'utils/importers/translators/postman_translation';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
import each from 'lodash/each';
const readFile = (files) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => resolve(e.target.result);
fileReader.onerror = (err) => reject(err);
fileReader.readAsText(files[0]);
});
};
import postmanTranslation from './postman-translations';
const parseGraphQLRequest = (graphqlSource) => {
try {
@ -95,28 +83,7 @@ const constructUrl = (url) => {
return '';
};
let translationLog = {};
/* struct of translation log
{
[collectionName]: {
script: [index1, index2],
test: [index1, index2]
}
}
*/
const pushTranslationLog = (type, index) => {
if (!translationLog[i.name]) {
translationLog[i.name] = {};
}
if (!translationLog[i.name][type]) {
translationLog[i.name][type] = [];
}
translationLog[i.name][type].push(index + 1);
};
const importScriptsFromEvents = (events, requestObject, options, pushTranslationLog) => {
const importScriptsFromEvents = (events, requestObject) => {
events.forEach((event) => {
if (event.script && event.script.exec) {
if (event.listen === 'prerequest') {
@ -124,22 +91,12 @@ const importScriptsFromEvents = (events, requestObject, options, pushTranslation
requestObject.script = {};
}
if (Array.isArray(event.script.exec)) {
if (event.script.exec.length > 0) {
requestObject.script.req = event.script.exec
.map((line, index) =>
options.enablePostmanTranslations.enabled
? postmanTranslation(line, () => pushTranslationLog('script', index))
: `// ${line}`
)
.join('\n');
} else {
requestObject.script.req = '';
}
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
requestObject.script.req = event.script.exec
.map((line) => postmanTranslation(line))
.join('\n');
} else if (typeof event.script.exec === 'string') {
requestObject.script.req = options.enablePostmanTranslations.enabled
? postmanTranslation(event.script.exec, () => pushTranslationLog('script', 0))
: `// ${event.script.exec}`;
requestObject.script.req = postmanTranslation(event.script.exec);
} else {
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
@ -150,22 +107,12 @@ const importScriptsFromEvents = (events, requestObject, options, pushTranslation
requestObject.tests = {};
}
if (Array.isArray(event.script.exec)) {
if (event.script.exec.length > 0) {
requestObject.tests = event.script.exec
.map((line, index) =>
options.enablePostmanTranslations.enabled
? postmanTranslation(line, () => pushTranslationLog('test', index))
: `// ${line}`
)
.join('\n');
} else {
requestObject.tests = '';
}
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
requestObject.tests = event.script.exec
.map((line) => postmanTranslation(line))
.join('\n');
} else if (typeof event.script.exec === 'string') {
requestObject.tests = options.enablePostmanTranslations.enabled
? postmanTranslation(event.script.exec, () => pushTranslationLog('test', 0))
: `// ${event.script.exec}`;
return postmanTranslation(event.script.exec);
} else {
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
@ -185,7 +132,7 @@ const importCollectionLevelVariables = (variables, requestObject) => {
requestObject.vars.req = vars;
};
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) => {
const importPostmanV2CollectionItem = (brunoParent, item, parentAuth) => {
brunoParent.items = brunoParent.items || [];
const folderMap = {};
const requestMap = {};
@ -227,11 +174,11 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
}
};
if (i.item && i.item.length) {
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth, options);
importPostmanV2CollectionItem(brunoFolderItem, i.item, i.auth ?? parentAuth);
}
if (i.event) {
importScriptsFromEvents(i.event, brunoFolderItem.root.request, options, pushTranslationLog);
importScriptsFromEvents(i.event, brunoFolderItem.root.request);
}
brunoParent.items.push(brunoFolderItem);
@ -288,22 +235,12 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
if (!brunoRequestItem.request.script) {
brunoRequestItem.request.script = {};
}
if (Array.isArray(event.script.exec)) {
if (event.script.exec.length > 0) {
brunoRequestItem.request.script.req = event.script.exec
.map((line, index) =>
options.enablePostmanTranslations.enabled
? postmanTranslation(line, () => pushTranslationLog('script', index))
: `// ${line}`
)
.join('\n');
} else {
brunoRequestItem.request.script.req = '';
}
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
brunoRequestItem.request.script.req = event.script.exec
.map((line) => postmanTranslation(line))
.join('\n');
} else if (typeof event.script.exec === 'string') {
brunoRequestItem.request.script.req = options.enablePostmanTranslations.enabled
? postmanTranslation(event.script.exec, () => pushTranslationLog('script', 0))
: `// ${event.script.exec}`;
brunoRequestItem.request.script.req = postmanTranslation(event.script.exec);
} else {
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
@ -312,22 +249,12 @@ const importPostmanV2CollectionItem = (brunoParent, item, parentAuth, options) =
if (!brunoRequestItem.request.tests) {
brunoRequestItem.request.tests = {};
}
if (Array.isArray(event.script.exec)) {
if (event.script.exec.length > 0) {
if (Array.isArray(event.script.exec) && event.script.exec.length > 0) {
brunoRequestItem.request.tests = event.script.exec
.map((line, index) =>
options.enablePostmanTranslations.enabled
? postmanTranslation(line, () => pushTranslationLog('test', index))
: `// ${line}`
)
.map((line) => postmanTranslation(line))
.join('\n');
} else {
brunoRequestItem.request.tests = '';
}
} else if (typeof event.script.exec === 'string') {
brunoRequestItem.request.tests = options.enablePostmanTranslations.enabled
? postmanTranslation(event.script.exec, () => pushTranslationLog('test', 0))
: `// ${event.script.exec}`;
return postmanTranslation(event.script.exec);
} else {
console.warn('Unexpected event.script.exec type', typeof event.script.exec);
}
@ -559,7 +486,7 @@ const searchLanguageByHeader = (headers) => {
return contentType;
};
const importPostmanV2Collection = (collection, options) => {
const importPostmanV2Collection = (collection) => {
const brunoCollection = {
name: collection.info.name || 'Untitled Collection',
uid: uuid(),
@ -587,81 +514,55 @@ const importPostmanV2Collection = (collection, options) => {
};
if (collection.event) {
importScriptsFromEvents(collection.event, brunoCollection.root.request, options, pushTranslationLog);
importScriptsFromEvents(collection.event, brunoCollection.root.request);
}
if (collection?.variable){
importCollectionLevelVariables(collection.variable, brunoCollection.root.request);
}
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth, options);
importPostmanV2CollectionItem(brunoCollection, collection.item, collection.auth);
return brunoCollection;
};
const parsePostmanCollection = (str, options) => {
return new Promise((resolve, reject) => {
try {
let collection = JSON.parse(str);
let schema = get(collection, 'info.schema');
const parsePostmanCollection = (collection) => {
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'
];
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 resolve(importPostmanV2Collection(collection, options));
}
throw new BrunoError('Unknown postman schema');
} catch (err) {
console.log(err);
if (err instanceof BrunoError) {
return reject(err);
}
return reject(new BrunoError('Unable to parse the postman collection json file'));
if (v2Schemas.includes(schema)) {
return importPostmanV2Collection(collection);
}
});
};
const logTranslationDetails = (translationLog) => {
if (Object.keys(translationLog || {}).length > 0) {
console.warn(
`[Postman Translation Logs]
Collections incomplete : ${Object.keys(translationLog || {}).length}` +
`\nTotal lines incomplete : ${Object.values(translationLog || {}).reduce(
(acc, curr) => acc + (curr.script?.length || 0) + (curr.test?.length || 0),
0
)}` +
`\nSee details below :`,
translationLog
);
throw new Error('Unknown postman schema');
} catch (err) {
console.log(err);
if (err instanceof Error) {
throw err;
}
throw new Error('Unable to parse the postman collection json file');
}
};
const importCollection = (options) => {
return new Promise((resolve, reject) => {
fileDialog({ accept: 'application/json' })
.then(readFile)
.then((str) => parsePostmanCollection(str, options))
.then(transformItemsInCollection)
.then(hydrateSeqInCollection)
.then(validateSchema)
.then((collection) => resolve({ collection, translationLog }))
.catch((err) => {
console.log(err);
translationLog = {};
reject(new BrunoError('Import collection failed'));
})
.then(() => {
logTranslationDetails(translationLog);
translationLog = {};
});
});
const postmanToBruno = (postmanCollection) => {
try {
const parsedPostmanCollection = parsePostmanCollection(postmanCollection);
const transformedCollection = transformItemsInCollection(parsedPostmanCollection);
const hydratedCollection = hydrateSeqInCollection(transformedCollection);
const validatedCollection = validateSchema(hydratedCollection);
return ({ collection: validatedCollection });
} catch(err) {
console.log(err);
throw new Error('Import collection failed');
}
};
export default importCollection;
export default postmanToBruno;

View File

@ -42,7 +42,7 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern,
replacement
}));
export const postmanTranslation = (script, logCallback) => {
const postmanTranslation = (script) => {
try {
let modifiedScript = script;
let modified = false;
@ -54,10 +54,11 @@ export const postmanTranslation = (script, logCallback) => {
}
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
//logCallback?.();
}
return modifiedScript;
} catch (e) {
return script;
}
};
export default postmanTranslation;

View File

@ -0,0 +1,190 @@
import { describe, it, expect } from '@jest/globals';
import insomniaToBruno from '../../src/insomnia/insomnia-to-bruno';
describe('insomnia-collection', () => {
it('should correctly import a valid Insomnia collection file', async () => {
const brunoCollection = insomniaToBruno(insomniaCollection);
expect(brunoCollection).toMatchObject(expectedOutput)
});
});
const insomniaCollection = {
"_type": "export",
"__export_format": 4,
"__export_date": "2024-05-20T10:02:44.123Z",
"__export_source": "insomnia.desktop.app:v2021.5.2",
"resources": [
{
"_id": "req_1",
"_type": "request",
"parentId": "fld_1",
"name": "Request1",
"method": "GET",
"url": "https://httpbin.org/get",
"parameters": []
},
{
"_id": "req_2",
"_type": "request",
"parentId": "fld_2",
"name": "Request2",
"method": "GET",
"url": "https://httpbin.org/get",
"parameters": []
},
{
"_id": "fld_1",
"_type": "request_group",
"parentId": "wrk_1",
"name": "Folder1"
},
{
"_id": "fld_2",
"_type": "request_group",
"parentId": "wrk_1",
"name": "Folder2"
},
{
"_id": "wrk_1",
"_type": "workspace",
"name": "Hello World Workspace Insomnia"
},
{
"_id": "env_1",
"_type": "environment",
"parentId": "wrk_1",
"data": {
"var1": "value1",
"var2": "value2"
}
}
]
};
const expectedOutput = {
"environments": [],
"items": [
{
"items": [
{
"name": "Request1",
"request": {
"auth": {
"basic": null,
"bearer": null,
"digest": null,
"mode": "none",
},
"body": {
"formUrlEncoded": [],
"json": null,
"mode": "none",
"multipartForm": [],
"text": null,
"xml": null,
},
"headers": [],
"method": "GET",
"params": [],
"url": "https://httpbin.org/get",
},
"seq": 1,
"type": "http-request",
"uid": "mockeduuidvalue123456",
},
{
"name": "Request1",
"request": {
"auth": {
"basic": null,
"bearer": null,
"digest": null,
"mode": "none",
},
"body": {
"formUrlEncoded": [],
"json": null,
"mode": "none",
"multipartForm": [],
"text": null,
"xml": null,
},
"headers": [],
"method": "GET",
"params": [],
"url": "https://httpbin.org/get",
},
"seq": 2,
"type": "http-request",
"uid": "mockeduuidvalue123456",
},
],
"name": "Folder1",
"type": "folder",
"uid": "mockeduuidvalue123456",
},
{
"items": [
{
"name": "Request2",
"request": {
"auth": {
"basic": null,
"bearer": null,
"digest": null,
"mode": "none",
},
"body": {
"formUrlEncoded": [],
"json": null,
"mode": "none",
"multipartForm": [],
"text": null,
"xml": null,
},
"headers": [],
"method": "GET",
"params": [],
"url": "https://httpbin.org/get",
},
"seq": 1,
"type": "http-request",
"uid": "mockeduuidvalue123456",
},
{
"name": "Request2",
"request": {
"auth": {
"basic": null,
"bearer": null,
"digest": null,
"mode": "none",
},
"body": {
"formUrlEncoded": [],
"json": null,
"mode": "none",
"multipartForm": [],
"text": null,
"xml": null,
},
"headers": [],
"method": "GET",
"params": [],
"url": "https://httpbin.org/get",
},
"seq": 2,
"type": "http-request",
"uid": "mockeduuidvalue123456",
},
],
"name": "Folder2",
"type": "folder",
"uid": "mockeduuidvalue123456",
},
],
"name": "Hello World Workspace Insomnia",
"uid": "mockeduuidvalue123456",
"version": "1",
};

View File

@ -0,0 +1,108 @@
import jsyaml from 'js-yaml';
import { describe, it, expect } from '@jest/globals';
import openApiToBruno from '../../src/openapi/openapi-to-bruno';
describe('openapi-collection', () => {
it('should correctly import a valid OpenAPI file', async () => {
const openApiSpecification = jsyaml.load(openApiCollectionString);
const brunoCollection = openApiToBruno(openApiSpecification);
expect(brunoCollection).toMatchObject(expectedOutput);
});
});
const openApiCollectionString = `
openapi: "3.0.0"
info:
version: "1.0.0"
title: "Hello World OpenAPI"
paths:
/get:
get:
tags:
- Folder1
- Folder2
summary: "Request1 and Request2"
operationId: "getRequests"
responses:
'200':
description: "Successful response"
components:
parameters:
var1:
in: "query"
name: "var1"
required: true
schema:
type: "string"
default: "value1"
var2:
in: "query"
name: "var2"
required: true
schema:
type: "string"
default: "value2"
servers:
- url: "https://httpbin.org"
`;
const expectedOutput = {
"environments": [
{
"name": "Environment 1",
"uid": "mockeduuidvalue123456",
"variables": [
{
"enabled": true,
"name": "baseUrl",
"secret": false,
"type": "text",
"uid": "mockeduuidvalue123456",
"value": "https://httpbin.org",
},
],
},
],
"items": [
{
"items": [
{
"name": "Request1 and Request2",
"request": {
"auth": {
"basic": null,
"bearer": null,
"digest": null,
"mode": "none",
},
"body": {
"formUrlEncoded": [],
"json": null,
"mode": "none",
"multipartForm": [],
"text": null,
"xml": null,
},
"headers": [],
"method": "GET",
"params": [],
"script": {
"res": null,
},
"url": "{{baseUrl}}/get",
},
"seq": 1,
"type": "http-request",
"uid": "mockeduuidvalue123456",
},
],
"name": "Folder1",
"type": "folder",
"uid": "mockeduuidvalue123456",
},
],
"name": "Hello World OpenAPI",
"uid": "mockeduuidvalue123456",
"version": "1",
};

View File

@ -1,4 +1,4 @@
const { sanitizeUrl, transformUrl } = require('./postman-collection');
import { sanitizeUrl, transformUrl } from "../../src/postman/bruno-to-postman";
describe('transformUrl', () => {
it('should handle basic URL with path variables', () => {

View File

@ -0,0 +1,67 @@
import { describe, it, expect } from '@jest/globals';
import postmanToBrunoEnvironment from '../../src/postman/postman-env-to-bruno-env';
describe('postmanToBrunoEnvironment Function', () => {
it('should correctly import a valid Postman environment file', async () => {
const postmanEnvironment = {
"id": "some-id",
"name": "My Environment",
"values": [
{
"key": "var1",
"value": "value1",
"enabled": true,
"type": "text"
},
{
"key": "var2",
"value": "value2",
"enabled": false,
"type": "secret"
}
]
};
const brunoEnvironment = await postmanToBrunoEnvironment(postmanEnvironment);
const expectedEnvironment = {
name: 'My Environment',
variables: [
{
name: 'var1',
value: 'value1',
enabled: true,
secret: false,
},
{
name: 'var2',
value: 'value2',
enabled: false,
secret: true,
},
],
};
expect(brunoEnvironment).toEqual(expectedEnvironment);
});
it.skip('should throw Error when JSON parsing fails', async () => {
const invalidBrunoEnvironment = {
"id": "some-id",
"name": "My Environment",
"values": [
{
"key": "var1",
"value": "value1",
"enabled": true,
"type": "text"
}
]
}
await expect(postmanToBrunoEnvironment(invalidBrunoEnvironment)).rejects.toThrow(Error);
await expect(postmanToBrunoEnvironment(invalidBrunoEnvironment)).rejects.toThrow(
'Unable to parse the postman environment json file'
);
});
});

View File

@ -0,0 +1,726 @@
import { describe, it, expect } from '@jest/globals';
import postmanToBruno from '../../src/postman/postman-to-bruno';
describe('postman-collection', () => {
it('should correctly import a valid Postman collection file', async () => {
const brunoCollection = postmanToBruno(postmanCollection);
expect(brunoCollection).toMatchObject(expectedOutput);
});
});
const postmanCollection = {
"info": {
"_postman_id": "0596d399-cfd2-4f8f-9869-65238eb40a45",
"name": "CRUD",
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
"_exporter_id": "32111649",
"_collection_link": "https://www.postman.com/fudzi9/workspace/nodejs/collection/16541095-0596d399-cfd2-4f8f-9869-65238eb40a45?action=share&source=collection_link&creator=32111649"
},
"item": [
{
"name": "GET",
"request": {
"method": "GET",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"response": [
{
"name": "1.GET",
"originalRequest": {
"method": "GET",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "2"
},
{
"key": "Etag",
"value": "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 21:30:45 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "[]"
},
{
"name": "3.GET",
"originalRequest": {
"method": "GET",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "96"
},
{
"key": "Etag",
"value": "W/\"60-ixboSJswZpL0hV7rJrY1IE5nQlM\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 21:58:32 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "[\n {\n \"id\": 1,\n \"title\": \"first\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
},
{
"name": "5.GET",
"originalRequest": {
"method": "GET",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "192"
},
{
"key": "Etag",
"value": "W/\"c0-rg+VAYKuV+nAzdAnddMXRNSM3tg\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 22:01:36 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "[\n {\n \"id\": 1,\n \"title\": \"first\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n },\n {\n \"id\": 2,\n \"title\": \"second\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
},
{
"name": "7.GET",
"originalRequest": {
"method": "GET",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "199"
},
{
"key": "Etag",
"value": "W/\"c7-SBFGBh+BSdmKqSUIW4VDODIOnaI\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 22:38:51 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "[\n {\n \"id\": 2,\n \"title\": \"second\",\n \"content\": \"some text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n },\n {\n \"id\": 1,\n \"title\": \"first changed\",\n \"content\": \"new text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
},
{
"name": "9.GET",
"originalRequest": {
"method": "GET",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "103"
},
{
"key": "Etag",
"value": "W/\"67-aR9NxSbB5ab73lSksdIWZNuQyq8\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 22:40:55 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "[\n {\n \"id\": 1,\n \"title\": \"first changed\",\n \"content\": \"new text\",\n \"createdAt\": \"some date\",\n \"updatedAt\": \"some date\"\n }\n]"
}
]
},
{
"name": "POST",
"event": [
{
"listen": "prerequest",
"script": {
"exec": [
""
],
"type": "text/javascript"
}
},
{
"listen": "test",
"script": {
"exec": [
""
],
"type": "text/javascript"
}
}
],
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"id\": 1, \"title\": \"first\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"response": [
{
"name": "2.POST",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"id\": 1, \"title\": \"first\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "123"
},
{
"key": "Etag",
"value": "W/\"7b-Zs+ZSZvDSG55ZK90aBqfAjoxdAg\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 21:58:17 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "\"{\\\"id\\\": 1, \\\"title\\\": \\\"first\\\", \\\"content\\\": \\\"some text\\\", \\\"createdAt\\\": \\\"some date\\\", \\\"updatedAt\\\": \\\"some date\\\"}\""
}
]
},
{
"name": "POST",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"id\": 2, \"title\": \"second\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"response": [
{
"name": "4.POST",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"id\": 2, \"title\": \"second\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "https://node-task2.herokuapp.com/api/notes/"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "124"
},
{
"key": "Etag",
"value": "W/\"7c-vtAEN2HlKwhD6OkasvICg9Ni+g0\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 22:00:49 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "\"{\\\"id\\\": 2, \\\"title\\\": \\\"second\\\", \\\"content\\\": \\\"some text\\\", \\\"createdAt\\\": \\\"some date\\\", \\\"updatedAt\\\": \\\"some date\\\"}\""
}
]
},
{
"name": "PUT",
"request": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"id\": 1, \"title\": \"first changed\", \"content\": \"new text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "https://node-task2.herokuapp.com/api/notes/1"
},
"response": [
{
"name": "6.PUT",
"originalRequest": {
"method": "PUT",
"header": [],
"body": {
"mode": "raw",
"raw": "{\"id\": 1, \"title\": \"first changed\", \"content\": \"new text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": "https://node-task2.herokuapp.com/api/notes/1"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "130"
},
{
"key": "Etag",
"value": "W/\"82-QdzTirfdP1+K+iNOkslStk0OPpg\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 22:03:36 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "\"{\\\"id\\\": 1, \\\"title\\\": \\\"first changed\\\", \\\"content\\\": \\\"new text\\\", \\\"createdAt\\\": \\\"some date\\\", \\\"updatedAt\\\": \\\"some date\\\"}\""
}
]
},
{
"name": "DELETE",
"request": {
"method": "DELETE",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/2"
},
"response": [
{
"name": "8.DELETE",
"originalRequest": {
"method": "DELETE",
"header": [],
"url": "https://node-task2.herokuapp.com/api/notes/2"
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Server",
"value": "Cowboy"
},
{
"key": "Connection",
"value": "keep-alive"
},
{
"key": "X-Powered-By",
"value": "Express"
},
{
"key": "Content-Type",
"value": "application/json; charset=utf-8"
},
{
"key": "Content-Length",
"value": "23"
},
{
"key": "Etag",
"value": "W/\"17-bCXlhEBJSVIeQ+m1i+6p7+rrNak\""
},
{
"key": "Date",
"value": "Tue, 06 Jul 2021 22:40:08 GMT"
},
{
"key": "Via",
"value": "1.1 vegur"
}
],
"cookie": [],
"body": "{\n \"success\": true,\n \"id\": 2\n}"
}
]
}
]
};
const expectedOutput = {
"collection": {
"name": "CRUD",
"uid": "mockeduuidvalue123456",
"version": "1",
"items": [
{
"uid": "mockeduuidvalue123456",
"name": "GET",
"type": "http-request",
"request": {
"url": "https://node-task2.herokuapp.com/api/notes/",
"method": "GET",
"auth": {
"mode": "none",
"basic": null,
"bearer": null,
"awsv4": null
},
"headers": [],
"params": [],
"body": {
"mode": "none",
"json": null,
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "mockeduuidvalue123456",
"name": "POST",
"type": "http-request",
"request": {
"url": "https://node-task2.herokuapp.com/api/notes/",
"method": "POST",
"auth": {
"mode": "none",
"basic": null,
"bearer": null,
"awsv4": null
},
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\"id\": 1, \"title\": \"first\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
},
"script": {
"req": ""
},
"tests": ""
}
},
{
"uid": "mockeduuidvalue123456",
"name": "POST_1",
"type": "http-request",
"request": {
"url": "https://node-task2.herokuapp.com/api/notes/",
"method": "POST",
"auth": {
"mode": "none",
"basic": null,
"bearer": null,
"awsv4": null
},
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\"id\": 2, \"title\": \"second\", \"content\": \"some text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "mockeduuidvalue123456",
"name": "PUT",
"type": "http-request",
"request": {
"url": "https://node-task2.herokuapp.com/api/notes/1",
"method": "PUT",
"auth": {
"mode": "none",
"basic": null,
"bearer": null,
"awsv4": null
},
"headers": [],
"params": [],
"body": {
"mode": "json",
"json": "{\"id\": 1, \"title\": \"first changed\", \"content\": \"new text\", \"createdAt\": \"some date\", \"updatedAt\": \"some date\"}",
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
},
{
"uid": "mockeduuidvalue123456",
"name": "DELETE",
"type": "http-request",
"request": {
"url": "https://node-task2.herokuapp.com/api/notes/2",
"method": "DELETE",
"auth": {
"mode": "none",
"basic": null,
"bearer": null,
"awsv4": null
},
"headers": [],
"params": [],
"body": {
"mode": "none",
"json": null,
"text": null,
"xml": null,
"formUrlEncoded": [],
"multipartForm": []
}
}
}
],
"environments": [],
"root": {
"docs": "",
"meta": {
"name": "CRUD"
},
"request": {
"auth": {
"mode": "none",
"basic": null,
"bearer": null,
"awsv4": null
},
"headers": [],
"script": {},
"tests": "",
"vars": {}
}
}
}
};

View File

@ -1,4 +1,4 @@
const { postmanTranslation } = require('./postman_translation'); // Adjust path as needed
const { default: postmanTranslation } = require("../../src/postman/postman-translations");
describe('postmanTranslation function', () => {
test('should translate pm commands correctly', () => {
@ -11,9 +11,6 @@ describe('postmanTranslation function', () => {
pm.collectionVariables.set('key', 'value');
const data = pm.response.json();
pm.expect(pm.environment.has('key')).to.be.true;
postman.setEnvironmentVariable('key', 'value');
postman.getEnvironmentVariable('key');
postman.clearEnvironmentVariable('key');
`;
const expectedOutput = `
bru.getEnvVar('key');
@ -24,9 +21,6 @@ describe('postmanTranslation function', () => {
bru.setVar('key', 'value');
const data = res.getBody();
expect(bru.getEnvVar('key') !== undefined && bru.getEnvVar('key') !== null).to.be.true;
bru.setEnvVar('key', 'value');
bru.getEnvVar('key');
bru.deleteEnvVar('key');
`;
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
});
@ -53,7 +47,7 @@ describe('postmanTranslation function', () => {
test('should handle multiple pm commands on the same line', () => {
const inputScript = "pm.environment.get('key'); pm.environment.set('key', 'value');";
const expectedOutput = "bru.getEnvVar('key'); bru.setEnvVar('key', 'value');";
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
});
test('should handle comments and other JavaScript code', () => {
const inputScript = `
@ -157,13 +151,3 @@ test('should handle response commands', () => {
`;
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
});
test('should handle tests object', () => {
const inputScript = `
tests['Status code is 200'] = responseCode.code === 200;
`;
const expectedOutput = `
test("Status code is 200", function() { expect(Boolean(responseCode.code === 200)).to.be.true; });
`;
expect(postmanTranslation(inputScript)).toBe(expectedOutput);
});

View File

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES6",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"emitDeclarationOnly": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
},
"exclude": ["dist", "node_modules", "tests"]
}

View File

@ -0,0 +1,6 @@
export declare const uuid: () => string;
export declare const normalizeFileName: (name: string) => string;
export declare const validateSchema: (collection?: {}) => Promise<unknown>;
export declare const updateUidsInCollection: (_collection: any) => any;
export declare const transformItemsInCollection: (collection: any) => any;
export declare const hydrateSeqInCollection: (collection: any) => any;

View File

@ -74,6 +74,7 @@ async function setup() {
execCommand('npm run build:graphql-docs', 'Building graphql-docs');
execCommand('npm run build:bruno-query', 'Building bruno-query');
execCommand('npm run build:bruno-common', 'Building bruno-common');
execCommand('npm run build:bruno-converters', 'Building bruno-converters');
// Bundle JS sandbox libraries
execCommand(