feat: add makeURLSearchParams utility function (#7744)

This commit is contained in:
A. Román
2022-04-12 17:14:30 +02:00
committed by GitHub
parent 8625d81714
commit 8eaec114a9
13 changed files with 149 additions and 85 deletions

View File

@@ -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) {

View File

@@ -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());
}
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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));

View File

@@ -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 },

View File

@@ -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 });
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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,
},
);

View 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']]);
});
});
});

View File

@@ -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';

View File

@@ -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