Support for Odata style path params (#5048)

* Support for Odata style path params

* Remove test statement

* Update packages/bruno-app/src/utils/url/index.spec.js

Add more testcases

Co-authored-by: sid-bruno <siddharth@usebruno.com>

* Performance improvements

* Add testcases for odata style url params

---------

Co-authored-by: sid-bruno <siddharth@usebruno.com>
This commit is contained in:
Anton
2025-09-23 09:01:00 +02:00
committed by Bijin A B
parent f24e1e78fe
commit 40b44de294
4 changed files with 181 additions and 28 deletions

View File

@@ -32,19 +32,24 @@ export const parsePathParams = (url) => {
paths = uri.split('/');
}
paths = paths.reduce((acc, path) => {
if (path !== '' && path[0] === ':') {
let name = path.slice(1, path.length);
if (name) {
let isExist = find(acc, (path) => path.name === name);
if (!isExist) {
acc.push({ name: path.slice(1, path.length), value: '' });
// Enhanced: also match :param inside parentheses and/or quotes
const paramRegex = /[:](\w+)/g;
const foundParams = new Set();
paths.forEach((segment) => {
let match;
while ((match = paramRegex.exec(segment))) {
if (match[1]) {
// Clean up: remove trailing quotes/parentheses if present
let name = match[1].replace(/[')"`]+$/, '');
// Remove leading quotes/parentheses if present
name = name.replace(/^[('"`]+/, '');
if (name && !foundParams.has(name)) {
foundParams.add(name);
}
}
}
return acc;
}, []);
return paths;
});
return Array.from(foundParams).map((name) => ({ name, value: '' }));
};
export const splitOnFirst = (str, char) => {
@@ -79,13 +84,25 @@ export const interpolateUrl = ({ url, variables }) => {
export const interpolateUrlPathParams = (url, params) => {
const getInterpolatedBasePath = (pathname, params) => {
const regex = /[:](\w+)/g;
return pathname
.split('/')
.map((segment) => {
if (segment.startsWith(':')) {
const pathParamName = segment.slice(1);
const pathParam = params.find((p) => p?.name === pathParamName && p?.type === 'path');
return pathParam ? pathParam.value : segment;
if(!segment.startsWith(":")) return segment
let match;
while ((match = regex.exec(segment))) {
if (match[1]) {
// Clean up: remove trailing quotes/parentheses if present
let name = match[1].replace(/[')"`]+$/, '');
// Remove leading quotes/parentheses if present
name = name.replace(/^[('"`]+/, '');
if (name) {
const pathParam = params.find((p) => p?.name === name && p?.type === 'path');
return pathParam ? pathParam.value : segment;
}
}
}
return segment;
})

View File

@@ -43,6 +43,132 @@ describe('Url Utils - parsePathParams', () => {
{ name: 'postId', value: '' }
]);
});
it('should parse path param inside parentheses and quotes', () => {
const params = parsePathParams("https://example.com/ExchangeRates(':ExchangeRateOID')");
expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]);
});
it('should parse path param inside parentheses and no quotes', () => {
const params = parsePathParams("https://example.com/ExchangeRates(:ExchangeRateOID)");
expect(params).toEqual([{ name: 'ExchangeRateOID', value: '' }]);
});
it('should parse multiple path params inside parentheses', () => {
const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/ExchangeRates(:ExchangeRateOID)");
expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'ExchangeRateOID', value: '' }]);
});
it('should parse mix and match of normal and param inside parentheses', () => {
const params = parsePathParams("https://example.com/Exchange(:ExchangeId)/:key");
expect(params).toEqual([{ name: 'ExchangeId', value: '' },{ name: 'key', value: '' }]);
});
// OData-specific test cases for enhanced path parameter parsing
it('should parse OData entity key with single quotes', () => {
const params = parsePathParams("https://example.com/odata/Products(':productId')");
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should parse OData entity key with double quotes', () => {
const params = parsePathParams('https://example.com/odata/Products(":productId")');
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should parse OData entity key with backticks', () => {
const params = parsePathParams('https://example.com/odata/Products(`:productId`)');
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should parse OData entity key with parentheses only', () => {
const params = parsePathParams("https://example.com/odata/Products(:productId)");
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should parse OData composite key with multiple parameters', () => {
const params = parsePathParams("https://example.com/odata/Orders(:orderId,ProductId=':productId')");
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'productId', value: '' }]);
});
it('should parse OData navigation property with key', () => {
const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')");
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }]);
});
it('should parse OData function with parameters', () => {
const params = parsePathParams("https://example.com/odata/GetProductsByCategory(categoryId=':categoryId')");
expect(params).toEqual([{ name: 'categoryId', value: '' }]);
});
it('should parse OData action with complex parameters', () => {
const params = parsePathParams("https://example.com/odata/Products(':productId')/Rate(rating=:rating,comment=':comment')");
expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'rating', value: '' }, { name: 'comment', value: '' }]);
});
it('should handle OData parameters with special characters in names', () => {
const params = parsePathParams("https://example.com/odata/Products(':product-id')");
expect(params).toEqual([{ name: 'product', value: '' }]);
});
it('should handle OData parameters with underscores in names', () => {
const params = parsePathParams("https://example.com/odata/Products(':product_id')");
expect(params).toEqual([{ name: 'product_id', value: '' }]);
});
it('should handle OData parameters with mixed quote types', () => {
const params = parsePathParams("https://example.com/odata/Products(':productId')/Categories(\":categoryId\")");
expect(params).toEqual([{ name: 'productId', value: '' }, { name: 'categoryId', value: '' }]);
});
it('should handle OData parameters with nested parentheses', () => {
const params = parsePathParams("https://example.com/odata/Products((':productId'))");
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should handle OData parameters with complex nested structures', () => {
const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')/Properties(':propName')");
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]);
});
it('should handle OData parameters with query options in path', () => {
const params = parsePathParams("https://example.com/odata/Products(':productId')?$expand=Category");
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should handle OData parameters with multiple segments and mixed syntax', () => {
const params = parsePathParams("https://example.com/odata/Orders(:orderId)/Items(':itemId')/Properties(:propName)");
expect(params).toEqual([{ name: 'orderId', value: '' }, { name: 'itemId', value: '' }, { name: 'propName', value: '' }]);
});
it('should handle OData parameters with empty string values', () => {
const params = parsePathParams("https://example.com/odata/Products('')");
expect(params).toEqual([]);
});
it('should handle OData parameters with function calls in parentheses', () => {
const params = parsePathParams("https://example.com/odata/Products(GetId(':productId'))");
expect(params).toEqual([{ name: 'productId', value: '' }]);
});
it('should handle OData parameters with escaped quotes', () => {
const params = parsePathParams("https://example.com/odata/Products('ABC''123')");
expect(params).toEqual([]);
});
it('should handle OData parameters with spaces in quotes', () => {
const params = parsePathParams("https://example.com/odata/Products('Product Name With Spaces')");
expect(params).toEqual([]);
});
it('should handle OData parameters with numeric keys', () => {
const params = parsePathParams("https://example.com/odata/Products(12345)");
expect(params).toEqual([]);
});
it('should handle OData parameters with GUID keys', () => {
const params = parsePathParams("https://example.com/odata/Products('123e4567-e89b-12d3-a456-426614174000')");
expect(params).toEqual([]);
});
});
describe('Url Utils - splitOnFirst', () => {

View File

@@ -115,16 +115,21 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
throw { message: 'Invalid URL format', originalError: e.message };
}
const paramRegex = /[:](\w+)/g;
const interpolatedUrlPath = url.pathname
.split('/')
.filter((path) => path !== '')
.map((path) => {
if (path[0] !== ':') {
return '/' + path;
const matches = path.match(paramRegex);
if (matches) {
const paramName = matches[0].slice(1); // Remove the : prefix
const existingPathParam = request.pathParams.find(param => param.name === paramName);
if (!existingPathParam) {
return '/' + path;
}
return '/' + path.replace(':' + paramName, existingPathParam.value);
} else {
const name = path.slice(1);
const existingPathParam = request?.pathParams?.find((param) => param.type === 'path' && param.name === name);
return existingPathParam ? '/' + existingPathParam.value : '';
return '/' + path;
}
})
.join('');

View File

@@ -91,15 +91,15 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
if (typeof request.data === 'string') {
if (request.data.length) {
request.data = _interpolate(request.data, {
escapeJSONStrings: true
});
escapeJSONStrings: true
});
}
} else if (typeof request.data === 'object') {
try {
const jsonDoc = JSON.stringify(request.data);
const parsed = _interpolate(jsonDoc, {
escapeJSONStrings: true
});
escapeJSONStrings: true
});
request.data = JSON.parse(parsed);
} catch (err) {}
}
@@ -142,16 +142,21 @@ const interpolateVars = (request, envVariables = {}, runtimeVariables = {}, proc
throw { message: 'Invalid URL format', originalError: e.message };
}
const paramRegex = /[:](\w+)/g;
const urlPathnameInterpolatedWithPathParams = url.pathname
.split('/')
.filter((path) => path !== '')
.map((path) => {
if (path[0] !== ':') {
return '/' + path;
const matches = path.match(paramRegex);
if (matches) {
const paramName = matches[0].slice(1); // Remove the : prefix
const existingPathParam = request.pathParams.find(param => param.name === paramName);
if (!existingPathParam) {
return '/' + path;
}
return '/' + path.replace(':' + paramName, existingPathParam.value);
} else {
const name = path.slice(1);
const existingPathParam = request.pathParams.find((param) => param.type === 'path' && param.name === name);
return existingPathParam ? '/' + existingPathParam.value : '';
return '/' + path;
}
})
.join('');