Skip to content

Commit 00fcd30

Browse files
authored
Improve how the URL values are transformed in postman export. (usebruno#3025)
* Improve how the URL values are transformed. * Made few changes and also added jsdoc comments * Removed the querystring values that are getting appended in the host array by filtering you the the queryvalues as we already have the queryparams values inside the request.params object. * Moved the transformUrl logic to a different file for testing. Added new tests. * Added tests and updated sanitizeUrl function. * Updates made in jsdocs. * Updated function params. * Review: Code restructure. * Small changes made. * Updated the return value when there is an error. * Changes
1 parent 07baa63 commit 00fcd30

File tree

2 files changed

+184
-36
lines changed

2 files changed

+184
-36
lines changed

Diff for: packages/bruno-app/src/utils/exporters/postman-collection.js

+103-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,105 @@
11
import map from 'lodash/map';
22
import * as FileSaver from 'file-saver';
3-
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from 'utils/collections/export';
3+
import { deleteSecretsInEnvs, deleteUidsInEnvs, deleteUidsInItems } from '../collections/export';
4+
5+
/**
6+
* Transforms a given URL string into an object representing the protocol, host, path, query, and variables.
7+
*
8+
* @param {string} url - The raw URL to be transformed.
9+
* @param {Object} params - The params object.
10+
* @returns {Object|null} An object containing the URL's protocol, host, path, query, and variables, or {} if an error occurs.
11+
*/
12+
export const transformUrl = (url, params) => {
13+
if (typeof url !== 'string' || !url.trim()) {
14+
throw new Error("Invalid URL input");
15+
}
16+
17+
const urlRegexPatterns = {
18+
protocolAndRestSeparator: /:\/\//,
19+
hostAndPathSeparator: /\/(.+)/,
20+
domainSegmentSeparator: /\./,
21+
pathSegmentSeparator: /\//,
22+
queryStringSeparator: /\?/
23+
};
24+
25+
const postmanUrl = { raw: url };
26+
27+
/**
28+
* Splits a URL into its protocol, host and path.
29+
*
30+
* @param {string} url - The URL to be split.
31+
* @returns {Object} An object containing the protocol and the raw host/path string.
32+
*/
33+
const splitUrl = (url) => {
34+
const urlParts = url.split(urlRegexPatterns.protocolAndRestSeparator);
35+
if (urlParts.length === 1) {
36+
return { protocol: '', rawHostAndPath: urlParts[0] };
37+
} else if (urlParts.length === 2) {
38+
const [hostAndPath, _] = urlParts[1].split(urlRegexPatterns.queryStringSeparator);
39+
return { protocol: urlParts[0], rawHostAndPath: hostAndPath };
40+
} else {
41+
throw new Error(`Invalid URL format: ${url}`);
42+
}
43+
};
44+
45+
/**
46+
* Splits the host and path from a raw host/path string.
47+
*
48+
* @param {string} rawHostAndPath - The raw host and path string to be split.
49+
* @returns {Object} An object containing the host and path.
50+
*/
51+
const splitHostAndPath = (rawHostAndPath) => {
52+
const [host, path = ''] = rawHostAndPath.split(urlRegexPatterns.hostAndPathSeparator);
53+
return { host, path };
54+
};
55+
56+
try {
57+
const { protocol, rawHostAndPath } = splitUrl(url);
58+
postmanUrl.protocol = protocol;
59+
60+
const { host, path } = splitHostAndPath(rawHostAndPath);
61+
postmanUrl.host = host ? host.split(urlRegexPatterns.domainSegmentSeparator) : [];
62+
postmanUrl.path = path ? path.split(urlRegexPatterns.pathSegmentSeparator) : [];
63+
} catch (error) {
64+
console.error(error.message);
65+
return {};
66+
}
67+
68+
// Construct query params.
69+
postmanUrl.query = params
70+
.filter((param) => param.type === 'query')
71+
.map(({ name, value, description }) => ({ key: name, value, description }));
72+
73+
// Construct path params.
74+
postmanUrl.variable = params
75+
.filter((param) => param.type === 'path')
76+
.map(({ name, value, description }) => ({ key: name, value, description }));
77+
78+
return postmanUrl;
79+
};
80+
81+
/**
82+
* Collapses multiple consecutive slashes (`//`) into a single slash, while skipping the protocol (e.g., `http://` or `https://`).
83+
*
84+
* @param {String} url - A URL string
85+
* @returns {String} The sanitized URL
86+
*
87+
*/
88+
const collapseDuplicateSlashes = (url) => {
89+
return url.replace(/(?<!:)\/{2,}/g, '/');
90+
};
91+
92+
/**
93+
* Replaces all `\\` (backslashes) with `//` (forward slashes) and collapses multiple slashes into one.
94+
*
95+
* @param {string} url - The URL to sanitize.
96+
* @returns {string} The sanitized URL.
97+
*
98+
*/
99+
export const sanitizeUrl = (url) => {
100+
let sanitizedUrl = collapseDuplicateSlashes(url.replace(/\\/g, '//'));
101+
return sanitizedUrl;
102+
};
4103

5104
export const exportCollection = (collection) => {
6105
delete collection.uid;
@@ -177,49 +276,17 @@ export const exportCollection = (collection) => {
177276
}
178277
};
179278

180-
const generateHost = (url) => {
181-
try {
182-
const { hostname } = new URL(url);
183-
return hostname.split('.');
184-
} catch (error) {
185-
console.error(`Invalid URL: ${url}`, error);
186-
return [];
187-
}
188-
};
189-
190-
const generatePathParams = (params) => {
191-
return params.filter((param) => param.type === 'path').map((param) => `:${param.name}`);
192-
};
193-
194-
const generateQueryParams = (params) => {
195-
return params
196-
.filter((param) => param.type === 'query')
197-
.map(({ name, value, description }) => ({ key: name, value, description }));
198-
};
199-
200-
const generateVariables = (params) => {
201-
return params
202-
.filter((param) => param.type === 'path')
203-
.map(({ name, value, description }) => ({ key: name, value, description }));
204-
};
205-
206279
const generateRequestSection = (itemRequest) => {
207280
const requestObject = {
208281
method: itemRequest.method,
209282
header: generateHeaders(itemRequest.headers),
210283
auth: generateAuth(itemRequest.auth),
211284
description: itemRequest.docs,
212-
url: {
213-
raw: itemRequest.url,
214-
host: generateHost(itemRequest.url),
215-
path: generatePathParams(itemRequest.params),
216-
query: generateQueryParams(itemRequest.params),
217-
variable: generateVariables(itemRequest.params)
218-
},
219-
auth: generateAuth(itemRequest.auth)
285+
// 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.
286+
url: transformUrl(sanitizeUrl(itemRequest.url), itemRequest.params)
220287
};
221288

222-
if (itemRequest.body.mode != 'none') {
289+
if (itemRequest.body.mode !== 'none') {
223290
requestObject.body = generateBody(itemRequest.body);
224291
}
225292
return requestObject;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
const { sanitizeUrl, transformUrl } = require('./postman-collection');
2+
3+
describe('transformUrl', () => {
4+
it('should handle basic URL with path variables', () => {
5+
const url = 'https://example.com/{{username}}/api/resource/:id';
6+
const params = [
7+
{ name: 'id', value: '123', type: 'path' },
8+
];
9+
10+
const result = transformUrl(url, params);
11+
12+
expect(result).toEqual({
13+
raw: 'https://example.com/{{username}}/api/resource/:id',
14+
protocol: 'https',
15+
host: ['example', 'com'],
16+
path: ['{{username}}', 'api', 'resource', ':id'],
17+
query: [],
18+
variable: [
19+
{ key: 'id', value: '123' },
20+
]
21+
});
22+
});
23+
24+
it('should handle URL with query parameters', () => {
25+
const url = 'https://example.com/api/resource?limit=10&offset=20';
26+
const params = [
27+
{ name: 'limit', value: '10', type: 'query' },
28+
{ name: 'offset', value: '20', type: 'query' }
29+
];
30+
31+
const result = transformUrl(url, params);
32+
33+
expect(result).toEqual({
34+
raw: 'https://example.com/api/resource?limit=10&offset=20',
35+
protocol: 'https',
36+
host: ['example', 'com'],
37+
path: ['api', 'resource'],
38+
query: [
39+
{ key: 'limit', value: '10' },
40+
{ key: 'offset', value: '20' }
41+
],
42+
variable: []
43+
});
44+
});
45+
46+
it('should handle URL without protocol', () => {
47+
const url = 'example.com/api/resource';
48+
const params = [];
49+
50+
const result = transformUrl(url, params);
51+
52+
expect(result).toEqual({
53+
raw: 'example.com/api/resource',
54+
protocol: '',
55+
host: ['example', 'com'],
56+
path: ['api', 'resource'],
57+
query: [],
58+
variable: []
59+
});
60+
});
61+
});
62+
63+
describe('sanitizeUrl', () => {
64+
it('should replace backslashes with slashes', () => {
65+
const input = 'http:\\\\example.com\\path\\to\\file';
66+
const expected = 'http://example.com/path/to/file';
67+
expect(sanitizeUrl(input)).toBe(expected);
68+
});
69+
70+
it('should collapse multiple slashes into a single slash', () => {
71+
const input = 'http://example.com//path///to////file';
72+
const expected = 'http://example.com/path/to/file';
73+
expect(sanitizeUrl(input)).toBe(expected);
74+
});
75+
76+
it('should handle URLs with mixed slashes', () => {
77+
const input = 'http:\\example.com//path\\to//file';
78+
const expected = 'http://example.com/path/to/file';
79+
expect(sanitizeUrl(input)).toBe(expected);
80+
});
81+
})

0 commit comments

Comments
 (0)