export & import in opencollection format (#6329)

This commit is contained in:
naman-bruno
2025-12-24 22:28:38 +05:30
committed by GitHub
parent 1b8eece173
commit 67903f26bc
33 changed files with 3180 additions and 222 deletions

View File

@@ -26,6 +26,21 @@ const StyledWrapper = styled.div`
}
}
}
.share-button {
display: flex;
border-radius: ${(props) => props.theme.border.radius.base};
padding: 10px;
border: 1px solid ${(props) => props.theme.sidebar.collection.item.indentBorder};
background-color: ${(props) => props.theme.sidebar.bg};
color: ${(props) => props.theme.text};
cursor: pointer;
transition: all 0.1s ease;
&:hover {
background-color: ${(props) => props.theme.listItem.hoverBg};
}
}
`;
export default StyledWrapper;

View File

@@ -1,10 +1,11 @@
import React, { useMemo } from 'react';
import Modal from 'components/Modal';
import { IconUpload, IconLoader2, IconAlertTriangle } from '@tabler/icons';
import { IconUpload, IconLoader2, IconAlertTriangle, IconFileExport } from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
import Bruno from 'components/Bruno';
import exportBrunoCollection from 'utils/collections/export';
import exportPostmanCollection from 'utils/exporters/postman-collection';
import exportOpenCollection from 'utils/exporters/opencollection';
import { cloneDeep } from 'lodash';
import { transformCollectionToSaveToExportAsFile } from 'utils/collections/index';
import { useSelector } from 'react-redux';
@@ -48,6 +49,12 @@ const ShareCollection = ({ onClose, collectionUid }) => {
onClose();
};
const handleExportOpenCollection = () => {
const collectionCopy = cloneDeep(collection);
exportOpenCollection(transformCollectionToSaveToExportAsFile(collectionCopy));
onClose();
};
return (
<Modal
size="md"
@@ -60,10 +67,10 @@ const ShareCollection = ({ onClose, collectionUid }) => {
<StyledWrapper className="flex flex-col h-full w-[500px]">
<div className="space-y-2">
<div
className={`flex border border-gray-200 dark:border-gray-600 items-center p-3 rounded-lg transition-colors ${
className={`share-button ${
isCollectionLoading
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
: 'cursor-pointer'
}`}
onClick={isCollectionLoading ? undefined : handleExportBrunoCollection}
>
@@ -77,10 +84,31 @@ const ShareCollection = ({ onClose, collectionUid }) => {
</div>
<div
className={`flex flex-col border border-gray-200 dark:border-gray-600 items-center rounded-lg transition-colors ${
className={`share-button ${
isCollectionLoading
? 'opacity-50 cursor-not-allowed'
: 'hover:bg-gray-100 dark:hover:bg-gray-500/10 cursor-pointer'
: 'cursor-pointer'
}`}
onClick={isCollectionLoading ? undefined : handleExportOpenCollection}
>
<div className="mr-3 p-1 rounded-full">
{isCollectionLoading ? (
<IconLoader2 size={28} className="animate-spin" />
) : (
<IconFileExport size={28} strokeWidth={1} />
)}
</div>
<div className="flex-1">
<div className="font-medium">OpenCollection</div>
<div className="text-xs">{isCollectionLoading ? 'Loading collection...' : 'Export in OpenCollection format'}</div>
</div>
</div>
<div
className={`flex !flex-col share-button ${
isCollectionLoading
? 'opacity-50 cursor-not-allowed'
: 'cursor-pointer'
}`}
onClick={isCollectionLoading ? undefined : handleExportPostmanCollection}
>

View File

@@ -8,6 +8,7 @@ import { isInsomniaCollection } from 'utils/importers/insomnia-collection';
import { isOpenApiSpec } from 'utils/importers/openapi-collection';
import { isWSDLCollection } from 'utils/importers/wsdl-collection';
import { isBrunoCollection } from 'utils/importers/bruno-collection';
import { isOpenCollection } from 'utils/importers/opencollection';
import FullscreenLoader from './FullscreenLoader/index';
const convertFileToObject = async (file) => {
@@ -72,6 +73,8 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
type = 'postman';
} else if (isInsomniaCollection(data)) {
type = 'insomnia';
} else if (isOpenCollection(data)) {
type = 'opencollection';
} else if (isBrunoCollection(data)) {
type = 'bruno';
} else {
@@ -159,7 +162,7 @@ const ImportCollection = ({ onClose, handleSubmit }) => {
</button>
</p>
<p className="text-xs text-gray-500 dark:text-gray-400">
Supports Bruno, Postman, Insomnia, OpenAPI v3, and WSDL formats
Supports Bruno, OpenCollection, Postman, Insomnia, OpenAPI v3, and WSDL formats
</p>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import { postmanToBruno } from 'utils/importers/postman-collection';
import { convertInsomniaToBruno } from 'utils/importers/insomnia-collection';
import { convertOpenapiToBruno } from 'utils/importers/openapi-collection';
import { processBrunoCollection } from 'utils/importers/bruno-collection';
import { processOpenCollection } from 'utils/importers/opencollection';
import { wsdlToBruno } from '@usebruno/converters';
import { toastError } from 'utils/common/error';
import Modal from 'components/Modal';
@@ -37,6 +38,8 @@ const getCollectionName = (format, rawData) => {
return rawData.name || 'Insomnia Collection';
case 'bruno':
return rawData.name || 'Bruno Collection';
case 'opencollection':
return rawData.info?.name || 'OpenCollection';
case 'wsdl':
return 'WSDL Collection';
default:
@@ -65,6 +68,9 @@ const convertCollection = async (format, rawData, groupingType) => {
case 'bruno':
collection = await processBrunoCollection(rawData);
break;
case 'opencollection':
collection = await processOpenCollection(rawData);
break;
default:
throw new Error('Unknown collection format');
}

View File

@@ -36,6 +36,10 @@ const transformCollection = async (collection, type) => {
const { convertOpenapiToBruno } = await import('utils/importers/openapi-collection');
return convertOpenapiToBruno(collection);
}
case 'opencollection': {
const { processOpenCollection } = await import('utils/importers/opencollection');
return processOpenCollection(collection);
}
case 'wsdl': {
const { wsdlToBruno } = await import('@usebruno/converters');
return wsdlToBruno(collection);

View File

@@ -0,0 +1,23 @@
import * as FileSaver from 'file-saver';
import jsyaml from 'js-yaml';
import { brunoToOpenCollection } from '@usebruno/converters';
import { sanitizeName } from 'utils/common/regex';
export const exportCollection = (collection) => {
const openCollection = brunoToOpenCollection(collection);
const yamlContent = jsyaml.dump(openCollection, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false
});
const sanitizedName = sanitizeName(collection.name);
const fileName = `${sanitizedName}.yml`;
const fileBlob = new Blob([yamlContent], { type: 'application/x-yaml' });
FileSaver.saveAs(fileBlob, fileName);
};
export default exportCollection;

View File

@@ -0,0 +1,81 @@
import each from 'lodash/each';
import { uuid } from 'utils/common';
import { BrunoError } from 'utils/common/error';
import { validateSchema, updateUidsInCollection, hydrateSeqInCollection } from './common';
import { openCollectionToBruno } from '@usebruno/converters';
const addUidsToRoot = (collection) => {
if (collection.root?.request?.headers) {
each(collection.root.request.headers, (header) => {
header.uid = uuid();
});
}
if (collection.root?.request?.vars?.req) {
each(collection.root.request.vars.req, (v) => {
v.uid = uuid();
});
}
if (collection.root?.request?.vars?.res) {
each(collection.root.request.vars.res, (v) => {
v.uid = uuid();
});
}
const addUidsToFolderRoot = (items) => {
each(items, (item) => {
if (item.type === 'folder') {
if (item.root?.request?.headers) {
each(item.root.request.headers, (header) => {
header.uid = uuid();
});
}
if (item.root?.request?.vars?.req) {
each(item.root.request.vars.req, (v) => {
v.uid = uuid();
});
}
if (item.root?.request?.vars?.res) {
each(item.root.request.vars.res, (v) => {
v.uid = uuid();
});
}
if (item.items?.length) {
addUidsToFolderRoot(item.items);
}
}
});
};
addUidsToFolderRoot(collection.items);
return collection;
};
export const processOpenCollection = async (jsonData) => {
try {
let collection = openCollectionToBruno(jsonData);
collection = hydrateSeqInCollection(collection);
collection = updateUidsInCollection(collection);
collection = addUidsToRoot(collection);
await validateSchema(collection);
return collection;
} catch (err) {
console.error('Error processing OpenCollection:', err);
throw new BrunoError('Import OpenCollection failed');
}
};
export const isOpenCollection = (data) => {
if (typeof data !== 'object' || data === null) {
return false;
}
if (typeof data.opencollection !== 'string' || !data.opencollection.trim()) {
return false;
}
if (typeof data.info !== 'object' || data.info === null) {
return false;
}
return true;
};

View File

@@ -4,7 +4,6 @@
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"src",
@@ -29,6 +28,8 @@
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@opencollection/types": "0.3.0",
"@usebruno/schema-types": "0.0.1",
"@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^23.0.2",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -1,43 +1,89 @@
const { nodeResolve } = require('@rollup/plugin-node-resolve');
const commonjs = require('@rollup/plugin-commonjs');
const typescript = require('@rollup/plugin-typescript');
const { terser } = require('rollup-plugin-terser');
const peerDepsExternal = require('rollup-plugin-peer-deps-external');
const { copy } = require('@web/rollup-plugin-copy');
const path = require('path');
const packageJson = require('./package.json');
const alias = require('@rollup/plugin-alias');
const path = require('path');
const externalDeps = [
'@usebruno/schema',
'@usebruno/schema-types',
/@usebruno\/schema-types\/.*/,
'@opencollection/types',
/@opencollection\/types\/.*/,
// Runtime dependencies
'lodash',
'lodash/each',
'lodash/get',
'lodash/cloneDeep',
'lodash/map',
'js-yaml',
'jscodeshift',
'nanoid',
'xml2js',
// Node built-ins
'path',
'fs'
];
module.exports = [
{
input: 'src/index.js',
output: [
{
dir: path.dirname(packageJson.main),
format: 'cjs',
sourcemap: true
},
{
dir: path.dirname(packageJson.module),
format: 'esm',
sourcemap: true
}
],
output: {
dir: path.dirname(packageJson.main),
format: 'cjs',
sourcemap: true,
exports: 'named',
entryFileNames: 'index.js'
},
plugins: [
peerDepsExternal(),
nodeResolve({
preferBuiltins: true,
extensions: ['.js', '.css'] // Resolve .js files
extensions: ['.js', '.ts', '.tsx', '.json']
}),
commonjs(),
terser(),
alias({
entries: [{ find: 'src', replacement: path.resolve(__dirname, 'src') }]
typescript({
tsconfig: './tsconfig.json',
sourceMap: true,
outDir: path.dirname(packageJson.main)
}),
terser(),
copy({
patterns: 'src/workers/scripts/**/*',
rootDir: '.'
})
]
],
external: externalDeps
},
{
input: 'src/index.js',
output: {
dir: path.dirname(packageJson.module),
format: 'esm',
sourcemap: true,
exports: 'named',
entryFileNames: 'index.js'
},
plugins: [
peerDepsExternal(),
nodeResolve({
extensions: ['.js', '.ts', '.tsx', '.json']
}),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
sourceMap: true,
outDir: path.dirname(packageJson.module)
}),
terser(),
copy({
patterns: 'src/workers/scripts/**/*',
rootDir: '.'
})
],
external: externalDeps
}
];

View File

@@ -5,3 +5,5 @@ export { default as openApiToBruno } from './openapi/openapi-to-bruno.js';
export { default as insomniaToBruno } from './insomnia/insomnia-to-bruno.js';
export { default as wsdlToBruno } from './wsdl/wsdl-to-bruno.js';
export { default as postmanTranslation } from './postman/postman-translations.js';
export { openCollectionToBruno } from './opencollection/opencollection-to-bruno.js';
export { brunoToOpenCollection } from './opencollection/bruno-to-opencollection.js';

View File

@@ -0,0 +1,172 @@
import { toOpenCollectionAuth, toOpenCollectionHeaders, toOpenCollectionScripts, toOpenCollectionVariables } from "./common";
import { toOpenCollectionEnvironments } from "./environment";
import { toOpenCollectionFolder } from "./folder";
import { toOpenCollectionItems } from "./items";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, ClientCertificate, CollectionConfig, OpenCollection, PemCertificate, Pkcs12Certificate, Protobuf } from "./types";
const toOpenCollectionConfig = (brunoConfig: BrunoConfig | undefined): CollectionConfig | undefined => {
if (!brunoConfig) {
return undefined;
}
const config: CollectionConfig = {};
if (brunoConfig.protobuf?.protoFiles?.length || brunoConfig.protobuf?.importPaths?.length) {
config.protobuf = {} as Protobuf;
if (brunoConfig.protobuf.protoFiles?.length) {
config.protobuf.protoFiles = brunoConfig.protobuf.protoFiles.map((f) => ({
type: 'file' as const,
path: f.path
}));
}
if (brunoConfig.protobuf.importPaths?.length) {
config.protobuf.importPaths = brunoConfig.protobuf.importPaths.map((p) => {
const importPath: { path: string; disabled?: boolean } = { path: p.path };
if (p.disabled) {
importPath.disabled = true;
}
return importPath;
});
}
}
if (brunoConfig.proxy?.enabled) {
if (brunoConfig.proxy.enabled === 'global') {
config.proxy = 'inherit';
} else {
config.proxy = {
protocol: brunoConfig.proxy.protocol || 'http',
hostname: brunoConfig.proxy.hostname || '',
port: brunoConfig.proxy.port || 0
};
if (brunoConfig.proxy.auth?.enabled) {
config.proxy.auth = {
username: brunoConfig.proxy.auth.username || '',
password: brunoConfig.proxy.auth.password || ''
};
}
if (brunoConfig.proxy.bypassProxy) {
config.proxy.bypassProxy = brunoConfig.proxy.bypassProxy;
}
}
}
if (brunoConfig.clientCertificates?.certs?.length) {
config.clientCertificates = brunoConfig.clientCertificates.certs
.map((cert): ClientCertificate | null => {
if (cert.type === 'pem') {
const pemCert: PemCertificate = {
domain: cert.domain || '',
type: 'pem',
certificateFilePath: cert.certFilePath || '',
privateKeyFilePath: cert.keyFilePath || ''
};
if (cert.passphrase) {
pemCert.passphrase = cert.passphrase;
}
return pemCert;
} else if (cert.type === 'pkcs12') {
const pkcs12Cert: Pkcs12Certificate = {
domain: cert.domain || '',
type: 'pkcs12',
pkcs12FilePath: cert.pfxFilePath || ''
};
if (cert.passphrase) {
pkcs12Cert.passphrase = cert.passphrase;
}
return pkcs12Cert;
}
return null;
})
.filter((cert): cert is ClientCertificate => cert !== null);
}
return Object.keys(config).length > 0 ? config : undefined;
};
const hasRequestDefaults = (root: BrunoCollectionRoot | undefined): boolean => {
const request = root?.request;
return Boolean(
request?.headers?.length ||
request?.vars?.req?.length ||
request?.script?.req ||
request?.script?.res ||
request?.tests ||
(request?.auth && request.auth.mode !== 'none')
);
};
export const brunoToOpenCollection = (collection: BrunoCollection): OpenCollection => {
const openCollection: OpenCollection = {
opencollection: '1.0.0',
info: {
name: collection.name || 'Untitled Collection'
}
};
const config = toOpenCollectionConfig(collection.brunoConfig as BrunoConfig);
if (config) {
openCollection.config = config;
}
const environments = toOpenCollectionEnvironments(collection.environments ?? undefined);
if (environments?.length) {
if (!openCollection.config) {
openCollection.config = {};
}
openCollection.config.environments = environments;
}
const items = toOpenCollectionItems(collection.items, toOpenCollectionFolder);
if (items.length) {
openCollection.items = items as OpenCollection['items'];
}
if (hasRequestDefaults(collection.root as BrunoCollectionRoot)) {
const request = (collection.root as BrunoCollectionRoot)?.request;
openCollection.request = {};
const headers = toOpenCollectionHeaders(request?.headers);
if (headers) {
openCollection.request.headers = headers;
}
const auth = toOpenCollectionAuth(request?.auth);
if (auth) {
openCollection.request.auth = auth;
}
const variables = toOpenCollectionVariables(request?.vars);
if (variables) {
openCollection.request.variables = variables;
}
const scripts = toOpenCollectionScripts(request as any);
if (scripts) {
openCollection.request.scripts = scripts;
}
}
if ((collection.root as BrunoCollectionRoot)?.docs) {
openCollection.docs = {
content: (collection.root as BrunoCollectionRoot).docs!,
type: 'text/markdown'
};
}
openCollection.bundled = true;
const extensions: { ignore?: string[] } = {};
if ((collection.brunoConfig as BrunoConfig)?.ignore?.length) {
extensions.ignore = (collection.brunoConfig as BrunoConfig).ignore;
}
if (Object.keys(extensions).length > 0) {
openCollection.extensions = extensions;
}
return openCollection;
};

View File

@@ -0,0 +1,83 @@
import { uuid } from '../../common/index.js';
import type {
Action,
ActionSetVariable,
ActionVariableScope,
BrunoVariable,
BrunoVariables
} from '../types';
/**
* Convert Bruno post-response variables to OpenCollection actions.
* Post-response variables in Bruno are converted to 'set-variable' actions
* with phase 'after-response'.
*/
export const toOpenCollectionActions = (resVariables: BrunoVariables | null | undefined): Action[] | undefined => {
if (!resVariables?.length) {
return undefined;
}
const actions: Action[] = resVariables.map((v: BrunoVariable): ActionSetVariable => {
const action: ActionSetVariable = {
type: 'set-variable',
phase: 'after-response',
selector: {
expression: v.value || '',
method: 'jsonq'
},
variable: {
name: v.name || '',
scope: v.local ? 'request' : 'runtime' as ActionVariableScope
}
};
if (v.description && typeof v.description === 'string' && v.description.trim().length) {
action.description = v.description;
}
if (v.enabled === false) {
action.disabled = true;
}
return action;
});
return actions.length > 0 ? actions : undefined;
};
/**
* Convert OpenCollection actions to Bruno post-response variables.
* Only 'set-variable' actions with phase 'after-response' are converted.
*/
export const fromOpenCollectionActions = (actions: Action[] | null | undefined): BrunoVariables => {
if (!actions?.length) {
return [];
}
const resVars: BrunoVariables = [];
actions.forEach((action: Action) => {
// Only process 'set-variable' actions with 'after-response' phase
if (action.type === 'set-variable' && action.phase === 'after-response') {
const setVarAction = action as ActionSetVariable;
const variable: BrunoVariable = {
uid: uuid(),
name: setVarAction.variable?.name || '',
value: setVarAction.selector?.expression || '',
enabled: setVarAction.disabled !== true,
local: setVarAction.variable?.scope === 'request'
};
if (setVarAction.description) {
variable.description = typeof setVarAction.description === 'string'
? setVarAction.description
: (setVarAction.description as { content?: string })?.content || '';
}
resVars.push(variable);
}
});
return resVars;
};

View File

@@ -0,0 +1,48 @@
import { uuid } from '../../common/index.js';
import type {
Assertion,
BrunoKeyValue
} from '../types';
export const fromOpenCollectionAssertions = (assertions: Assertion[] | undefined): BrunoKeyValue[] => {
if (!assertions?.length) {
return [];
}
return assertions.map((a): BrunoKeyValue => ({
uid: uuid(),
name: a.expression || '',
value: `${a.operator || 'eq'} ${a.value || ''}`.trim(),
description: typeof a.description === 'string' ? a.description : a.description?.content || null,
enabled: a.disabled !== true
}));
};
export const toOpenCollectionAssertions = (assertions: BrunoKeyValue[] | null | undefined): Assertion[] | undefined => {
if (!assertions?.length) {
return undefined;
}
return assertions.map((a): Assertion => {
const valueStr = a.value || '';
const parts = valueStr.split(' ');
const operator = parts[0] || 'eq';
const value = parts.slice(1).join(' ');
const ocAssertion: Assertion = {
expression: a.name || '',
operator,
value
};
if (a.enabled === false) {
ocAssertion.disabled = true;
}
if (a.description && typeof a.description === 'string' && a.description.trim().length) {
ocAssertion.description = a.description;
}
return ocAssertion;
});
};

View File

@@ -0,0 +1,470 @@
import type {
Auth,
AuthBasic,
AuthBearer,
AuthDigest,
AuthNTLM,
AuthAwsV4,
AuthApiKey,
AuthWsse,
AuthOAuth2,
BrunoAuth,
BrunoOAuth2
} from '../types';
const fromOpenCollectionOAuth2 = (auth: AuthOAuth2): BrunoAuth => {
const getTokenPlacement = (tokenConfig: AuthOAuth2['tokenConfig']): string => {
if (tokenConfig?.placement && 'query' in tokenConfig.placement) {
return 'query';
}
return 'header';
};
const getTokenHeaderPrefix = (tokenConfig: AuthOAuth2['tokenConfig']): string => {
if (tokenConfig?.placement && 'header' in tokenConfig.placement) {
return tokenConfig.placement.header;
}
return 'Bearer';
};
const getTokenQueryKey = (tokenConfig: AuthOAuth2['tokenConfig']): string => {
if (tokenConfig?.placement && 'query' in tokenConfig.placement) {
return tokenConfig.placement.query;
}
return 'access_token';
};
const getCredentialsPlacement = (credentials: AuthOAuth2['credentials']): 'body' | 'basic_auth_header' => {
if (credentials && 'placement' in credentials && credentials.placement === 'basic_auth_header') {
return 'basic_auth_header';
}
return 'body';
};
const buildOAuth2Config = (base: Partial<BrunoOAuth2>): BrunoAuth => {
const brunoAuth: BrunoAuth = {
mode: 'oauth2',
awsv4: null,
basic: null,
bearer: null,
digest: null,
ntlm: null,
oauth2: {
grantType: base.grantType || 'client_credentials',
username: base.username || null,
password: base.password || null,
callbackUrl: base.callbackUrl || null,
authorizationUrl: base.authorizationUrl || null,
accessTokenUrl: base.accessTokenUrl || null,
clientId: base.clientId || null,
clientSecret: base.clientSecret || null,
scope: base.scope || null,
state: base.state || null,
pkce: base.pkce || null,
credentialsPlacement: base.credentialsPlacement || null,
credentialsId: base.credentialsId || null,
tokenPlacement: base.tokenPlacement || null,
tokenHeaderPrefix: base.tokenHeaderPrefix || null,
tokenQueryKey: base.tokenQueryKey || null,
refreshTokenUrl: base.refreshTokenUrl || null,
autoRefreshToken: base.autoRefreshToken || null,
autoFetchToken: base.autoFetchToken || null,
additionalParameters: null
},
wsse: null,
apikey: null
};
return brunoAuth;
};
switch (auth.flow) {
case 'client_credentials':
return buildOAuth2Config({
grantType: 'client_credentials',
accessTokenUrl: auth.accessTokenUrl || null,
refreshTokenUrl: auth.refreshTokenUrl || null,
clientId: auth.credentials?.clientId || null,
clientSecret: auth.credentials?.clientSecret || null,
scope: auth.scope || null,
credentialsPlacement: getCredentialsPlacement(auth.credentials),
credentialsId: auth.tokenConfig?.id || 'credentials',
tokenPlacement: getTokenPlacement(auth.tokenConfig),
tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),
tokenQueryKey: getTokenQueryKey(auth.tokenConfig),
autoFetchToken: auth.settings?.autoFetchToken !== false,
autoRefreshToken: auth.settings?.autoRefreshToken !== false
});
case 'resource_owner_password_credentials':
return buildOAuth2Config({
grantType: 'password',
accessTokenUrl: auth.accessTokenUrl || null,
refreshTokenUrl: auth.refreshTokenUrl || null,
clientId: auth.credentials?.clientId || null,
clientSecret: auth.credentials?.clientSecret || null,
username: auth.resourceOwner?.username || null,
password: auth.resourceOwner?.password || null,
scope: auth.scope || null,
credentialsPlacement: getCredentialsPlacement(auth.credentials),
credentialsId: auth.tokenConfig?.id || 'credentials',
tokenPlacement: getTokenPlacement(auth.tokenConfig),
tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),
tokenQueryKey: getTokenQueryKey(auth.tokenConfig),
autoFetchToken: auth.settings?.autoFetchToken !== false,
autoRefreshToken: auth.settings?.autoRefreshToken !== false
});
case 'authorization_code':
return buildOAuth2Config({
grantType: 'authorization_code',
authorizationUrl: auth.authorizationUrl || null,
accessTokenUrl: auth.accessTokenUrl || null,
refreshTokenUrl: auth.refreshTokenUrl || null,
callbackUrl: auth.callbackUrl || null,
clientId: auth.credentials?.clientId || null,
clientSecret: auth.credentials?.clientSecret || null,
scope: auth.scope || null,
pkce: auth.pkce?.enabled || null,
credentialsPlacement: getCredentialsPlacement(auth.credentials),
credentialsId: auth.tokenConfig?.id || 'credentials',
tokenPlacement: getTokenPlacement(auth.tokenConfig),
tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),
tokenQueryKey: getTokenQueryKey(auth.tokenConfig),
autoFetchToken: auth.settings?.autoFetchToken !== false,
autoRefreshToken: auth.settings?.autoRefreshToken !== false
});
case 'implicit':
return buildOAuth2Config({
grantType: 'implicit',
authorizationUrl: auth.authorizationUrl || null,
callbackUrl: auth.callbackUrl || null,
clientId: auth.credentials?.clientId || null,
scope: auth.scope || null,
state: auth.state || null,
credentialsId: auth.tokenConfig?.id || 'credentials',
tokenPlacement: getTokenPlacement(auth.tokenConfig),
tokenHeaderPrefix: getTokenHeaderPrefix(auth.tokenConfig),
tokenQueryKey: getTokenQueryKey(auth.tokenConfig),
autoFetchToken: auth.settings?.autoFetchToken !== false
});
default:
return {
mode: 'none',
awsv4: null,
basic: null,
bearer: null,
digest: null,
ntlm: null,
oauth2: null,
wsse: null,
apikey: null
};
}
};
export const fromOpenCollectionAuth = (auth: Auth | undefined): BrunoAuth => {
const defaultAuth: BrunoAuth = {
mode: 'none',
awsv4: null,
basic: null,
bearer: null,
digest: null,
ntlm: null,
oauth2: null,
wsse: null,
apikey: null
};
if (!auth) {
return defaultAuth;
}
if (auth === 'inherit') {
return { ...defaultAuth, mode: 'inherit' };
}
switch (auth.type) {
case 'basic': {
const basicAuth = auth as AuthBasic;
return {
...defaultAuth,
mode: 'basic',
basic: {
username: basicAuth.username || null,
password: basicAuth.password || null
}
};
}
case 'bearer': {
const bearerAuth = auth as AuthBearer;
return {
...defaultAuth,
mode: 'bearer',
bearer: {
token: bearerAuth.token || null
}
};
}
case 'digest': {
const digestAuth = auth as AuthDigest;
return {
...defaultAuth,
mode: 'digest',
digest: {
username: digestAuth.username || null,
password: digestAuth.password || null
}
};
}
case 'ntlm': {
const ntlmAuth = auth as AuthNTLM;
return {
...defaultAuth,
mode: 'ntlm',
ntlm: {
username: ntlmAuth.username || null,
password: ntlmAuth.password || null,
domain: ntlmAuth.domain || null
}
};
}
case 'awsv4': {
const awsAuth = auth as AuthAwsV4;
return {
...defaultAuth,
mode: 'awsv4',
awsv4: {
accessKeyId: awsAuth.accessKeyId || null,
secretAccessKey: awsAuth.secretAccessKey || null,
sessionToken: awsAuth.sessionToken || null,
service: awsAuth.service || null,
region: awsAuth.region || null,
profileName: awsAuth.profileName || null
}
};
}
case 'apikey': {
const apiKeyAuth = auth as AuthApiKey;
return {
...defaultAuth,
mode: 'apikey',
apikey: {
key: apiKeyAuth.key || null,
value: apiKeyAuth.value || null,
placement: apiKeyAuth.placement === 'query' ? 'queryparams' : (apiKeyAuth.placement === 'header' ? 'header' : null)
}
};
}
case 'wsse': {
const wsseAuth = auth as AuthWsse;
return {
...defaultAuth,
mode: 'wsse',
wsse: {
username: wsseAuth.username || null,
password: wsseAuth.password || null
}
};
}
case 'oauth2':
return fromOpenCollectionOAuth2(auth as AuthOAuth2);
default:
return defaultAuth;
}
};
const toOpenCollectionOAuth2 = (oauth2: BrunoOAuth2 | null | undefined): AuthOAuth2 | undefined => {
if (!oauth2) {
return undefined;
}
const base = { type: 'oauth2' as const };
switch (oauth2.grantType) {
case 'client_credentials':
return {
...base,
flow: 'client_credentials',
accessTokenUrl: oauth2.accessTokenUrl || '',
refreshTokenUrl: oauth2.refreshTokenUrl || '',
credentials: {
clientId: oauth2.clientId || '',
clientSecret: oauth2.clientSecret || '',
placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'
},
scope: oauth2.scope || '',
tokenConfig: {
id: oauth2.credentialsId || 'credentials',
placement: oauth2.tokenPlacement === 'query'
? { query: oauth2.tokenQueryKey || 'access_token' }
: { header: oauth2.tokenHeaderPrefix || 'Bearer' }
},
settings: {
autoFetchToken: oauth2.autoFetchToken !== false,
autoRefreshToken: oauth2.autoRefreshToken !== false
}
};
case 'password':
return {
...base,
flow: 'resource_owner_password_credentials',
accessTokenUrl: oauth2.accessTokenUrl || '',
refreshTokenUrl: oauth2.refreshTokenUrl || '',
credentials: {
clientId: oauth2.clientId || '',
clientSecret: oauth2.clientSecret || '',
placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'
},
resourceOwner: {
username: oauth2.username || '',
password: oauth2.password || ''
},
scope: oauth2.scope || '',
tokenConfig: {
id: oauth2.credentialsId || 'credentials',
placement: oauth2.tokenPlacement === 'query'
? { query: oauth2.tokenQueryKey || 'access_token' }
: { header: oauth2.tokenHeaderPrefix || 'Bearer' }
},
settings: {
autoFetchToken: oauth2.autoFetchToken !== false,
autoRefreshToken: oauth2.autoRefreshToken !== false
}
};
case 'authorization_code':
return {
...base,
flow: 'authorization_code',
authorizationUrl: oauth2.authorizationUrl || '',
accessTokenUrl: oauth2.accessTokenUrl || '',
refreshTokenUrl: oauth2.refreshTokenUrl || '',
callbackUrl: oauth2.callbackUrl || '',
credentials: {
clientId: oauth2.clientId || '',
clientSecret: oauth2.clientSecret || '',
placement: oauth2.credentialsPlacement === 'basic_auth_header' ? 'basic_auth_header' : 'body'
},
scope: oauth2.scope || '',
pkce: oauth2.pkce ? { enabled: true, method: 'S256' } : undefined,
tokenConfig: {
id: oauth2.credentialsId || 'credentials',
placement: oauth2.tokenPlacement === 'query'
? { query: oauth2.tokenQueryKey || 'access_token' }
: { header: oauth2.tokenHeaderPrefix || 'Bearer' }
},
settings: {
autoFetchToken: oauth2.autoFetchToken !== false,
autoRefreshToken: oauth2.autoRefreshToken !== false
}
};
case 'implicit':
return {
...base,
flow: 'implicit',
authorizationUrl: oauth2.authorizationUrl || '',
callbackUrl: oauth2.callbackUrl || '',
credentials: {
clientId: oauth2.clientId || ''
},
scope: oauth2.scope || '',
state: oauth2.state || '',
tokenConfig: {
id: oauth2.credentialsId || 'credentials',
placement: oauth2.tokenPlacement === 'query'
? { query: oauth2.tokenQueryKey || 'access_token' }
: { header: oauth2.tokenHeaderPrefix || 'Bearer' }
},
settings: {
autoFetchToken: oauth2.autoFetchToken !== false
}
};
default:
return undefined;
}
};
export const toOpenCollectionAuth = (auth: BrunoAuth | null | undefined): Auth | undefined => {
if (!auth || auth.mode === 'none') {
return undefined;
}
if (auth.mode === 'inherit') {
return 'inherit';
}
switch (auth.mode) {
case 'basic':
return {
type: 'basic',
username: auth.basic?.username || '',
password: auth.basic?.password || ''
};
case 'bearer':
return {
type: 'bearer',
token: auth.bearer?.token || ''
};
case 'digest':
return {
type: 'digest',
username: auth.digest?.username || '',
password: auth.digest?.password || ''
};
case 'ntlm':
return {
type: 'ntlm',
username: auth.ntlm?.username || '',
password: auth.ntlm?.password || '',
domain: auth.ntlm?.domain || ''
};
case 'awsv4':
return {
type: 'awsv4',
accessKeyId: auth.awsv4?.accessKeyId || '',
secretAccessKey: auth.awsv4?.secretAccessKey || '',
sessionToken: auth.awsv4?.sessionToken || '',
service: auth.awsv4?.service || '',
region: auth.awsv4?.region || '',
profileName: auth.awsv4?.profileName || ''
};
case 'apikey':
return {
type: 'apikey',
key: auth.apikey?.key || '',
value: auth.apikey?.value || '',
placement: auth.apikey?.placement === 'queryparams' ? 'query' : 'header'
};
case 'wsse':
return {
type: 'wsse',
username: auth.wsse?.username || '',
password: auth.wsse?.password || ''
};
case 'oauth2':
return toOpenCollectionOAuth2(auth.oauth2);
default:
return undefined;
}
};

View File

@@ -0,0 +1,227 @@
import { uuid } from '../../common/index.js';
import type {
HttpRequestBody,
RawBody,
FormUrlEncodedBody,
FormUrlEncodedEntry,
MultipartFormBody,
MultipartFormEntry,
FileBody,
FileBodyVariant,
GraphQLBody,
BrunoHttpRequestBody,
BrunoKeyValue,
BrunoMultipartFormEntry,
BrunoFileEntry,
BrunoGraphqlBody
} from '../types';
export const fromOpenCollectionBody = (body: HttpRequestBody | GraphQLBody | undefined, requestType: string = 'http'): BrunoHttpRequestBody => {
const defaultBody: BrunoHttpRequestBody = {
mode: 'none',
json: null,
text: null,
xml: null,
sparql: null,
formUrlEncoded: [],
multipartForm: [],
graphql: null,
file: []
};
if (!body) {
return defaultBody;
}
if (requestType === 'graphql') {
const gqlBody = body as GraphQLBody;
return {
...defaultBody,
mode: 'graphql',
graphql: {
query: gqlBody.query || '',
variables: gqlBody.variables || ''
}
};
}
const httpBody = body as HttpRequestBody;
if ('type' in httpBody) {
switch (httpBody.type) {
case 'json': {
const rawBody = httpBody as RawBody;
return {
...defaultBody,
mode: 'json',
json: rawBody.data || ''
};
}
case 'text': {
const rawBody = httpBody as RawBody;
return {
...defaultBody,
mode: 'text',
text: rawBody.data || ''
};
}
case 'xml': {
const rawBody = httpBody as RawBody;
return {
...defaultBody,
mode: 'xml',
xml: rawBody.data || ''
};
}
case 'sparql': {
const rawBody = httpBody as RawBody;
return {
...defaultBody,
mode: 'sparql',
sparql: rawBody.data || ''
};
}
case 'form-urlencoded': {
const formBody = httpBody as FormUrlEncodedBody;
return {
...defaultBody,
mode: 'formUrlEncoded',
formUrlEncoded: (formBody.data || []).map((field): BrunoKeyValue => ({
uid: uuid(),
name: field.name || '',
value: field.value || '',
description: typeof field.description === 'string' ? field.description : field.description?.content || null,
enabled: field.disabled !== true
}))
};
}
case 'multipart-form': {
const multipartBody = httpBody as MultipartFormBody;
return {
...defaultBody,
mode: 'multipartForm',
multipartForm: (multipartBody.data || []).map((field): BrunoMultipartFormEntry => ({
uid: uuid(),
type: field.type || 'text',
name: field.name || '',
value: Array.isArray(field.value) ? field.value : (field.value || ''),
description: typeof field.description === 'string' ? field.description : field.description?.content || null,
contentType: null,
enabled: field.disabled !== true
}))
};
}
case 'file': {
const fileBody = httpBody as FileBody;
return {
...defaultBody,
mode: 'file',
file: (fileBody.data || []).map((file): BrunoFileEntry => ({
uid: uuid(),
filePath: file.filePath || '',
contentType: file.contentType || '',
selected: file.selected !== false
}))
};
}
}
}
return defaultBody;
};
export const toOpenCollectionBody = (body: BrunoHttpRequestBody | null | undefined): HttpRequestBody | undefined => {
if (!body || body.mode === 'none') {
return undefined;
}
switch (body.mode) {
case 'json':
return { type: 'json', data: body.json || '' };
case 'text':
return { type: 'text', data: body.text || '' };
case 'xml':
return { type: 'xml', data: body.xml || '' };
case 'sparql':
return { type: 'sparql', data: body.sparql || '' };
case 'formUrlEncoded': {
const formData: FormUrlEncodedEntry[] = (body.formUrlEncoded || []).map((field): FormUrlEncodedEntry => {
const entry: FormUrlEncodedEntry = {
name: field.name || '',
value: field.value || ''
};
if (field.description && typeof field.description === 'string' && field.description.trim().length) {
entry.description = field.description;
}
if (field.enabled === false) {
entry.disabled = true;
}
return entry;
});
return { type: 'form-urlencoded', data: formData };
}
case 'multipartForm': {
const multipartData: MultipartFormEntry[] = (body.multipartForm || []).map((field): MultipartFormEntry => {
const entry: MultipartFormEntry = {
name: field.name || '',
type: field.type || 'text',
value: field.value || ''
};
if (field.description && typeof field.description === 'string' && field.description.trim().length) {
entry.description = field.description;
}
if (field.enabled === false) {
entry.disabled = true;
}
return entry;
});
return { type: 'multipart-form', data: multipartData };
}
case 'file': {
const fileData: FileBodyVariant[] = (body.file || []).map((file): FileBodyVariant => ({
filePath: file.filePath || '',
contentType: file.contentType || '',
selected: file.selected !== false
}));
return { type: 'file', data: fileData };
}
case 'graphql':
return undefined;
default:
return undefined;
}
};
export const toOpenCollectionGraphqlBody = (body: BrunoHttpRequestBody | null | undefined): GraphQLBody | undefined => {
if (!body || body.mode !== 'graphql' || !body.graphql) {
return undefined;
}
return {
query: body.graphql.query || '',
variables: body.graphql.variables || ''
};
};

View File

@@ -0,0 +1,44 @@
import { uuid } from '../../common/index.js';
import type {
HttpRequestHeader,
BrunoKeyValue
} from '../types';
export const fromOpenCollectionHeaders = (headers: HttpRequestHeader[] | undefined): BrunoKeyValue[] => {
if (!headers?.length) {
return [];
}
return headers.map((header): BrunoKeyValue => ({
uid: uuid(),
name: header.name || '',
value: header.value || '',
description: typeof header.description === 'string' ? header.description : header.description?.content || null,
enabled: header.disabled !== true
}));
};
export const toOpenCollectionHeaders = (headers: BrunoKeyValue[] | null | undefined): HttpRequestHeader[] | undefined => {
if (!headers?.length) {
return undefined;
}
const ocHeaders = headers.map((header): HttpRequestHeader => {
const httpHeader: HttpRequestHeader = {
name: header.name || '',
value: header.value || ''
};
if (header.description && typeof header.description === 'string' && header.description.trim().length) {
httpHeader.description = header.description;
}
if (header.enabled === false) {
httpHeader.disabled = true;
}
return httpHeader;
});
return ocHeaders.length ? ocHeaders : undefined;
};

View File

@@ -0,0 +1,8 @@
export { fromOpenCollectionAuth, toOpenCollectionAuth } from './auth';
export { fromOpenCollectionHeaders, toOpenCollectionHeaders } from './headers';
export { fromOpenCollectionParams, toOpenCollectionParams } from './params';
export { fromOpenCollectionBody, toOpenCollectionBody, toOpenCollectionGraphqlBody } from './body';
export { fromOpenCollectionVariables, toOpenCollectionVariables } from './variables';
export { fromOpenCollectionActions, toOpenCollectionActions } from './actions';
export { fromOpenCollectionScripts, toOpenCollectionScripts } from './scripts';
export { fromOpenCollectionAssertions, toOpenCollectionAssertions } from './assertions';

View File

@@ -0,0 +1,47 @@
import { uuid } from '../../common/index.js';
import type {
HttpRequestParam,
BrunoHttpRequestParam,
BrunoHttpRequestParamType
} from '../types';
export const fromOpenCollectionParams = (params: HttpRequestParam[] | undefined): BrunoHttpRequestParam[] => {
if (!params?.length) {
return [];
}
return params.map((param): BrunoHttpRequestParam => ({
uid: uuid(),
name: param.name || '',
value: param.value || '',
description: typeof param.description === 'string' ? param.description : param.description?.content || null,
type: (param.type || 'query') as BrunoHttpRequestParamType,
enabled: param.disabled !== true
}));
};
export const toOpenCollectionParams = (params: BrunoHttpRequestParam[] | null | undefined): HttpRequestParam[] | undefined => {
if (!params?.length) {
return undefined;
}
const ocParams = params.map((param): HttpRequestParam => {
const httpParam: HttpRequestParam = {
name: param.name || '',
value: param.value || '',
type: param.type || 'query'
};
if (param.description && typeof param.description === 'string' && param.description.trim().length) {
httpParam.description = param.description;
}
if (param.enabled === false) {
httpParam.disabled = true;
}
return httpParam;
});
return ocParams.length ? ocParams : undefined;
};

View File

@@ -0,0 +1,64 @@
import type { Scripts, Script } from '@opencollection/types/common/scripts';
import type { FolderRequest as BrunoFolderRequest } from '@usebruno/schema-types/collection/folder';
import type { HttpRequest as BrunoHttpRequest } from '@usebruno/schema-types/requests/http';
import type { WebSocketRequest as BrunoWebSocketRequest } from '@usebruno/schema-types/requests/websocket';
import type { GrpcRequest as BrunoGrpcRequest } from '@usebruno/schema-types/requests/grpc';
export const toOpenCollectionScripts = (request: BrunoFolderRequest | BrunoHttpRequest | BrunoWebSocketRequest | BrunoGrpcRequest | null | undefined): Scripts | undefined => {
const ocScripts: Scripts = [];
if (request?.script?.req?.trim().length) {
ocScripts.push({
type: 'before-request',
code: request.script.req.trim()
});
}
if (request?.script?.res?.trim().length) {
ocScripts.push({
type: 'after-response',
code: request.script.res.trim()
});
}
if (request?.tests?.trim().length) {
ocScripts.push({
type: 'tests',
code: request.tests.trim()
});
}
return ocScripts.length > 0 ? ocScripts : undefined;
};
export const fromOpenCollectionScripts = (scripts: Scripts | null | undefined): {
script?: { req?: string | null; res?: string | null };
tests?: string | null;
} | undefined => {
if (!scripts || !Array.isArray(scripts) || scripts.length === 0) {
return undefined;
}
const brunoScripts: {
script?: { req?: string | null; res?: string | null };
tests?: string | null;
} = {};
for (const script of scripts) {
if (script.type === 'before-request' && script.code) {
if (!brunoScripts.script) {
brunoScripts.script = {};
}
brunoScripts.script.req = script.code;
}
if (script.type === 'after-response' && script.code) {
if (!brunoScripts.script) {
brunoScripts.script = {};
}
brunoScripts.script.res = script.code;
}
if (script.type === 'tests' && script.code) {
brunoScripts.tests = script.code;
}
}
return Object.keys(brunoScripts).length > 0 ? brunoScripts : undefined;
};

View File

@@ -0,0 +1,75 @@
import { uuid } from '../../common/index.js';
import type {
Variable,
BrunoVariable,
BrunoVariables
} from '../types';
interface BrunoVars {
req: BrunoVariables;
res: BrunoVariables;
}
export const fromOpenCollectionVariables = (variables: Variable[] | undefined): BrunoVars => {
if (!variables?.length) {
return { req: [], res: [] };
}
const reqVars: BrunoVariable[] = [];
variables.forEach((v: Variable) => {
let value = '';
if (typeof v.value === 'string') {
value = v.value;
} else if (v.value && typeof v.value === 'object' && 'data' in v.value) {
value = (v.value as { data: string }).data || '';
}
const variable: BrunoVariable = {
uid: uuid(),
name: v.name || '',
value,
enabled: v.disabled !== true,
local: false
};
if (v.description) {
variable.description = typeof v.description === 'string' ? v.description : (v.description as { content?: string })?.content || '';
}
reqVars.push(variable);
});
return { req: reqVars, res: [] };
};
export const toOpenCollectionVariables = (vars: BrunoVars | { req?: BrunoVariables; res?: BrunoVariables } | null | undefined): Variable[] | undefined => {
// Handle folder variables (has req/res structure) - only use req vars
const hasReqRes = vars && 'req' in vars;
const reqVars = hasReqRes ? vars.req : vars as BrunoVariables;
const reqVarsArray = Array.isArray(reqVars) ? reqVars : [];
if (!reqVarsArray.length) {
return undefined;
}
const ocVariables: Variable[] = reqVarsArray.map((v: BrunoVariable): Variable => {
const variable: Variable = {
name: v.name || '',
value: v.value || ''
};
if (v.description && typeof v.description === 'string' && v.description.trim().length) {
variable.description = v.description;
}
if (v.enabled === false) {
variable.disabled = true;
}
return variable;
});
return ocVariables.length > 0 ? ocVariables : undefined;
};

View File

@@ -0,0 +1,79 @@
import { uuid } from '../common/index.js';
import type {
Environment,
Variable,
BrunoEnvironment,
BrunoEnvironmentVariable
} from './types';
interface OCVariable extends Omit<Variable, 'value'> {
name: string;
value?: string | { data: string };
secret?: boolean;
disabled?: boolean;
}
export const fromOpenCollectionEnvironments = (environments: Environment[] | undefined): BrunoEnvironment[] => {
if (!environments?.length) {
return [];
}
return environments.map((env): BrunoEnvironment => ({
uid: uuid(),
name: env.name || 'Untitled Environment',
variables: (env.variables || []).map((v): BrunoEnvironmentVariable => {
const variable = v as OCVariable;
const isSecret = variable.secret === true;
let value = '';
if (!isSecret && variable.value !== undefined) {
if (typeof variable.value === 'string') {
value = variable.value;
} else if (variable.value && typeof variable.value === 'object' && 'data' in variable.value) {
value = variable.value.data;
}
}
return {
uid: uuid(),
name: variable.name || '',
value,
type: 'text',
enabled: variable.disabled !== true,
secret: isSecret
};
})
}));
};
export const toOpenCollectionEnvironments = (environments: BrunoEnvironment[] | undefined): Environment[] | undefined => {
if (!environments?.length) {
return undefined;
}
return environments.map((env): Environment => {
const ocEnv: Environment = {
name: env.name || 'Untitled Environment',
variables: (env.variables || []).map((v): OCVariable => {
const ocVar: OCVariable = {
name: v.name || '',
value: typeof v.value === 'string' ? v.value : String(v.value ?? '')
};
if (v.secret) {
ocVar.secret = true;
// Secret variables don't include the value in export
delete ocVar.value;
}
if (v.enabled === false) {
ocVar.disabled = true;
}
return ocVar;
}) as Variable[]
};
return ocEnv;
});
};

View File

@@ -0,0 +1,135 @@
import { uuid } from '../common/index.js';
import {
fromOpenCollectionHeaders,
toOpenCollectionHeaders,
fromOpenCollectionAuth,
toOpenCollectionAuth,
fromOpenCollectionScripts,
toOpenCollectionScripts,
fromOpenCollectionVariables,
toOpenCollectionVariables
} from './common';
import { fromOpenCollectionItems, toOpenCollectionItems } from './items';
import type {
Folder,
FolderInfo,
RequestDefaults,
Auth,
BrunoItem,
BrunoFolderRoot,
BrunoKeyValue
} from './types';
export const fromOpenCollectionFolder = (folder: Folder): BrunoItem => {
const info = folder.info || {};
const brunoFolder: BrunoItem = {
uid: uuid(),
type: 'folder',
name: info.name || 'Untitled Folder',
seq: info.seq || 1
};
if (folder.request || folder.docs) {
const root: BrunoFolderRoot = {};
if (folder.request) {
const scripts = fromOpenCollectionScripts(folder.request.scripts);
root.request = {
headers: fromOpenCollectionHeaders(folder.request.headers),
auth: fromOpenCollectionAuth(folder.request.auth as Auth),
script: scripts?.script,
vars: fromOpenCollectionVariables(folder.request.variables),
tests: scripts?.tests
};
}
if (folder.docs) {
if (typeof folder.docs === 'string') {
root.docs = folder.docs;
} else if (folder.docs && typeof folder.docs === 'object' && 'content' in folder.docs) {
root.docs = folder.docs.content || '';
}
}
root.meta = {
name: info.name || 'Untitled Folder',
seq: info.seq || 1
};
brunoFolder.root = root;
}
if (info.tags?.length) {
brunoFolder.tags = info.tags;
}
if (folder.items?.length) {
brunoFolder.items = fromOpenCollectionItems(folder.items, fromOpenCollectionFolder as (f: unknown) => BrunoItem);
}
return brunoFolder;
};
export const toOpenCollectionFolder = (folder: BrunoItem): Folder => {
const info: FolderInfo = {
name: folder.name || 'Untitled Folder',
type: 'folder'
};
if (folder.seq) {
info.seq = folder.seq;
}
if (folder.tags?.length) {
info.tags = folder.tags;
}
const ocFolder: Folder = {
info
};
if (folder.root) {
const folderRequest = folder.root.request || {};
const headers = toOpenCollectionHeaders(folderRequest.headers as BrunoKeyValue[]);
const auth = toOpenCollectionAuth(folderRequest.auth);
const scripts = toOpenCollectionScripts(folderRequest as { script?: { req: string | null; res: string | null } | null; tests?: string | null });
const variables = toOpenCollectionVariables(folderRequest.vars);
if (headers || auth || scripts || variables) {
const request: RequestDefaults = {};
if (headers) {
request.headers = headers;
}
if (auth) {
request.auth = auth;
}
if (scripts) {
request.scripts = scripts;
}
if (variables) {
request.variables = variables;
}
ocFolder.request = request;
}
if (folder.root.docs) {
ocFolder.docs = {
content: folder.root.docs,
type: 'text/markdown'
};
}
}
if (folder.items?.length) {
ocFolder.items = toOpenCollectionItems(folder.items, toOpenCollectionFolder as (f: BrunoItem) => unknown) as Folder['items'];
}
return ocFolder;
};

View File

@@ -0,0 +1,37 @@
export { openCollectionToBruno } from './opencollection-to-bruno';
export { brunoToOpenCollection } from './bruno-to-opencollection';
export { fromOpenCollectionFolder, toOpenCollectionFolder } from './folder';
export { fromOpenCollectionEnvironments, toOpenCollectionEnvironments } from './environment';
export {
fromOpenCollectionItem,
toOpenCollectionItem,
fromOpenCollectionItems,
toOpenCollectionItems,
fromOpenCollectionHttpItem,
toOpenCollectionHttpItem,
fromOpenCollectionGraphqlItem,
toOpenCollectionGraphqlItem,
fromOpenCollectionGrpcItem,
toOpenCollectionGrpcItem,
fromOpenCollectionWebsocketItem,
toOpenCollectionWebsocketItem
} from './items';
export {
fromOpenCollectionAuth,
toOpenCollectionAuth,
fromOpenCollectionHeaders,
toOpenCollectionHeaders,
fromOpenCollectionParams,
toOpenCollectionParams,
fromOpenCollectionBody,
toOpenCollectionBody,
toOpenCollectionGraphqlBody,
fromOpenCollectionVariables,
toOpenCollectionVariables,
fromOpenCollectionScripts,
toOpenCollectionScripts,
fromOpenCollectionAssertions,
toOpenCollectionAssertions
} from './common';

View File

@@ -0,0 +1,192 @@
import { uuid } from '../../common/index.js';
import {
fromOpenCollectionHeaders,
toOpenCollectionHeaders,
fromOpenCollectionParams,
toOpenCollectionParams,
fromOpenCollectionBody,
toOpenCollectionGraphqlBody,
fromOpenCollectionAuth,
toOpenCollectionAuth,
fromOpenCollectionScripts,
toOpenCollectionScripts,
fromOpenCollectionVariables,
toOpenCollectionVariables,
fromOpenCollectionActions,
toOpenCollectionActions,
fromOpenCollectionAssertions,
toOpenCollectionAssertions
} from '../common';
import type {
GraphQLRequest,
GraphQLRequestInfo,
GraphQLRequestDetails,
GraphQLRequestRuntime,
GraphQLRequestSettings,
GraphQLBody,
GraphQLBodyVariant,
Auth,
BrunoItem,
BrunoKeyValue,
BrunoHttpRequestParam
} from '../types';
const getGraphqlBody = (body: GraphQLBody | GraphQLBodyVariant[] | undefined): GraphQLBody | undefined => {
if (!body) return undefined;
if (Array.isArray(body)) {
const selected = body.find((v) => v.selected);
return selected?.body || body[0]?.body;
}
return body;
};
export const fromOpenCollectionGraphqlItem = (item: GraphQLRequest): BrunoItem => {
const info = item.info || {};
const graphql = item.graphql || {};
const runtime = item.runtime || {};
const scripts = fromOpenCollectionScripts(runtime.scripts);
const graphqlBody = getGraphqlBody(graphql.body);
// variables (pre-request from variables, post-response from actions)
const variables = fromOpenCollectionVariables(runtime.variables);
const postResponseVars = fromOpenCollectionActions(runtime.actions);
const brunoItem: BrunoItem = {
uid: uuid(),
type: 'graphql-request',
name: info.name || 'Untitled Request',
seq: info.seq || 1,
request: {
url: graphql.url || '',
method: graphql.method || 'POST',
headers: fromOpenCollectionHeaders(graphql.headers),
params: fromOpenCollectionParams(graphql.params),
body: fromOpenCollectionBody(graphqlBody, 'graphql'),
auth: fromOpenCollectionAuth(runtime.auth as Auth),
script: scripts?.script,
vars: {
req: variables.req,
res: postResponseVars
},
assertions: fromOpenCollectionAssertions(runtime.assertions),
tests: scripts?.tests,
docs: item.docs || ''
}
};
const settings = item.settings;
if (settings) {
brunoItem.settings = {};
if (settings.encodeUrl !== undefined) {
(brunoItem.settings as Record<string, unknown>).encodeUrl = settings.encodeUrl;
}
if (settings.timeout !== undefined) {
(brunoItem.settings as Record<string, unknown>).timeout = settings.timeout;
}
if (settings.followRedirects !== undefined) {
(brunoItem.settings as Record<string, unknown>).followRedirects = settings.followRedirects;
}
if (settings.maxRedirects !== undefined) {
(brunoItem.settings as Record<string, unknown>).maxRedirects = settings.maxRedirects;
}
}
if (info.tags?.length) {
brunoItem.tags = info.tags;
}
return brunoItem;
};
export const toOpenCollectionGraphqlItem = (item: BrunoItem): GraphQLRequest => {
const request = (item.request || {}) as Record<string, unknown>;
const brunoSettings = (item.settings || {}) as Record<string, unknown>;
const info: GraphQLRequestInfo = {
name: item.name || 'Untitled Request',
type: 'graphql'
};
if (item.seq) {
info.seq = item.seq;
}
if (item.tags?.length) {
info.tags = item.tags;
}
const graphql: GraphQLRequestDetails = {
url: request.url as string || '',
method: request.method as string || 'POST'
};
const headers = toOpenCollectionHeaders(request.headers as BrunoKeyValue[]);
if (headers) {
graphql.headers = headers;
}
const params = toOpenCollectionParams(request.params as BrunoHttpRequestParam[]);
if (params) {
graphql.params = params;
}
const body = toOpenCollectionGraphqlBody(request.body as Parameters<typeof toOpenCollectionGraphqlBody>[0]);
if (body) {
graphql.body = body;
}
const ocRequest: GraphQLRequest = {
info,
graphql
};
const auth = toOpenCollectionAuth(request.auth as Parameters<typeof toOpenCollectionAuth>[0]);
const scripts = toOpenCollectionScripts(request as Parameters<typeof toOpenCollectionScripts>[0]);
const variables = toOpenCollectionVariables(request.vars as Parameters<typeof toOpenCollectionVariables>[0]);
const assertions = toOpenCollectionAssertions(request.assertions as BrunoKeyValue[]);
// actions (from post-response variables)
const vars = request.vars as { req?: unknown[]; res?: unknown[] } | undefined;
const actions = toOpenCollectionActions(vars?.res as Parameters<typeof toOpenCollectionActions>[0]);
if (auth || scripts || variables || assertions || actions) {
const runtime: GraphQLRequestRuntime = {};
if (auth) {
runtime.auth = auth;
}
if (scripts) {
runtime.scripts = scripts;
}
if (variables) {
runtime.variables = variables;
}
if (assertions) {
runtime.assertions = assertions;
}
if (actions) {
runtime.actions = actions;
}
ocRequest.runtime = runtime;
}
const settings: GraphQLRequestSettings = {
encodeUrl: typeof brunoSettings.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,
timeout: typeof brunoSettings.timeout === 'number' ? brunoSettings.timeout : 0,
followRedirects: typeof brunoSettings.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,
maxRedirects: typeof brunoSettings.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5
};
ocRequest.settings = settings;
if (request.docs) {
ocRequest.docs = request.docs as string;
}
return ocRequest;
};

View File

@@ -0,0 +1,214 @@
import { uuid } from '../../common/index.js';
import {
fromOpenCollectionAuth,
toOpenCollectionAuth,
fromOpenCollectionScripts,
toOpenCollectionScripts,
fromOpenCollectionVariables,
toOpenCollectionVariables,
fromOpenCollectionActions,
toOpenCollectionActions,
fromOpenCollectionAssertions,
toOpenCollectionAssertions
} from '../common';
import type {
GrpcRequest,
GrpcRequestInfo,
GrpcRequestDetails,
GrpcRequestRuntime,
GrpcMetadata,
GrpcMessageVariant,
GrpcMessagePayload,
Auth,
BrunoItem,
BrunoGrpcMessage,
BrunoKeyValue
} from '../types';
const fromGrpcMetadata = (metadata: GrpcMetadata[] | undefined): BrunoKeyValue[] => {
if (!metadata?.length) {
return [];
}
return metadata.map((m): BrunoKeyValue => ({
uid: uuid(),
name: m.name || '',
value: m.value || '',
description: typeof m.description === 'string' ? m.description : (m.description as { content?: string } | undefined)?.content || null,
enabled: m.disabled !== true
}));
};
export const fromOpenCollectionGrpcItem = (item: GrpcRequest): BrunoItem => {
const info = item.info || {};
const grpc = item.grpc || {};
const runtime = item.runtime || {};
const grpcMessages: BrunoGrpcMessage[] = [];
if (grpc.message) {
if (typeof grpc.message === 'string') {
grpcMessages.push({ name: 'message 1', content: grpc.message });
} else if (Array.isArray(grpc.message)) {
grpc.message.forEach((msg, index) => {
grpcMessages.push({
name: msg.title || `message ${index + 1}`,
content: typeof msg.message === 'string' ? msg.message : ''
});
});
}
}
const scripts = fromOpenCollectionScripts(runtime.scripts);
// variables (pre-request from variables, post-response from actions)
const variables = fromOpenCollectionVariables(runtime.variables);
const postResponseVars = fromOpenCollectionActions((runtime as { actions?: Parameters<typeof fromOpenCollectionActions>[0] }).actions);
const brunoItem: BrunoItem = {
uid: uuid(),
type: 'grpc-request',
name: info.name || 'Untitled Request',
seq: info.seq || 1,
request: {
url: grpc.url || '',
method: grpc.method || '',
headers: fromGrpcMetadata(grpc.metadata),
body: {
mode: 'grpc',
grpc: grpcMessages
},
auth: fromOpenCollectionAuth(runtime.auth as Auth),
script: scripts?.script,
vars: {
req: variables.req,
res: postResponseVars
},
assertions: fromOpenCollectionAssertions(runtime.assertions),
tests: scripts?.tests,
docs: ''
}
};
// Add grpc-specific properties
if (grpc.methodType) {
(brunoItem.request as unknown as Record<string, unknown>).methodType = grpc.methodType;
}
if (grpc.protoFilePath) {
(brunoItem.request as unknown as Record<string, unknown>).protoPath = grpc.protoFilePath;
}
if (info.tags?.length) {
brunoItem.tags = info.tags;
}
return brunoItem;
};
export const toOpenCollectionGrpcItem = (item: BrunoItem): GrpcRequest => {
const request = (item.request || {}) as Record<string, unknown>;
const info: GrpcRequestInfo = {
name: item.name || 'Untitled Request',
type: 'grpc'
};
if (item.seq) {
info.seq = item.seq;
}
if (item.tags?.length) {
info.tags = item.tags;
}
const grpc: GrpcRequestDetails = {
url: request.url as string || '',
method: request.method as string || ''
};
if (request.methodType) {
grpc.methodType = request.methodType as GrpcRequestDetails['methodType'];
}
if (request.protoPath) {
grpc.protoFilePath = request.protoPath as string;
}
const headers = request.headers as BrunoKeyValue[] | undefined;
if (headers?.length) {
grpc.metadata = headers.map((h): GrpcMetadata => {
const metadata: GrpcMetadata = {
name: h.name || '',
value: h.value || ''
};
if (h.description && typeof h.description === 'string' && h.description.trim().length) {
metadata.description = h.description;
}
if (h.enabled === false) {
metadata.disabled = true;
}
return metadata;
});
}
const body = request.body as { grpc?: BrunoGrpcMessage[] } | undefined;
if (body?.grpc?.length) {
const messages = body.grpc;
if (messages.length === 1) {
grpc.message = messages[0].content || '';
} else {
grpc.message = messages.map((msg): GrpcMessageVariant => ({
title: msg.name || 'Untitled',
message: msg.content || ''
}));
}
}
const ocRequest: GrpcRequest = {
info,
grpc
};
const auth = toOpenCollectionAuth(request.auth as Parameters<typeof toOpenCollectionAuth>[0]);
const scripts = toOpenCollectionScripts(request as Parameters<typeof toOpenCollectionScripts>[0]);
const variables = toOpenCollectionVariables(request.vars as Parameters<typeof toOpenCollectionVariables>[0]);
const assertions = toOpenCollectionAssertions(request.assertions as BrunoKeyValue[]);
// actions (from post-response variables)
const vars = request.vars as { req?: unknown[]; res?: unknown[] } | undefined;
const actions = toOpenCollectionActions(vars?.res as Parameters<typeof toOpenCollectionActions>[0]);
if (auth || scripts || variables || assertions || actions) {
const runtime: GrpcRequestRuntime = {};
if (auth) {
runtime.auth = auth;
}
if (scripts) {
runtime.scripts = scripts;
}
if (variables) {
runtime.variables = variables;
}
if (assertions) {
runtime.assertions = assertions;
}
if (actions) {
(runtime as { actions?: typeof actions }).actions = actions;
}
ocRequest.runtime = runtime;
}
if (request.docs) {
ocRequest.docs = request.docs as string;
}
return ocRequest;
};

View File

@@ -0,0 +1,295 @@
import { uuid } from '../../common/index.js';
import {
fromOpenCollectionHeaders,
toOpenCollectionHeaders,
fromOpenCollectionParams,
toOpenCollectionParams,
fromOpenCollectionBody,
toOpenCollectionBody,
fromOpenCollectionAuth,
toOpenCollectionAuth,
fromOpenCollectionScripts,
toOpenCollectionScripts,
fromOpenCollectionVariables,
toOpenCollectionVariables,
fromOpenCollectionActions,
toOpenCollectionActions,
fromOpenCollectionAssertions,
toOpenCollectionAssertions
} from '../common';
import type {
HttpRequest,
HttpRequestSettings,
HttpRequestExample,
HttpRequestInfo,
HttpRequestDetails,
HttpRequestRuntime,
HttpRequestHeader,
HttpRequestBody,
Auth,
BrunoItem,
BrunoKeyValue,
BrunoHttpRequestParam,
BrunoExample,
BrunoHttpRequest
} from '../types';
import type { HttpItemSettings as BrunoHttpItemSettings } from '@usebruno/schema-types/collection/item';
const getHttpBody = (body: HttpRequestBody | Array<{ title: string; selected?: boolean; body: HttpRequestBody }> | undefined): HttpRequestBody | undefined => {
if (!body) return undefined;
if (Array.isArray(body)) {
const selected = body.find((v) => v.selected);
return selected?.body || body[0]?.body;
}
return body;
};
export const fromOpenCollectionHttpItem = (ocRequest: HttpRequest): BrunoItem => {
const info = ocRequest.info;
const http = ocRequest.http;
const runtime = ocRequest.runtime;
const scripts = fromOpenCollectionScripts(runtime?.scripts);
const httpBody = getHttpBody(http?.body as HttpRequestBody);
// variables (pre-request from variables, post-response from actions)
const variables = fromOpenCollectionVariables(runtime?.variables);
const postResponseVars = fromOpenCollectionActions(runtime?.actions);
const brunoRequest: BrunoHttpRequest = {
url: http?.url || '',
method: http?.method || 'GET',
headers: fromOpenCollectionHeaders(http?.headers) || [],
params: fromOpenCollectionParams(http?.params) || [],
body: fromOpenCollectionBody(httpBody) || {
mode: 'none',
json: null,
text: null,
xml: null,
sparql: null,
formUrlEncoded: [],
multipartForm: [],
graphql: null,
file: []
},
auth: fromOpenCollectionAuth(runtime?.auth as Auth),
script: {
req: scripts?.script?.req || null,
res: scripts?.script?.res || null
},
vars: {
req: variables.req,
res: postResponseVars
},
assertions: fromOpenCollectionAssertions(runtime?.assertions) || [],
tests: scripts?.tests || null,
docs: ocRequest.docs || null
};
const brunoItem: BrunoItem = {
uid: uuid(),
type: 'http-request',
seq: info?.seq || 1,
name: info?.name || 'Untitled Request',
tags: info?.tags || [],
request: brunoRequest,
settings: null,
fileContent: null,
root: null,
items: [],
examples: [],
filename: null,
pathname: null
};
if (ocRequest.settings) {
const settings: BrunoHttpItemSettings = {
encodeUrl: typeof ocRequest.settings.encodeUrl === 'boolean' ? ocRequest.settings.encodeUrl : true,
timeout: typeof ocRequest.settings.timeout === 'number' ? ocRequest.settings.timeout : 0,
followRedirects: typeof ocRequest.settings.followRedirects === 'boolean' ? ocRequest.settings.followRedirects : true,
maxRedirects: typeof ocRequest.settings.maxRedirects === 'number' ? ocRequest.settings.maxRedirects : 5
};
brunoItem.settings = settings;
}
if (ocRequest.examples?.length) {
brunoItem.examples = ocRequest.examples.map((example): BrunoExample => ({
uid: uuid(),
itemUid: brunoItem.uid,
name: example.name || 'Untitled Example',
description: typeof example.description === 'string' ? example.description : (example.description as { content?: string })?.content || null,
type: 'http-request',
request: {
url: example.request?.url || '',
method: example.request?.method || 'GET',
headers: fromOpenCollectionHeaders(example.request?.headers) || [],
params: fromOpenCollectionParams(example.request?.params) || [],
body: fromOpenCollectionBody(example.request?.body) || null
},
response: example.response ? {
status: String(example.response.status || 200),
statusText: example.response.statusText || 'OK',
headers: fromOpenCollectionHeaders(example.response.headers as HttpRequestHeader[]) || [],
body: example.response.body ? {
type: example.response.body.type || 'text',
content: example.response.body.data || ''
} : null
} : null
}));
}
return brunoItem;
};
export const toOpenCollectionHttpItem = (item: BrunoItem): HttpRequest => {
const ocRequest: HttpRequest = {};
const brunoRequest = item.request as BrunoHttpRequest;
const brunoSettings = item.settings as BrunoHttpItemSettings | undefined;
const info: HttpRequestInfo = {
name: item.name || 'Untitled Request',
type: 'http'
};
if (item.seq) {
info.seq = item.seq;
}
if (item.tags?.length) {
info.tags = item.tags;
}
ocRequest.info = info;
const http: HttpRequestDetails = {
method: brunoRequest?.method || 'GET',
url: brunoRequest?.url || ''
};
const headers = toOpenCollectionHeaders(brunoRequest?.headers as BrunoKeyValue[]);
if (headers) {
http.headers = headers;
}
const params = toOpenCollectionParams(brunoRequest?.params as BrunoHttpRequestParam[]);
if (params) {
http.params = params;
}
const body = toOpenCollectionBody(brunoRequest?.body);
if (body) {
http.body = body;
}
ocRequest.http = http;
const runtime: HttpRequestRuntime = {};
let hasRuntime = false;
const variables = toOpenCollectionVariables(brunoRequest?.vars);
if (variables) {
runtime.variables = variables;
hasRuntime = true;
}
const scripts = toOpenCollectionScripts(brunoRequest);
if (scripts) {
runtime.scripts = scripts;
hasRuntime = true;
}
const assertions = toOpenCollectionAssertions(brunoRequest?.assertions as BrunoKeyValue[]);
if (assertions) {
runtime.assertions = assertions;
hasRuntime = true;
}
// actions (from post-response variables)
const resVars = brunoRequest?.vars?.res;
const actions = toOpenCollectionActions(resVars);
if (actions) {
runtime.actions = actions;
hasRuntime = true;
}
const auth = toOpenCollectionAuth(brunoRequest?.auth);
if (auth) {
runtime.auth = auth;
hasRuntime = true;
}
if (hasRuntime) {
ocRequest.runtime = runtime;
}
const settings: HttpRequestSettings = {
encodeUrl: typeof brunoSettings?.encodeUrl === 'boolean' ? brunoSettings.encodeUrl : true,
timeout: typeof brunoSettings?.timeout === 'number' ? brunoSettings.timeout : 0,
followRedirects: typeof brunoSettings?.followRedirects === 'boolean' ? brunoSettings.followRedirects : true,
maxRedirects: typeof brunoSettings?.maxRedirects === 'number' ? brunoSettings.maxRedirects : 5
};
ocRequest.settings = settings;
if (brunoRequest?.docs) {
ocRequest.docs = brunoRequest.docs;
}
if (item.examples?.length) {
ocRequest.examples = item.examples.map((example): HttpRequestExample => {
const ocExample: HttpRequestExample = {
name: example.name || 'Untitled Example'
};
if (example.description) {
ocExample.description = example.description;
}
if (example.request) {
ocExample.request = {
url: example.request.url || '',
method: example.request.method || 'GET'
};
const exampleHeaders = toOpenCollectionHeaders(example.request.headers as BrunoKeyValue[]);
if (exampleHeaders) {
ocExample.request.headers = exampleHeaders;
}
const exampleParams = toOpenCollectionParams(example.request.params as BrunoHttpRequestParam[]);
if (exampleParams) {
ocExample.request.params = exampleParams;
}
const exampleBody = toOpenCollectionBody(example.request.body);
if (exampleBody) {
ocExample.request.body = exampleBody;
}
}
if (example.response) {
ocExample.response = {};
if (example.response.status !== undefined) {
ocExample.response.status = Number(example.response.status);
}
if (example.response.statusText) {
ocExample.response.statusText = example.response.statusText;
}
const responseHeaders = toOpenCollectionHeaders(example.response.headers as BrunoKeyValue[]);
if (responseHeaders) {
ocExample.response.headers = responseHeaders;
}
if (example.response.body) {
ocExample.response.body = {
type: (example.response.body.type as 'json' | 'text' | 'xml' | 'html' | 'binary') || 'text',
data: String(example.response.body.content || '')
};
}
}
return ocExample;
});
}
return ocRequest;
};

View File

@@ -0,0 +1,125 @@
import { uuid } from '../../common/index.js';
import { fromOpenCollectionHttpItem, toOpenCollectionHttpItem } from './http';
import { fromOpenCollectionGraphqlItem, toOpenCollectionGraphqlItem } from './graphql';
import { fromOpenCollectionGrpcItem, toOpenCollectionGrpcItem } from './grpc';
import { fromOpenCollectionWebsocketItem, toOpenCollectionWebsocketItem } from './websocket';
import type {
BrunoItem
} from '../types';
interface OCItem {
info?: {
type?: string;
name?: string;
seq?: number;
};
http?: unknown;
graphql?: unknown;
grpc?: unknown;
websocket?: unknown;
items?: unknown[];
script?: string;
}
const getItemType = (item: OCItem): string => {
if (item.info?.type) {
return item.info.type;
}
if ('items' in item && item.items) {
return 'folder';
}
if ('http' in item && item.http) {
return 'http';
}
if ('graphql' in item && item.graphql) {
return 'graphql';
}
if ('grpc' in item && item.grpc) {
return 'grpc';
}
if ('websocket' in item && item.websocket) {
return 'websocket';
}
if ('script' in item && typeof item.script === 'string') {
return 'script';
}
return 'unknown';
};
export const fromOpenCollectionItem = (item: unknown, parseFolder: (folder: unknown) => BrunoItem): BrunoItem | null => {
const ocItem = item as OCItem;
const itemType = getItemType(ocItem);
switch (itemType) {
case 'http':
return fromOpenCollectionHttpItem(item as Parameters<typeof fromOpenCollectionHttpItem>[0]);
case 'graphql':
return fromOpenCollectionGraphqlItem(item as Parameters<typeof fromOpenCollectionGraphqlItem>[0]);
case 'grpc':
return fromOpenCollectionGrpcItem(item as Parameters<typeof fromOpenCollectionGrpcItem>[0]);
case 'websocket':
return fromOpenCollectionWebsocketItem(item as Parameters<typeof fromOpenCollectionWebsocketItem>[0]);
case 'folder':
return parseFolder(item);
case 'script': {
const scriptItem = item as { script?: string; info?: { name?: string } };
return {
uid: uuid(),
type: 'js',
name: scriptItem.info?.name || 'script.js',
fileContent: scriptItem.script || ''
};
}
default:
return null;
}
};
export const toOpenCollectionItem = (item: BrunoItem, stringifyFolder: (folder: BrunoItem) => unknown): unknown | null => {
switch (item.type) {
case 'http-request':
return toOpenCollectionHttpItem(item);
case 'graphql-request':
return toOpenCollectionGraphqlItem(item);
case 'grpc-request':
return toOpenCollectionGrpcItem(item);
case 'ws-request':
return toOpenCollectionWebsocketItem(item);
case 'folder':
return stringifyFolder(item);
case 'js':
return {
info: {
name: item.name || 'script.js',
type: 'script'
},
script: item.fileContent || ''
};
default:
return null;
}
};
export const fromOpenCollectionItems = (items: unknown[] | undefined, parseFolder: (folder: unknown) => BrunoItem): BrunoItem[] => {
return (items || [])
.map((item) => fromOpenCollectionItem(item, parseFolder))
.filter((item): item is BrunoItem => item !== null);
};
export const toOpenCollectionItems = (items: BrunoItem[] | undefined | null, stringifyFolder: (folder: BrunoItem) => unknown): unknown[] => {
return (items || [])
.map((item) => toOpenCollectionItem(item, stringifyFolder))
.filter((item): item is unknown => item !== null);
};
export { fromOpenCollectionHttpItem, toOpenCollectionHttpItem } from './http';
export { fromOpenCollectionGraphqlItem, toOpenCollectionGraphqlItem } from './graphql';
export { fromOpenCollectionGrpcItem, toOpenCollectionGrpcItem } from './grpc';
export { fromOpenCollectionWebsocketItem, toOpenCollectionWebsocketItem } from './websocket';

View File

@@ -0,0 +1,176 @@
import { uuid } from '../../common/index.js';
import {
fromOpenCollectionHeaders,
toOpenCollectionHeaders,
fromOpenCollectionAuth,
toOpenCollectionAuth,
fromOpenCollectionScripts,
toOpenCollectionScripts,
fromOpenCollectionVariables,
toOpenCollectionVariables,
fromOpenCollectionActions,
toOpenCollectionActions
} from '../common';
import type {
WebSocketRequest,
WebSocketRequestInfo,
WebSocketRequestDetails,
WebSocketRequestRuntime,
WebSocketMessage,
WebSocketMessageVariant,
Auth,
BrunoItem,
BrunoWsMessage,
BrunoKeyValue,
BrunoWebSocketRequestBody
} from '../types';
export const fromOpenCollectionWebsocketItem = (item: WebSocketRequest): BrunoItem => {
const info = item.info || {};
const websocket = item.websocket || {};
const runtime = item.runtime || {};
const wsMessages: BrunoWsMessage[] = [];
if (websocket.message) {
if ('type' in websocket.message && 'data' in websocket.message) {
const msg = websocket.message as WebSocketMessage;
wsMessages.push({
name: 'message 1',
type: msg.type || 'json',
content: msg.data || ''
});
} else if (Array.isArray(websocket.message)) {
websocket.message.forEach((m, index) => {
wsMessages.push({
name: m.title || `message ${index + 1}`,
type: m.message?.type || 'json',
content: m.message?.data || ''
});
});
}
}
const scripts = fromOpenCollectionScripts(runtime.scripts);
// variables (pre-request from variables, post-response from actions)
const variables = fromOpenCollectionVariables(runtime.variables);
const postResponseVars = fromOpenCollectionActions((runtime as { actions?: Parameters<typeof fromOpenCollectionActions>[0] }).actions);
const wsBody: BrunoWebSocketRequestBody = {
mode: 'ws',
ws: wsMessages
};
const brunoItem: BrunoItem = {
uid: uuid(),
type: 'ws-request',
name: info.name || 'Untitled Request',
seq: info.seq || 1,
request: {
url: websocket.url || '',
headers: fromOpenCollectionHeaders(websocket.headers),
body: wsBody,
auth: fromOpenCollectionAuth(runtime.auth as Auth),
script: scripts?.script,
vars: {
req: variables.req,
res: postResponseVars
},
tests: scripts?.tests,
docs: item.docs || ''
}
};
if (info.tags?.length) {
brunoItem.tags = info.tags;
}
return brunoItem;
};
export const toOpenCollectionWebsocketItem = (item: BrunoItem): WebSocketRequest => {
const request = (item.request || {}) as Record<string, unknown>;
const info: WebSocketRequestInfo = {
name: item.name || 'Untitled Request',
type: 'websocket'
};
if (item.seq) {
info.seq = item.seq;
}
if (item.tags?.length) {
info.tags = item.tags;
}
const websocket: WebSocketRequestDetails = {
url: request.url as string || ''
};
const headers = toOpenCollectionHeaders(request.headers as BrunoKeyValue[]);
if (headers) {
websocket.headers = headers;
}
const body = request.body as { ws?: BrunoWsMessage[] } | undefined;
if (body?.ws?.length) {
const messages = body.ws;
if (messages.length === 1) {
websocket.message = {
type: (messages[0].type as WebSocketMessage['type']) || 'json',
data: messages[0].content || ''
};
} else {
websocket.message = messages.map((msg): WebSocketMessageVariant => ({
title: msg.name || 'Untitled',
message: {
type: (msg.type as WebSocketMessage['type']) || 'json',
data: msg.content || ''
}
}));
}
}
const ocRequest: WebSocketRequest = {
info,
websocket
};
const auth = toOpenCollectionAuth(request.auth as Parameters<typeof toOpenCollectionAuth>[0]);
const scripts = toOpenCollectionScripts(request as Parameters<typeof toOpenCollectionScripts>[0]);
const variables = toOpenCollectionVariables(request.vars as Parameters<typeof toOpenCollectionVariables>[0]);
// actions (from post-response variables)
const vars = request.vars as { req?: unknown[]; res?: unknown[] } | undefined;
const actions = toOpenCollectionActions(vars?.res as Parameters<typeof toOpenCollectionActions>[0]);
if (auth || scripts || variables || actions) {
const runtime: WebSocketRequestRuntime = {};
if (auth) {
runtime.auth = auth;
}
if (scripts) {
runtime.scripts = scripts;
}
if (variables) {
runtime.variables = variables;
}
if (actions) {
(runtime as { actions?: typeof actions }).actions = actions;
}
ocRequest.runtime = runtime;
}
if (request.docs) {
ocRequest.docs = request.docs as string;
}
return ocRequest;
};

View File

@@ -0,0 +1,133 @@
import { OpenCollection } from "@opencollection/types";
import { BrunoCollection, BrunoCollectionRoot, BrunoConfig, PemCertificate, Pkcs12Certificate } from "./types";
import { fromOpenCollectionAuth, fromOpenCollectionHeaders, fromOpenCollectionScripts, fromOpenCollectionVariables } from "./common";
import { uuid } from "../common";
import { fromOpenCollectionItems } from "./items";
import { fromOpenCollectionFolder } from "./folder";
import { fromOpenCollectionEnvironments } from "./environment";
const fromOpenCollectionConfig = (oc: OpenCollection): BrunoConfig => {
const extensions = oc.extensions;
const ignoreList = extensions && Array.isArray(extensions.ignore)
? extensions.ignore as string[]
: ['node_modules', '.git'];
const brunoConfig: BrunoConfig = {
version: '1',
name: oc.info?.name || 'Untitled Collection',
type: 'collection',
ignore: ignoreList
};
const config = oc.config;
if (!config) {
return brunoConfig;
}
if (config.protobuf) {
brunoConfig.protobuf = {
protoFiles: config.protobuf.protoFiles?.map((f) => ({
path: f.path
})),
importPaths: config.protobuf.importPaths?.map((p) => ({
path: p.path,
disabled: p.disabled || false
}))
};
}
if (config.proxy && typeof config.proxy !== 'boolean') {
if (config.proxy === 'inherit') {
brunoConfig.proxy = { enabled: 'global' };
} else {
const proxyConfig = config.proxy;
brunoConfig.proxy = {
enabled: true,
protocol: proxyConfig.protocol || 'http',
hostname: proxyConfig.hostname || '',
port: proxyConfig.port || 0
};
if (proxyConfig.auth) {
brunoConfig.proxy.auth = {
enabled: true,
username: proxyConfig.auth.username || '',
password: proxyConfig.auth.password || ''
};
}
if (proxyConfig.bypassProxy) {
brunoConfig.proxy.bypassProxy = proxyConfig.bypassProxy;
}
}
}
if (config.clientCertificates?.length) {
brunoConfig.clientCertificates = {
certs: config.clientCertificates.map((cert) => {
if (cert.type === 'pem') {
const pemCert = cert as PemCertificate;
return {
domain: pemCert.domain || '',
type: 'pem' as const,
certFilePath: pemCert.certificateFilePath || '',
keyFilePath: pemCert.privateKeyFilePath || '',
passphrase: pemCert.passphrase || ''
};
} else if (cert.type === 'pkcs12') {
const pkcs12Cert = cert as Pkcs12Certificate;
return {
domain: pkcs12Cert.domain || '',
type: 'pkcs12' as const,
pfxFilePath: pkcs12Cert.pkcs12FilePath || '',
passphrase: pkcs12Cert.passphrase || ''
};
}
return null;
}).filter((cert): cert is NonNullable<typeof cert> => cert !== null)
};
}
return brunoConfig;
};
const fromOpenCollectionRoot = (oc: OpenCollection): BrunoCollectionRoot => {
const root: BrunoCollectionRoot = {};
if (oc.request) {
const scripts = fromOpenCollectionScripts(oc.request.scripts);
root.request = {
headers: fromOpenCollectionHeaders(oc.request.headers),
auth: fromOpenCollectionAuth(oc.request.auth),
script: scripts?.script,
vars: fromOpenCollectionVariables(oc.request.variables),
tests: scripts?.tests
};
}
if (oc.docs) {
root.docs = typeof oc.docs === 'string'
? oc.docs
: oc.docs.content || '';
}
root.meta = {
name: oc.info?.name || 'Untitled Collection'
};
return root;
};
export const openCollectionToBruno = (openCollection: OpenCollection): BrunoCollection => {
const brunoCollection: BrunoCollection = {
uid: uuid(),
name: openCollection.info?.name || 'Untitled Collection',
version: '1',
items: fromOpenCollectionItems(openCollection.items, (folder: unknown) => fromOpenCollectionFolder(folder as Parameters<typeof fromOpenCollectionFolder>[0])),
environments: fromOpenCollectionEnvironments(openCollection.config?.environments),
brunoConfig: fromOpenCollectionConfig(openCollection) as Record<string, unknown>,
root: fromOpenCollectionRoot(openCollection)
};
return brunoCollection;
};

View File

@@ -0,0 +1,208 @@
// OpenCollection types - main module
export type { OpenCollection, Extensions } from '@opencollection/types';
// OpenCollection collection/item types
export type { Item, Folder, FolderInfo } from '@opencollection/types/collection/item';
// OpenCollection HTTP request types
export type {
HttpRequest,
HttpRequestHeader,
HttpResponseHeader,
HttpRequestParam,
HttpRequestBody,
HttpRequestSettings,
HttpRequestExample,
HttpRequestExampleRequest,
HttpRequestExampleResponse,
HttpRequestExampleResponseBody,
HttpRequestBodyVariant,
HttpRequestInfo,
HttpRequestDetails,
HttpRequestRuntime,
RawBody,
FormUrlEncodedBody,
FormUrlEncodedEntry,
MultipartFormBody,
MultipartFormEntry,
FileBody,
FileBodyVariant
} from '@opencollection/types/requests/http';
// OpenCollection GraphQL request types
export type {
GraphQLRequest,
GraphQLRequestSettings,
GraphQLRequestInfo,
GraphQLRequestDetails,
GraphQLRequestRuntime,
GraphQLBody,
GraphQLBodyVariant
} from '@opencollection/types/requests/graphql';
// OpenCollection gRPC request types
export type {
GrpcRequest,
GrpcRequestInfo,
GrpcRequestDetails,
GrpcRequestRuntime,
GrpcMetadata,
GrpcMessage,
GrpcMessageVariant,
GrpcMessagePayload,
GrpcMethodType
} from '@opencollection/types/requests/grpc';
// OpenCollection WebSocket request types
export type {
WebSocketRequest,
WebSocketRequestInfo,
WebSocketRequestDetails,
WebSocketRequestRuntime,
WebSocketMessage,
WebSocketMessageVariant,
WebSocketPayload,
WebSocketMessageType
} from '@opencollection/types/requests/websocket';
// OpenCollection config types
export type { Environment, CollectionConfig } from '@opencollection/types/config/environments';
export type { Protobuf, ProtoFileItem, ProtoFileImportPath } from '@opencollection/types/config/protobuf';
export type { Proxy, ProxyAuth } from '@opencollection/types/config/proxy';
export type { ClientCertificate, PemCertificate, Pkcs12Certificate } from '@opencollection/types/config/certificates';
// OpenCollection common types
export type { RequestDefaults, RequestSettings } from '@opencollection/types/common/request-defaults';
export type { Documentation } from '@opencollection/types/common/documentation';
export type { Description } from '@opencollection/types/common/description';
export type { Info, Author } from '@opencollection/types/common/info';
export type {
Variable,
VariableValueVariant
} from '@opencollection/types/common/variables';
export type { Scripts } from '@opencollection/types/common/scripts';
export type { Assertion } from '@opencollection/types/common/assertions';
export type { Tag } from '@opencollection/types/common/tags';
export type {
Action,
ActionSetVariable,
ActionPhase,
ActionVariableScope,
SetVariableActionSelector,
SetVariableActionTarget
} from '@opencollection/types/common/actions';
// OpenCollection auth types
export type {
Auth,
AuthBasic,
AuthBearer,
AuthDigest,
AuthNTLM,
AuthAwsV4,
AuthApiKey,
AuthWsse
} from '@opencollection/types/common/auth';
export type { AuthOAuth2 } from '@opencollection/types/common/auth-oauth2';
// Bruno types - collection
export type { Item as BrunoItem } from '@usebruno/schema-types/collection/item';
export type {
FolderRoot as BrunoFolderRoot,
FolderRequest as BrunoFolderRequest,
FolderMeta as BrunoFolderMeta
} from '@usebruno/schema-types/collection/folder';
export type { Collection as BrunoCollection } from '@usebruno/schema-types/collection/collection';
export type {
Environment as BrunoEnvironment,
EnvironmentVariable as BrunoEnvironmentVariable
} from '@usebruno/schema-types/collection/environment';
export type {
Example as BrunoExample,
ExampleRequest as BrunoExampleRequest,
ExampleResponse as BrunoExampleResponse,
ExampleResponseBody as BrunoExampleResponseBody
} from '@usebruno/schema-types/collection/examples';
// Bruno types - common
export type { KeyValue as BrunoKeyValue } from '@usebruno/schema-types/common/key-value';
export type { Variable as BrunoVariable, Variables as BrunoVariables } from '@usebruno/schema-types/common/variables';
export type { Script as BrunoScript } from '@usebruno/schema-types/common/scripts';
export type {
Auth as BrunoAuth,
AuthMode as BrunoAuthMode,
AuthAwsV4 as BrunoAuthAwsV4,
AuthBasic as BrunoAuthBasic,
AuthBearer as BrunoAuthBearer,
AuthDigest as BrunoAuthDigest,
AuthNTLM as BrunoAuthNTLM,
AuthWsse as BrunoAuthWsse,
AuthApiKey as BrunoAuthApiKey,
OAuth2 as BrunoOAuth2
} from '@usebruno/schema-types/common/auth';
export type { MultipartFormEntry as BrunoMultipartFormEntry, MultipartForm as BrunoMultipartForm } from '@usebruno/schema-types/common/multipart-form';
export type { FileEntry as BrunoFileEntry, FileList as BrunoFileList } from '@usebruno/schema-types/common/file';
export type { GraphqlBody as BrunoGraphqlBody } from '@usebruno/schema-types/common/graphql';
// Bruno types - requests
export type {
HttpRequest as BrunoHttpRequest,
HttpRequestBody as BrunoHttpRequestBody,
HttpRequestBodyMode as BrunoHttpRequestBodyMode,
HttpRequestParam as BrunoHttpRequestParam,
HttpRequestParamType as BrunoHttpRequestParamType
} from '@usebruno/schema-types/requests/http';
export type {
GrpcRequest as BrunoGrpcRequest,
GrpcRequestBody as BrunoGrpcRequestBody,
GrpcMessage as BrunoGrpcMessage,
GrpcMethodType as BrunoGrpcMethodType
} from '@usebruno/schema-types/requests/grpc';
export type {
WebSocketRequest as BrunoWebSocketRequest,
WebSocketRequestBody as BrunoWebSocketRequestBody,
WebSocketMessage as BrunoWsMessage
} from '@usebruno/schema-types/requests/websocket';
export interface BrunoConfig {
version?: string;
name?: string;
type?: string;
ignore?: string[];
protobuf?: {
protoFiles?: { path: string }[];
importPaths?: { path: string; disabled?: boolean }[];
};
proxy?: {
enabled?: boolean | 'global';
protocol?: string;
hostname?: string;
port?: number;
auth?: {
enabled?: boolean;
username?: string;
password?: string;
};
bypassProxy?: string;
};
clientCertificates?: {
certs?: Array<{
domain?: string;
type?: 'pem' | 'pkcs12';
certFilePath?: string;
keyFilePath?: string;
pfxFilePath?: string;
passphrase?: string;
}>;
};
}
export interface BrunoCollectionRoot {
request?: any;
docs?: string;
meta?: {
name?: string;
seq?: number;
};
}

View File

@@ -8,7 +8,7 @@ const generateUID = () => {
return result;
};
import { get, each, filter } from 'lodash';
import { get, each } from 'lodash';
import { collectionSchema } from '@usebruno/schema';
// --- Inlined from src/common/index.js ---

View File

@@ -1,19 +1,31 @@
{
"compilerOptions": {
"target": "ES6",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"jsx": "react",
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"declarationDir": "types",
"sourceMap": true,
"outDir": "dist",
"moduleResolution": "node",
"emitDeclarationOnly": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "./src",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"allowJs": true,
"checkJs": false,
"types": ["node"],
"lib": ["ES2020"],
"typeRoots": ["../../node_modules/@types", "./node_modules/@types"],
"baseUrl": "../..",
"paths": {
"@usebruno/schema-types": ["packages/bruno-schema-types/dist/index.d.ts"],
"@usebruno/schema-types/*": ["packages/bruno-schema-types/dist/*"],
"@opencollection/types": ["node_modules/@opencollection/types/dist/opencollection.d.ts"],
"@opencollection/types/*": ["node_modules/@opencollection/types/dist/*"]
}
},
"exclude": ["dist", "node_modules", "tests"]
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.js"],
"exclude": ["node_modules", "dist"]
}