Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve how the URL values are transformed in postman export. #3025

Merged
merged 11 commits into from
Sep 18, 2024
72 changes: 72 additions & 0 deletions packages/bruno-app/src/utils/collections/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,76 @@ export const exportCollection = (collection) => {
FileSaver.saveAs(fileBlob, fileName);
};

/**
* Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
*
* @param {string} url - The raw URL to be transformed.
* @param {Object} params - The params object.
* @returns {Object} An object containing the URL's protocol, host, path, query, and variables.
*/
export const transformUrl = (url, params) => {
const urlRegexPatterns = {
protocolAndRestSeparator: /:\/\//,
hostAndPathSeparator: /\/(.+)/,
domainSegmentSeparator: /\./,
pathSegmentSeparator: /\//,
queryStringSeparator: /\?/
};

const postmanUrl = { raw: url };

/**
* Splits a URL into its protocol, host and path.
*
* @param {string} url - The URL to be split.
* @returns {Object} An object containing the protocol and the raw host/path string.
*/
const splitUrl = (url) => {
const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);
if (urlParts.length === 1) {
return { protocol: '', rawHostAndPath: urlParts[0] };
} else if (urlParts.length === 2) {
const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);
return { protocol: urlParts[0], rawHostAndPath: hostAndPath };
} else {
throw new Error(`Invalid URL format: ${url}`);
}
};

/**
* Splits the host and path from a raw host/path string.
*
* @param {string} rawHostAndPath - The raw host and path string to be split.
* @returns {Object} An object containing the host and path.
*/
const splitHostAndPath = (rawHostAndPath) => {
const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);
return { host, path };
};

try {
const { protocol, rawHostAndPath } = splitUrl(url);
postmanUrl.protocol = protocol;

const { host, path } = splitHostAndPath(rawHostAndPath);
postmanUrl.host = host.split(urlRegexPatterns.domainSegmentSeparator);
postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator).filter(Boolean) : [];
} catch (error) {
console.error(error.message);
return {};
}

// Construct query params.
postmanUrl.query = params
.filter((param) => param.type === 'query')
.map(({ name, value, description }) => ({ key: name, value, description }));

// Construct path params.
postmanUrl.variable = params
.filter((param) => param.type === 'path')
.map(({ name, value, description }) => ({ key: name, value, description }));

return postmanUrl;
};

export default exportCollection;
41 changes: 5 additions & 36 deletions packages/bruno-app/src/utils/exporters/postman-collection.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import map from 'lodash/map';
import * as FileSaver from 'file-saver';
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from 'utils/collections/export';
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems, transformUrl } from 'utils/collections/export';
import { sanitizeUrl } from 'utils/url/index';

export const exportCollection = (collection) => {
delete collection.uid;
Expand Down Expand Up @@ -177,49 +178,17 @@ export const exportCollection = (collection) => {
}
};

const generateHost = (url) => {
try {
const { hostname } = new URL(url);
return hostname.split('.');
} catch (error) {
console.error(`Invalid URL: ${url}`, error);
return [];
}
};

