Skip to content

Commit 0de04b5

Browse files
nuthanmunaiahNuthan Munaiah
andauthored
Fix array-type query parameter handling (#645)
In this change, a bug in the way VsoClient.queryParamsToStringHelper handles array-typed query parameters values is fix. After this change, a query parameter dictionary like {states: ["active", "inactive"]} will produce states=active,inactive instead of states.0=active&states.1=inactive. Fix #643 Changes * Add test to recreate array-typed query param bug * Handle array-typed query parameter values * Update version from `15.1.0` to `15.1.1` * Make array-type parameter comma-separated value * Add a more exhaustive test for query parameters * Add check for trailing `.`. --------- Co-authored-by: Nuthan Munaiah <[email protected]>
1 parent 82ddae6 commit 0de04b5

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-4
lines changed

api/VsoClient.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ export class VsoClient {
194194
}
195195
let queryString: string = '';
196196

197+
if (Array.isArray(queryParams)) {
198+
// Remove trailing '.' from prefix if it exists, otherwise use the prefix as-is
199+
const paramName = prefix.endsWith('.') ? prefix.slice(0, -1) : prefix;
200+
const values = queryParams.map(value => value.toString()).join(',');
201+
queryString += paramName + '=' + encodeURIComponent(values) + '&';
202+
return queryString;
203+
}
204+
197205
if (typeof (queryParams) !== 'string') {
198206
for (let property in queryParams) {
199207
if (queryParams.hasOwnProperty(property)) {
@@ -209,9 +217,9 @@ export class VsoClient {
209217
// Need to specially call `toUTCString()` instead for such cases
210218
const queryValue = typeof queryParams === 'object' && 'toUTCString' in queryParams ? (queryParams as Date).toUTCString() : queryParams.toString();
211219

212-
213-
// Will always need to chop period off of end of prefix
214-
queryString = prefix.slice(0, -1) + '=' + encodeURIComponent(queryValue) + '&';
220+
// Will always need to chop period off of end of prefix, if it exists
221+
const paramName = prefix.endsWith('.') ? prefix.slice(0, -1) : prefix;
222+
queryString = paramName + '=' + encodeURIComponent(queryValue) + '&';
215223
}
216224
return queryString;
217225
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "azure-devops-node-api",
33
"description": "Node client for Azure DevOps and TFS REST APIs",
4-
"version": "15.1.0",
4+
"version": "15.1.1",
55
"main": "./WebApi.js",
66
"types": "./WebApi.d.ts",
77
"scripts": {

test/units/tests.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,62 @@ describe('VSOClient Units', function () {
163163
assert.equal(res.requestUrl, 'https://dev.azure.com/testTemplate?status.innerstatus=2&version=1&nestedObject.nestedField=value&nestedObject.innerNestedObject.key=val2');
164164
});
165165

166+
it('gets versioning data with array-typed query params', async () => {
167+
//Arrange
168+
nock('https://dev.azure.com/_apis/testArea5', {
169+
reqheaders: {
170+
'accept': 'application/json',
171+
'user-agent': 'testAgent'
172+
}})
173+
.options('')
174+
.reply(200, {
175+
value: [{id: 'testLocation', maxVersion: '1', releasedVersion: '1', routeTemplate: 'testTemplate', area: 'testArea5', resourceName: 'testName', resourceVersion: '1'}]
176+
});
177+
178+
//Act
179+
const queryParams = {states: ["active", "inactive"]};
180+
const res: vsom.ClientVersioningData = await vsoClient.getVersioningData('1', 'testArea5', 'testLocation', {'testKey': 'testValue'}, queryParams);
181+
182+
//Assert
183+
assert.equal(res.apiVersion, '1');
184+
assert.equal(res.requestUrl, 'https://dev.azure.com/testTemplate?states=active%2Cinactive');
185+
});
186+
187+
it('gets versioning data with mixed query parameter types', async () => {
188+
//Arrange
189+
nock('https://dev.azure.com/_apis/testArea9', {
190+
reqheaders: {
191+
'accept': 'application/json',
192+
'user-agent': 'testAgent'
193+
}})
194+
.options('')
195+
.reply(200, {
196+
value: [{id: 'testLocation', maxVersion: '1', releasedVersion: '1', routeTemplate: 'testTemplate', area: 'testArea9', resourceName: 'testName', resourceVersion: '1'}]
197+
});
198+
199+
//Act
200+
const queryParams = {
201+
// Simple parameter
202+
status: 'completed',
203+
// Nested parameter
204+
filter: { type: 'build' },
205+
// Complex nested parameter
206+
options: {
207+
sorting: { field: 'date', order: 'desc' },
208+
pagination: { limit: 50 }
209+
},
210+
// Array parameter
211+
tags: ['important', 'release', 'verified'],
212+
// Another array parameter
213+
assignedTo: ['user1', 'user2']
214+
};
215+
const res: vsom.ClientVersioningData = await vsoClient.getVersioningData('1', 'testArea9', 'testLocation', {'testKey': 'testValue'}, queryParams);
216+
217+
//Assert
218+
assert.equal(res.apiVersion, '1');
219+
assert.equal(res.requestUrl, 'https://dev.azure.com/testTemplate?status=completed&filter.type=build&options.sorting.field=date&options.sorting.order=desc&options.pagination.limit=50&tags=important%2Crelease%2Cverified&assignedTo=user1%2Cuser2');
220+
});
221+
166222
it('gets versioning datafor dates', async () => {
167223
//Arrange
168224
nock('https://dev.azure.com/_apis/testArea5', {

0 commit comments

Comments
 (0)