diff --git a/packages/rest/__tests__/utils.test.ts b/packages/rest/__tests__/utils.test.ts index f924f1791..ffab42541 100644 --- a/packages/rest/__tests__/utils.test.ts +++ b/packages/rest/__tests__/utils.test.ts @@ -39,6 +39,23 @@ describe('makeURLSearchParams', () => { ]); }); + describe('arrays', () => { + test('GIVEN an array THEN defaults to repeated URLSearchParams without nullish values', () => { + const params = makeURLSearchParams({ foo: ['bar', null, undefined, 'baz'], empty: [] }); + + expect([...params.entries()]).toEqual([ + ['foo', 'bar'], + ['foo', 'baz'], + ]); + }); + + test('GIVEN an array and comma array format THEN returns a comma-separated value without nullish values', () => { + const params = makeURLSearchParams({ foo: ['bar', null, undefined, 'baz'], empty: [] }, { arrayFormat: 'comma' }); + + expect([...params.entries()]).toEqual([['foo', 'bar,baz']]); + }); + }); + describe('objects', () => { test('GIVEN a record of date values THEN URLSearchParams with ISO string values', () => { const params = makeURLSearchParams({ before: new Date('2022-04-04T15:43:05.108Z'), after: new Date(Number.NaN) }); diff --git a/packages/rest/src/lib/utils/utils.ts b/packages/rest/src/lib/utils/utils.ts index 8395d3e87..aed08cb63 100644 --- a/packages/rest/src/lib/utils/utils.ts +++ b/packages/rest/src/lib/utils/utils.ts @@ -27,18 +27,60 @@ function serializeSearchParam(value: unknown): string | null { } } +/** + * Options for serializing URL search parameters. + */ +export interface MakeURLSearchParamsOptions { + /** + * How array values should be serialized. + * + * @defaultValue `'repeat'` + * @see {@link https://docs.discord.com/developers/reference#array-query-strings} + */ + arrayFormat?: 'comma' | 'repeat'; +} + /** * Creates and populates an URLSearchParams instance from an object, stripping * out null and undefined values, while also coercing non-strings to strings. * - * @param options - The options to use + * @param parameters - The parameters to use + * @param options - The options for serializing URL search parameters * @returns A populated URLSearchParams instance */ -export function makeURLSearchParams(options?: Readonly) { +export function makeURLSearchParams( + parameters?: Readonly, + options: MakeURLSearchParamsOptions = {}, +) { const params = new URLSearchParams(); - if (!options) return params; + if (!parameters) return params; + const { arrayFormat = 'repeat' } = options; + + for (const [key, value] of Object.entries(parameters)) { + if (Array.isArray(value)) { + const commaSeparatedElements: string[] | null = arrayFormat === 'comma' ? [] : null; + + for (const element of value) { + const serialized = serializeSearchParam(element); + + if (serialized === null) { + continue; + } + + if (commaSeparatedElements) { + commaSeparatedElements.push(serialized); + } else { + params.append(key, serialized); + } + } + + if (commaSeparatedElements?.length) { + params.append(key, commaSeparatedElements.join(',')); + } + + continue; + } - for (const [key, value] of Object.entries(options)) { const serialized = serializeSearchParam(value); if (serialized !== null) params.append(key, serialized); } diff --git a/packages/rest/src/shared.ts b/packages/rest/src/shared.ts index 37781f58e..03fe1337c 100644 --- a/packages/rest/src/shared.ts +++ b/packages/rest/src/shared.ts @@ -5,7 +5,12 @@ export * from './lib/errors/RateLimitError.js'; export * from './lib/REST.js'; export * from './lib/utils/constants.js'; export * from './lib/utils/types.js'; -export { calculateUserDefaultAvatarIndex, makeURLSearchParams, parseResponse } from './lib/utils/utils.js'; +export { + calculateUserDefaultAvatarIndex, + makeURLSearchParams, + type MakeURLSearchParamsOptions, + parseResponse, +} from './lib/utils/utils.js'; /** * The {@link https://github.com/discordjs/discord.js/blob/main/packages/rest#readme | @discordjs/rest} version