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-electron",
"packages/bruno-cli", "packages/bruno-cli",
"packages/bruno-common", "packages/bruno-common",
"packages/bruno-converters",
"packages/bruno-schema", "packages/bruno-schema",
"packages/bruno-query", "packages/bruno-query",
"packages/bruno-js", "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": { "node_modules/@rollup/plugin-commonjs": {
"version": "23.0.7", "version": "23.0.7",
"resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-23.0.7.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-23.0.7.tgz",
@@ -7937,6 +7956,10 @@
"resolved": "packages/bruno-common", "resolved": "packages/bruno-common",
"link": true "link": true
}, },
"node_modules/@usebruno/converters": {
"resolved": "packages/bruno-converters",
"link": true
},
"node_modules/@usebruno/crypto-js": { "node_modules/@usebruno/crypto-js": {
"version": "3.1.9", "version": "3.1.9",
"resolved": "https://registry.npmjs.org/@usebruno/crypto-js/-/crypto-js-3.1.9.tgz", "resolved": "https://registry.npmjs.org/@usebruno/crypto-js/-/crypto-js-3.1.9.tgz",
@@ -26274,6 +26297,130 @@
"typescript": "^4.8.4" "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": { "packages/bruno-electron": {
"name": "bruno", "name": "bruno",
"version": "2.0.0", "version": "2.0.0",

View File

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

View File

@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import exportBrunoCollection from 'utils/collections/export'; import exportBrunoCollection from 'utils/collections/export';
import exportPostmanCollection from 'utils/exporters/postman-collection'; import exportPostmanCollection from 'utils/exporters/postman-collection';
import { toastError } from 'utils/common/error';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index'; 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 importBrunoCollection from 'utils/importers/bruno-collection';
import importPostmanCollection from 'utils/importers/postman-collection'; import importPostmanCollection from 'utils/importers/postman-collection';
import importInsomniaCollection from 'utils/importers/insomnia-collection'; import importInsomniaCollection from 'utils/importers/insomnia-collection';
@@ -7,14 +7,6 @@ import { toastError } from 'utils/common/error';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
const ImportCollection = ({ onClose, handleSubmit }) => { 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 = () => { const handleImportBrunoCollection = () => {
importBrunoCollection() importBrunoCollection()
.then(({ collection }) => { .then(({ collection }) => {
@@ -24,9 +16,9 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
}; };
const handleImportPostmanCollection = () => { const handleImportPostmanCollection = () => {
importPostmanCollection(options) importPostmanCollection()
.then(({ collection, translationLog }) => { .then(({ collection }) => {
handleSubmit({ collection, translationLog }); handleSubmit({ collection });
}) })
.catch((err) => toastError(err, 'Postman Import collection failed')); .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')); .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 }) => { const CollectionButton = ({ children, className, onClick }) => {
return ( return (
<button <button
@@ -77,31 +60,6 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
<CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton> <CollectionButton onClick={handleImportInsomniaCollection}>Insomnia Collection</CollectionButton>
<CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton> <CollectionButton onClick={handleImportOpenapiCollection}>OpenAPI V3 Spec</CollectionButton>
</div> </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> </div>
</Modal> </Modal>
); );

View File

@@ -4,105 +4,8 @@ import { useFormik } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions'; import { browseDirectory } from 'providers/ReduxStore/slices/collections/actions';
import Modal from 'components/Modal'; import Modal from 'components/Modal';
import { IconAlertTriangle, IconArrowRight, IconCaretDown, IconCaretRight, IconCopy } from '@tabler/icons';
import toast from 'react-hot-toast';
const TranslationLog = ({ translationLog }) => { const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName }) => {
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 inputRef = useRef(); const inputRef = useRef();
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -150,9 +53,6 @@ const ImportCollectionLocation = ({ onClose, handleSubmit, collectionName, trans
Name Name
</label> </label>
<div className="mt-2">{collectionName}</div> <div className="mt-2">{collectionName}</div>
{translationLog && Object.keys(translationLog).length > 0 && (
<TranslationLog translationLog={translationLog} />
)}
<> <>
<label htmlFor="collectionLocation" className="block font-semibold mt-3"> <label htmlFor="collectionLocation" className="block font-semibold mt-3">
Location Location

View File

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

View File

@@ -15,7 +15,6 @@ const Welcome = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { t } = useTranslation(); const { t } = useTranslation();
const [importedCollection, setImportedCollection] = useState(null); const [importedCollection, setImportedCollection] = useState(null);
const [importedTranslationLog, setImportedTranslationLog] = useState({});
const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false); const [createCollectionModalOpen, setCreateCollectionModalOpen] = useState(false);
const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false); const [importCollectionModalOpen, setImportCollectionModalOpen] = useState(false);
const [importCollectionLocationModalOpen, setImportCollectionLocationModalOpen] = 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'))); dispatch(openCollection()).catch((err) => console.log(err) && toast.error(t('WELCOME.COLLECTION_OPEN_ERROR')));
}; };
const handleImportCollection = ({ collection, translationLog }) => { const handleImportCollection = ({ collection }) => {
setImportedCollection(collection); setImportedCollection(collection);
if (translationLog) {
setImportedTranslationLog(translationLog);
}
setImportCollectionModalOpen(false); setImportCollectionModalOpen(false);
setImportCollectionLocationModalOpen(true); setImportCollectionLocationModalOpen(true);
}; };
@@ -55,7 +51,6 @@ const Welcome = () => {
) : null} ) : null}
{importCollectionLocationModalOpen ? ( {importCollectionLocationModalOpen ? (
<ImportCollectionLocation <ImportCollectionLocation
translationLog={importedTranslationLog}
collectionName={importedCollection.name} collectionName={importedCollection.name}
onClose={() => setImportCollectionLocationModalOpen(false)} onClose={() => setImportCollectionLocationModalOpen(false)}
handleSubmit={handleImportCollectionLocation} 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 each from 'lodash/each';
import get from 'lodash/get'; import get from 'lodash/get';
import { customAlphabet } from 'nanoid';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { uuid, normalizeFileName } from 'utils/common';
import { isItemARequest } from 'utils/collections';
import { collectionSchema } from '@usebruno/schema'; 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 = {}) => { export const validateSchema = (collection = {}) => {
return new Promise((resolve, reject) => { try {
collectionSchema collectionSchema.validateSync(collection);
.validate(collection) return collection;
.then(() => resolve(collection)) } catch (err) {
.catch((err) => { throw new Error('The Collection has an invalid schema');
console.log(err); }
reject(new BrunoError('The Collection file is corrupted'));
});
});
}; };
export const updateUidsInCollection = (_collection) => { export const updateUidsInCollection = (_collection) => {
@@ -117,3 +149,63 @@ export const hydrateSeqInCollection = (collection) => {
return 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 each from 'lodash/each';
import get from 'lodash/get'; import get from 'lodash/get';
import fileDialog from 'file-dialog'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
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]);
});
};
const parseGraphQL = (text) => { const parseGraphQL = (text) => {
try { try {
@@ -187,7 +159,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
return brunoRequestItem; return brunoRequestItem;
}; };
const parseInsomniaCollection = (data) => { const parseInsomniaCollection = (_insomniaCollection) => {
const brunoCollection = { const brunoCollection = {
name: '', name: '',
uid: uuid(), uid: uuid(),
@@ -196,14 +168,13 @@ const parseInsomniaCollection = (data) => {
environments: [] environments: []
}; };
return new Promise((resolve, reject) => {
try { try {
const insomniaExport = data; const insomniaExport = _insomniaCollection;
const insomniaResources = get(insomniaExport, 'resources', []); const insomniaResources = get(insomniaExport, 'resources', []);
const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace'); const insomniaCollection = insomniaResources.find((resource) => resource._type === 'workspace');
if (!insomniaCollection) { if (!insomniaCollection) {
reject(new BrunoError('Collection not found inside Insomnia export')); throw new Error('Collection not found inside Insomnia export');
} }
brunoCollection.name = insomniaCollection.name; brunoCollection.name = insomniaCollection.name;
@@ -234,28 +205,24 @@ const parseInsomniaCollection = (data) => {
return folders.concat(requests.map(transformInsomniaRequestItem)); return folders.concat(requests.map(transformInsomniaRequestItem));
} }
(brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id)), brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);
resolve(brunoCollection); return brunoCollection;
} catch (err) { } catch (err) {
reject(new BrunoError('An error occurred while parsing the Insomnia collection')); throw new Error('An error occurred while parsing the Insomnia collection');
} }
});
}; };
const importCollection = () => { export const insomniaToBruno = (insomniaCollection) => {
return new Promise((resolve, reject) => { try {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' }) const collection = parseInsomniaCollection(insomniaCollection);
.then(readFile) const transformedCollection = transformItemsInCollection(collection);
.then(parseInsomniaCollection) const hydratedCollection = hydrateSeqInCollection(transformedCollection);
.then(transformItemsInCollection) const validatedCollection = validateSchema(hydratedCollection);
.then(hydrateSeqInCollection) return validatedCollection;
.then(validateSchema) } catch (err) {
.then((collection) => resolve({ collection }))
.catch((err) => {
console.error(err); console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message)); throw new Error('Import collection failed');
}); }
});
}; };
export default importCollection; export default insomniaToBruno;

View File

@@ -1,34 +1,6 @@
import jsyaml from 'js-yaml';
import each from 'lodash/each'; import each from 'lodash/each';
import get from 'lodash/get'; import get from 'lodash/get';
import fileDialog from 'file-dialog'; import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
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]);
});
};
const ensureUrl = (url) => { const ensureUrl = (url) => {
// removing multiple slashes after the protocol if it exists, or after the beginning of the string otherwise // 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: [], items: [],
environments: [] environments: []
}; };
return new Promise((resolve, reject) => {
try { try {
const collectionData = resolveRefs(data); const collectionData = resolveRefs(data);
if (!collectionData) { if (!collectionData) {
reject(new BrunoError('Invalid OpenAPI collection. Failed to resolve refs.')); throw new Error('Invalid OpenAPI collection. Failed to resolve refs.');
return; return;
} }
@@ -377,7 +347,7 @@ export const parseOpenApiCollection = (data) => {
// Assumes v3 if not defined. v2 is not supported yet // Assumes v3 if not defined. v2 is not supported yet
if (collectionData.openapi && !collectionData.openapi.startsWith('3')) { 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; return;
} }
@@ -443,28 +413,24 @@ export const parseOpenApiCollection = (data) => {
let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem); let ungroupedItems = ungroupedRequests.map(transformOpenapiRequestItem);
let brunoCollectionItems = brunoFolders.concat(ungroupedItems); let brunoCollectionItems = brunoFolders.concat(ungroupedItems);
brunoCollection.items = brunoCollectionItems; brunoCollection.items = brunoCollectionItems;
resolve(brunoCollection); return brunoCollection;
} catch (err) { } catch (err) {
console.error(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 = () => { export const openApiToBruno = (openApiSpecification) => {
return new Promise((resolve, reject) => { try {
fileDialog({ accept: '.json, .yaml, .yml, application/json, application/yaml, application/x-yaml' }) const collection = parseOpenApiCollection(openApiSpecification);
.then(readFile) const transformedCollection = transformItemsInCollection(collection);
.then(parseOpenApiCollection) const hydratedCollection = hydrateSeqInCollection(transformedCollection);
.then(transformItemsInCollection) const validatedCollection = validateSchema(hydratedCollection);
.then(hydrateSeqInCollection) return validatedCollection
.then(validateSchema) } catch (err) {
.then((collection) => resolve({ collection }))
.catch((err) => {
console.error(err); console.error(err);
reject(new BrunoError('Import collection failed: ' + err.message)); throw new Error('Import collection failed');
}); }
});
}; };
export default importCollection; export default openApiToBruno;

View File

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

View File

@@ -1,16 +1,4 @@
import each from 'lodash/each'; 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) => { const isSecret = (type) => {
return type === 'secret'; return type === 'secret';
}; };
@@ -40,42 +28,13 @@ const importPostmanEnvironment = (environment) => {
return brunoEnvironment; return brunoEnvironment;
}; };
const parsePostmanEnvironment = (str) => { export const postmanToBrunoEnvironment = (postmanEnvironment) => {
return new Promise((resolve, reject) => {
try { try {
let environment = JSON.parse(str); return importPostmanEnvironment(postmanEnvironment);
return resolve(importPostmanEnvironment(environment));
} catch (err) { } catch (err) {
console.log(err); console.log(err);
if (err instanceof BrunoError) { throw new Error('Unable to parse the postman environment json file');
return reject(err);
} }
return reject(new BrunoError('Unable to parse the postman environment json file'));
}
});
}; };
const importEnvironment = () => { export default postmanToBrunoEnvironment;
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;

View File

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

View File

@@ -42,7 +42,7 @@ const compiledReplacements = Object.entries(extendedReplacements).map(([pattern,
replacement replacement
})); }));
export const postmanTranslation = (script, logCallback) => { const postmanTranslation = (script) => {
try { try {
let modifiedScript = script; let modifiedScript = script;
let modified = false; let modified = false;
@@ -54,10 +54,11 @@ export const postmanTranslation = (script, logCallback) => {
} }
if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) { if (modifiedScript.includes('pm.') || modifiedScript.includes('postman.')) {
modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1'); modifiedScript = modifiedScript.replace(/^(.*(pm\.|postman\.).*)$/gm, '// $1');
//logCallback?.();
} }
return modifiedScript; return modifiedScript;
} catch (e) { } catch (e) {
return script; 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', () => { describe('transformUrl', () => {
it('should handle basic URL with path variables', () => { 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', () => { describe('postmanTranslation function', () => {
test('should translate pm commands correctly', () => { test('should translate pm commands correctly', () => {
@@ -11,9 +11,6 @@ describe('postmanTranslation function', () => {
pm.collectionVariables.set('key', 'value'); pm.collectionVariables.set('key', 'value');
const data = pm.response.json(); const data = pm.response.json();
pm.expect(pm.environment.has('key')).to.be.true; pm.expect(pm.environment.has('key')).to.be.true;
postman.setEnvironmentVariable('key', 'value');
postman.getEnvironmentVariable('key');
postman.clearEnvironmentVariable('key');
`; `;
const expectedOutput = ` const expectedOutput = `
bru.getEnvVar('key'); bru.getEnvVar('key');
@@ -24,9 +21,6 @@ describe('postmanTranslation function', () => {
bru.setVar('key', 'value'); bru.setVar('key', 'value');
const data = res.getBody(); const data = res.getBody();
expect(bru.getEnvVar('key') !== undefined && bru.getEnvVar('key') !== null).to.be.true; 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); expect(postmanTranslation(inputScript)).toBe(expectedOutput);
}); });
@@ -157,13 +151,3 @@ test('should handle response commands', () => {
`; `;
expect(postmanTranslation(inputScript)).toBe(expectedOutput); 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:graphql-docs', 'Building graphql-docs');
execCommand('npm run build:bruno-query', 'Building bruno-query'); execCommand('npm run build:bruno-query', 'Building bruno-query');
execCommand('npm run build:bruno-common', 'Building bruno-common'); execCommand('npm run build:bruno-common', 'Building bruno-common');
execCommand('npm run build:bruno-converters', 'Building bruno-converters');
// Bundle JS sandbox libraries // Bundle JS sandbox libraries
execCommand( execCommand(