Skip to content

Commit 29e78fe

Browse files
AkaHarshitsindresorhus
andauthored
Merge searchParams with input URL (#840)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 2481afd commit 29e78fe

7 files changed

Lines changed: 359 additions & 17 deletions

File tree

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,11 @@ Shortcut for sending JSON. Use this instead of the `body` option. Accepts any pl
187187
Type: `string | object<string, string | number | boolean | undefined> | Array<Array<string | number | boolean>> | URLSearchParams`\
188188
Default: `''`
189189

190-
Search parameters to include in the request URL. Setting this will override all existing search parameters in the input URL.
190+
Search parameters to include in the request URL. Setting this will merge with any existing search parameters in the input URL.
191191

192192
Accepts any value supported by [`URLSearchParams()`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams).
193193

194-
When passing an object, `undefined` values are automatically filtered out, while `null` values are preserved and converted to the string `'null'`.
194+
When passing an object, setting a value to `undefined` deletes the parameter, while `null` values are preserved and converted to the string `'null'`.
195195

196196
##### baseUrl
197197

source/core/Ky.ts

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@ import type {
1515
import {type ResponsePromise} from '../types/ResponsePromise.js';
1616
import type {StandardSchemaV1} from '../types/standard-schema.js';
1717
import {streamRequest, streamResponse} from '../utils/body.js';
18-
import {cloneShallow, mergeHeaders, mergeHooks} from '../utils/merge.js';
18+
import {
19+
cloneShallow,
20+
mergeHeaders,
21+
mergeHooks,
22+
deletedParametersSymbol,
23+
} from '../utils/merge.js';
1924
import {normalizeRequestMethod, normalizeRetryOptions} from '../utils/normalize.js';
2025
import timeout from '../utils/timeout.js';
2126
import delay from '../utils/delay.js';
@@ -375,13 +380,40 @@ export class Ky {
375380
this.request = new globalThis.Request(this.#input, this.#options);
376381

377382
if (hasSearchParameters(this.#options.searchParams)) {
378-
// eslint-disable-next-line unicorn/prevent-abbreviations
379-
const textSearchParams = typeof this.#options.searchParams === 'string'
380-
? this.#options.searchParams.replace(/^\?/, '')
381-
: new URLSearchParams(Ky.#normalizeSearchParams(this.#options.searchParams) as unknown as SearchParamsInit).toString();
382-
// eslint-disable-next-line unicorn/prevent-abbreviations
383-
const searchParams = '?' + textSearchParams;
384-
const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams);
383+
const url = new URL(this.request.url);
384+
385+
if (typeof this.#options.searchParams === 'string') {
386+
const stringSearchParameters = this.#options.searchParams.replace(/^\?/, '');
387+
if (stringSearchParameters !== '') {
388+
url.search = url.search ? `${url.search}&${stringSearchParameters}` : `?${stringSearchParameters}`;
389+
}
390+
} else {
391+
const optionsSearchParameters = new URLSearchParams(Ky.#normalizeSearchParams(this.#options.searchParams) as unknown as SearchParamsInit);
392+
393+
for (const [key, value] of optionsSearchParameters.entries()) {
394+
url.searchParams.append(key, value);
395+
}
396+
}
397+
398+
if (
399+
this.#options.searchParams
400+
&& typeof this.#options.searchParams === 'object'
401+
&& !Array.isArray(this.#options.searchParams)
402+
&& !(this.#options.searchParams instanceof URLSearchParams)
403+
) {
404+
for (const [key, value] of Object.entries(this.#options.searchParams)) {
405+
if (value === undefined) {
406+
url.searchParams.delete(key);
407+
}
408+
}
409+
}
410+
411+
const deleted = (this.#options.searchParams as any)?.[deletedParametersSymbol] as Set<string> | undefined;
412+
if (deleted) {
413+
for (const key of deleted) {
414+
url.searchParams.delete(key);
415+
}
416+
}
385417

386418
// Recreate request with the updated URL. We already have all options in this.#options, including duplex.
387419
this.request = new globalThis.Request(url, this.#options as RequestInit);

source/types/options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,11 @@ export type KyOptions = {
102102
stringifyJson?: (data: unknown) => string;
103103

104104
/**
105-
Search parameters to include in the request URL. Setting this will override all existing search parameters in the input URL.
105+
Search parameters to include in the request URL. Setting this will merge with any existing search parameters in the input URL.
106106
107107
Accepts any value supported by [`URLSearchParams()`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams).
108108
109-
When passing an object, `undefined` values are automatically filtered out, while `null` values are preserved and converted to the string `'null'`.
109+
When passing an object, setting a value to `undefined` deletes the parameter, while `null` values are preserved and converted to the string `'null'`.
110110
*/
111111
searchParams?: SearchParamsOption;
112112

source/utils/merge.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,11 @@ export const mergeHooks = (original: Hooks = {}, incoming: Hooks = {}): Required
136136
}
137137
);
138138

139+
export const deletedParametersSymbol = Symbol('deletedParameters');
140+
139141
const appendSearchParameters = (target: any, source: any): URLSearchParams => {
140-
const result = new URLSearchParams();
142+
const result = new URLSearchParams() as URLSearchParams & {[deletedParametersSymbol]?: Set<string>};
143+
const deleted = new Set<string>();
141144

142145
for (const input of [target, source]) {
143146
if (input === undefined) {
@@ -147,6 +150,15 @@ const appendSearchParameters = (target: any, source: any): URLSearchParams => {
147150
if (input instanceof URLSearchParams) {
148151
for (const [key, value] of input.entries()) {
149152
result.append(key, value);
153+
deleted.delete(key);
154+
}
155+
156+
const inputDeleted = (input as any)[deletedParametersSymbol] as Set<string> | undefined;
157+
if (inputDeleted) {
158+
for (const key of inputDeleted) {
159+
result.delete(key);
160+
deleted.add(key);
161+
}
150162
}
151163
} else if (Array.isArray(input)) {
152164
for (const pair of input) {
@@ -155,22 +167,32 @@ const appendSearchParameters = (target: any, source: any): URLSearchParams => {
155167
}
156168

157169
result.append(String(pair[0]), String(pair[1]));
170+
deleted.delete(String(pair[0]));
158171
}
159172
} else if (isObject(input)) {
160173
for (const [key, value] of Object.entries(input)) {
161-
if (value !== undefined) {
174+
if (value === undefined) {
175+
result.delete(key);
176+
deleted.add(key);
177+
} else {
162178
result.append(key, String(value));
179+
deleted.delete(key);
163180
}
164181
}
165182
} else {
166183
// String
167184
const parameters = new URLSearchParams(input);
168185
for (const [key, value] of parameters.entries()) {
169186
result.append(key, value);
187+
deleted.delete(key);
170188
}
171189
}
172190
}
173191

192+
if (deleted.size > 0) {
193+
result[deletedParametersSymbol] = deleted;
194+
}
195+
174196
return result;
175197
};
176198

source/utils/options.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {kyOptionKeys, requestOptionsRegistry, vendorSpecificOptions} from '../core/constants.js';
22
import type {SearchParamsOption} from '../types/options.js';
3+
import {deletedParametersSymbol} from './merge.js';
34

45
export const findUnknownOptions = (
56
request: Request,
@@ -38,7 +39,7 @@ export const hasSearchParameters = (search: SearchParamsOption): boolean => {
3839
}
3940

4041
if (search instanceof URLSearchParams) {
41-
return search.size > 0;
42+
return search.size > 0 || Boolean((search as any)[deletedParametersSymbol]?.size);
4243
}
4344

4445
// Record

test/fetch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,14 @@ test('fetch option takes a custom fetch function', async t => {
6363
fetch: customFetch,
6464
searchParams: 'new',
6565
}).text(),
66-
`${fixture}?new`,
66+
`${fixture}?old&new`,
6767
);
6868
t.is(
6969
await ky(`${fixture}?old#hash`, {
7070
fetch: customFetch,
7171
searchParams: 'new',
7272
}).text(),
73-
`${fixture}?new#hash`,
73+
`${fixture}?old&new#hash`,
7474
);
7575
t.is(await ky('unicorn', {fetch: customFetch, prefix: `${fixture}/api/`}).text(), `${fixture}/api/unicorn`);
7676
});

0 commit comments

Comments
 (0)