diff --git a/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts b/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts index 0affe76c8b8a..8b9d09b07965 100644 --- a/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts +++ b/packages/builders/__tests__/interactions/ChatInputCommands/ChatInputCommands.test.ts @@ -376,6 +376,17 @@ describe('ChatInput Commands', () => { }); }); + describe('Subcommand builder and subcommand group builder', () => { + test('GIVEN both types THEN does not throw error', () => { + expect(() => + getNamedBuilder() + .addSubcommands(getSubcommand()) + .addSubcommandGroups(getSubcommandGroup().addSubcommands(getSubcommand())) + .toJSON(), + ).not.toThrowError(); + }); + }); + describe('ChatInput command localizations', () => { const expectedSingleLocale = { [Locale.EnglishUS]: 'foobar' }; const expectedMultipleLocales = { diff --git a/packages/builders/src/interactions/commands/chatInput/Assertions.ts b/packages/builders/src/interactions/commands/chatInput/Assertions.ts index 8d16afd8ac44..d09b15531a6d 100644 --- a/packages/builders/src/interactions/commands/chatInput/Assertions.ts +++ b/packages/builders/src/interactions/commands/chatInput/Assertions.ts @@ -2,6 +2,7 @@ import { ApplicationIntegrationType, InteractionContextType, ApplicationCommandOptionType, + ApplicationCommandType, } from 'discord-api-types/v10'; import { z } from 'zod'; import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js'; @@ -47,38 +48,55 @@ const choiceBasePredicate = z.object({ name: choiceValueStringPredicate, name_localizations: localeMapPredicate.optional(), }); -const choiceStringPredicate = choiceBasePredicate.extend({ +const choiceStringPredicate = z.object({ + ...choiceBasePredicate.shape, value: choiceValueStringPredicate, }); -const choiceNumberPredicate = choiceBasePredicate.extend({ +const choiceNumberPredicate = z.object({ + ...choiceBasePredicate.shape, value: choiceValueNumberPredicate, }); const choiceBaseMixinPredicate = z.object({ autocomplete: z.literal(false).optional(), }); -const choiceStringMixinPredicate = choiceBaseMixinPredicate.extend({ +const choiceStringMixinPredicate = z.object({ + ...choiceBaseMixinPredicate.shape, choices: choiceStringPredicate.array().max(25).optional(), }); -const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({ +const choiceNumberMixinPredicate = z.object({ + ...choiceBaseMixinPredicate.shape, choices: choiceNumberPredicate.array().max(25).optional(), }); -const basicOptionTypesPredicate = z.literal([ - ApplicationCommandOptionType.Attachment, - ApplicationCommandOptionType.Boolean, - ApplicationCommandOptionType.Channel, - ApplicationCommandOptionType.Integer, - ApplicationCommandOptionType.Mentionable, - ApplicationCommandOptionType.Number, - ApplicationCommandOptionType.Role, - ApplicationCommandOptionType.String, - ApplicationCommandOptionType.User, -]); - -export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({ +export const baseBasicOptionPredicate = z.object({ + ...sharedNameAndDescriptionPredicate.shape, required: z.boolean().optional(), - type: basicOptionTypesPredicate, +}); + +export const attachmentOptionPredicate = z.object({ + ...baseBasicOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Attachment), +}); + +export const booleanOptionPredicate = z.object({ + ...baseBasicOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Boolean), +}); + +export const mentionableOptionPredicate = z.object({ + ...baseBasicOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Mentionable), +}); + +export const roleOptionPredicate = z.object({ + ...baseBasicOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Role), +}); + +export const userOptionPredicate = z.object({ + ...baseBasicOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.User), }); const autocompleteOrStringChoicesMixinOptionPredicate = z.discriminatedUnion('autocomplete', [ @@ -92,58 +110,71 @@ const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('au ]); export const channelOptionPredicate = z.object({ - ...basicOptionPredicate.shape, + ...baseBasicOptionPredicate.shape, ...channelMixinOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Channel), }); export const integerOptionPredicate = z .object({ - ...basicOptionPredicate.shape, + ...baseBasicOptionPredicate.shape, ...numericMixinIntegerOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Integer), }) .and(autocompleteOrNumberChoicesMixinOptionPredicate); export const numberOptionPredicate = z .object({ - ...basicOptionPredicate.shape, + ...baseBasicOptionPredicate.shape, ...numericMixinNumberOptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Number), }) .and(autocompleteOrNumberChoicesMixinOptionPredicate); -export const stringOptionPredicate = basicOptionPredicate - .extend({ +export const stringOptionPredicate = z + .object({ + ...baseBasicOptionPredicate.shape, max_length: z.number().min(0).max(6_000).optional(), min_length: z.number().min(1).max(6_000).optional(), + type: z.literal(ApplicationCommandOptionType.String), }) .and(autocompleteOrStringChoicesMixinOptionPredicate); -const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({ - contexts: z.array(z.enum(InteractionContextType)).optional(), - default_member_permissions: memberPermissionsPredicate.optional(), - integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(), - nsfw: z.boolean().optional(), -}); - -// Because you can only add options via builders, there's no need to validate whole objects here otherwise -const chatInputCommandOptionsPredicate = z.union([ - z.object({ type: basicOptionTypesPredicate }).array(), - z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) }).array(), - z.object({ type: z.literal(ApplicationCommandOptionType.SubcommandGroup) }).array(), -]); - -export const chatInputCommandPredicate = baseChatInputCommandPredicate.extend({ - options: chatInputCommandOptionsPredicate.optional(), +const basicOptionPredicates = [ + attachmentOptionPredicate, + booleanOptionPredicate, + channelOptionPredicate, + integerOptionPredicate, + mentionableOptionPredicate, + numberOptionPredicate, + roleOptionPredicate, + stringOptionPredicate, + userOptionPredicate, +]; + +export const chatInputCommandSubcommandPredicate = z.object({ + ...sharedNameAndDescriptionPredicate.shape, + type: z.literal(ApplicationCommandOptionType.Subcommand), + options: z.array(z.union(basicOptionPredicates)).max(25), }); -export const chatInputCommandSubcommandGroupPredicate = sharedNameAndDescriptionPredicate.extend({ +export const chatInputCommandSubcommandGroupPredicate = z.object({ + ...sharedNameAndDescriptionPredicate.shape, type: z.literal(ApplicationCommandOptionType.SubcommandGroup), - options: z - .array(z.object({ type: z.literal(ApplicationCommandOptionType.Subcommand) })) - .min(1) - .max(25), + options: z.array(chatInputCommandSubcommandPredicate).min(1).max(25), }); -export const chatInputCommandSubcommandPredicate = sharedNameAndDescriptionPredicate.extend({ - type: z.literal(ApplicationCommandOptionType.Subcommand), - options: z.array(z.object({ type: basicOptionTypesPredicate })).max(25), +export const chatInputCommandPredicate = z.strictObject({ + ...sharedNameAndDescriptionPredicate.shape, + contexts: z.array(z.enum(InteractionContextType)).optional(), + default_member_permissions: memberPermissionsPredicate.optional(), + integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(), + nsfw: z.boolean().optional(), + options: z + .union([ + z.array(z.union(basicOptionPredicates)).max(25), + z.array(z.union([chatInputCommandSubcommandPredicate, chatInputCommandSubcommandGroupPredicate])).max(25), + ]) + .optional(), + type: z.literal(ApplicationCommandType.ChatInput).optional(), }); diff --git a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts index 4d90c2cceff9..e5f59125c553 100644 --- a/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts +++ b/packages/builders/src/interactions/commands/chatInput/ChatInputCommand.ts @@ -30,7 +30,7 @@ export class ChatInputCommandBuilder extends Mixin( const data: RESTPostAPIChatInputApplicationCommandsJSONBody = { ...structuredClone(rest as Omit), type: ApplicationCommandType.ChatInput, - options: options?.map((option) => option.toJSON(validationOverride)), + options: options?.map((option) => option.toJSON(false)), }; validate(chatInputCommandPredicate, data, validationOverride); diff --git a/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts b/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts index 9416cb1cff30..76ad1b41b557 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/ApplicationCommandOptionBase.ts @@ -8,7 +8,6 @@ import type { z } from 'zod'; import { validate } from '../../../../util/validation.js'; import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js'; import { SharedNameAndDescription } from '../../SharedNameAndDescription.js'; -import { basicOptionPredicate } from '../Assertions.js'; export interface ApplicationCommandOptionBaseData extends Partial> { type: ApplicationCommandOptionType; @@ -24,7 +23,7 @@ export abstract class ApplicationCommandOptionBase /** * @internal */ - protected static readonly predicate: z.ZodType = basicOptionPredicate; + protected static readonly predicate: z.ZodType; /** * @internal diff --git a/packages/builders/src/interactions/commands/chatInput/options/attachment.ts b/packages/builders/src/interactions/commands/chatInput/options/attachment.ts index 2cf2be045aa8..097c67f29db2 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/attachment.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/attachment.ts @@ -1,10 +1,16 @@ import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { attachmentOptionPredicate } from '../Assertions.js'; import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; /** * A chat input command attachment option. */ export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBase { + /** + * @internal + */ + protected static override readonly predicate = attachmentOptionPredicate; + /** * Creates a new attachment option. */ diff --git a/packages/builders/src/interactions/commands/chatInput/options/boolean.ts b/packages/builders/src/interactions/commands/chatInput/options/boolean.ts index 27d4ae5402a4..a08273bd4691 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/boolean.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/boolean.ts @@ -1,10 +1,16 @@ import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { booleanOptionPredicate } from '../Assertions.js'; import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; /** * A chat input command boolean option. */ export class ChatInputCommandBooleanOption extends ApplicationCommandOptionBase { + /** + * @internal + */ + protected static override readonly predicate = booleanOptionPredicate; + /** * Creates a new boolean option. */ diff --git a/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts b/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts index e039db61f6e0..866b3ecacfbb 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/mentionable.ts @@ -1,10 +1,16 @@ import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { mentionableOptionPredicate } from '../Assertions.js'; import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; /** * A chat input command mentionable option. */ export class ChatInputCommandMentionableOption extends ApplicationCommandOptionBase { + /** + * @internal + */ + protected static override readonly predicate = mentionableOptionPredicate; + /** * Creates a new mentionable option. */ diff --git a/packages/builders/src/interactions/commands/chatInput/options/role.ts b/packages/builders/src/interactions/commands/chatInput/options/role.ts index 146ce22d07e3..4ba215cc5a6c 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/role.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/role.ts @@ -1,10 +1,16 @@ import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { roleOptionPredicate } from '../Assertions.js'; import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; /** * A chat input command role option. */ export class ChatInputCommandRoleOption extends ApplicationCommandOptionBase { + /** + * @internal + */ + protected static override readonly predicate = roleOptionPredicate; + /** * Creates a new role option. */ diff --git a/packages/builders/src/interactions/commands/chatInput/options/user.ts b/packages/builders/src/interactions/commands/chatInput/options/user.ts index 7f5a83f5ed98..8f7c4ef7c42c 100644 --- a/packages/builders/src/interactions/commands/chatInput/options/user.ts +++ b/packages/builders/src/interactions/commands/chatInput/options/user.ts @@ -1,10 +1,16 @@ import { ApplicationCommandOptionType } from 'discord-api-types/v10'; +import { userOptionPredicate } from '../Assertions.js'; import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js'; /** * A chat input command user option. */ export class ChatInputCommandUserOption extends ApplicationCommandOptionBase { + /** + * @internal + */ + protected static override readonly predicate = userOptionPredicate; + /** * Creates a new user option. */