Skip to content

Commit cbf1ff3

Browse files
committed
refactor(requestFn): Simplify the getQueryString function by using the URLSearchParams power instead of the custom implementation weakness
1 parent e4a0e79 commit cbf1ff3

2 files changed

Lines changed: 247 additions & 27 deletions

File tree

packages/react-client/src/lib/requestFn.ts

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -116,37 +116,24 @@ export function urlSerializer(
116116
}
117117

118118
function getQueryString(params: Record<string, any>): string {
119-
const qs: string[] = [];
120-
121-
const append = (key: string, value: any) => {
122-
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
123-
};
124-
125-
const process = (key: string, value: any) => {
126-
if (typeof value === 'undefined' || value === null) return;
127-
128-
if (Array.isArray(value)) {
129-
value.forEach((v) => {
130-
process(key, v);
131-
});
132-
} else if (typeof value === 'object') {
133-
Object.entries(value).forEach(([k, v]) => {
134-
process(`${key}[${k}]`, v);
135-
});
136-
} else {
137-
append(key, value);
119+
const search = new URLSearchParams();
120+
121+
const walk = (prefix: string, value: any) => {
122+
if (value == null) return;
123+
if (value instanceof Date)
124+
return search.append(prefix, value.toISOString());
125+
if (Array.isArray(value)) return value.forEach((v) => walk(prefix, v));
126+
if (typeof value === 'object') {
127+
return Object.entries(value).forEach(([k, v]) =>
128+
walk(`${prefix}[${k}]`, v)
129+
);
138130
}
131+
search.append(prefix, String(value));
139132
};
140133

141-
Object.entries(params).forEach(([key, value]) => {
142-
process(key, value);
143-
});
144-
145-
if (qs.length > 0) {
146-
return `?${qs.join('&')}`;
147-
}
134+
Object.entries(params).forEach(([k, v]) => walk(k, v));
148135

149-
return '';
136+
return search.toString() ? `?${search.toString()}` : '';
150137
}
151138

152139
export function mergeHeaders(...allHeaders: (HeadersOptions | undefined)[]) {

packages/react-client/src/lib/urlSerializer.test.ts

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,237 @@ describe('urlSerializer', () => {
247247
)
248248
).toBe('https://api.example.com/users/undefined/posts/456');
249249
});
250+
251+
it('should correctly serialize query parameters with Date objects', () => {
252+
expect(
253+
urlSerializer(
254+
{ url: '/events', method: 'get' },
255+
{
256+
baseUrl: 'https://api.example.com',
257+
parameters: {
258+
query: {
259+
startDate: new Date('2023-12-25T10:30:00.000Z'),
260+
limit: 5,
261+
},
262+
},
263+
}
264+
)
265+
).toBe(
266+
`https://api.example.com/events?startDate=2023-12-25T10%3A30%3A00.000Z&limit=5`
267+
);
268+
});
269+
270+
it('should correctly serialize query parameters with Date objects in arrays', () => {
271+
expect(
272+
urlSerializer(
273+
{ url: '/events', method: 'get' },
274+
{
275+
baseUrl: 'https://api.example.com',
276+
parameters: {
277+
query: {
278+
dates: [
279+
new Date('2023-01-01T00:00:00.000Z'),
280+
new Date('2023-12-31T23:59:59.999Z'),
281+
],
282+
active: true,
283+
},
284+
},
285+
}
286+
)
287+
).toBe(
288+
'https://api.example.com/events?dates=2023-01-01T00%3A00%3A00.000Z&dates=2023-12-31T23%3A59%3A59.999Z&active=true'
289+
);
290+
});
291+
292+
it('should correctly serialize query parameters with Date objects in nested objects', () => {
293+
const startDate = new Date('2023-01-01T00:00:00.000Z');
294+
expect(
295+
urlSerializer(
296+
{ url: '/events', method: 'get' },
297+
{
298+
baseUrl: 'https://api.example.com',
299+
parameters: {
300+
query: {
301+
timeRange: {
302+
start: startDate,
303+
end: null,
304+
},
305+
limit: 10,
306+
},
307+
},
308+
}
309+
)
310+
).toBe(
311+
'https://api.example.com/events?timeRange%5Bstart%5D=2023-01-01T00%3A00%3A00.000Z&limit=10'
312+
);
313+
});
314+
315+
it('should correctly serialize query parameters with boolean values', () => {
316+
expect(
317+
urlSerializer(
318+
{ url: '/users', method: 'get' },
319+
{
320+
baseUrl: 'https://api.example.com',
321+
parameters: {
322+
query: {
323+
active: true,
324+
verified: false,
325+
premium: null,
326+
limit: 10,
327+
},
328+
},
329+
}
330+
)
331+
).toBe('https://api.example.com/users?active=true&verified=false&limit=10');
332+
});
333+
334+
it('should correctly serialize query parameters with numeric values', () => {
335+
expect(
336+
urlSerializer(
337+
{ url: '/products', method: 'get' },
338+
{
339+
baseUrl: 'https://api.example.com',
340+
parameters: {
341+
query: {
342+
price: 0,
343+
discount: 25.5,
344+
quantity: -1,
345+
maxPrice: 1000,
346+
},
347+
},
348+
}
349+
)
350+
).toBe(
351+
'https://api.example.com/products?price=0&discount=25.5&quantity=-1&maxPrice=1000'
352+
);
353+
});
354+
355+
it('should correctly serialize query parameters with special numeric values', () => {
356+
expect(
357+
urlSerializer(
358+
{ url: '/data', method: 'get' },
359+
{
360+
baseUrl: 'https://api.example.com',
361+
parameters: {
362+
query: {
363+
infinity: Infinity,
364+
negativeInfinity: -Infinity,
365+
notANumber: NaN,
366+
normal: 42,
367+
},
368+
},
369+
}
370+
)
371+
).toBe(
372+
'https://api.example.com/data?infinity=Infinity&negativeInfinity=-Infinity&notANumber=NaN&normal=42'
373+
);
374+
});
375+
376+
it('should correctly serialize query parameters with empty string values', () => {
377+
expect(
378+
urlSerializer(
379+
{ url: '/search', method: 'get' },
380+
{
381+
baseUrl: 'https://api.example.com',
382+
parameters: {
383+
query: {
384+
query: '',
385+
category: 'books',
386+
sort: null,
387+
},
388+
},
389+
}
390+
)
391+
).toBe('https://api.example.com/search?query=&category=books');
392+
});
393+
394+
it('should correctly serialize deeply nested objects', () => {
395+
expect(
396+
urlSerializer(
397+
{ url: '/complex', method: 'get' },
398+
{
399+
baseUrl: 'https://api.example.com',
400+
parameters: {
401+
query: {
402+
level1: {
403+
level2: {
404+
level3: {
405+
value: 'deep',
406+
number: 42,
407+
},
408+
array: ['a', 'b'],
409+
},
410+
},
411+
simple: 'value',
412+
},
413+
},
414+
}
415+
)
416+
).toBe(
417+
'https://api.example.com/complex?level1%5Blevel2%5D%5Blevel3%5D%5Bvalue%5D=deep&level1%5Blevel2%5D%5Blevel3%5D%5Bnumber%5D=42&level1%5Blevel2%5D%5Barray%5D=a&level1%5Blevel2%5D%5Barray%5D=b&simple=value'
418+
);
419+
});
420+
421+
it('should correctly serialize mixed arrays with different types', () => {
422+
const testDate = new Date('2023-06-15T12:00:00.000Z');
423+
expect(
424+
urlSerializer(
425+
{ url: '/mixed', method: 'get' },
426+
{
427+
baseUrl: 'https://api.example.com',
428+
parameters: {
429+
query: {
430+
mixed: ['string', 123, true, testDate, null, undefined],
431+
clean: 'value',
432+
},
433+
},
434+
}
435+
)
436+
).toBe(
437+
'https://api.example.com/mixed?mixed=string&mixed=123&mixed=true&mixed=2023-06-15T12%3A00%3A00.000Z&clean=value'
438+
);
439+
});
440+
441+
it('should handle objects with arrays containing objects', () => {
442+
expect(
443+
urlSerializer(
444+
{ url: '/nested-array-objects', method: 'get' },
445+
{
446+
baseUrl: 'https://api.example.com',
447+
parameters: {
448+
query: {
449+
items: [
450+
{ name: 'item1', value: 100 },
451+
{ name: 'item2', value: null },
452+
],
453+
total: 2,
454+
},
455+
},
456+
}
457+
)
458+
).toBe(
459+
'https://api.example.com/nested-array-objects?items%5Bname%5D=item1&items%5Bvalue%5D=100&items%5Bname%5D=item2&total=2'
460+
);
461+
});
462+
463+
it('should handle edge cases with String() conversion', () => {
464+
// Test to verify correct processing of different types of data
465+
expect(
466+
urlSerializer(
467+
{ url: '/edge-cases', method: 'get' },
468+
{
469+
baseUrl: 'https://api.example.com',
470+
parameters: {
471+
query: {
472+
bigint: BigInt(123),
473+
symbol: 'symbol-as-string', // Symbol cannot be serialized directly
474+
func: '[Function]', // Functions should not get into query, but if they do - as a string
475+
},
476+
},
477+
}
478+
)
479+
).toBe(
480+
'https://api.example.com/edge-cases?bigint=123&symbol=symbol-as-string&func=%5BFunction%5D'
481+
);
482+
});
250483
});

0 commit comments

Comments
 (0)