From cab53f291b1c170733a40e71345988771c43fe1a Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Fri, 4 Feb 2022 17:06:30 -0500 Subject: [PATCH 01/18] refactor: make public builder props getters --- .../__tests__/components/selectMenu.test.ts | 15 ++- .../builders/__tests__/messages/embed.test.ts | 26 ++--- packages/builders/babel.config.js | 7 +- packages/builders/src/components/ActionRow.ts | 21 ++-- packages/builders/src/components/Component.ts | 15 ++- .../builders/src/components/button/Button.ts | 2 +- .../src/components/button/UnsafeButton.ts | 72 +++++++----- .../src/components/selectMenu/SelectMenu.ts | 2 +- .../components/selectMenu/UnsafeSelectMenu.ts | 74 +++++++----- .../selectMenu/UnsafeSelectMenuOption.ts | 44 ++++--- .../src/messages/embed/UnsafeEmbed.ts | 109 ++++++++++-------- 11 files changed, 225 insertions(+), 162 deletions(-) diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index 102fc98784d3..8089c7434e98 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -55,17 +55,26 @@ describe('Button Components', () => { description: 'test', }; - const selectMenuData: APISelectMenuComponent = { + const selectMenuDataWithoutOptions = { type: ComponentType.SelectMenu, custom_id: 'test', max_values: 10, min_values: 3, disabled: true, - options: [selectMenuOptionData], placeholder: 'test', + } as const; + + const selectMenuData: APISelectMenuComponent = { + ...selectMenuDataWithoutOptions, + options: [selectMenuOptionData], }; - expect(new SelectMenuComponent(selectMenuData).toJSON()).toEqual(selectMenuData); + expect( + // @ts-expect-error + new SelectMenuComponent(selectMenuDataWithoutOptions) + .addOptions(new SelectMenuOption(selectMenuOptionData)) + .toJSON(), + ).toEqual(selectMenuData); expect(new SelectMenuOption(selectMenuOptionData).toJSON()).toEqual(selectMenuOptionData); }); }); diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index 3483ac797399..5b381bbe36e3 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -2,17 +2,7 @@ import { Embed } from '../../src'; import type { APIEmbed } from 'discord-api-types/v9'; const emptyEmbed: APIEmbed = { - author: undefined, - color: undefined, - description: undefined, fields: [], - footer: undefined, - image: undefined, - provider: undefined, - thumbnail: undefined, - title: undefined, - url: undefined, - video: undefined, }; const alpha = 'abcdefghijklmnopqrstuvwxyz'; @@ -55,7 +45,7 @@ describe('Embed', () => { const embed = new Embed({ title: 'foo' }); embed.setTitle(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: undefined }); }); test('GIVEN an embed with an invalid title THEN throws error', () => { @@ -82,7 +72,7 @@ describe('Embed', () => { const embed = new Embed({ description: 'foo' }); embed.setDescription(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: undefined }); }); test('GIVEN an embed with an invalid description THEN throws error', () => { @@ -115,7 +105,7 @@ describe('Embed', () => { const embed = new Embed({ url: 'https://discord.js.org' }); embed.setURL(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, url: undefined }); }); test('GIVEN an embed with an invalid URL THEN throws error', () => { @@ -142,7 +132,7 @@ describe('Embed', () => { const embed = new Embed({ color: 0xff0000 }); embed.setColor(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: undefined }); }); test('GIVEN an embed with an invalid color THEN throws error', () => { @@ -213,7 +203,7 @@ describe('Embed', () => { const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); embed.setThumbnail(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, thumbnail: undefined }); }); test('GIVEN an embed with an invalid thumbnail THEN throws error', () => { @@ -246,7 +236,7 @@ describe('Embed', () => { const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } }); embed.setImage(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, image: undefined }); }); test('GIVEN an embed with an invalid image THEN throws error', () => { @@ -287,7 +277,7 @@ describe('Embed', () => { }); embed.setAuthor(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, author: undefined }); }); test('GIVEN an embed with an invalid author name THEN throws error', () => { @@ -322,7 +312,7 @@ describe('Embed', () => { const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } }); embed.setFooter(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed }); + expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, footer: undefined }); }); test('GIVEN an embed with invalid footer text THEN throws error', () => { diff --git a/packages/builders/babel.config.js b/packages/builders/babel.config.js index 7236fed9bea7..5cec1521580a 100644 --- a/packages/builders/babel.config.js +++ b/packages/builders/babel.config.js @@ -12,7 +12,12 @@ module.exports = { modules: 'commonjs', }, ], - '@babel/preset-typescript', + [ + '@babel/preset-typescript', + { + allowDeclareFields: true, + }, + ], ], plugins: ['babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }]], }; diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 12cac4a21d04..3e9f1b7bc6e0 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -1,6 +1,6 @@ -import { APIActionRowComponent, ComponentType } from 'discord-api-types/v9'; +import { type APIActionRowComponent, ComponentType } from 'discord-api-types/v9'; import type { ButtonComponent, SelectMenuComponent } from '..'; -import type { Component } from './Component'; +import { Component } from './Component'; import { createComponent } from './Components'; export type MessageComponent = ActionRowComponent | ActionRow; @@ -12,14 +12,20 @@ export type ActionRowComponent = ButtonComponent | SelectMenuComponent; /** * Represents an action row component */ -export class ActionRow implements Component { +export class ActionRow extends Component { + protected declare data: APIActionRowComponent; public readonly components: T[] = []; - public readonly type = ComponentType.ActionRow; public constructor(data?: APIActionRowComponent & { type?: ComponentType.ActionRow }) { + super(data); + this.data.type ??= ComponentType.ActionRow; this.components = (data?.components.map(createComponent) ?? []) as T[]; } + public get type(): ComponentType.ActionRow { + return this.data.type; + } + /** * Adds components to this action row. * @param components The components to add to this action row. @@ -34,14 +40,15 @@ export class ActionRow implem * Sets the components in this action row * @param components The components to set this row to */ - public setComponents(...components: T[]) { - Reflect.set(this, 'components', [...components]); + public setComponents(components: T[]) { + this.components.slice(0, this.components.length); + this.components.push(...components); return this; } public toJSON(): APIActionRowComponent { return { - ...this, + ...this.data, components: this.components.map((component) => component.toJSON()), }; } diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 992c9199d445..3b0edece3db8 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -4,9 +4,20 @@ import type { JSONEncodable } from '../util/jsonEncodable'; /** * Represents a discord component */ -export interface Component extends JSONEncodable { +export abstract class Component implements JSONEncodable { + /** + * The api data associated with this component + */ + protected data!: APIMessageComponent; + /** * The type of this component */ - readonly type: ComponentType; + public abstract readonly type: ComponentType; + + public constructor(data: APIMessageComponent & { type?: ComponentType } = {} as APIMessageComponent) { + this.data = data; + } + + public abstract toJSON(): APIMessageComponent; } diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index 45827d54399f..37082505b77a 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -36,7 +36,7 @@ export class ButtonComponent extends UnsafeButtonComponent { } public override toJSON(): APIButtonComponent { - validateRequiredButtonParameters(this.style, this.label, this.emoji, this.custom_id, this.url); + validateRequiredButtonParameters(this.style, this.label, this.emoji, this.customId, this.url); return super.toJSON(); } } diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 182e72531f8b..391580f390a8 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -3,33 +3,45 @@ import { ButtonStyle, type APIMessageComponentEmoji, type APIButtonComponent, + APIButtonComponentWithURL, + APIButtonComponentWithCustomId, } from 'discord-api-types/v9'; -import type { Component } from '../Component'; +import { Component } from '../Component'; -export class UnsafeButtonComponent implements Component { - public readonly type = ComponentType.Button as const; - public readonly style!: ButtonStyle; - public readonly label?: string; - public readonly emoji?: APIMessageComponentEmoji; - public readonly disabled?: boolean; - public readonly custom_id!: string; - public readonly url!: string; +export class UnsafeButtonComponent extends Component { + protected declare data: APIButtonComponent; public constructor(data?: APIButtonComponent & { type?: ComponentType.Button }) { - /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ - this.style = data?.style as ButtonStyle; - this.label = data?.label; - this.emoji = data?.emoji; - this.disabled = data?.disabled; - - // This if/else makes typescript happy - if (data?.style === ButtonStyle.Link) { - this.url = data.url; - } else { - this.custom_id = data?.custom_id as string; - } - - /* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */ + super(data); + this.data.type ??= ComponentType.Button; + } + + public get type(): ComponentType.Button { + return this.data.type; + } + + public get style() { + return this.data.style; + } + + public get label() { + return this.data.label; + } + + public get emoji() { + return this.data.emoji; + } + + public get disabled() { + return this.data.disabled; + } + + public get customId() { + return (this.data as APIButtonComponentWithCustomId).custom_id; + } + + public get url() { + return (this.data as APIButtonComponentWithURL).url; } /** @@ -37,7 +49,7 @@ export class UnsafeButtonComponent implements Component { * @param style The style of the button */ public setStyle(style: ButtonStyle) { - Reflect.set(this, 'style', style); + this.data.style = style; return this; } @@ -46,7 +58,7 @@ export class UnsafeButtonComponent implements Component { * @param url The URL to open when this button is clicked */ public setURL(url: string) { - Reflect.set(this, 'url', url); + (this.data as APIButtonComponentWithURL).url = url; return this; } @@ -55,7 +67,7 @@ export class UnsafeButtonComponent implements Component { * @param customId The custom ID to use for this button */ public setCustomId(customId: string) { - Reflect.set(this, 'custom_id', customId); + (this.data as APIButtonComponentWithCustomId).custom_id = customId; return this; } @@ -64,7 +76,7 @@ export class UnsafeButtonComponent implements Component { * @param emoji The emoji to display on this button */ public setEmoji(emoji: APIMessageComponentEmoji) { - Reflect.set(this, 'emoji', emoji); + this.data.emoji = emoji; return this; } @@ -73,7 +85,7 @@ export class UnsafeButtonComponent implements Component { * @param disabled Whether or not to disable this button or not */ public setDisabled(disabled: boolean) { - Reflect.set(this, 'disabled', disabled); + this.data.disabled = disabled; return this; } @@ -82,13 +94,13 @@ export class UnsafeButtonComponent implements Component { * @param label The label to display on this button */ public setLabel(label: string) { - Reflect.set(this, 'label', label); + this.data.label = label; return this; } public toJSON(): APIButtonComponent { return { - ...this, + ...this.data, }; } } diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts index f5c45462f4cf..d344354fe2d2 100644 --- a/packages/builders/src/components/selectMenu/SelectMenu.ts +++ b/packages/builders/src/components/selectMenu/SelectMenu.ts @@ -33,7 +33,7 @@ export class SelectMenuComponent extends UnsafeSelectMenuComponent { } public override toJSON(): APISelectMenuComponent { - validateRequiredSelectMenuParameters(this.options, this.custom_id); + validateRequiredSelectMenuParameters(this.options, this.customId); return super.toJSON(); } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 67b847b245e5..49761c153111 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -1,28 +1,43 @@ import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; -import type { Component } from '../Component'; -import { SelectMenuOption } from './SelectMenuOption'; +import { Component } from '../Component'; +import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; /** * Represents a non-validated select menu component */ -export class UnsafeSelectMenuComponent implements Component { - public readonly type = ComponentType.SelectMenu as const; - public readonly options: SelectMenuOption[]; - public readonly placeholder?: string; - public readonly min_values?: number; - public readonly max_values?: number; - public readonly custom_id!: string; - public readonly disabled?: boolean; +export class UnsafeSelectMenuComponent extends Component { + protected declare data: APISelectMenuComponent; + public readonly options: UnsafeSelectMenuOption[] = []; public constructor(data?: APISelectMenuComponent) { - this.options = data?.options.map((option) => new SelectMenuOption(option)) ?? []; - this.placeholder = data?.placeholder; - this.min_values = data?.min_values; - this.max_values = data?.max_values; - /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ - this.custom_id = data?.custom_id as string; - /* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */ - this.disabled = data?.disabled; + super(data); + this.data.type ??= ComponentType.SelectMenu; + this.data.options ??= []; + this.options = this.data.options.map((o) => new UnsafeSelectMenuOption(o)); + } + + public get type(): ComponentType.SelectMenu { + return this.data.type; + } + + public get placeholder() { + return this.data.placeholder; + } + + public get maxValues() { + return this.data.max_values; + } + + public get minValues() { + return this.data.min_values; + } + + public get customId() { + return this.data.custom_id; + } + + public get disabled() { + return this.data.disabled; } /** @@ -30,16 +45,16 @@ export class UnsafeSelectMenuComponent implements Component { * @param placeholder The placeholder to use for this select menu */ public setPlaceholder(placeholder: string) { - Reflect.set(this, 'placeholder', placeholder); + this.data.placeholder = placeholder; return this; } /** - * Sets thes minimum values that must be selected in the select menu + * Sets the minimum values that must be selected in the select menu * @param minValues The minimum values that must be selected */ public setMinValues(minValues: number) { - Reflect.set(this, 'min_values', minValues); + this.data.min_values = minValues; return this; } @@ -48,7 +63,7 @@ export class UnsafeSelectMenuComponent implements Component { * @param minValues The maximum values that must be selected */ public setMaxValues(maxValues: number) { - Reflect.set(this, 'max_values', maxValues); + this.data.max_values = maxValues; return this; } @@ -57,7 +72,7 @@ export class UnsafeSelectMenuComponent implements Component { * @param customId The custom ID to use for this select menu */ public setCustomId(customId: string) { - Reflect.set(this, 'custom_id', customId); + this.data.custom_id = customId; return this; } @@ -66,7 +81,7 @@ export class UnsafeSelectMenuComponent implements Component { * @param disabled Whether or not this select menu is disabled */ public setDisabled(disabled: boolean) { - Reflect.set(this, 'disabled', disabled); + this.data.disabled = disabled; return this; } @@ -75,7 +90,7 @@ export class UnsafeSelectMenuComponent implements Component { * @param options The options to add to this select menu * @returns */ - public addOptions(...options: SelectMenuOption[]) { + public addOptions(...options: UnsafeSelectMenuOption[]) { this.options.push(...options); return this; } @@ -84,15 +99,16 @@ export class UnsafeSelectMenuComponent implements Component { * Sets the options on this select menu * @param options The options to set on this select menu */ - public setOptions(...options: SelectMenuOption[]) { - Reflect.set(this, 'options', [...options]); + public setOptions(options: UnsafeSelectMenuOption[]) { + this.options.splice(0, this.options.length); + this.options.push(...options); return this; } public toJSON(): APISelectMenuComponent { return { - ...this, - options: this.options.map((option) => option.toJSON()), + ...this.data, + options: this.options.map((o) => o.toJSON()), }; } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts index a93b97321b80..7c9a312274f7 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts @@ -4,20 +4,26 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api- * Represents a non-validated option within a select menu component */ export class UnsafeSelectMenuOption { - public readonly label!: string; - public readonly value!: string; - public readonly description?: string; - public readonly emoji?: APIMessageComponentEmoji; - public readonly default?: boolean; + public constructor(protected data: APISelectMenuOption = {} as APISelectMenuOption) {} - public constructor(data?: APISelectMenuOption) { - /* eslint-disable @typescript-eslint/non-nullable-type-assertion-style */ - this.label = data?.label as string; - this.value = data?.value as string; - /* eslint-enable @typescript-eslint/non-nullable-type-assertion-style */ - this.description = data?.description; - this.emoji = data?.emoji; - this.default = data?.default; + public get label() { + return this.data.label; + } + + public get value() { + return this.data.value; + } + + public get description() { + return this.data.description; + } + + public get emoji() { + return this.data.emoji; + } + + public get default() { + return this.data.default; } /** @@ -25,7 +31,7 @@ export class UnsafeSelectMenuOption { * @param label The label to show on this option */ public setLabel(label: string) { - Reflect.set(this, 'label', label); + this.data.label = label; return this; } @@ -34,7 +40,7 @@ export class UnsafeSelectMenuOption { * @param value The value of this option */ public setValue(value: string) { - Reflect.set(this, 'value', value); + this.data.value = value; return this; } @@ -43,7 +49,7 @@ export class UnsafeSelectMenuOption { * @param description The description of this option */ public setDescription(description: string) { - Reflect.set(this, 'description', description); + this.data.description = description; return this; } @@ -52,7 +58,7 @@ export class UnsafeSelectMenuOption { * @param isDefault Whether or not this option is selected by default */ public setDefault(isDefault: boolean) { - Reflect.set(this, 'default', isDefault); + this.data.default = isDefault; return this; } @@ -61,13 +67,13 @@ export class UnsafeSelectMenuOption { * @param emoji The emoji to display on this button */ public setEmoji(emoji: APIMessageComponentEmoji) { - Reflect.set(this, 'emoji', emoji); + this.data.emoji = emoji; return this; } public toJSON(): APISelectMenuOption { return { - ...this, + ...this.data, }; } } diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index b286e6168d0b..ecd3db183857 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -1,14 +1,4 @@ -import type { - APIEmbed, - APIEmbedAuthor, - APIEmbedField, - APIEmbedFooter, - APIEmbedImage, - APIEmbedProvider, - APIEmbedThumbnail, - APIEmbedVideo, -} from 'discord-api-types/v9'; -import type { JSONEncodable } from '../../util/jsonEncodable'; +import type { APIEmbed, APIEmbedField } from 'discord-api-types/v9'; export interface AuthorOptions { name: string; @@ -21,81 +11,98 @@ export interface FooterOptions { iconURL?: string; } -export class UnsafeEmbed implements APIEmbed, JSONEncodable { +export class UnsafeEmbed implements APIEmbed { + protected data!: APIEmbed; + + public constructor(data: APIEmbed = {}) { + this.data = data; + this.data.fields = data.fields ?? []; + + if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); + } + /** * An array of fields of this embed */ - public readonly fields: APIEmbedField[]; + public get fields() { + return this.data.fields ?? []; + } /** * The embed title */ - public readonly title?: string; + public get title() { + return this.data.title; + } /** * The embed description */ - public readonly description?: string; + public get description() { + return this.data.description; + } /** * The embed url */ - public readonly url?: string; + public get url() { + return this.data.url; + } /** * The embed color */ - public readonly color?: number; + public get color() { + return this.data.color; + } /** * The timestamp of the embed in the ISO format */ - public readonly timestamp?: string; + public get timestamp() { + return this.data.timestamp; + } /** * The embed thumbnail data */ - public readonly thumbnail?: APIEmbedThumbnail; + public get thumbnail() { + return this.data.thumbnail; + } /** * The embed image data */ - public readonly image?: APIEmbedImage; + public get image() { + return this.data.image; + } /** * Received video data */ - public readonly video?: APIEmbedVideo; + public get video() { + return this.data.video; + } /** * The embed author data */ - public readonly author?: APIEmbedAuthor; + public get author() { + return this.data.author; + } /** * Received data about the embed provider */ - public readonly provider?: APIEmbedProvider; + public get provider() { + return this.data.provider; + } /** * The embed footer data */ - public readonly footer?: APIEmbedFooter; - - public constructor(data: APIEmbed = {}) { - this.title = data.title; - this.description = data.description; - this.url = data.url; - this.color = data.color; - this.thumbnail = data.thumbnail; - this.image = data.image; - this.video = data.video; - this.author = data.author; - this.provider = data.provider; - this.footer = data.footer; - this.fields = data.fields ?? []; - - if (data.timestamp) this.timestamp = new Date(data.timestamp).toISOString(); + public get footer() { + return this.data.footer; } /** @@ -158,11 +165,11 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { */ public setAuthor(options: AuthorOptions | null): this { if (options === null) { - Reflect.set(this, 'author', undefined); + this.data.author = undefined; return this; } - Reflect.set(this, 'author', { name: options.name, url: options.url, icon_url: options.iconURL }); + this.data.author = { name: options.name, url: options.url, icon_url: options.iconURL }; return this; } @@ -172,7 +179,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param color The color of the embed */ public setColor(color: number | null): this { - Reflect.set(this, 'color', color ?? undefined); + this.data.color = color ?? undefined; return this; } @@ -182,7 +189,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param description The description */ public setDescription(description: string | null): this { - Reflect.set(this, 'description', description ?? undefined); + this.data.description = description ?? undefined; return this; } @@ -193,11 +200,11 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { */ public setFooter(options: FooterOptions | null): this { if (options === null) { - Reflect.set(this, 'footer', undefined); + this.data.footer = undefined; return this; } - Reflect.set(this, 'footer', { text: options.text, icon_url: options.iconURL }); + this.data.footer = { text: options.text, icon_url: options.iconURL }; return this; } @@ -207,7 +214,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param url The URL of the image */ public setImage(url: string | null): this { - Reflect.set(this, 'image', url ? { url } : undefined); + this.data.image = url ? { url } : undefined; return this; } @@ -217,7 +224,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param url The URL of the thumbnail */ public setThumbnail(url: string | null): this { - Reflect.set(this, 'thumbnail', url ? { url } : undefined); + this.data.thumbnail = url ? { url } : undefined; return this; } @@ -227,7 +234,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param timestamp The timestamp or date */ public setTimestamp(timestamp: number | Date | null = Date.now()): this { - Reflect.set(this, 'timestamp', timestamp ? new Date(timestamp).toISOString() : undefined); + this.data.timestamp = timestamp ? new Date(timestamp).toISOString() : undefined; return this; } @@ -237,7 +244,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param title The title */ public setTitle(title: string | null): this { - Reflect.set(this, 'title', title ?? undefined); + this.data.title = title ?? undefined; return this; } @@ -247,7 +254,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * @param url The URL */ public setURL(url: string | null): this { - Reflect.set(this, 'url', url ?? undefined); + this.data.url = url ?? undefined; return this; } @@ -255,7 +262,7 @@ export class UnsafeEmbed implements APIEmbed, JSONEncodable { * Transforms the embed to a plain object */ public toJSON(): APIEmbed { - return { ...this }; + return { ...this.data }; } /** From 5078625b31aa1dbbb1f52c593223d47b2d8a5cc2 Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sat, 5 Feb 2022 00:04:23 -0500 Subject: [PATCH 02/18] chore: make requested changes --- packages/builders/src/components/ActionRow.ts | 6 +-- packages/builders/src/components/Component.ts | 9 +++-- .../src/components/button/UnsafeButton.ts | 5 +-- .../components/selectMenu/UnsafeSelectMenu.ts | 7 +--- .../src/messages/embed/UnsafeEmbed.ts | 37 ++++++++++++++----- packages/discord.js/typings/index.test-d.ts | 2 +- 6 files changed, 40 insertions(+), 26 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 3e9f1b7bc6e0..5b7178be34ce 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -17,8 +17,7 @@ export class ActionRow extend public readonly components: T[] = []; public constructor(data?: APIActionRowComponent & { type?: ComponentType.ActionRow }) { - super(data); - this.data.type ??= ComponentType.ActionRow; + super({ type: ComponentType.ActionRow, ...data }); this.components = (data?.components.map(createComponent) ?? []) as T[]; } @@ -41,8 +40,7 @@ export class ActionRow extend * @param components The components to set this row to */ public setComponents(components: T[]) { - this.components.slice(0, this.components.length); - this.components.push(...components); + this.components.splice(0, this.components.length, ...components); return this; } diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 3b0edece3db8..0757c58e6910 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -1,5 +1,5 @@ -import type { APIMessageComponent, ComponentType } from 'discord-api-types/v9'; import type { JSONEncodable } from '../util/jsonEncodable'; +import type { APIBaseMessageComponent, APIMessageComponent, ComponentType } from 'discord-api-types/v9'; /** * Represents a discord component @@ -8,16 +8,19 @@ export abstract class Component implements JSONEncodable { /** * The api data associated with this component */ - protected data!: APIMessageComponent; + protected data: APIBaseMessageComponent; /** * The type of this component */ public abstract readonly type: ComponentType; - public constructor(data: APIMessageComponent & { type?: ComponentType } = {} as APIMessageComponent) { + public constructor(data: APIBaseMessageComponent) { this.data = data; } + /** + * Converts this component to an API-compatible JSON object + */ public abstract toJSON(): APIMessageComponent; } diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 391580f390a8..e82d6b5b3ed8 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -11,9 +11,8 @@ import { Component } from '../Component'; export class UnsafeButtonComponent extends Component { protected declare data: APIButtonComponent; - public constructor(data?: APIButtonComponent & { type?: ComponentType.Button }) { - super(data); - this.data.type ??= ComponentType.Button; + public constructor(data?: APIButtonComponent) { + super({ type: ComponentType.Button, ...data }); } public get type(): ComponentType.Button { diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 49761c153111..3706b5089bc0 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -10,9 +10,7 @@ export class UnsafeSelectMenuComponent extends Component { public readonly options: UnsafeSelectMenuOption[] = []; public constructor(data?: APISelectMenuComponent) { - super(data); - this.data.type ??= ComponentType.SelectMenu; - this.data.options ??= []; + super({ type: ComponentType.SelectMenu, options: [], ...data }); this.options = this.data.options.map((o) => new UnsafeSelectMenuOption(o)); } @@ -100,8 +98,7 @@ export class UnsafeSelectMenuComponent extends Component { * @param options The options to set on this select menu */ public setOptions(options: UnsafeSelectMenuOption[]) { - this.options.splice(0, this.options.length); - this.options.push(...options); + this.options.splice(0, this.options.length, ...options); return this; } diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index ecd3db183857..1e05bfafe121 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -11,13 +11,11 @@ export interface FooterOptions { iconURL?: string; } -export class UnsafeEmbed implements APIEmbed { +export class UnsafeEmbed { protected data!: APIEmbed; public constructor(data: APIEmbed = {}) { - this.data = data; - this.data.fields = data.fields ?? []; - + this.data = { fields: [], ...data }; if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); } @@ -67,14 +65,24 @@ export class UnsafeEmbed implements APIEmbed { * The embed thumbnail data */ public get thumbnail() { - return this.data.thumbnail; + return { + url: this.data.thumbnail?.url, + proxyURL: this.data.thumbnail?.proxy_url, + height: this.data.thumbnail?.height, + width: this.data.thumbnail?.width, + }; } /** * The embed image data */ public get image() { - return this.data.image; + return { + url: this.data.image?.url, + proxyURL: this.data.image?.proxy_url, + height: this.data.image?.height, + width: this.data.image?.width, + }; } /** @@ -88,7 +96,12 @@ export class UnsafeEmbed implements APIEmbed { * The embed author data */ public get author() { - return this.data.author; + return { + name: this.data.author?.name, + url: this.data.author?.url, + iconURL: this.data.author?.icon_url, + proxyIconURL: this.data.author?.proxy_icon_url, + }; } /** @@ -102,7 +115,11 @@ export class UnsafeEmbed implements APIEmbed { * The embed footer data */ public get footer() { - return this.data.footer; + return { + text: this.data.footer?.text, + iconURL: this.data.footer?.icon_url, + proxyIconURL: this.data.footer?.proxy_icon_url, + }; } /** @@ -113,8 +130,8 @@ export class UnsafeEmbed implements APIEmbed { (this.title?.length ?? 0) + (this.description?.length ?? 0) + this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + - (this.footer?.text.length ?? 0) + - (this.author?.name.length ?? 0) + (this.footer.text?.length ?? 0) + + (this.author.name?.length ?? 0) ); } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 7acb15089d85..cf73733d95d4 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -723,7 +723,7 @@ client.on('interactionCreate', async interaction => { const button = new ButtonComponent(); - const actionRow = new ActionRow({ type: ComponentType.ActionRow, components: [button] }); + const actionRow = new ActionRow({ type: ComponentType.ActionRow, components: [button.toJSON()] }); await interaction.reply({ content: 'Hi!', components: [actionRow] }); From 12af465b7225f83c04320fd7543c571823d0aa3e Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sat, 5 Feb 2022 12:39:59 -0500 Subject: [PATCH 03/18] types: make many type improvements --- packages/builders/babel.config.js | 7 +- packages/builders/src/components/ActionRow.ts | 3 +- packages/builders/src/components/Component.ts | 15 ++-- .../src/components/button/UnsafeButton.ts | 4 +- .../components/selectMenu/UnsafeSelectMenu.ts | 14 ++-- packages/builders/src/messages/embed/Embed.ts | 6 +- .../src/messages/embed/UnsafeEmbed.ts | 74 ++++++++++++------- 7 files changed, 69 insertions(+), 54 deletions(-) diff --git a/packages/builders/babel.config.js b/packages/builders/babel.config.js index 5cec1521580a..7236fed9bea7 100644 --- a/packages/builders/babel.config.js +++ b/packages/builders/babel.config.js @@ -12,12 +12,7 @@ module.exports = { modules: 'commonjs', }, ], - [ - '@babel/preset-typescript', - { - allowDeclareFields: true, - }, - ], + '@babel/preset-typescript', ], plugins: ['babel-plugin-transform-typescript-metadata', ['@babel/plugin-proposal-decorators', { legacy: true }]], }; diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 5b7178be34ce..c3c012bb2de1 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -12,8 +12,7 @@ export type ActionRowComponent = ButtonComponent | SelectMenuComponent; /** * Represents an action row component */ -export class ActionRow extends Component { - protected declare data: APIActionRowComponent; +export class ActionRow extends Component { public readonly components: T[] = []; public constructor(data?: APIActionRowComponent & { type?: ComponentType.ActionRow }) { diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 0757c58e6910..651a2cabf67b 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -4,23 +4,26 @@ import type { APIBaseMessageComponent, APIMessageComponent, ComponentType } from /** * Represents a discord component */ -export abstract class Component implements JSONEncodable { +export abstract class Component< + DataType extends APIBaseMessageComponent = APIBaseMessageComponent, +> implements JSONEncodable +{ /** * The api data associated with this component */ - protected data: APIBaseMessageComponent; + protected data: DataType; /** * The type of this component */ public abstract readonly type: ComponentType; - public constructor(data: APIBaseMessageComponent) { - this.data = data; - } - /** * Converts this component to an API-compatible JSON object */ public abstract toJSON(): APIMessageComponent; + + public constructor(data: APIBaseMessageComponent) { + this.data = data as DataType; + } } diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index e82d6b5b3ed8..023ea07fef1a 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -8,9 +8,7 @@ import { } from 'discord-api-types/v9'; import { Component } from '../Component'; -export class UnsafeButtonComponent extends Component { - protected declare data: APIButtonComponent; - +export class UnsafeButtonComponent extends Component { public constructor(data?: APIButtonComponent) { super({ type: ComponentType.Button, ...data }); } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 3706b5089bc0..6a540484d121 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -1,17 +1,19 @@ -import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; +import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; import { Component } from '../Component'; import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; /** * Represents a non-validated select menu component */ -export class UnsafeSelectMenuComponent extends Component { - protected declare data: APISelectMenuComponent; +export class UnsafeSelectMenuComponent extends Component> { public readonly options: UnsafeSelectMenuOption[] = []; - public constructor(data?: APISelectMenuComponent) { - super({ type: ComponentType.SelectMenu, options: [], ...data }); - this.options = this.data.options.map((o) => new UnsafeSelectMenuOption(o)); + public constructor( + data?: APISelectMenuComponent & { type?: ComponentType.SelectMenu; options?: APISelectMenuOption[] }, + ) { + super({ type: ComponentType.SelectMenu, ...data }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.options = data?.options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; } public get type(): ComponentType.SelectMenu { diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index 3e0096db5c14..1fef40c96275 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -13,7 +13,7 @@ import { urlPredicate, validateFieldLength, } from './Assertions'; -import { AuthorOptions, FooterOptions, UnsafeEmbed } from './UnsafeEmbed'; +import { EmbedAuthorOptions, EmbedFooterOptions, UnsafeEmbed } from './UnsafeEmbed'; /** * Represents an embed in a message (image/video preview, rich embed, etc.) @@ -35,7 +35,7 @@ export class Embed extends UnsafeEmbed { return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields)); } - public override setAuthor(options: AuthorOptions | null): this { + public override setAuthor(options: EmbedAuthorOptions | null): this { if (options === null) { return super.setAuthor(null); } @@ -58,7 +58,7 @@ export class Embed extends UnsafeEmbed { return super.setDescription(descriptionPredicate.parse(description)); } - public override setFooter(options: FooterOptions | null): this { + public override setFooter(options: EmbedFooterOptions | null): this { if (options === null) { return super.setFooter(null); } diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index 1e05bfafe121..2e88562383b3 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -1,18 +1,31 @@ -import type { APIEmbed, APIEmbedField } from 'discord-api-types/v9'; +import type { APIEmbed, APIEmbedField, APIEmbedVideo } from 'discord-api-types/v9'; -export interface AuthorOptions { +export interface EmbedAuthorData { name: string; url?: string; iconURL?: string; + proxyIconURL?: string; } -export interface FooterOptions { +export type EmbedAuthorOptions = Omit; + +export interface EmbedFooterData { text: string; iconURL?: string; + proxyIconURL?: string; +} + +export type EmbedFooterOptions = Omit; + +export interface EmbedImageData { + url: string; + proxyURL?: string; + height?: number; + width?: number; } export class UnsafeEmbed { - protected data!: APIEmbed; + protected data: APIEmbed; public constructor(data: APIEmbed = {}) { this.data = { fields: [], ...data }; @@ -64,43 +77,47 @@ export class UnsafeEmbed { /** * The embed thumbnail data */ - public get thumbnail() { + public get thumbnail(): EmbedImageData | undefined { + if (!this.data.thumbnail) return undefined; return { - url: this.data.thumbnail?.url, - proxyURL: this.data.thumbnail?.proxy_url, - height: this.data.thumbnail?.height, - width: this.data.thumbnail?.width, + url: this.data.thumbnail.url, + proxyURL: this.data.thumbnail.proxy_url, + height: this.data.thumbnail.height, + width: this.data.thumbnail.width, }; } /** * The embed image data */ - public get image() { + public get image(): EmbedImageData | undefined { + if (!this.data.image) return undefined; return { - url: this.data.image?.url, - proxyURL: this.data.image?.proxy_url, - height: this.data.image?.height, - width: this.data.image?.width, + url: this.data.image.url, + proxyURL: this.data.image.proxy_url, + height: this.data.image.height, + width: this.data.image.width, }; } /** * Received video data */ - public get video() { + public get video(): APIEmbedVideo | undefined { + if (!this.data.video) return undefined; return this.data.video; } /** * The embed author data */ - public get author() { + public get author(): EmbedAuthorData | undefined { + if (!this.data.author) return undefined; return { - name: this.data.author?.name, - url: this.data.author?.url, - iconURL: this.data.author?.icon_url, - proxyIconURL: this.data.author?.proxy_icon_url, + name: this.data.author.name, + url: this.data.author.url, + iconURL: this.data.author.icon_url, + proxyIconURL: this.data.author.proxy_icon_url, }; } @@ -114,11 +131,12 @@ export class UnsafeEmbed { /** * The embed footer data */ - public get footer() { + public get footer(): EmbedFooterData | undefined { + if (!this.data.footer) return undefined; return { - text: this.data.footer?.text, - iconURL: this.data.footer?.icon_url, - proxyIconURL: this.data.footer?.proxy_icon_url, + text: this.data.footer.text, + iconURL: this.data.footer.icon_url, + proxyIconURL: this.data.footer.proxy_icon_url, }; } @@ -130,8 +148,8 @@ export class UnsafeEmbed { (this.title?.length ?? 0) + (this.description?.length ?? 0) + this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + - (this.footer.text?.length ?? 0) + - (this.author.name?.length ?? 0) + (this.footer?.text.length ?? 0) + + (this.author?.name.length ?? 0) ); } @@ -180,7 +198,7 @@ export class UnsafeEmbed { * * @param options The options for the author */ - public setAuthor(options: AuthorOptions | null): this { + public setAuthor(options: EmbedAuthorOptions | null): this { if (options === null) { this.data.author = undefined; return this; @@ -215,7 +233,7 @@ export class UnsafeEmbed { * * @param options The options for the footer */ - public setFooter(options: FooterOptions | null): this { + public setFooter(options: EmbedFooterOptions | null): this { if (options === null) { this.data.footer = undefined; return this; From cb64330bc91b7452dede8588762431a5d5f04ba3 Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sat, 5 Feb 2022 22:00:49 -0500 Subject: [PATCH 04/18] chore: make requested changes --- packages/builders/src/components/ActionRow.ts | 8 +++++--- packages/builders/src/components/Component.ts | 4 ++-- packages/builders/src/components/button/UnsafeButton.ts | 4 ++-- .../src/components/selectMenu/UnsafeSelectMenu.ts | 9 +++------ packages/builders/src/messages/embed/UnsafeEmbed.ts | 2 +- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index c3c012bb2de1..618b580bc26a 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -12,12 +12,14 @@ export type ActionRowComponent = ButtonComponent | SelectMenuComponent; /** * Represents an action row component */ -export class ActionRow extends Component { +export class ActionRow extends Component< + Omit +> { public readonly components: T[] = []; - public constructor(data?: APIActionRowComponent & { type?: ComponentType.ActionRow }) { + public constructor({ components, ...data }: Omit) { super({ type: ComponentType.ActionRow, ...data }); - this.components = (data?.components.map(createComponent) ?? []) as T[]; + this.components = components.map(createComponent) as T[]; } public get type(): ComponentType.ActionRow { diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 651a2cabf67b..13f78e0ce4af 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -23,7 +23,7 @@ export abstract class Component< */ public abstract toJSON(): APIMessageComponent; - public constructor(data: APIBaseMessageComponent) { - this.data = data as DataType; + public constructor(data: DataType) { + this.data = data; } } diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 023ea07fef1a..24cc93bdafc9 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -9,8 +9,8 @@ import { import { Component } from '../Component'; export class UnsafeButtonComponent extends Component { - public constructor(data?: APIButtonComponent) { - super({ type: ComponentType.Button, ...data }); + public constructor(data?: Omit) { + super({ type: ComponentType.Button, ...data } as APIButtonComponent); } public get type(): ComponentType.Button { diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 6a540484d121..5ba50f07fe54 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -1,4 +1,4 @@ -import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; +import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; import { Component } from '../Component'; import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; @@ -8,12 +8,9 @@ import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; export class UnsafeSelectMenuComponent extends Component> { public readonly options: UnsafeSelectMenuOption[] = []; - public constructor( - data?: APISelectMenuComponent & { type?: ComponentType.SelectMenu; options?: APISelectMenuOption[] }, - ) { + public constructor({ options, ...data }: Omit) { super({ type: ComponentType.SelectMenu, ...data }); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.options = data?.options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; + this.options = options.map((o) => new UnsafeSelectMenuOption(o)); } public get type(): ComponentType.SelectMenu { diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index 2e88562383b3..004d1eacfd70 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -36,7 +36,7 @@ export class UnsafeEmbed { * An array of fields of this embed */ public get fields() { - return this.data.fields ?? []; + return this.data.fields!; } /** From efe1615791016cdfe4215d42cc57ae8ef6090b8f Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sun, 6 Feb 2022 20:13:18 -0500 Subject: [PATCH 05/18] refactor: remove bad typings --- packages/builders/src/components/ActionRow.ts | 24 ++++++++++++++---- .../components/selectMenu/UnsafeSelectMenu.ts | 25 ++++++++++++++----- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 618b580bc26a..8e9651f621cf 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -1,4 +1,4 @@ -import { type APIActionRowComponent, ComponentType } from 'discord-api-types/v9'; +import { type APIActionRowComponent, ComponentType, APIMessageComponent } from 'discord-api-types/v9'; import type { ButtonComponent, SelectMenuComponent } from '..'; import { Component } from './Component'; import { createComponent } from './Components'; @@ -9,17 +9,31 @@ export type ActionRowComponent = ButtonComponent | SelectMenuComponent; // TODO: Add valid form component types +export interface ActionRowData extends Omit { + type?: ComponentType.ActionRow; + components?: Exclude[]; +} + /** * Represents an action row component */ export class ActionRow extends Component< Omit > { - public readonly components: T[] = []; + public readonly components: T[]; + + public constructor(data?: ActionRowData) { + // We don't destructure directly in the constructor because it can't properly + // handle possibly-undefined data, which causes invalid destructure runtime errors. + if (data?.components) { + const { components: initComponents, ...initData } = data; + super({ type: ComponentType.ActionRow, ...initData }); + } else { + super({ type: ComponentType.ActionRow, ...data }); + } - public constructor({ components, ...data }: Omit) { - super({ type: ComponentType.ActionRow, ...data }); - this.components = components.map(createComponent) as T[]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + this.components = (data?.components?.map(createComponent) ?? []) as T[]; } public get type(): ComponentType.ActionRow { diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 5ba50f07fe54..3686aeda3f71 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -1,16 +1,29 @@ -import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; +import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; import { Component } from '../Component'; import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; +export interface SelectMenuComponentData extends Omit { + type?: ComponentType.SelectMenu; + options?: APISelectMenuOption[]; +} + /** * Represents a non-validated select menu component */ export class UnsafeSelectMenuComponent extends Component> { - public readonly options: UnsafeSelectMenuOption[] = []; - - public constructor({ options, ...data }: Omit) { - super({ type: ComponentType.SelectMenu, ...data }); - this.options = options.map((o) => new UnsafeSelectMenuOption(o)); + public readonly options: UnsafeSelectMenuOption[]; + + public constructor(data?: SelectMenuComponentData) { + // We don't destructure directly in the constructor because it can't properly + // handle possibly-undefined data, which causes invalid destructure runtime errors. + if (data?.options) { + const { options: initOptions, ...initData } = data; + super({ type: ComponentType.SelectMenu, ...initData }); + } else { + super({ type: ComponentType.SelectMenu, ...data! }); + } + + this.options = data?.options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; } public get type(): ComponentType.SelectMenu { From 2648b8bf10056faf1129d0664827396fb1a3d116 Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sun, 6 Feb 2022 20:19:39 -0500 Subject: [PATCH 06/18] chore: lazily init embed field array --- packages/builders/src/messages/embed/Assertions.ts | 3 ++- packages/builders/src/messages/embed/UnsafeEmbed.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 2d0270978b1d..9484856409e1 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/packages/builders/src/messages/embed/Assertions.ts @@ -17,7 +17,8 @@ export const embedFieldsArrayPredicate = embedFieldPredicate.array(); export const fieldLengthPredicate = z.number().lte(25); -export function validateFieldLength(fields: APIEmbedField[], amountAdding: number): void { +export function validateFieldLength(fields: APIEmbedField[] | undefined, amountAdding: number): void { + if (!fields) return; fieldLengthPredicate.parse(fields.length + amountAdding); } diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index 004d1eacfd70..f50bf98e1e66 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -28,7 +28,7 @@ export class UnsafeEmbed { protected data: APIEmbed; public constructor(data: APIEmbed = {}) { - this.data = { fields: [], ...data }; + this.data = { ...data }; if (data.timestamp) this.data.timestamp = new Date(data.timestamp).toISOString(); } @@ -36,7 +36,7 @@ export class UnsafeEmbed { * An array of fields of this embed */ public get fields() { - return this.data.fields!; + return this.data.fields; } /** @@ -147,7 +147,7 @@ export class UnsafeEmbed { return ( (this.title?.length ?? 0) + (this.description?.length ?? 0) + - this.fields.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + + (this.fields ?? []).reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + (this.footer?.text.length ?? 0) + (this.author?.name.length ?? 0) ); @@ -168,7 +168,8 @@ export class UnsafeEmbed { * @param fields The fields to add */ public addFields(...fields: APIEmbedField[]): this { - this.fields.push(...UnsafeEmbed.normalizeFields(...fields)); + if (!this.data.fields) this.data.fields = []; + this.fields?.push(...UnsafeEmbed.normalizeFields(...fields)); return this; } @@ -180,7 +181,7 @@ export class UnsafeEmbed { * @param fields The replacing field objects */ public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { - this.fields.splice(index, deleteCount, ...UnsafeEmbed.normalizeFields(...fields)); + this.fields?.splice(index, deleteCount, ...UnsafeEmbed.normalizeFields(...fields)); return this; } @@ -189,7 +190,7 @@ export class UnsafeEmbed { * @param fields The fields to set */ public setFields(...fields: APIEmbedField[]) { - this.spliceFields(0, this.fields.length, ...fields); + this.spliceFields(0, this.fields?.length ?? 0, ...fields); return this; } From bd45156e81638cbc1a557b9697e907c1bd0727f4 Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sun, 6 Feb 2022 20:34:15 -0500 Subject: [PATCH 07/18] fix: tests --- packages/builders/__tests__/messages/embed.test.ts | 4 +--- packages/builders/src/components/button/UnsafeButton.ts | 6 +++++- .../builders/src/components/selectMenu/UnsafeSelectMenu.ts | 4 ++-- packages/builders/src/messages/embed/Assertions.ts | 3 +-- packages/builders/src/messages/embed/Embed.ts | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index 5b381bbe36e3..017cc83d7d79 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -1,9 +1,7 @@ import { Embed } from '../../src'; import type { APIEmbed } from 'discord-api-types/v9'; -const emptyEmbed: APIEmbed = { - fields: [], -}; +const emptyEmbed: APIEmbed = {}; const alpha = 'abcdefghijklmnopqrstuvwxyz'; diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 24cc93bdafc9..d098cb078d61 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -8,8 +8,12 @@ import { } from 'discord-api-types/v9'; import { Component } from '../Component'; +export interface ButtonComponentData extends Omit { + type?: ComponentType.Button; +} + export class UnsafeButtonComponent extends Component { - public constructor(data?: Omit) { + public constructor(data?: ButtonComponentData) { super({ type: ComponentType.Button, ...data } as APIButtonComponent); } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 3686aeda3f71..8fe4e84715d8 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -17,7 +17,7 @@ export class UnsafeSelectMenuComponent extends Component Date: Sun, 6 Feb 2022 21:21:52 -0500 Subject: [PATCH 08/18] chore: make requested changes --- .../builders/src/messages/embed/UnsafeEmbed.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index f50bf98e1e66..fb11dfb5b976 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -104,7 +104,6 @@ export class UnsafeEmbed { * Received video data */ public get video(): APIEmbedVideo | undefined { - if (!this.data.video) return undefined; return this.data.video; } @@ -145,11 +144,11 @@ export class UnsafeEmbed { */ public get length(): number { return ( - (this.title?.length ?? 0) + - (this.description?.length ?? 0) + - (this.fields ?? []).reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + - (this.footer?.text.length ?? 0) + - (this.author?.name.length ?? 0) + (this.data.title?.length ?? 0) + + (this.data.description?.length ?? 0) + + (this.data.fields ?? []).reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + + (this.data.footer?.text.length ?? 0) + + (this.data.author?.name.length ?? 0) ); } @@ -168,8 +167,8 @@ export class UnsafeEmbed { * @param fields The fields to add */ public addFields(...fields: APIEmbedField[]): this { - if (!this.data.fields) this.data.fields = []; - this.fields?.push(...UnsafeEmbed.normalizeFields(...fields)); + this.data.fields ??= []; + this.data.fields.push(...UnsafeEmbed.normalizeFields(...fields)); return this; } @@ -181,7 +180,7 @@ export class UnsafeEmbed { * @param fields The replacing field objects */ public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { - this.fields?.splice(index, deleteCount, ...UnsafeEmbed.normalizeFields(...fields)); + this.data.fields?.splice(index, deleteCount, ...UnsafeEmbed.normalizeFields(...fields)); return this; } From 05b098cbc31ad2bd01e0465e2993d48c99e0b42a Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sun, 6 Feb 2022 21:48:42 -0500 Subject: [PATCH 09/18] chore: cleanup and document --- packages/builders/src/components/ActionRow.ts | 4 -- packages/builders/src/components/Component.ts | 12 +++--- .../builders/src/components/button/Button.ts | 3 ++ .../src/components/button/UnsafeButton.ts | 29 +++++++++++--- .../src/components/selectMenu/SelectMenu.ts | 2 +- .../components/selectMenu/SelectMenuOption.ts | 2 +- .../components/selectMenu/UnsafeSelectMenu.ts | 19 +++++++-- .../selectMenu/UnsafeSelectMenuOption.ts | 15 +++++++ packages/builders/src/messages/embed/Embed.ts | 2 +- .../src/messages/embed/UnsafeEmbed.ts | 40 ++++++++++++------- 10 files changed, 92 insertions(+), 36 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 8e9651f621cf..4967f92a2444 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -36,10 +36,6 @@ export class ActionRow extend this.components = (data?.components?.map(createComponent) ?? []) as T[]; } - public get type(): ComponentType.ActionRow { - return this.data.type; - } - /** * Adds components to this action row. * @param components The components to add to this action row. diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 13f78e0ce4af..f8220a4e9fbe 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -13,11 +13,6 @@ export abstract class Component< */ protected data: DataType; - /** - * The type of this component - */ - public abstract readonly type: ComponentType; - /** * Converts this component to an API-compatible JSON object */ @@ -26,4 +21,11 @@ export abstract class Component< public constructor(data: DataType) { this.data = data; } + + /** + * The type of this component + */ + public get type(): DataType['type'] { + return this.data.type; + } } diff --git a/packages/builders/src/components/button/Button.ts b/packages/builders/src/components/button/Button.ts index 37082505b77a..bc68ecdb371f 100644 --- a/packages/builders/src/components/button/Button.ts +++ b/packages/builders/src/components/button/Button.ts @@ -10,6 +10,9 @@ import { } from '../Assertions'; import { UnsafeButtonComponent } from './UnsafeButton'; +/** + * Represents a validated button component + */ export class ButtonComponent extends UnsafeButtonComponent { public override setStyle(style: ButtonStyle) { return super.setStyle(buttonStyleValidator.parse(style)); diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index d098cb078d61..7dd1dd01a67f 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -12,36 +12,53 @@ export interface ButtonComponentData extends Omit { type?: ComponentType.Button; } +/** + * Represents a non-validated button component + */ export class UnsafeButtonComponent extends Component { public constructor(data?: ButtonComponentData) { super({ type: ComponentType.Button, ...data } as APIButtonComponent); } - public get type(): ComponentType.Button { - return this.data.type; - } - + /** + * The style of this button + */ public get style() { return this.data.style; } + /** + * The label of this button + */ public get label() { return this.data.label; } + /** + * The emoji used in this button + */ public get emoji() { return this.data.emoji; } + /** + * Whether or not this button is disabled + */ public get disabled() { return this.data.disabled; } - public get customId() { + /** + * The custom ID of this button (only defined on non-link buttons) + */ + public get customId(): string | undefined { return (this.data as APIButtonComponentWithCustomId).custom_id; } - public get url() { + /** + * The URL of this button (only defined on link buttons) + */ + public get url(): string | undefined { return (this.data as APIButtonComponentWithURL).url; } diff --git a/packages/builders/src/components/selectMenu/SelectMenu.ts b/packages/builders/src/components/selectMenu/SelectMenu.ts index d344354fe2d2..665157d29271 100644 --- a/packages/builders/src/components/selectMenu/SelectMenu.ts +++ b/packages/builders/src/components/selectMenu/SelectMenu.ts @@ -9,7 +9,7 @@ import { import { UnsafeSelectMenuComponent } from './UnsafeSelectMenu'; /** - * Represents a select menu component + * Represents a validated select menu component */ export class SelectMenuComponent extends UnsafeSelectMenuComponent { public override setPlaceholder(placeholder: string) { diff --git a/packages/builders/src/components/selectMenu/SelectMenuOption.ts b/packages/builders/src/components/selectMenu/SelectMenuOption.ts index 7da5bddfe1b6..9f4e456ee715 100644 --- a/packages/builders/src/components/selectMenu/SelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/SelectMenuOption.ts @@ -8,7 +8,7 @@ import { import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; /** - * Represents an option within a select menu component + * Represents a validated option within a select menu component */ export class SelectMenuOption extends UnsafeSelectMenuOption { public override setDescription(description: string) { diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 8fe4e84715d8..5cc5824c1a4b 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -26,26 +26,37 @@ export class UnsafeSelectMenuComponent extends Component new UnsafeSelectMenuOption(o)) ?? []; } - public get type(): ComponentType.SelectMenu { - return this.data.type; - } - + /** + * The placeholder for this select menu + */ public get placeholder() { return this.data.placeholder; } + /** + * The maximum amount of options that can be selected + */ public get maxValues() { return this.data.max_values; } + /** + * The minimum amount of options that must be selected + */ public get minValues() { return this.data.min_values; } + /** + * The custom ID of this select menu + */ public get customId() { return this.data.custom_id; } + /** + * Whether or not this select menu is disabled + */ public get disabled() { return this.data.disabled; } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts index 7c9a312274f7..5fea929325af 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts @@ -6,22 +6,37 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api- export class UnsafeSelectMenuOption { public constructor(protected data: APISelectMenuOption = {} as APISelectMenuOption) {} + /** + * The label for this option + */ public get label() { return this.data.label; } + /** + * The value for this option + */ public get value() { return this.data.value; } + /** + * The description for this option + */ public get description() { return this.data.description; } + /** + * The emoji for this option + */ public get emoji() { return this.data.emoji; } + /** + * Whether or not this option is selected by default + */ public get default() { return this.data.default; } diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index b1a41b1130e3..b120b12cab7d 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -16,7 +16,7 @@ import { import { EmbedAuthorOptions, EmbedFooterOptions, UnsafeEmbed } from './UnsafeEmbed'; /** - * Represents an embed in a message (image/video preview, rich embed, etc.) + * Represents a validated embed in a message (image/video preview, rich embed, etc.) */ export class Embed extends UnsafeEmbed { public override addFields(...fields: APIEmbedField[]): this { diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index fb11dfb5b976..be9960fc6898 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -1,29 +1,41 @@ -import type { APIEmbed, APIEmbedField, APIEmbedVideo } from 'discord-api-types/v9'; - -export interface EmbedAuthorData { - name: string; - url?: string; +import type { + APIEmbed, + APIEmbedAuthor, + APIEmbedField, + APIEmbedFooter, + APIEmbedImage, + APIEmbedVideo, +} from 'discord-api-types/v9'; + +export interface IconData { + /** + * The url of the icon + */ iconURL?: string; + /** + * The proxy url of the icon + */ proxyIconURL?: string; } +export type EmbedAuthorData = Omit & IconData; + export type EmbedAuthorOptions = Omit; -export interface EmbedFooterData { - text: string; - iconURL?: string; - proxyIconURL?: string; -} +export type EmbedFooterData = Omit & IconData; export type EmbedFooterOptions = Omit; -export interface EmbedImageData { - url: string; +export interface EmbedImageData extends Omit { + /** + * The proxy URL for the image + */ proxyURL?: string; - height?: number; - width?: number; } +/** + * Represents a non-validated embed in a message (image/video preview, rich embed, etc.) + */ export class UnsafeEmbed { protected data: APIEmbed; From 646b4bfbb1bcb73eae198db8d27ec0fae084ea07 Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Mon, 7 Feb 2022 15:10:07 -0500 Subject: [PATCH 10/18] chore: make requested changes --- packages/builders/src/components/ActionRow.ts | 15 +++------------ .../src/components/selectMenu/UnsafeSelectMenu.ts | 13 +++---------- .../builders/src/messages/embed/Assertions.ts | 4 ++-- packages/builders/src/messages/embed/Embed.ts | 4 ++-- .../builders/src/messages/embed/UnsafeEmbed.ts | 11 +++++++---- 5 files changed, 17 insertions(+), 30 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 4967f92a2444..8d87ddaed43f 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -22,18 +22,9 @@ export class ActionRow extend > { public readonly components: T[]; - public constructor(data?: ActionRowData) { - // We don't destructure directly in the constructor because it can't properly - // handle possibly-undefined data, which causes invalid destructure runtime errors. - if (data?.components) { - const { components: initComponents, ...initData } = data; - super({ type: ComponentType.ActionRow, ...initData }); - } else { - super({ type: ComponentType.ActionRow, ...data }); - } - - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - this.components = (data?.components?.map(createComponent) ?? []) as T[]; + public constructor({ components, ...data }: ActionRowData = {}) { + super({ type: ComponentType.ActionRow, ...data }); + this.components = (components?.map(createComponent) ?? []) as T[]; } /** diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 5cc5824c1a4b..550aa7cda0d7 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -14,16 +14,9 @@ export class UnsafeSelectMenuComponent extends Component new UnsafeSelectMenuOption(o)) ?? []; + const { options, ...initData } = data ?? {}; + super({ type: ComponentType.SelectMenu, ...initData } as APISelectMenuComponent); + this.options = options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; } /** diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 2d0270978b1d..0cd7a706d961 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/packages/builders/src/messages/embed/Assertions.ts @@ -17,8 +17,8 @@ export const embedFieldsArrayPredicate = embedFieldPredicate.array(); export const fieldLengthPredicate = z.number().lte(25); -export function validateFieldLength(fields: APIEmbedField[], amountAdding: number): void { - fieldLengthPredicate.parse(fields.length + amountAdding); +export function validateFieldLength(fields: APIEmbedField[] | undefined, amountAdding: number): void { + fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding); } export const authorNamePredicate = fieldNamePredicate.nullable(); diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index b120b12cab7d..368a61e485dd 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -21,7 +21,7 @@ import { EmbedAuthorOptions, EmbedFooterOptions, UnsafeEmbed } from './UnsafeEmb export class Embed extends UnsafeEmbed { public override addFields(...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(this.fields ?? [], fields.length); + validateFieldLength(this.fields, fields.length); // Data assertions return super.addFields(...embedFieldsArrayPredicate.parse(fields)); @@ -29,7 +29,7 @@ export class Embed extends UnsafeEmbed { public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(this.fields ?? [], fields.length - deleteCount); + validateFieldLength(this.fields, fields.length - deleteCount); // Data assertions return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields)); diff --git a/packages/builders/src/messages/embed/UnsafeEmbed.ts b/packages/builders/src/messages/embed/UnsafeEmbed.ts index be9960fc6898..33136846a01a 100644 --- a/packages/builders/src/messages/embed/UnsafeEmbed.ts +++ b/packages/builders/src/messages/embed/UnsafeEmbed.ts @@ -158,7 +158,7 @@ export class UnsafeEmbed { return ( (this.data.title?.length ?? 0) + (this.data.description?.length ?? 0) + - (this.data.fields ?? []).reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) + + (this.data.fields?.reduce((prev, curr) => prev + curr.name.length + curr.value.length, 0) ?? 0) + (this.data.footer?.text.length ?? 0) + (this.data.author?.name.length ?? 0) ); @@ -179,8 +179,9 @@ export class UnsafeEmbed { * @param fields The fields to add */ public addFields(...fields: APIEmbedField[]): this { - this.data.fields ??= []; - this.data.fields.push(...UnsafeEmbed.normalizeFields(...fields)); + fields = UnsafeEmbed.normalizeFields(...fields); + if (this.data.fields) this.data.fields.push(...fields); + else this.data.fields = fields; return this; } @@ -192,7 +193,9 @@ export class UnsafeEmbed { * @param fields The replacing field objects */ public spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { - this.data.fields?.splice(index, deleteCount, ...UnsafeEmbed.normalizeFields(...fields)); + fields = UnsafeEmbed.normalizeFields(...fields); + if (this.data.fields) this.data.fields.splice(index, deleteCount, ...fields); + else this.data.fields = fields; return this; } From ed882a380f6996c68b853c7619b3dbdd5342aee3 Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Wed, 9 Feb 2022 11:53:54 -0500 Subject: [PATCH 11/18] Apply suggestions from code review Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- packages/builders/src/components/button/UnsafeButton.ts | 6 +++--- .../src/components/selectMenu/UnsafeSelectMenu.ts | 4 ++-- .../src/components/selectMenu/UnsafeSelectMenuOption.ts | 4 ++-- packages/builders/src/messages/embed/UnsafeEmbed.ts | 8 ++++---- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 7dd1dd01a67f..ac883f788c68 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -42,14 +42,14 @@ export class UnsafeButtonComponent extends Component { } /** - * Whether or not this button is disabled + * Whether this button is disabled */ public get disabled() { return this.data.disabled; } /** - * The custom ID of this button (only defined on non-link buttons) + * The custom id of this button (only defined on non-link buttons) */ public get customId(): string | undefined { return (this.data as APIButtonComponentWithCustomId).custom_id; @@ -82,7 +82,7 @@ export class UnsafeButtonComponent extends Component { /** * Sets the custom Id for this button - * @param customId The custom ID to use for this button + * @param customId The custom id to use for this button */ public setCustomId(customId: string) { (this.data as APIButtonComponentWithCustomId).custom_id = customId; diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 550aa7cda0d7..db4d77c80e5e 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -48,7 +48,7 @@ export class UnsafeSelectMenuComponent extends Component Date: Wed, 9 Feb 2022 11:54:37 -0500 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> --- packages/builders/src/components/Component.ts | 2 +- packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index f8220a4e9fbe..a200652fb8d5 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -9,7 +9,7 @@ export abstract class Component< > implements JSONEncodable { /** - * The api data associated with this component + * The API data associated with this component */ protected data: DataType; diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index db4d77c80e5e..6d8c2ff4772f 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -41,7 +41,7 @@ export class UnsafeSelectMenuComponent extends Component Date: Wed, 9 Feb 2022 12:13:11 -0500 Subject: [PATCH 13/18] Update packages/builders/src/components/button/UnsafeButton.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Antonio Román --- packages/builders/src/components/button/UnsafeButton.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index ac883f788c68..6c1ffbdd6786 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -3,8 +3,8 @@ import { ButtonStyle, type APIMessageComponentEmoji, type APIButtonComponent, - APIButtonComponentWithURL, - APIButtonComponentWithCustomId, + type APIButtonComponentWithURL, + type APIButtonComponentWithCustomId, } from 'discord-api-types/v9'; import { Component } from '../Component'; From 63a93ed932d11a7812e8afa5c88226686ed08f6b Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Thu, 10 Feb 2022 15:19:32 -0500 Subject: [PATCH 14/18] chore: use partials --- packages/builders/src/components/ActionRow.ts | 15 +++++---------- packages/builders/src/components/Assertions.ts | 2 +- packages/builders/src/components/Component.ts | 2 +- packages/builders/src/components/Components.ts | 6 +++--- .../src/components/button/UnsafeButton.ts | 13 +++++-------- .../components/selectMenu/UnsafeSelectMenu.ts | 16 ++++++---------- .../selectMenu/UnsafeSelectMenuOption.ts | 5 +++-- 7 files changed, 24 insertions(+), 35 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 8d87ddaed43f..4428b71d265e 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -1,4 +1,4 @@ -import { type APIActionRowComponent, ComponentType, APIMessageComponent } from 'discord-api-types/v9'; +import { type APIActionRowComponent, ComponentType } from 'discord-api-types/v9'; import type { ButtonComponent, SelectMenuComponent } from '..'; import { Component } from './Component'; import { createComponent } from './Components'; @@ -8,21 +8,15 @@ export type MessageComponent = ActionRowComponent | ActionRow; export type ActionRowComponent = ButtonComponent | SelectMenuComponent; // TODO: Add valid form component types - -export interface ActionRowData extends Omit { - type?: ComponentType.ActionRow; - components?: Exclude[]; -} - /** * Represents an action row component */ export class ActionRow extends Component< - Omit + Omit, 'components'> > { public readonly components: T[]; - public constructor({ components, ...data }: ActionRowData = {}) { + public constructor({ components, ...data }: Partial = {}) { super({ type: ComponentType.ActionRow, ...data }); this.components = (components?.map(createComponent) ?? []) as T[]; } @@ -47,9 +41,10 @@ export class ActionRow extend } public toJSON(): APIActionRowComponent { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...this.data, components: this.components.map((component) => component.toJSON()), - }; + } as APIActionRowComponent; } } diff --git a/packages/builders/src/components/Assertions.ts b/packages/builders/src/components/Assertions.ts index 9348bc9763c7..2f45e17832e7 100644 --- a/packages/builders/src/components/Assertions.ts +++ b/packages/builders/src/components/Assertions.ts @@ -40,7 +40,7 @@ export function validateRequiredSelectMenuOptionParameters(label?: string, value export const urlValidator = z.string().url(); export function validateRequiredButtonParameters( - style: ButtonStyle, + style?: ButtonStyle, label?: string, emoji?: APIMessageComponentEmoji, customId?: string, diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index a200652fb8d5..45886b4ac705 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -5,7 +5,7 @@ import type { APIBaseMessageComponent, APIMessageComponent, ComponentType } from * Represents a discord component */ export abstract class Component< - DataType extends APIBaseMessageComponent = APIBaseMessageComponent, + DataType extends Partial> = APIBaseMessageComponent, > implements JSONEncodable { /** diff --git a/packages/builders/src/components/Components.ts b/packages/builders/src/components/Components.ts index f0cb5d95eebb..cb61639bb545 100644 --- a/packages/builders/src/components/Components.ts +++ b/packages/builders/src/components/Components.ts @@ -19,11 +19,11 @@ export function createComponent(data: C): C; export function createComponent(data: APIMessageComponent | MessageComponent): Component { switch (data.type) { case ComponentType.ActionRow: - return data instanceof ActionRow ? data : new ActionRow(data); + return (data instanceof ActionRow ? data : new ActionRow(data)) as Component; case ComponentType.Button: - return data instanceof ButtonComponent ? data : new ButtonComponent(data); + return (data instanceof ButtonComponent ? data : new ButtonComponent(data)) as Component; case ComponentType.SelectMenu: - return data instanceof SelectMenuComponent ? data : new SelectMenuComponent(data); + return (data instanceof SelectMenuComponent ? data : new SelectMenuComponent(data)) as Component; default: // @ts-expect-error throw new Error(`Cannot serialize component type: ${data.type as number}`); diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 6c1ffbdd6786..83f831ea5c2e 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -8,16 +8,12 @@ import { } from 'discord-api-types/v9'; import { Component } from '../Component'; -export interface ButtonComponentData extends Omit { - type?: ComponentType.Button; -} - /** * Represents a non-validated button component */ -export class UnsafeButtonComponent extends Component { - public constructor(data?: ButtonComponentData) { - super({ type: ComponentType.Button, ...data } as APIButtonComponent); +export class UnsafeButtonComponent extends Component> { + public constructor(data?: Partial) { + super({ type: ComponentType.Button, ...data }); } /** @@ -117,8 +113,9 @@ export class UnsafeButtonComponent extends Component { } public toJSON(): APIButtonComponent { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...this.data, - }; + } as APIButtonComponent; } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 6d8c2ff4772f..3c304aa6c7da 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -1,21 +1,16 @@ -import { APISelectMenuOption, ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; +import { ComponentType, type APISelectMenuComponent } from 'discord-api-types/v9'; import { Component } from '../Component'; import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; -export interface SelectMenuComponentData extends Omit { - type?: ComponentType.SelectMenu; - options?: APISelectMenuOption[]; -} - /** * Represents a non-validated select menu component */ -export class UnsafeSelectMenuComponent extends Component> { +export class UnsafeSelectMenuComponent extends Component>> { public readonly options: UnsafeSelectMenuOption[]; - public constructor(data?: SelectMenuComponentData) { + public constructor(data?: Partial) { const { options, ...initData } = data ?? {}; - super({ type: ComponentType.SelectMenu, ...initData } as APISelectMenuComponent); + super({ type: ComponentType.SelectMenu, ...initData }); this.options = options?.map((o) => new UnsafeSelectMenuOption(o)) ?? []; } @@ -119,9 +114,10 @@ export class UnsafeSelectMenuComponent extends Component o.toJSON()), - }; + } as APISelectMenuComponent; } } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts index 1db4391100f5..94269300621b 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenuOption.ts @@ -4,7 +4,7 @@ import type { APIMessageComponentEmoji, APISelectMenuOption } from 'discord-api- * Represents a non-validated option within a select menu component */ export class UnsafeSelectMenuOption { - public constructor(protected data: APISelectMenuOption = {} as APISelectMenuOption) {} + public constructor(protected data: Partial = {}) {} /** * The label for this option @@ -87,8 +87,9 @@ export class UnsafeSelectMenuOption { } public toJSON(): APISelectMenuOption { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...this.data, - }; + } as APISelectMenuOption; } } From eaf779bfd236dc78fcc8485c8344a79231a767b6 Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Thu, 10 Feb 2022 15:25:31 -0500 Subject: [PATCH 15/18] chore: make requested changes --- .../__tests__/components/selectMenu.test.ts | 1 - .../builders/__tests__/messages/embed.test.ts | 57 +++++++------------ packages/builders/src/components/ActionRow.ts | 2 +- packages/builders/src/components/Component.ts | 2 +- .../builders/src/messages/embed/Assertions.ts | 2 +- packages/builders/src/messages/embed/Embed.ts | 4 +- 6 files changed, 25 insertions(+), 43 deletions(-) diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index 8089c7434e98..8cc7babaef16 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -70,7 +70,6 @@ describe('Button Components', () => { }; expect( - // @ts-expect-error new SelectMenuComponent(selectMenuDataWithoutOptions) .addOptions(new SelectMenuOption(selectMenuOptionData)) .toJSON(), diff --git a/packages/builders/__tests__/messages/embed.test.ts b/packages/builders/__tests__/messages/embed.test.ts index 017cc83d7d79..10520efb01bf 100644 --- a/packages/builders/__tests__/messages/embed.test.ts +++ b/packages/builders/__tests__/messages/embed.test.ts @@ -1,7 +1,4 @@ import { Embed } from '../../src'; -import type { APIEmbed } from 'discord-api-types/v9'; - -const emptyEmbed: APIEmbed = {}; const alpha = 'abcdefghijklmnopqrstuvwxyz'; @@ -29,21 +26,21 @@ describe('Embed', () => { describe('Embed title', () => { test('GIVEN an embed with a pre-defined title THEN return valid toJSON data', () => { const embed = new Embed({ title: 'foo' }); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ title: 'foo' }); }); test('GIVEN an embed using Embed#setTitle THEN return valid toJSON data', () => { const embed = new Embed(); embed.setTitle('foo'); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ title: 'foo' }); }); test('GIVEN an embed with a pre-defined title THEN unset title THEN return valid toJSON data', () => { const embed = new Embed({ title: 'foo' }); embed.setTitle(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, title: undefined }); + expect(embed.toJSON()).toStrictEqual({ title: undefined }); }); test('GIVEN an embed with an invalid title THEN throws error', () => { @@ -55,22 +52,22 @@ describe('Embed', () => { describe('Embed description', () => { test('GIVEN an embed with a pre-defined description THEN return valid toJSON data', () => { - const embed = new Embed({ ...emptyEmbed, description: 'foo' }); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' }); + const embed = new Embed({ description: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ description: 'foo' }); }); test('GIVEN an embed using Embed#setDescription THEN return valid toJSON data', () => { const embed = new Embed(); embed.setDescription('foo'); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: 'foo' }); + expect(embed.toJSON()).toStrictEqual({ description: 'foo' }); }); test('GIVEN an embed with a pre-defined description THEN unset description THEN return valid toJSON data', () => { const embed = new Embed({ description: 'foo' }); embed.setDescription(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, description: undefined }); + expect(embed.toJSON()).toStrictEqual({ description: undefined }); }); test('GIVEN an embed with an invalid description THEN throws error', () => { @@ -84,7 +81,6 @@ describe('Embed', () => { test('GIVEN an embed with a pre-defined url THEN returns valid toJSON data', () => { const embed = new Embed({ url: 'https://discord.js.org/' }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, url: 'https://discord.js.org/', }); }); @@ -94,7 +90,6 @@ describe('Embed', () => { embed.setURL('https://discord.js.org/'); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, url: 'https://discord.js.org/', }); }); @@ -103,7 +98,7 @@ describe('Embed', () => { const embed = new Embed({ url: 'https://discord.js.org' }); embed.setURL(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, url: undefined }); + expect(embed.toJSON()).toStrictEqual({ url: undefined }); }); test('GIVEN an embed with an invalid URL THEN throws error', () => { @@ -116,21 +111,21 @@ describe('Embed', () => { describe('Embed Color', () => { test('GIVEN an embed with a pre-defined color THEN returns valid toJSON data', () => { const embed = new Embed({ color: 0xff0000 }); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 }); + expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 }); }); test('GIVEN an embed using Embed#setColor THEN returns valid toJSON data', () => { const embed = new Embed(); embed.setColor(0xff0000); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: 0xff0000 }); + expect(embed.toJSON()).toStrictEqual({ color: 0xff0000 }); }); test('GIVEN an embed with a pre-defined color THEN unset color THEN return valid toJSON data', () => { const embed = new Embed({ color: 0xff0000 }); embed.setColor(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, color: undefined }); + expect(embed.toJSON()).toStrictEqual({ color: undefined }); }); test('GIVEN an embed with an invalid color THEN throws error', () => { @@ -146,35 +141,35 @@ describe('Embed', () => { test('GIVEN an embed with a pre-defined timestamp THEN returns valid toJSON data', () => { const embed = new Embed({ timestamp: now.toISOString() }); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() }); + expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); }); test('given an embed using Embed#setTimestamp (with Date) THEN returns valid toJSON data', () => { const embed = new Embed(); embed.setTimestamp(now); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() }); + expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); }); test('GIVEN an embed using Embed#setTimestamp (with int) THEN returns valid toJSON data', () => { const embed = new Embed(); embed.setTimestamp(now.getTime()); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: now.toISOString() }); + expect(embed.toJSON()).toStrictEqual({ timestamp: now.toISOString() }); }); test('GIVEN an embed using Embed#setTimestamp (default) THEN returns valid toJSON data', () => { const embed = new Embed(); embed.setTimestamp(); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: embed.timestamp }); + expect(embed.toJSON()).toStrictEqual({ timestamp: embed.timestamp }); }); test('GIVEN an embed with a pre-defined timestamp THEN unset timestamp THEN return valid toJSON data', () => { const embed = new Embed({ timestamp: now.toISOString() }); embed.setTimestamp(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, timestamp: undefined }); + expect(embed.toJSON()).toStrictEqual({ timestamp: undefined }); }); }); @@ -182,7 +177,6 @@ describe('Embed', () => { test('GIVEN an embed with a pre-defined thumbnail THEN returns valid toJSON data', () => { const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, }); }); @@ -192,7 +186,6 @@ describe('Embed', () => { embed.setThumbnail('https://discord.js.org/static/logo.svg'); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, thumbnail: { url: 'https://discord.js.org/static/logo.svg' }, }); }); @@ -201,7 +194,7 @@ describe('Embed', () => { const embed = new Embed({ thumbnail: { url: 'https://discord.js.org/static/logo.svg' } }); embed.setThumbnail(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, thumbnail: undefined }); + expect(embed.toJSON()).toStrictEqual({ thumbnail: undefined }); }); test('GIVEN an embed with an invalid thumbnail THEN throws error', () => { @@ -215,7 +208,6 @@ describe('Embed', () => { test('GIVEN an embed with a pre-defined image THEN returns valid toJSON data', () => { const embed = new Embed({ image: { url: 'https://discord.js.org/static/logo.svg' } }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, image: { url: 'https://discord.js.org/static/logo.svg' }, }); }); @@ -225,7 +217,6 @@ describe('Embed', () => { embed.setImage('https://discord.js.org/static/logo.svg'); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, image: { url: 'https://discord.js.org/static/logo.svg' }, }); }); @@ -234,7 +225,7 @@ describe('Embed', () => { const embed = new Embed({ image: { url: 'https://discord.js/org/static/logo.svg' } }); embed.setImage(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, image: undefined }); + expect(embed.toJSON()).toStrictEqual({ image: undefined }); }); test('GIVEN an embed with an invalid image THEN throws error', () => { @@ -250,7 +241,6 @@ describe('Embed', () => { author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); }); @@ -264,7 +254,6 @@ describe('Embed', () => { }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, author: { name: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg', url: 'https://discord.js.org' }, }); }); @@ -275,7 +264,7 @@ describe('Embed', () => { }); embed.setAuthor(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, author: undefined }); + expect(embed.toJSON()).toStrictEqual({ author: undefined }); }); test('GIVEN an embed with an invalid author name THEN throws error', () => { @@ -291,7 +280,6 @@ describe('Embed', () => { footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); }); @@ -301,7 +289,6 @@ describe('Embed', () => { embed.setFooter({ text: 'Wumpus', iconURL: 'https://discord.js.org/static/logo.svg' }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' }, }); }); @@ -310,7 +297,7 @@ describe('Embed', () => { const embed = new Embed({ footer: { text: 'Wumpus', icon_url: 'https://discord.js.org/static/logo.svg' } }); embed.setFooter(null); - expect(embed.toJSON()).toStrictEqual({ ...emptyEmbed, footer: undefined }); + expect(embed.toJSON()).toStrictEqual({ footer: undefined }); }); test('GIVEN an embed with invalid footer text THEN throws error', () => { @@ -326,7 +313,6 @@ describe('Embed', () => { fields: [{ name: 'foo', value: 'bar', inline: undefined }], }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, fields: [{ name: 'foo', value: 'bar', inline: undefined }], }); }); @@ -336,7 +322,6 @@ describe('Embed', () => { embed.addField({ name: 'foo', value: 'bar' }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, fields: [{ name: 'foo', value: 'bar', inline: undefined }], }); }); @@ -346,7 +331,6 @@ describe('Embed', () => { embed.addFields({ name: 'foo', value: 'bar' }); expect(embed.toJSON()).toStrictEqual({ - ...emptyEmbed, fields: [{ name: 'foo', value: 'bar', inline: undefined }], }); }); @@ -356,7 +340,6 @@ describe('Embed', () => { embed.addFields({ name: 'foo', value: 'bar' }, { name: 'foo', value: 'baz' }); expect(embed.spliceFields(0, 1).toJSON()).toStrictEqual({ - ...emptyEmbed, fields: [{ name: 'foo', value: 'baz', inline: undefined }], }); }); diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index 4428b71d265e..d68031f2faad 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -18,7 +18,7 @@ export class ActionRow extend public constructor({ components, ...data }: Partial = {}) { super({ type: ComponentType.ActionRow, ...data }); - this.components = (components?.map(createComponent) ?? []) as T[]; + this.components = (components?.map((c) => createComponent(c)) ?? []) as T[]; } /** diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index 45886b4ac705..beca493467b8 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -11,7 +11,7 @@ export abstract class Component< /** * The API data associated with this component */ - protected data: DataType; + protected readonly data: DataType; /** * Converts this component to an API-compatible JSON object diff --git a/packages/builders/src/messages/embed/Assertions.ts b/packages/builders/src/messages/embed/Assertions.ts index 0cd7a706d961..8dade68f9fa0 100644 --- a/packages/builders/src/messages/embed/Assertions.ts +++ b/packages/builders/src/messages/embed/Assertions.ts @@ -17,7 +17,7 @@ export const embedFieldsArrayPredicate = embedFieldPredicate.array(); export const fieldLengthPredicate = z.number().lte(25); -export function validateFieldLength(fields: APIEmbedField[] | undefined, amountAdding: number): void { +export function validateFieldLength(amountAdding: number, fields?: APIEmbedField[]): void { fieldLengthPredicate.parse((fields?.length ?? 0) + amountAdding); } diff --git a/packages/builders/src/messages/embed/Embed.ts b/packages/builders/src/messages/embed/Embed.ts index 368a61e485dd..c167988d2723 100644 --- a/packages/builders/src/messages/embed/Embed.ts +++ b/packages/builders/src/messages/embed/Embed.ts @@ -21,7 +21,7 @@ import { EmbedAuthorOptions, EmbedFooterOptions, UnsafeEmbed } from './UnsafeEmb export class Embed extends UnsafeEmbed { public override addFields(...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(this.fields, fields.length); + validateFieldLength(fields.length, this.fields); // Data assertions return super.addFields(...embedFieldsArrayPredicate.parse(fields)); @@ -29,7 +29,7 @@ export class Embed extends UnsafeEmbed { public override spliceFields(index: number, deleteCount: number, ...fields: APIEmbedField[]): this { // Ensure adding these fields won't exceed the 25 field limit - validateFieldLength(this.fields, fields.length - deleteCount); + validateFieldLength(fields.length - deleteCount, this.fields); // Data assertions return super.spliceFields(index, deleteCount, ...embedFieldsArrayPredicate.parse(fields)); From 4be95ae269efc976add463a0169615743a2f0aad Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sat, 12 Feb 2022 22:31:50 -0500 Subject: [PATCH 16/18] fix: remove cast --- packages/builders/src/components/ActionRow.ts | 5 ++--- packages/builders/src/components/Component.ts | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/builders/src/components/ActionRow.ts b/packages/builders/src/components/ActionRow.ts index d68031f2faad..2789bae26144 100644 --- a/packages/builders/src/components/ActionRow.ts +++ b/packages/builders/src/components/ActionRow.ts @@ -12,7 +12,7 @@ export type ActionRowComponent = ButtonComponent | SelectMenuComponent; * Represents an action row component */ export class ActionRow extends Component< - Omit, 'components'> + Omit & { type: ComponentType.ActionRow }, 'components'> > { public readonly components: T[]; @@ -41,10 +41,9 @@ export class ActionRow extend } public toJSON(): APIActionRowComponent { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return { ...this.data, components: this.components.map((component) => component.toJSON()), - } as APIActionRowComponent; + }; } } diff --git a/packages/builders/src/components/Component.ts b/packages/builders/src/components/Component.ts index beca493467b8..ed5da17fc0f2 100644 --- a/packages/builders/src/components/Component.ts +++ b/packages/builders/src/components/Component.ts @@ -5,7 +5,9 @@ import type { APIBaseMessageComponent, APIMessageComponent, ComponentType } from * Represents a discord component */ export abstract class Component< - DataType extends Partial> = APIBaseMessageComponent, + DataType extends Partial> & { + type: ComponentType; + } = APIBaseMessageComponent, > implements JSONEncodable { /** From 82e8a9f0f523538f58592c9fdfb3a4d2753eb1fc Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sat, 12 Feb 2022 22:36:34 -0500 Subject: [PATCH 17/18] fix: compiler errors --- packages/builders/src/components/button/UnsafeButton.ts | 2 +- .../builders/src/components/selectMenu/UnsafeSelectMenu.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/builders/src/components/button/UnsafeButton.ts b/packages/builders/src/components/button/UnsafeButton.ts index 83f831ea5c2e..149ed87d1cd5 100644 --- a/packages/builders/src/components/button/UnsafeButton.ts +++ b/packages/builders/src/components/button/UnsafeButton.ts @@ -11,7 +11,7 @@ import { Component } from '../Component'; /** * Represents a non-validated button component */ -export class UnsafeButtonComponent extends Component> { +export class UnsafeButtonComponent extends Component & { type: ComponentType.Button }> { public constructor(data?: Partial) { super({ type: ComponentType.Button, ...data }); } diff --git a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts index 3c304aa6c7da..ee0ab23d67c5 100644 --- a/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts +++ b/packages/builders/src/components/selectMenu/UnsafeSelectMenu.ts @@ -5,7 +5,9 @@ import { UnsafeSelectMenuOption } from './UnsafeSelectMenuOption'; /** * Represents a non-validated select menu component */ -export class UnsafeSelectMenuComponent extends Component>> { +export class UnsafeSelectMenuComponent extends Component< + Partial> & { type: ComponentType.SelectMenu } +> { public readonly options: UnsafeSelectMenuOption[]; public constructor(data?: Partial) { From 7fe4a0b220b6272165afaa86fe04bc90684a0a4f Mon Sep 17 00:00:00 2001 From: suneettipirneni Date: Sat, 12 Feb 2022 22:41:20 -0500 Subject: [PATCH 18/18] fix: tests --- packages/builders/__tests__/components/actionRow.test.ts | 6 +++--- packages/builders/__tests__/components/selectMenu.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/builders/__tests__/components/actionRow.test.ts b/packages/builders/__tests__/components/actionRow.test.ts index d55d501f6ab8..235259201930 100644 --- a/packages/builders/__tests__/components/actionRow.test.ts +++ b/packages/builders/__tests__/components/actionRow.test.ts @@ -5,7 +5,7 @@ describe('Action Row Components', () => { describe('Assertion Tests', () => { test('GIVEN valid components THEN do not throw', () => { expect(() => new ActionRow().addComponents(new ButtonComponent())).not.toThrowError(); - expect(() => new ActionRow().setComponents(new ButtonComponent())).not.toThrowError(); + expect(() => new ActionRow().setComponents([new ButtonComponent()])).not.toThrowError(); }); test('GIVEN valid JSON input THEN valid JSON output is given', () => { @@ -84,10 +84,10 @@ describe('Action Row Components', () => { .setCustomId('1234') .setMaxValues(10) .setMinValues(12) - .setOptions( + .setOptions([ new SelectMenuOption().setLabel('one').setValue('one'), new SelectMenuOption().setLabel('two').setValue('two'), - ); + ]); expect(new ActionRow().addComponents(button).toJSON()).toEqual(rowWithButtonData); expect(new ActionRow().addComponents(selectMenu).toJSON()).toEqual(rowWithSelectMenuData); diff --git a/packages/builders/__tests__/components/selectMenu.test.ts b/packages/builders/__tests__/components/selectMenu.test.ts index 8cc7babaef16..6b4692df591c 100644 --- a/packages/builders/__tests__/components/selectMenu.test.ts +++ b/packages/builders/__tests__/components/selectMenu.test.ts @@ -23,7 +23,7 @@ describe('Button Components', () => { .setEmoji({ name: 'test' }) .setDescription('description'); expect(() => selectMenu().addOptions(option)).not.toThrowError(); - expect(() => selectMenu().setOptions(option)).not.toThrowError(); + expect(() => selectMenu().setOptions([option])).not.toThrowError(); }); test('GIVEN invalid inputs THEN Select Menu does throw', () => {