Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ const EnvironmentVariables = ({ environment, collection, setIsModified, original
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
<td className="text-center">
<input
type="checkbox"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ const EnvironmentVariables = ({ environment, setIsModified, originalEnvironmentV
</thead>
<tbody>
{formik.values.map((variable, index) => (
<tr key={variable.uid}>
<tr key={variable.uid} data-testid={`env-var-row-${variable.name}`}>
<td className="text-center">
<input
type="checkbox"
Expand Down
90 changes: 90 additions & 0 deletions packages/bruno-converters/src/insomnia/env-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { uuid } from '../common';
import { flattenObject } from '../utils/flatten';

/**
* Converts an Insomnia environment node into a Bruno environment using JSON-path-like keys.
* - Flattens env.data to dot-notation keys; values are converted to strings.
*/
export const toBrunoEnv = (env, index = 0) => {
const variables = [];
const flatEnvData = flattenObject(env?.data || {});
Object.entries(flatEnvData).forEach(([name, value]) => {
variables.push({
uid: uuid(),
name,
value: String(value),
type: 'text',
enabled: true,
secret: false
});
});

return {
uid: uuid(),
name: (env?.name && String(env.name).trim()) || `Environment ${index + 1}`,
variables
};
};

/**
* Shallowly merges two flattened env data objects.
* - Keys in override replace keys in base.
* - No recursive merging.
*/
const shallowMergeFlat = (baseFlat = {}, overrideFlat = {}) => ({ ...baseFlat, ...overrideFlat });

/**
* Builds Bruno environments from Insomnia v5 environments.
* - Expects a single object (base env) with optional subEnvironments.
* - Creates one env for base and one env per sub using flattened, shallow-merged keys.
*/
export const buildV5Environments = (baseEnv) => {
if (!baseEnv || typeof baseEnv !== 'object') return [];

const result = [];

// include base as standalone
result.push(toBrunoEnv(baseEnv));

const subs = Array.isArray(baseEnv.subEnvironments) ? baseEnv.subEnvironments : [];
const baseFlat = flattenObject(baseEnv?.data || {});
subs.forEach((sub, i) => {
const subFlat = flattenObject(sub?.data || {});
const mergedFlat = shallowMergeFlat(baseFlat, subFlat);
result.push(toBrunoEnv({ name: sub?.name, data: mergedFlat }, i + 1));
});
return result;
};

/**
* Builds Bruno environments from Insomnia v4 resources.
* - Base env: parentId equals workspaceId; included as-is (flattened).
* - Sub envs: merge base (flattened) with sub (flattened) and import.
*
* Note: Insomnia supports only ONE base environment per workspace.
*/
export const buildV4Environments = (resources, workspaceId) => {
const allEnvResources = resources.filter((r) => r._type === 'environment') || [];
const envById = {};
allEnvResources.forEach((e) => (envById[e._id] = e));

const isBaseEnv = (env) => env.parentId === workspaceId;

const result = [];

const baseEnv = allEnvResources.find(isBaseEnv);
if (baseEnv) {
result.push(toBrunoEnv(baseEnv));
}

// sub envs - all inherit from the single base environment
const subEnvs = allEnvResources.filter((e) => !isBaseEnv(e));
const baseFlat = flattenObject(baseEnv?.data || {});
subEnvs.forEach((sub, idx) => {
const subFlat = flattenObject(sub.data || {});
const mergedFlat = shallowMergeFlat(baseFlat, subFlat);
result.push(toBrunoEnv({ name: sub.name, data: mergedFlat }, idx + 1));
});

return result;
};
26 changes: 15 additions & 11 deletions packages/bruno-converters/src/insomnia/insomnia-to-bruno.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import each from 'lodash/each';
import get from 'lodash/get';
import jsyaml from 'js-yaml';
import { validateSchema, transformItemsInCollection, hydrateSeqInCollection, uuid } from '../common';
import { buildV5Environments, buildV4Environments } from './env-utils';

const parseGraphQL = (text) => {
try {
const graphql = JSON.parse(text);

return {
query: graphql.query,
query: normalizeVariables(graphql.query),
variables: JSON.stringify(graphql.variables, null, 2)
};
} catch (e) {
Expand Down Expand Up @@ -49,7 +50,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
name,
type: 'http-request',
request: {
url: request.url,
url: normalizeVariables(request.url),
method: request.method,
auth: {
mode: 'none',
Expand All @@ -74,7 +75,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
brunoRequestItem.request.headers.push({
uid: uuid(),
name: header.name,
value: header.value,
value: normalizeVariables(header.value),
description: header.description,
enabled: !header.disabled
});
Expand All @@ -84,7 +85,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: param.description,
type: 'query',
enabled: !param.disabled
Expand All @@ -95,7 +96,7 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
brunoRequestItem.request.params.push({
uid: uuid(),
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: '',
type: 'path',
enabled: true
Expand All @@ -121,14 +122,14 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {

if (mimeType === 'application/json') {
brunoRequestItem.request.body.mode = 'json';
brunoRequestItem.request.body.json = request.body.text;
brunoRequestItem.request.body.json = normalizeVariables(request.body.text);
} else if (mimeType === 'application/x-www-form-urlencoded') {
brunoRequestItem.request.body.mode = 'formUrlEncoded';
each(request.body.params, (param) => {
brunoRequestItem.request.body.formUrlEncoded.push({
uid: uuid(),
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: param.description,
enabled: !param.disabled
});
Expand All @@ -140,17 +141,17 @@ const transformInsomniaRequestItem = (request, index, allRequests) => {
uid: uuid(),
type: 'text',
name: param.name,
value: param.value,
value: normalizeVariables(param.value),
description: param.description,
enabled: !param.disabled
});
});
} else if (mimeType === 'text/plain') {
brunoRequestItem.request.body.mode = 'text';
brunoRequestItem.request.body.text = request.body.text;
brunoRequestItem.request.body.text = normalizeVariables(request.body.text);
} else if (mimeType === 'text/xml' || mimeType === 'application/xml') {
brunoRequestItem.request.body.mode = 'xml';
brunoRequestItem.request.body.xml = request.body.text;
brunoRequestItem.request.body.xml = normalizeVariables(request.body.text);
} else if (mimeType === 'application/graphql') {
brunoRequestItem.type = 'graphql-request';
brunoRequestItem.request.body.mode = 'graphql';
Expand Down Expand Up @@ -229,7 +230,7 @@ const parseInsomniaV5Collection = (data) => {

// Parse environments if available
if (data.environments) {
// Handle environments implementation if needed
brunoCollection.environments = buildV5Environments(data.environments);
}

return brunoCollection;
Expand Down Expand Up @@ -287,6 +288,9 @@ const parseInsomniaCollection = (data) => {
}

brunoCollection.items = createFolderStructure(requestsAndFolders, insomniaCollection._id);

// Build environments from resources
brunoCollection.environments = buildV4Environments(insomniaResources, insomniaCollection._id);
return brunoCollection;
} catch (err) {
console.error('Error parsing collection:', err);
Expand Down
51 changes: 51 additions & 0 deletions packages/bruno-converters/src/utils/flatten.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Adapted from flat library by Hugh Kennedy (https://github.com/hughsk/flat)
// MIT License

/**
* Recursively flattens a nested object or array into a flat object with JavaScript-style keys.
* Arrays use square bracket notation (e.g., items[0].id).
* Only primitives and null are included as values.
*
* @param {object|array} obj - The object or array to flatten.
* @param {string} [prefix] - Used internally for recursion to build the path.
* @returns {object} A flat object with JavaScript-style keys.
*/
function flattenObject(obj, prefix = '') {
// Store the final flat result
const result = {};

/**
* Internal recursive function to process each value.
* @param {*} value - The current value (can be object, array, primitive, or null)
* @param {string} path - The JavaScript-style key up to this point
*/
function step(value, path) {
// If value is a primitive (string, number, boolean) or null, add it to the result
if (value === null || typeof value !== 'object') {
result[path] = value;
return;
}

// If value is an array, iterate over each item by index
if (Array.isArray(value)) {
value.forEach((item, idx) => {
// Build the next path with array index using square brackets (e.g. "items[0]")
step(item, path ? `${path}[${idx}]` : `[${idx}]`);
});
} else {
// If value is an object, iterate over its keys
Object.entries(value).forEach(([key, val]) => {
// Build the next path with object key (e.g. "user.name")
step(val, path ? `${path}.${key}` : key);
});
}
}

// Start recursive flattening from the root object
step(obj, prefix);

// Return the flat result object
return result;
}

export { flattenObject };
100 changes: 100 additions & 0 deletions packages/bruno-converters/tests/insomnia/env-utils.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { describe, it, expect } from '@jest/globals';
import { buildV5Environments, buildV4Environments } from '../../src/insomnia/env-utils';

const getVar = (env, name) => {
return env.variables.find((v) => v.name === name);
};

describe('env-utils', () => {
describe('buildV5Environments', () => {
it('creates base and sub environments with flattened keys and shallow overrides', () => {
const environmentsNode = {
name: 'Base',
data: {
baseurl: 'https://api.example.com',
nested: { name: 'alice', roles: ['admin'] },
numbers: [1, 2]
},
subEnvironments: [
{
name: 'Staging',
data: {
baseurl: 'https://staging.example.com',
nested: { name: 'bob' }
}
},
{ name: 'Dev', data: {} }
]
};

const envs = buildV5Environments(environmentsNode);
expect(envs.length).toBe(3);

const base = envs[0];
const staging = envs[1];
const dev = envs[2];

expect(base.name).toBe('Base');
expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(base, 'nested.name')?.value).toBe('alice');
expect(getVar(base, 'nested.roles[0]')?.value).toBe('admin');
expect(getVar(base, 'numbers[1]')?.value).toBe('2');

expect(staging.name).toBe('Staging');
// baseurl overridden in sub
expect(getVar(staging, 'baseurl')?.value).toBe('https://staging.example.com');
// nested.name overridden, nested array preserved from base
expect(getVar(staging, 'nested.name')?.value).toBe('bob');
expect(getVar(staging, 'nested.roles[0]')?.value).toBe('admin');

expect(dev.name).toBe('Dev');
// no sub data => inherits base
expect(getVar(dev, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(dev, 'nested.name')?.value).toBe('alice');
});
});

describe('buildV4Environments', () => {
it('merges nearest base and sub env data (flattened) into standalone Bruno envs', () => {
const workspaceId = 'wrk_1';
const resources = [
{ _id: workspaceId, _type: 'workspace', name: 'WS' },
{
_id: 'env_base',
_type: 'environment',
parentId: workspaceId,
name: 'Base',
data: {
baseurl: 'https://api.example.com',
user: { name: 'alice' },
arr: [{ id: 1 }]
}
},
{
_id: 'env_sub',
_type: 'environment',
parentId: 'env_base',
name: 'Sub',
data: {
user: { name: 'bob' }
}
}
];

const envs = buildV4Environments(resources, workspaceId);
expect(envs.length).toBe(2);

const base = envs.find((e) => e.name === 'Base');
const sub = envs.find((e) => e.name === 'Sub');

expect(getVar(base, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(base, 'user.name')?.value).toBe('alice');
expect(getVar(base, 'arr[0].id')?.value).toBe('1');

// sub should inherit base, override user.name
expect(getVar(sub, 'baseurl')?.value).toBe('https://api.example.com');
expect(getVar(sub, 'user.name')?.value).toBe('bob');
expect(getVar(sub, 'arr[0].id')?.value).toBe('1');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,27 @@ environments:
`

const expectedOutput = {
"environments": [],
environments: [
{
name: 'Imported Environment',
variables: [
{
name: 'var1',
value: 'value1',
type: 'text',
enabled: true,
secret: false
},
{
name: 'var2',
value: 'value2',
type: 'text',
enabled: true,
secret: false
}
]
}
],
"items": [
{
"items": [
Expand Down
Loading
Loading