const generatePathParams = (params) => {
return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`);
};

const generateQueryParams = (params) => {
return params
.filter((param) => param.type === 'query')
.map(({ name, value, description }) => ({ key: name, value, description }));
};

const generateVariables = (params) => {
return params
.filter((param) => param.type === 'path')
.map(({ name, value, description }) => ({ key: name, value, description }));
};

const generateRequestSection = (itemRequest) => {
const requestObject = {
method: itemRequest.method,
header: generateHeaders(itemRequest.headers),
auth: generateAuth(itemRequest.auth),
description: itemRequest.docs,
url: {
raw: itemRequest.url,
host: generateHost(itemRequest.url),
path: generatePathParams(itemRequest.params),
query: generateQueryParams(itemRequest.params),
variable: generateVariables(itemRequest.params)
},
auth: generateAuth(itemRequest.auth)
// We sanitize the URL to make sure it's in the right format before passing it to the transformUrl func. This means changing backslashes to forward slashes and reducing multiple slashes to a single one, except in the protocol part.
url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params)
};

if (itemRequest.body.mode != 'none') {
if (itemRequest.body.mode !== 'none') {
requestObject.body = generateBody(itemRequest.body);
}
return requestObject;
Expand Down
62 changes: 62 additions & 0 deletions packages/bruno-app/src/utils/exporters/postman-collection.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const { transformUrl } = require('../collections/export');

describe('transformUrl', () => {
it('should handle basic URL with path variables', () => {
const url = 'https://example.com/{{username}}/api/resource/:id';
const params = [
{ name: 'id', value: '123', type: 'path' },
];

const result = transformUrl(url, params);

expect(result).toEqual({
raw: 'https://example.com/{{username}}/api/resource/:id',
protocol: 'https',
host: ['example', 'com'],
path: ['{{username}}', 'api', 'resource', ':id'],
query: [],
variable: [
{ key: 'id', value: '123' },
]
});
});

it('should handle URL with query parameters', () => {
const url = 'https://example.com/api/resource?limit=10&offset=20';
const params = [
{ name: 'limit', value: '10', type: 'query' },
{ name: 'offset', value: '20', type: 'query' }
];

const result = transformUrl(url, params);

expect(result).toEqual({
raw: 'https://example.com/api/resource?limit=10&offset=20',
protocol: 'https',
host: ['example', 'com'],
path: ['api', 'resource'],
query: [
{ key: 'limit', value: '10' },
{ key: 'offset', value: '20' }
],
variable: []
});
});

it('should handle URL without protocol', () => {
const url = 'example.com/api/resource';
const params = [];

const result = transformUrl(url, params);

expect(result).toEqual({
raw: 'example.com/api/resource',
protocol: '',
host: ['example', 'com'],
path: ['api', 'resource'],
query: [],
variable: []
});
});
});

23 changes: 23 additions & 0 deletions packages/bruno-app/src/utils/url/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,29 @@ const hasLength = (str) => {
return str.length > 0;
};

/**
* Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).
*
* @param {String} url - A URL string
* @returns {String} The sanitized URL
*
*/
const collapseDuplicateSlashes = (url) => {
return url.replace(/(?<!:)\/{2,}/g, '/');
};

/**
* Replaces all `\\` (backslashes) with `//` (forward slashes) and collapses multiple slashes into one.
*
* @param {string} url - The URL to sanitize.
* @returns {string} The sanitized URL.
*
*/
export const sanitizeUrl = (url) => {
let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//'));
return sanitizedUrl;
};

export const parseQueryParams = (query) => {
try {
if (!query || !query.length) {
Expand Down
22 changes: 21 additions & 1 deletion packages/bruno-app/src/utils/url/index.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { parseQueryParams, splitOnFirst, parsePathParams, interpolateUrl, interpolateUrlPathParams } from './index';
import { parseQueryParams, splitOnFirst, parsePathParams, interpolateUrl, interpolateUrlPathParams, sanitizeUrl } from './index';

describe('Url Utils - parseQueryParams', () => {
it('should parse query - case 1', () => {
Expand Down Expand Up @@ -209,3 +209,23 @@ describe('Url Utils - interpolateUrl, interpolateUrlPathParams', () => {
expect(result).toEqual(expectedUrl);
});
});

describe('Url Utils - sanitizeUrl', () => {
test('should replace backslashes with slashes', () => {
const input = 'http:\\\\example.com\\path\\to\\file';
const expected = 'http://example.com/path/to/file';
expect(sanitizeUrl(input)).toBe(expected);
});

test('should collapse multiple slashes into a single slash', () => {
const input = 'http://example.com//path///to////file';
const expected = 'http://example.com/path/to/file';
expect(sanitizeUrl(input)).toBe(expected);
});

test('should handle URLs with mixed slashes', () => {
const input = 'http:\\example.com//path\\to//file';
const expected = 'http://example.com/path/to/file';
expect(sanitizeUrl(input)).toBe(expected);
});
});