mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-12 09:33:32 +01:00
feat: add makeURLSearchParams utility function (#7744)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
const process = require('node:process');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { OAuth2Scopes, Routes } = require('discord-api-types/v10');
|
||||
const BaseClient = require('./BaseClient');
|
||||
const ActionsManager = require('./actions/ActionsManager');
|
||||
@@ -277,13 +278,11 @@ class Client extends BaseClient {
|
||||
*/
|
||||
async fetchInvite(invite, options) {
|
||||
const code = DataResolver.resolveInviteCode(invite);
|
||||
const query = new URLSearchParams({
|
||||
const query = makeURLSearchParams({
|
||||
with_counts: true,
|
||||
with_expiration: true,
|
||||
guild_scheduled_event_id: options?.guildScheduledEventId,
|
||||
});
|
||||
if (options?.guildScheduledEventId) {
|
||||
query.set('guild_scheduled_event_id', options.guildScheduledEventId);
|
||||
}
|
||||
const data = await this.rest.get(Routes.invite(code), { query });
|
||||
return new Invite(this, data);
|
||||
}
|
||||
@@ -417,10 +416,6 @@ class Client extends BaseClient {
|
||||
if (typeof options !== 'object') throw new TypeError('INVALID_TYPE', 'options', 'object', true);
|
||||
if (!this.application) throw new Error('CLIENT_NOT_READY', 'generate an invite link');
|
||||
|
||||
const query = new URLSearchParams({
|
||||
client_id: this.application.id,
|
||||
});
|
||||
|
||||
const { scopes } = options;
|
||||
if (typeof scopes === 'undefined') {
|
||||
throw new TypeError('INVITE_MISSING_SCOPES');
|
||||
@@ -436,15 +431,16 @@ class Client extends BaseClient {
|
||||
if (invalidScope) {
|
||||
throw new TypeError('INVALID_ELEMENT', 'Array', 'scopes', invalidScope);
|
||||
}
|
||||
query.set('scope', scopes.join(' '));
|
||||
|
||||
const query = makeURLSearchParams({
|
||||
client_id: this.application.id,
|
||||
scope: scopes.join(' '),
|
||||
disable_guild_select: options.disableGuildSelect,
|
||||
});
|
||||
|
||||
if (options.permissions) {
|
||||
const permissions = PermissionsBitField.resolve(options.permissions);
|
||||
if (permissions) query.set('permissions', permissions);
|
||||
}
|
||||
|
||||
if (options.disableGuildSelect) {
|
||||
query.set('disable_guild_select', true);
|
||||
if (permissions) query.set('permissions', permissions.toString());
|
||||
}
|
||||
|
||||
if (options.guild) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const process = require('node:process');
|
||||
const { setTimeout, clearTimeout } = require('node:timers');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
const CachedManager = require('./CachedManager');
|
||||
const { Guild } = require('../structures/Guild');
|
||||
@@ -271,12 +272,12 @@ class GuildManager extends CachedManager {
|
||||
}
|
||||
|
||||
const data = await this.client.rest.get(Routes.guild(id), {
|
||||
query: new URLSearchParams({ with_counts: options.withCounts ?? true }),
|
||||
query: makeURLSearchParams({ with_counts: options.withCounts ?? true }),
|
||||
});
|
||||
return this._add(data, options.cache);
|
||||
}
|
||||
|
||||
const data = await this.client.rest.get(Routes.userGuilds(), { query: new URLSearchParams(options) });
|
||||
const data = await this.client.rest.get(Routes.userGuilds(), { query: makeURLSearchParams(options) });
|
||||
return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const { Buffer } = require('node:buffer');
|
||||
const { setTimeout, clearTimeout } = require('node:timers');
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { DiscordSnowflake } = require('@sapphire/snowflake');
|
||||
const { Routes, GatewayOpcodes } = require('discord-api-types/v10');
|
||||
const CachedManager = require('./CachedManager');
|
||||
@@ -205,7 +206,7 @@ class GuildMemberManager extends CachedManager {
|
||||
*/
|
||||
async search({ query, limit, cache = true } = {}) {
|
||||
const data = await this.client.rest.get(Routes.guildMembersSearch(this.guild.id), {
|
||||
query: new URLSearchParams({ query, limit }),
|
||||
query: makeURLSearchParams({ query, limit }),
|
||||
});
|
||||
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
|
||||
}
|
||||
@@ -224,10 +225,7 @@ class GuildMemberManager extends CachedManager {
|
||||
* @returns {Promise<Collection<Snowflake, GuildMember>>}
|
||||
*/
|
||||
async list({ after, limit, cache = true } = {}) {
|
||||
const query = new URLSearchParams({ limit });
|
||||
if (after) {
|
||||
query.set('after', after);
|
||||
}
|
||||
const query = makeURLSearchParams({ limit, after });
|
||||
const data = await this.client.rest.get(Routes.guildMembers(this.guild.id), { query });
|
||||
return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection());
|
||||
}
|
||||
@@ -346,7 +344,7 @@ class GuildMemberManager extends CachedManager {
|
||||
const endpoint = Routes.guildPrune(this.guild.id);
|
||||
|
||||
const { pruned } = await (dry
|
||||
? this.client.rest.get(endpoint, { query: new URLSearchParams(query), reason })
|
||||
? this.client.rest.get(endpoint, { query: makeURLSearchParams(query), reason })
|
||||
: this.client.rest.post(endpoint, { body: { ...query, compute_prune_count }, reason }));
|
||||
|
||||
return pruned;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { GuildScheduledEventEntityType, Routes } = require('discord-api-types/v10');
|
||||
const CachedManager = require('./CachedManager');
|
||||
const { TypeError, Error } = require('../errors');
|
||||
@@ -141,13 +142,13 @@ class GuildScheduledEventManager extends CachedManager {
|
||||
}
|
||||
|
||||
const data = await this.client.rest.get(Routes.guildScheduledEvent(this.guild.id, id), {
|
||||
query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
|
||||
query: makeURLSearchParams({ with_user_count: options.withUserCount ?? true }),
|
||||
});
|
||||
return this._add(data, options.cache);
|
||||
}
|
||||
|
||||
const data = await this.client.rest.get(Routes.guildScheduledEvents(this.guild.id), {
|
||||
query: new URLSearchParams({ with_user_count: options.withUserCount ?? true }),
|
||||
query: makeURLSearchParams({ with_user_count: options.withUserCount ?? true }),
|
||||
});
|
||||
|
||||
return data.reduce(
|
||||
@@ -270,25 +271,12 @@ class GuildScheduledEventManager extends CachedManager {
|
||||
const guildScheduledEventId = this.resolveId(guildScheduledEvent);
|
||||
if (!guildScheduledEventId) throw new Error('GUILD_SCHEDULED_EVENT_RESOLVE');
|
||||
|
||||
let { limit, withMember, before, after } = options;
|
||||
|
||||
const query = new URLSearchParams();
|
||||
|
||||
if (limit) {
|
||||
query.set('limit', limit);
|
||||
}
|
||||
|
||||
if (typeof withMember !== 'undefined') {
|
||||
query.set('with_member', withMember);
|
||||
}
|
||||
|
||||
if (before) {
|
||||
query.set('before', before);
|
||||
}
|
||||
|
||||
if (after) {
|
||||
query.set('after', after);
|
||||
}
|
||||
const query = makeURLSearchParams({
|
||||
limit: options.limit,
|
||||
with_member: options.withMember,
|
||||
before: options.before,
|
||||
after: options.after,
|
||||
});
|
||||
|
||||
const data = await this.client.rest.get(Routes.guildScheduledEventUsers(this.guild.id, guildScheduledEventId), {
|
||||
query,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
const CachedManager = require('./CachedManager');
|
||||
const { TypeError } = require('../errors');
|
||||
@@ -224,7 +225,7 @@ class MessageManager extends CachedManager {
|
||||
|
||||
async _fetchMany(options = {}, cache) {
|
||||
const data = await this.client.rest.get(Routes.channelMessages(this.channel.id), {
|
||||
query: new URLSearchParams(options),
|
||||
query: makeURLSearchParams(options),
|
||||
});
|
||||
const messages = new Collection();
|
||||
for (const message of data) messages.set(message.id, this._add(message, cache));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
const CachedManager = require('./CachedManager');
|
||||
const { Error } = require('../errors');
|
||||
@@ -41,10 +42,7 @@ class ReactionUserManager extends CachedManager {
|
||||
*/
|
||||
async fetch({ limit = 100, after } = {}) {
|
||||
const message = this.reaction.message;
|
||||
const query = new URLSearchParams({ limit });
|
||||
if (after) {
|
||||
query.set('after', after);
|
||||
}
|
||||
const query = makeURLSearchParams({ limit, after });
|
||||
const data = await this.client.rest.get(
|
||||
Routes.channelMessageReaction(message.channelId, message.id, this.reaction.emoji.identifier),
|
||||
{ query },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { ChannelType, Routes } = require('discord-api-types/v10');
|
||||
const CachedManager = require('./CachedManager');
|
||||
const { TypeError } = require('../errors');
|
||||
@@ -206,7 +207,7 @@ class ThreadManager extends CachedManager {
|
||||
}
|
||||
let timestamp;
|
||||
let id;
|
||||
const query = new URLSearchParams();
|
||||
const query = makeURLSearchParams({ limit });
|
||||
if (typeof before !== 'undefined') {
|
||||
if (before instanceof ThreadChannel || /^\d{16,19}$/.test(String(before))) {
|
||||
id = this.resolveId(before);
|
||||
@@ -227,9 +228,6 @@ class ThreadManager extends CachedManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
query.set('limit', limit);
|
||||
}
|
||||
const raw = await this.client.rest.get(path, { query });
|
||||
return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { DiscordSnowflake } = require('@sapphire/snowflake');
|
||||
const { Routes } = require('discord-api-types/v10');
|
||||
const Base = require('./Base');
|
||||
@@ -101,7 +102,7 @@ class BaseGuild extends Base {
|
||||
*/
|
||||
async fetch() {
|
||||
const data = await this.client.rest.get(Routes.guild(this.id), {
|
||||
query: new URLSearchParams({ with_counts: true }),
|
||||
query: makeURLSearchParams({ with_counts: true }),
|
||||
});
|
||||
return this.client.guilds._add(data);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const { Collection } = require('@discordjs/collection');
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { ChannelType, GuildPremiumTier, Routes } = require('discord-api-types/v10');
|
||||
const AnonymousGuild = require('./AnonymousGuild');
|
||||
const GuildAuditLogs = require('./GuildAuditLogs');
|
||||
@@ -712,15 +713,11 @@ class Guild extends AnonymousGuild {
|
||||
async fetchAuditLogs(options = {}) {
|
||||
if (options.before && options.before instanceof GuildAuditLogsEntry) options.before = options.before.id;
|
||||
|
||||
const query = new URLSearchParams();
|
||||
|
||||
if (options.before) {
|
||||
query.set('before', options.before);
|
||||
}
|
||||
|
||||
if (options.limit) {
|
||||
query.set('limit', options.limit);
|
||||
}
|
||||
const query = makeURLSearchParams({
|
||||
before: options.before,
|
||||
limit: options.limit,
|
||||
action_type: options.type,
|
||||
});
|
||||
|
||||
if (options.user) {
|
||||
const id = this.client.users.resolveId(options.user);
|
||||
@@ -728,10 +725,6 @@ class Guild extends AnonymousGuild {
|
||||
query.set('user_id', id);
|
||||
}
|
||||
|
||||
if (options.type) {
|
||||
query.set('action_type', options.type);
|
||||
}
|
||||
|
||||
const data = await this.client.rest.get(Routes.guildAuditLog(this.id), { query });
|
||||
return new GuildAuditLogs(this, data);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { makeURLSearchParams } = require('@discordjs/rest');
|
||||
const { DiscordSnowflake } = require('@sapphire/snowflake');
|
||||
const { Routes, WebhookType } = require('discord-api-types/v10');
|
||||
const MessagePayload = require('./MessagePayload');
|
||||
@@ -199,11 +200,10 @@ class Webhook {
|
||||
messagePayload = MessagePayload.create(this, options).resolveBody();
|
||||
}
|
||||
|
||||
const query = new URLSearchParams({ wait: true });
|
||||
|
||||
if (messagePayload.options.threadId) {
|
||||
query.set('thread_id', messagePayload.options.threadId);
|
||||
}
|
||||
const query = makeURLSearchParams({
|
||||
wait: true,
|
||||
thread_id: messagePayload.options.threadId,
|
||||
});
|
||||
|
||||
const { body, files } = await messagePayload.resolveFiles();
|
||||
const d = await this.client.rest.post(Routes.webhook(this.id, this.token), { body, files, query, auth: false });
|
||||
@@ -232,7 +232,7 @@ class Webhook {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
const data = await this.client.rest.post(Routes.webhookPlatform(this.id, this.token, 'slack'), {
|
||||
query: new URLSearchParams({ wait: true }),
|
||||
query: makeURLSearchParams({ wait: true }),
|
||||
auth: false,
|
||||
body,
|
||||
});
|
||||
@@ -289,11 +289,7 @@ class Webhook {
|
||||
if (!this.token) throw new Error('WEBHOOK_TOKEN_UNAVAILABLE');
|
||||
|
||||
const data = await this.client.rest.get(Routes.webhookMessage(this.id, this.token, message), {
|
||||
query: threadId
|
||||
? new URLSearchParams({
|
||||
thread_id: threadId,
|
||||
})
|
||||
: undefined,
|
||||
query: threadId ? makeURLSearchParams({ thread_id: threadId }) : undefined,
|
||||
auth: false,
|
||||
});
|
||||
return this.client.channels?.cache.get(data.channel_id)?.messages._add(data, cache) ?? data;
|
||||
@@ -322,9 +318,7 @@ class Webhook {
|
||||
body,
|
||||
files,
|
||||
query: messagePayload.options.threadId
|
||||
? new URLSearchParams({
|
||||
thread_id: messagePayload.options.threadId,
|
||||
})
|
||||
? makeURLSearchParams({ thread_id: messagePayload.options.threadId })
|
||||
: undefined,
|
||||
auth: false,
|
||||
},
|
||||
@@ -362,11 +356,7 @@ class Webhook {
|
||||
await this.client.rest.delete(
|
||||
Routes.webhookMessage(this.id, this.token, typeof message === 'string' ? message : message.id),
|
||||
{
|
||||
query: threadId
|
||||
? new URLSearchParams({
|
||||
thread_id: threadId,
|
||||
})
|
||||
: undefined,
|
||||
query: threadId ? makeURLSearchParams({ thread_id: threadId }) : undefined,
|
||||
auth: false,
|
||||
},
|
||||
);
|
||||
|
||||
60
packages/rest/__tests__/utils.test.ts
Normal file
60
packages/rest/__tests__/utils.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { makeURLSearchParams } from '../src';
|
||||
|
||||
describe('makeURLSearchParams', () => {
|
||||
test('GIVEN undefined THEN returns empty URLSearchParams', () => {
|
||||
const params = makeURLSearchParams();
|
||||
|
||||
expect([...params.entries()]).toEqual([]);
|
||||
});
|
||||
|
||||
test('GIVEN empty object THEN returns empty URLSearchParams', () => {
|
||||
const params = makeURLSearchParams({});
|
||||
|
||||
expect([...params.entries()]).toEqual([]);
|
||||
});
|
||||
|
||||
test('GIVEN a record of strings THEN returns URLSearchParams with strings', () => {
|
||||
const params = makeURLSearchParams({ foo: 'bar', hello: 'world' });
|
||||
|
||||
expect([...params.entries()]).toEqual([
|
||||
['foo', 'bar'],
|
||||
['hello', 'world'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('GIVEN a record of strings with nullish values THEN returns URLSearchParams without nullish values', () => {
|
||||
const params = makeURLSearchParams({ foo: 'bar', hello: null, one: undefined });
|
||||
|
||||
expect([...params.entries()]).toEqual([['foo', 'bar']]);
|
||||
});
|
||||
|
||||
test('GIVEN a record of non-string values THEN returns URLSearchParams with string values', () => {
|
||||
const params = makeURLSearchParams({ life: 42, big: 100n, bool: true });
|
||||
|
||||
expect([...params.entries()]).toEqual([
|
||||
['life', '42'],
|
||||
['big', '100'],
|
||||
['bool', 'true'],
|
||||
]);
|
||||
});
|
||||
|
||||
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(NaN) });
|
||||
|
||||
expect([...params.entries()]).toEqual([['before', '2022-04-04T15:43:05.108Z']]);
|
||||
});
|
||||
|
||||
test('GIVEN a record of plain object values THEN returns empty URLSearchParams', () => {
|
||||
const params = makeURLSearchParams({ foo: {}, hello: { happy: true } });
|
||||
|
||||
expect([...params.entries()]).toEqual([]);
|
||||
});
|
||||
|
||||
test('GIVEN a record of objects with overridden toString THEN returns non-empty URLSearchParams', () => {
|
||||
const params = makeURLSearchParams({ foo: { toString: () => 'bar' } });
|
||||
|
||||
expect([...params.entries()]).toEqual([['foo', 'bar']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,3 +8,4 @@ export * from './lib/errors/RateLimitError';
|
||||
export * from './lib/RequestManager';
|
||||
export * from './lib/REST';
|
||||
export * from './lib/utils/constants';
|
||||
export { makeURLSearchParams } from './lib/utils/utils';
|
||||
|
||||
@@ -2,6 +2,45 @@ import type { RESTPatchAPIChannelJSONBody } from 'discord-api-types/v10';
|
||||
import type { Response } from 'node-fetch';
|
||||
import { RequestMethod } from '../RequestManager';
|
||||
|
||||
function serializeSearchParam(value: unknown): string | null {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
return value;
|
||||
case 'number':
|
||||
case 'bigint':
|
||||
case 'boolean':
|
||||
return value.toString();
|
||||
case 'object':
|
||||
if (value === null) return null;
|
||||
if (value instanceof Date) {
|
||||
return Number.isNaN(value.getTime()) ? null : value.toISOString();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
if (typeof value.toString === 'function' && value.toString !== Object.prototype.toString) return value.toString();
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns A populated URLSearchParams instance
|
||||
*/
|
||||
export function makeURLSearchParams(options?: Record<string, unknown>) {
|
||||
const params = new URLSearchParams();
|
||||
if (!options) return params;
|
||||
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
const serialized = serializeSearchParam(value);
|
||||
if (serialized !== null) params.append(key, serialized);
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the response to usable data
|
||||
* @param res The node-fetch response
|
||||
|
||||
Reference in New Issue
Block a user