mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-09 08:03:30 +01:00
feat!: use zod v4 (#10922)
* feat: zod 4 * feat: zod v3, but v4 feat: validation error class Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com> * chore: bump --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { enableValidators, disableValidators, isValidationEnabled, normalizeArray } from '../src/index.js';
|
||||
import { z } from 'zod/v4';
|
||||
import {
|
||||
enableValidators,
|
||||
disableValidators,
|
||||
isValidationEnabled,
|
||||
normalizeArray,
|
||||
ValidationError,
|
||||
} from '../src/index.js';
|
||||
import { validate } from '../src/util/validation.js';
|
||||
|
||||
describe('validation', () => {
|
||||
test('enables validation', () => {
|
||||
@@ -11,6 +19,17 @@ describe('validation', () => {
|
||||
disableValidators();
|
||||
expect(isValidationEnabled()).toBeFalsy();
|
||||
});
|
||||
|
||||
test('validation error', () => {
|
||||
try {
|
||||
validate(z.never(), 1, true);
|
||||
throw new Error('validation should have failed');
|
||||
} catch (error) {
|
||||
expect(error).toBeInstanceOf(ValidationError);
|
||||
expect((error as ValidationError).message).toBe('✖ Invalid input: expected never, received number');
|
||||
expect((error as ValidationError).cause).toBeInstanceOf(z.ZodError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizeArray', () => {
|
||||
|
||||
@@ -69,8 +69,7 @@
|
||||
"discord-api-types": "^0.38.14",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.8.1",
|
||||
"zod": "^3.24.2",
|
||||
"zod-validation-error": "^3.4.0"
|
||||
"zod": "^3.25.69"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@discordjs/api-extractor": "workspace:^",
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import { Locale } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
export const customIdPredicate = z.string().min(1).max(100);
|
||||
|
||||
export const memberPermissionsPredicate = z.coerce.bigint();
|
||||
|
||||
export const localeMapPredicate = z
|
||||
.object(
|
||||
Object.fromEntries(Object.values(Locale).map((loc) => [loc, z.string().optional()])) as Record<
|
||||
Locale,
|
||||
z.ZodOptional<z.ZodString>
|
||||
>,
|
||||
)
|
||||
.strict();
|
||||
|
||||
export const refineURLPredicate = (allowedProtocols: string[]) => (value: string) => {
|
||||
// eslint-disable-next-line n/prefer-global/url
|
||||
const url = new URL(value);
|
||||
return allowedProtocols.includes(url.protocol);
|
||||
};
|
||||
export const localeMapPredicate = z.strictObject(
|
||||
Object.fromEntries(Object.values(Locale).map((loc) => [loc, z.string().optional()])) as Record<
|
||||
Locale,
|
||||
z.ZodOptional<z.ZodString>
|
||||
>,
|
||||
);
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { customIdPredicate, refineURLPredicate } from '../Assertions.js';
|
||||
import { z } from 'zod/v4';
|
||||
import { customIdPredicate } from '../Assertions.js';
|
||||
|
||||
const labelPredicate = z.string().min(1).max(80);
|
||||
|
||||
export const emojiPredicate = z
|
||||
.object({
|
||||
.strictObject({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(2).max(32).optional(),
|
||||
animated: z.boolean().optional(),
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => data.id !== undefined || data.name !== undefined, {
|
||||
message: "Either 'id' or 'name' must be provided",
|
||||
error: "Either 'id' or 'name' must be provided",
|
||||
});
|
||||
|
||||
const buttonPredicateBase = z.object({
|
||||
const buttonPredicateBase = z.strictObject({
|
||||
type: z.literal(ComponentType.Button),
|
||||
disabled: z.boolean().optional(),
|
||||
});
|
||||
@@ -26,31 +25,22 @@ const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
|
||||
label: labelPredicate,
|
||||
});
|
||||
|
||||
const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) }).strict();
|
||||
const buttonSecondaryPredicate = buttonCustomIdPredicateBase
|
||||
.extend({ style: z.literal(ButtonStyle.Secondary) })
|
||||
.strict();
|
||||
const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) }).strict();
|
||||
const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) }).strict();
|
||||
const buttonPrimaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Primary) });
|
||||
const buttonSecondaryPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Secondary) });
|
||||
const buttonSuccessPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Success) });
|
||||
const buttonDangerPredicate = buttonCustomIdPredicateBase.extend({ style: z.literal(ButtonStyle.Danger) });
|
||||
|
||||
const buttonLinkPredicate = buttonPredicateBase
|
||||
.extend({
|
||||
style: z.literal(ButtonStyle.Link),
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['http:', 'https:', 'discord:'])),
|
||||
emoji: emojiPredicate.optional(),
|
||||
label: labelPredicate,
|
||||
})
|
||||
.strict();
|
||||
const buttonLinkPredicate = buttonPredicateBase.extend({
|
||||
style: z.literal(ButtonStyle.Link),
|
||||
url: z.url({ protocol: /^(?:https?|discord)$/ }),
|
||||
emoji: emojiPredicate.optional(),
|
||||
label: labelPredicate,
|
||||
});
|
||||
|
||||
const buttonPremiumPredicate = buttonPredicateBase
|
||||
.extend({
|
||||
style: z.literal(ButtonStyle.Premium),
|
||||
sku_id: z.string(),
|
||||
})
|
||||
.strict();
|
||||
const buttonPremiumPredicate = buttonPredicateBase.extend({
|
||||
style: z.literal(ButtonStyle.Premium),
|
||||
sku_id: z.string(),
|
||||
});
|
||||
|
||||
export const buttonPredicate = z.discriminatedUnion('style', [
|
||||
buttonLinkPredicate,
|
||||
@@ -71,7 +61,7 @@ const selectMenuBasePredicate = z.object({
|
||||
|
||||
export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
|
||||
type: z.literal(ComponentType.ChannelSelect),
|
||||
channel_types: z.nativeEnum(ChannelType).array().optional(),
|
||||
channel_types: z.enum(ChannelType).array().optional(),
|
||||
default_values: z
|
||||
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
|
||||
.array()
|
||||
@@ -84,7 +74,7 @@ export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
|
||||
default_values: z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.union([z.literal(SelectMenuDefaultValueType.Role), z.literal(SelectMenuDefaultValueType.User)]),
|
||||
type: z.literal([SelectMenuDefaultValueType.Role, SelectMenuDefaultValueType.User]),
|
||||
})
|
||||
.array()
|
||||
.max(25)
|
||||
@@ -113,23 +103,25 @@ export const selectMenuStringPredicate = selectMenuBasePredicate
|
||||
type: z.literal(ComponentType.StringSelect),
|
||||
options: selectMenuStringOptionPredicate.array().min(1).max(25),
|
||||
})
|
||||
.superRefine((menu, ctx) => {
|
||||
.check((ctx) => {
|
||||
const addIssue = (name: string, minimum: number) =>
|
||||
ctx.addIssue({
|
||||
ctx.issues.push({
|
||||
code: 'too_small',
|
||||
message: `The number of options must be greater than or equal to ${name}`,
|
||||
inclusive: true,
|
||||
minimum,
|
||||
type: 'number',
|
||||
path: ['options'],
|
||||
origin: 'number',
|
||||
input: minimum,
|
||||
});
|
||||
|
||||
if (menu.max_values !== undefined && menu.options.length < menu.max_values) {
|
||||
addIssue('max_values', menu.max_values);
|
||||
if (ctx.value.max_values !== undefined && ctx.value.options.length < ctx.value.max_values) {
|
||||
addIssue('max_values', ctx.value.max_values);
|
||||
}
|
||||
|
||||
if (menu.min_values !== undefined && menu.options.length < menu.min_values) {
|
||||
addIssue('min_values', menu.min_values);
|
||||
if (ctx.value.min_values !== undefined && ctx.value.options.length < ctx.value.min_values) {
|
||||
addIssue('min_values', ctx.value.min_values);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,14 +144,13 @@ export const actionRowPredicate = z.object({
|
||||
.max(5),
|
||||
z
|
||||
.object({
|
||||
type: z.union([
|
||||
z.literal(ComponentType.ChannelSelect),
|
||||
z.literal(ComponentType.MentionableSelect),
|
||||
z.literal(ComponentType.RoleSelect),
|
||||
z.literal(ComponentType.StringSelect),
|
||||
z.literal(ComponentType.UserSelect),
|
||||
// And this!
|
||||
z.literal(ComponentType.TextInput),
|
||||
type: z.literal([
|
||||
ComponentType.ChannelSelect,
|
||||
ComponentType.MentionableSelect,
|
||||
ComponentType.StringSelect,
|
||||
ComponentType.RoleSelect,
|
||||
ComponentType.TextInput,
|
||||
ComponentType.UserSelect,
|
||||
]),
|
||||
})
|
||||
.array()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { customIdPredicate } from '../../Assertions.js';
|
||||
|
||||
export const textInputPredicate = z.object({
|
||||
type: z.literal(ComponentType.TextInput),
|
||||
custom_id: customIdPredicate,
|
||||
label: z.string().min(1).max(45),
|
||||
style: z.nativeEnum(TextInputStyle),
|
||||
style: z.enum(TextInputStyle),
|
||||
min_length: z.number().min(0).max(4_000).optional(),
|
||||
max_length: z.number().min(1).max(4_000).optional(),
|
||||
placeholder: z.string().max(100).optional(),
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import { ComponentType, SeparatorSpacingSize } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { refineURLPredicate } from '../../Assertions.js';
|
||||
import { z } from 'zod/v4';
|
||||
import { actionRowPredicate } from '../Assertions.js';
|
||||
|
||||
const unfurledMediaItemPredicate = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['http:', 'https:', 'attachment:']), {
|
||||
message: 'Invalid protocol for media URL. Must be http:, https:, or attachment:',
|
||||
}),
|
||||
url: z.url({ protocol: /^(?:https?|attachment)$/ }),
|
||||
});
|
||||
|
||||
export const thumbnailPredicate = z.object({
|
||||
@@ -19,12 +13,7 @@ export const thumbnailPredicate = z.object({
|
||||
});
|
||||
|
||||
const unfurledMediaItemAttachmentOnlyPredicate = z.object({
|
||||
url: z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['attachment:']), {
|
||||
message: 'Invalid protocol for file URL. Must be attachment:',
|
||||
}),
|
||||
url: z.url({ protocol: /^attachment$/ }),
|
||||
});
|
||||
|
||||
export const filePredicate = z.object({
|
||||
@@ -34,7 +23,7 @@ export const filePredicate = z.object({
|
||||
|
||||
export const separatorPredicate = z.object({
|
||||
divider: z.boolean().optional(),
|
||||
spacing: z.nativeEnum(SeparatorSpacingSize).optional(),
|
||||
spacing: z.enum(SeparatorSpacingSize).optional(),
|
||||
});
|
||||
|
||||
export const textDisplayPredicate = z.object({
|
||||
@@ -73,5 +62,5 @@ export const containerPredicate = z.object({
|
||||
)
|
||||
.min(1),
|
||||
spoiler: z.boolean().optional(),
|
||||
accent_color: z.number().int().min(0).max(0xffffff).nullish(),
|
||||
accent_color: z.int().min(0).max(0xffffff).nullish(),
|
||||
});
|
||||
|
||||
@@ -87,6 +87,7 @@ export * from './util/componentUtil.js';
|
||||
export * from './util/normalizeArray.js';
|
||||
export * from './util/resolveBuilder.js';
|
||||
export { disableValidators, enableValidators, isValidationEnabled } from './util/validation.js';
|
||||
export * from './util/ValidationError.js';
|
||||
|
||||
export * from './Assertions.js';
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
InteractionContextType,
|
||||
ApplicationCommandOptionType,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { ZodTypeAny } from 'zod';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
|
||||
import { ApplicationCommandOptionAllowedChannelTypes } from './mixins/ApplicationCommandOptionChannelTypesMixin.js';
|
||||
|
||||
@@ -24,26 +23,17 @@ const sharedNameAndDescriptionPredicate = z.object({
|
||||
});
|
||||
|
||||
const numericMixinNumberOptionPredicate = z.object({
|
||||
max_value: z.number().safe().optional(),
|
||||
min_value: z.number().safe().optional(),
|
||||
max_value: z.float32().optional(),
|
||||
min_value: z.float32().optional(),
|
||||
});
|
||||
|
||||
const numericMixinIntegerOptionPredicate = z.object({
|
||||
max_value: z.number().safe().int().optional(),
|
||||
min_value: z.number().safe().int().optional(),
|
||||
max_value: z.int().optional(),
|
||||
min_value: z.int().optional(),
|
||||
});
|
||||
|
||||
const channelMixinOptionPredicate = z.object({
|
||||
channel_types: z
|
||||
.union(
|
||||
ApplicationCommandOptionAllowedChannelTypes.map((type) => z.literal(type)) as unknown as [
|
||||
ZodTypeAny,
|
||||
ZodTypeAny,
|
||||
...ZodTypeAny[],
|
||||
],
|
||||
)
|
||||
.array()
|
||||
.optional(),
|
||||
channel_types: z.literal(ApplicationCommandOptionAllowedChannelTypes).array().optional(),
|
||||
});
|
||||
|
||||
const autocompleteMixinOptionPredicate = z.object({
|
||||
@@ -52,7 +42,7 @@ const autocompleteMixinOptionPredicate = z.object({
|
||||
});
|
||||
|
||||
const choiceValueStringPredicate = z.string().min(1).max(100);
|
||||
const choiceValueNumberPredicate = z.number().safe();
|
||||
const choiceValueNumberPredicate = z.number();
|
||||
const choiceBasePredicate = z.object({
|
||||
name: choiceValueStringPredicate,
|
||||
name_localizations: localeMapPredicate.optional(),
|
||||
@@ -74,7 +64,7 @@ const choiceNumberMixinPredicate = choiceBaseMixinPredicate.extend({
|
||||
choices: choiceNumberPredicate.array().max(25).optional(),
|
||||
});
|
||||
|
||||
const basicOptionTypes = [
|
||||
const basicOptionTypesPredicate = z.literal([
|
||||
ApplicationCommandOptionType.Attachment,
|
||||
ApplicationCommandOptionType.Boolean,
|
||||
ApplicationCommandOptionType.Channel,
|
||||
@@ -84,11 +74,7 @@ const basicOptionTypes = [
|
||||
ApplicationCommandOptionType.Role,
|
||||
ApplicationCommandOptionType.String,
|
||||
ApplicationCommandOptionType.User,
|
||||
] as const;
|
||||
|
||||
const basicOptionTypesPredicate = z.union(
|
||||
basicOptionTypes.map((type) => z.literal(type)) as unknown as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]],
|
||||
);
|
||||
]);
|
||||
|
||||
export const basicOptionPredicate = sharedNameAndDescriptionPredicate.extend({
|
||||
required: z.boolean().optional(),
|
||||
@@ -105,14 +91,23 @@ const autocompleteOrNumberChoicesMixinOptionPredicate = z.discriminatedUnion('au
|
||||
choiceNumberMixinPredicate,
|
||||
]);
|
||||
|
||||
export const channelOptionPredicate = basicOptionPredicate.merge(channelMixinOptionPredicate);
|
||||
export const channelOptionPredicate = z.object({
|
||||
...basicOptionPredicate.shape,
|
||||
...channelMixinOptionPredicate.shape,
|
||||
});
|
||||
|
||||
export const integerOptionPredicate = basicOptionPredicate
|
||||
.merge(numericMixinIntegerOptionPredicate)
|
||||
export const integerOptionPredicate = z
|
||||
.object({
|
||||
...basicOptionPredicate.shape,
|
||||
...numericMixinIntegerOptionPredicate.shape,
|
||||
})
|
||||
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
||||
|
||||
export const numberOptionPredicate = basicOptionPredicate
|
||||
.merge(numericMixinNumberOptionPredicate)
|
||||
export const numberOptionPredicate = z
|
||||
.object({
|
||||
...basicOptionPredicate.shape,
|
||||
...numericMixinNumberOptionPredicate.shape,
|
||||
})
|
||||
.and(autocompleteOrNumberChoicesMixinOptionPredicate);
|
||||
|
||||
export const stringOptionPredicate = basicOptionPredicate
|
||||
@@ -123,9 +118,9 @@ export const stringOptionPredicate = basicOptionPredicate
|
||||
.and(autocompleteOrStringChoicesMixinOptionPredicate);
|
||||
|
||||
const baseChatInputCommandPredicate = sharedNameAndDescriptionPredicate.extend({
|
||||
contexts: z.array(z.nativeEnum(InteractionContextType)).optional(),
|
||||
contexts: z.array(z.enum(InteractionContextType)).optional(),
|
||||
default_member_permissions: memberPermissionsPredicate.optional(),
|
||||
integration_types: z.array(z.nativeEnum(ApplicationIntegrationType)).optional(),
|
||||
integration_types: z.array(z.enum(ApplicationIntegrationType)).optional(),
|
||||
nsfw: z.boolean().optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ApplicationCommandOptionAllowedChannelTypes = [
|
||||
/**
|
||||
* Allowed channel types used for a channel option.
|
||||
*/
|
||||
export type ApplicationCommandOptionAllowedChannelTypes = (typeof ApplicationCommandOptionAllowedChannelTypes)[number];
|
||||
export type ApplicationCommandOptionAllowedChannelType = (typeof ApplicationCommandOptionAllowedChannelTypes)[number];
|
||||
|
||||
export interface ApplicationCommandOptionChannelTypesData
|
||||
extends Pick<APIApplicationCommandChannelOption, 'channel_types'> {}
|
||||
@@ -36,7 +36,7 @@ export class ApplicationCommandOptionChannelTypesMixin {
|
||||
*
|
||||
* @param channelTypes - The channel types
|
||||
*/
|
||||
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
|
||||
public addChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelType>) {
|
||||
this.data.channel_types ??= [];
|
||||
this.data.channel_types.push(...normalizeArray(channelTypes));
|
||||
|
||||
@@ -48,7 +48,7 @@ export class ApplicationCommandOptionChannelTypesMixin {
|
||||
*
|
||||
* @param channelTypes - The channel types
|
||||
*/
|
||||
public setChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelTypes>) {
|
||||
public setChannelTypes(...channelTypes: RestOrArray<ApplicationCommandOptionAllowedChannelType>) {
|
||||
this.data.channel_types = normalizeArray(channelTypes);
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
APIApplicationCommandOption,
|
||||
ApplicationCommandOptionType,
|
||||
} from 'discord-api-types/v10';
|
||||
import type { z } from 'zod';
|
||||
import type { z } from 'zod/v4';
|
||||
import { validate } from '../../../../util/validation.js';
|
||||
import type { SharedNameAndDescriptionData } from '../../SharedNameAndDescription.js';
|
||||
import { SharedNameAndDescription } from '../../SharedNameAndDescription.js';
|
||||
@@ -24,7 +24,7 @@ export abstract class ApplicationCommandOptionBase
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
protected static readonly predicate: z.ZodTypeAny = basicOptionPredicate;
|
||||
protected static readonly predicate: z.ZodType = basicOptionPredicate;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApplicationCommandType, ApplicationIntegrationType, InteractionContextType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
|
||||
|
||||
const namePredicate = z
|
||||
@@ -8,8 +8,8 @@ const namePredicate = z
|
||||
.max(32)
|
||||
.regex(/^(?:(?: *[\p{P}\p{L}\p{N}\p{sc=Devanagari}\p{sc=Thai}\p{Extended_Pictographic}\p{Emoji_Component}]) *)+$/u);
|
||||
|
||||
const contextsPredicate = z.array(z.nativeEnum(InteractionContextType));
|
||||
const integrationTypesPredicate = z.array(z.nativeEnum(ApplicationIntegrationType));
|
||||
const contextsPredicate = z.array(z.enum(InteractionContextType));
|
||||
const integrationTypesPredicate = z.array(z.enum(ApplicationIntegrationType));
|
||||
|
||||
const baseContextMenuCommandPredicate = z.object({
|
||||
contexts: contextsPredicate.optional(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ComponentType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { customIdPredicate } from '../../Assertions.js';
|
||||
|
||||
const titlePredicate = z.string().min(1).max(45);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AllowedMentionsTypes, ComponentType, MessageFlags, MessageReferenceType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { embedPredicate } from './embed/Assertions.js';
|
||||
import { pollPredicate } from './poll/Assertions.js';
|
||||
|
||||
@@ -17,7 +17,7 @@ export const attachmentPredicate = z.object({
|
||||
|
||||
export const allowedMentionPredicate = z
|
||||
.object({
|
||||
parse: z.nativeEnum(AllowedMentionsTypes).array().optional(),
|
||||
parse: z.enum(AllowedMentionsTypes).array().optional(),
|
||||
roles: z.string().array().max(100).optional(),
|
||||
users: z.string().array().max(100).optional(),
|
||||
replied_user: z.boolean().optional(),
|
||||
@@ -29,7 +29,7 @@ export const allowedMentionPredicate = z
|
||||
(data.parse?.includes(AllowedMentionsTypes.Role) && data.roles?.length)
|
||||
),
|
||||
{
|
||||
message:
|
||||
error:
|
||||
'Cannot specify both parse: ["users"] and non-empty users array, or parse: ["roles"] and non-empty roles array. These are mutually exclusive',
|
||||
},
|
||||
);
|
||||
@@ -39,7 +39,7 @@ export const messageReferencePredicate = z.object({
|
||||
fail_if_not_exists: z.boolean().optional(),
|
||||
guild_id: z.string().optional(),
|
||||
message_id: z.string(),
|
||||
type: z.nativeEnum(MessageReferenceType).optional(),
|
||||
type: z.enum(MessageReferenceType).optional(),
|
||||
});
|
||||
|
||||
const baseMessagePredicate = z.object({
|
||||
@@ -55,13 +55,13 @@ const basicActionRowPredicate = z.object({
|
||||
type: z.literal(ComponentType.ActionRow),
|
||||
components: z
|
||||
.object({
|
||||
type: z.union([
|
||||
z.literal(ComponentType.Button),
|
||||
z.literal(ComponentType.ChannelSelect),
|
||||
z.literal(ComponentType.MentionableSelect),
|
||||
z.literal(ComponentType.RoleSelect),
|
||||
z.literal(ComponentType.StringSelect),
|
||||
z.literal(ComponentType.UserSelect),
|
||||
type: z.literal([
|
||||
ComponentType.Button,
|
||||
ComponentType.ChannelSelect,
|
||||
ComponentType.MentionableSelect,
|
||||
ComponentType.RoleSelect,
|
||||
ComponentType.StringSelect,
|
||||
ComponentType.UserSelect,
|
||||
]),
|
||||
})
|
||||
.array(),
|
||||
@@ -75,15 +75,10 @@ const messageNoComponentsV2Predicate = baseMessagePredicate
|
||||
poll: pollPredicate.optional(),
|
||||
components: basicActionRowPredicate.array().max(5).optional(),
|
||||
flags: z
|
||||
.number()
|
||||
.int()
|
||||
.optional()
|
||||
.refine((flags) => {
|
||||
// If we have flags, ensure we don't have the ComponentsV2 flag
|
||||
if (flags) {
|
||||
return (flags & MessageFlags.IsComponentsV2) === 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
.refine((flags) => !flags || (flags & MessageFlags.IsComponentsV2) === 0, {
|
||||
error: 'Cannot set content, embeds, stickers, or poll with IsComponentsV2 flag set',
|
||||
}),
|
||||
})
|
||||
.refine(
|
||||
@@ -94,22 +89,22 @@ const messageNoComponentsV2Predicate = baseMessagePredicate
|
||||
(data.attachments !== undefined && data.attachments.length > 0) ||
|
||||
(data.components !== undefined && data.components.length > 0) ||
|
||||
(data.sticker_ids !== undefined && data.sticker_ids.length > 0),
|
||||
{ message: 'Messages must have content, embeds, a poll, attachments, components or stickers' },
|
||||
{ error: 'Messages must have content, embeds, a poll, attachments, components or stickers' },
|
||||
);
|
||||
|
||||
const allTopLevelComponentsPredicate = z
|
||||
.union([
|
||||
basicActionRowPredicate,
|
||||
z.object({
|
||||
type: z.union([
|
||||
type: z.literal([
|
||||
// Components v2
|
||||
z.literal(ComponentType.Container),
|
||||
z.literal(ComponentType.File),
|
||||
z.literal(ComponentType.MediaGallery),
|
||||
z.literal(ComponentType.Section),
|
||||
z.literal(ComponentType.Separator),
|
||||
z.literal(ComponentType.TextDisplay),
|
||||
z.literal(ComponentType.Thumbnail),
|
||||
ComponentType.Container,
|
||||
ComponentType.File,
|
||||
ComponentType.MediaGallery,
|
||||
ComponentType.Section,
|
||||
ComponentType.Separator,
|
||||
ComponentType.TextDisplay,
|
||||
ComponentType.Thumbnail,
|
||||
]),
|
||||
}),
|
||||
])
|
||||
@@ -119,7 +114,9 @@ const allTopLevelComponentsPredicate = z
|
||||
|
||||
const messageComponentsV2Predicate = baseMessagePredicate.extend({
|
||||
components: allTopLevelComponentsPredicate,
|
||||
flags: z.number().refine((flags) => (flags & MessageFlags.IsComponentsV2) === MessageFlags.IsComponentsV2),
|
||||
flags: z.int().refine((flags) => (flags & MessageFlags.IsComponentsV2) === MessageFlags.IsComponentsV2, {
|
||||
error: 'Must set IsComponentsV2 flag to use Components V2',
|
||||
}),
|
||||
// These fields cannot be set
|
||||
content: z.string().length(0).nullish(),
|
||||
embeds: z.array(z.never()).nullish(),
|
||||
|
||||
@@ -1,20 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
import { refineURLPredicate } from '../../Assertions.js';
|
||||
import { z } from 'zod/v4';
|
||||
import { embedLength } from '../../util/componentUtil.js';
|
||||
|
||||
const namePredicate = z.string().max(256);
|
||||
|
||||
const URLPredicate = z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['http:', 'https:']), { message: 'Invalid protocol for URL. Must be http: or https:' });
|
||||
const URLPredicate = z.url({ protocol: /^https?$/ });
|
||||
|
||||
const URLWithAttachmentProtocolPredicate = z
|
||||
.string()
|
||||
.url()
|
||||
.refine(refineURLPredicate(['http:', 'https:', 'attachment:']), {
|
||||
message: 'Invalid protocol for URL. Must be http:, https:, or attachment:',
|
||||
});
|
||||
const URLWithAttachmentProtocolPredicate = z.url({ protocol: /^(?:https?|attachment)$/ });
|
||||
|
||||
export const embedFieldPredicate = z.object({
|
||||
name: namePredicate,
|
||||
@@ -39,7 +30,7 @@ export const embedPredicate = z
|
||||
description: z.string().min(1).max(4_096).optional(),
|
||||
url: URLPredicate.optional(),
|
||||
timestamp: z.string().optional(),
|
||||
color: z.number().int().min(0).max(0xffffff).optional(),
|
||||
color: z.int().min(0).max(0xffffff).optional(),
|
||||
footer: embedFooterPredicate.optional(),
|
||||
image: z.object({ url: URLWithAttachmentProtocolPredicate }).optional(),
|
||||
thumbnail: z.object({ url: URLWithAttachmentProtocolPredicate }).optional(),
|
||||
@@ -56,7 +47,7 @@ export const embedPredicate = z
|
||||
embed.image !== undefined ||
|
||||
embed.thumbnail !== undefined,
|
||||
{
|
||||
message: 'Embed must have at least a title, description, a field, a footer, an author, an image, OR a thumbnail.',
|
||||
error: 'Embed must have at least a title, description, a field, a footer, an author, an image, OR a thumbnail.',
|
||||
},
|
||||
)
|
||||
.refine((embed) => embedLength(embed) <= 6_000, { message: 'Embeds must not exceed 6000 characters in total.' });
|
||||
.refine((embed) => embedLength(embed) <= 6_000, { error: 'Embeds must not exceed 6000 characters in total.' });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PollLayoutType } from 'discord-api-types/v10';
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod/v4';
|
||||
import { emojiPredicate } from '../../components/Assertions';
|
||||
|
||||
export const pollQuestionPredicate = z.object({ text: z.string().min(1).max(300) });
|
||||
@@ -16,5 +16,5 @@ export const pollPredicate = z.object({
|
||||
answers: z.array(pollAnswerPredicate).min(1).max(10),
|
||||
duration: z.number().min(1).max(768).optional(),
|
||||
allow_multiselect: z.boolean().optional(),
|
||||
layout_type: z.nativeEnum(PollLayoutType).optional(),
|
||||
layout_type: z.enum(PollLayoutType).optional(),
|
||||
});
|
||||
|
||||
21
packages/builders/src/util/ValidationError.ts
Normal file
21
packages/builders/src/util/ValidationError.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { z } from 'zod/v4';
|
||||
|
||||
/**
|
||||
* An error that is thrown when validation fails.
|
||||
*/
|
||||
export class ValidationError extends Error {
|
||||
/**
|
||||
* The underlying cause of the validation error.
|
||||
*/
|
||||
public override readonly cause: z.ZodError;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public constructor(error: z.ZodError) {
|
||||
super(z.prettifyError(error));
|
||||
|
||||
this.name = 'ValidationError';
|
||||
this.cause = error;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { z } from 'zod';
|
||||
import { fromZodError } from 'zod-validation-error';
|
||||
import type { z } from 'zod/v4';
|
||||
import { ValidationError } from './ValidationError.js';
|
||||
|
||||
let validationEnabled = true;
|
||||
|
||||
@@ -35,21 +35,23 @@ export function isValidationEnabled() {
|
||||
* @param value - The value to parse
|
||||
* @param validationOverride - Force validation to run/not run regardless of your global preference
|
||||
* @returns The result from parsing
|
||||
* @throws {@link ValidationError}
|
||||
* Throws if the value does not pass validation, if enabled.
|
||||
* @internal
|
||||
*/
|
||||
export function validate<Validator extends z.ZodTypeAny>(
|
||||
export function validate<Validator extends z.ZodType>(
|
||||
validator: Validator,
|
||||
value: unknown,
|
||||
validationOverride?: boolean,
|
||||
): z.output<Validator> {
|
||||
if (validationOverride === false || !isValidationEnabled()) {
|
||||
return value;
|
||||
if (validationOverride === false || (validationOverride === undefined && !isValidationEnabled())) {
|
||||
return value as z.output<Validator>;
|
||||
}
|
||||
|
||||
const result = validator.safeParse(value);
|
||||
|
||||
if (!result.success) {
|
||||
throw fromZodError(result.error);
|
||||
throw new ValidationError(result.error);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
|
||||
@@ -579,7 +579,7 @@ class ApplicationCommand extends Base {
|
||||
* {@link ApplicationCommandOptionType.Number} option
|
||||
* @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from
|
||||
* @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group)
|
||||
* @property {ApplicationCommandOptionAllowedChannelTypes[]} [channelTypes] When the option type is channel,
|
||||
* @property {ApplicationCommandOptionAllowedChannelType[]} [channelTypes] When the option type is channel,
|
||||
* the allowed types of channels that can be selected
|
||||
* @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or
|
||||
* {@link ApplicationCommandOptionType.Number} option
|
||||
@@ -653,6 +653,6 @@ class ApplicationCommand extends Base {
|
||||
exports.ApplicationCommand = ApplicationCommand;
|
||||
|
||||
/**
|
||||
* @external ApplicationCommandOptionAllowedChannelTypes
|
||||
* @see {@link https://discord.js.org/docs/packages/builders/stable/ApplicationCommandOptionAllowedChannelTypes:TypeAlias}
|
||||
* @external ApplicationCommandOptionAllowedChannelType
|
||||
* @see {@link https://discord.js.org/docs/packages/builders/stable/ApplicationCommandOptionAllowedChannelType:TypeAlias}
|
||||
*/
|
||||
|
||||
8
packages/discord.js/typings/index.d.ts
vendored
8
packages/discord.js/typings/index.d.ts
vendored
@@ -2,7 +2,7 @@ import { Buffer } from 'node:buffer';
|
||||
import { ChildProcess } from 'node:child_process';
|
||||
import { Stream } from 'node:stream';
|
||||
import { MessagePort, Worker } from 'node:worker_threads';
|
||||
import { ApplicationCommandOptionAllowedChannelTypes, MessageActionRowComponentBuilder } from '@discordjs/builders';
|
||||
import { ApplicationCommandOptionAllowedChannelType, MessageActionRowComponentBuilder } from '@discordjs/builders';
|
||||
import { Collection, ReadonlyCollection } from '@discordjs/collection';
|
||||
import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions, EmojiURLOptions } from '@discordjs/rest';
|
||||
import { Awaitable, JSONEncodable } from '@discordjs/util';
|
||||
@@ -4823,13 +4823,13 @@ export type ApplicationCommandData =
|
||||
| UserApplicationCommandData;
|
||||
|
||||
export interface ApplicationCommandChannelOptionData extends BaseApplicationCommandOptionsData {
|
||||
channelTypes?: readonly ApplicationCommandOptionAllowedChannelTypes[];
|
||||
channel_types?: readonly ApplicationCommandOptionAllowedChannelTypes[];
|
||||
channelTypes?: readonly ApplicationCommandOptionAllowedChannelType[];
|
||||
channel_types?: readonly ApplicationCommandOptionAllowedChannelType[];
|
||||
type: CommandOptionChannelResolvableType;
|
||||
}
|
||||
|
||||
export interface ApplicationCommandChannelOption extends BaseApplicationCommandOptionsData {
|
||||
channelTypes?: readonly ApplicationCommandOptionAllowedChannelTypes[];
|
||||
channelTypes?: readonly ApplicationCommandOptionAllowedChannelType[];
|
||||
type: ApplicationCommandOptionType.Channel;
|
||||
}
|
||||
|
||||
|
||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -759,11 +759,8 @@ importers:
|
||||
specifier: ^2.8.1
|
||||
version: 2.8.1
|
||||
zod:
|
||||
specifier: ^3.24.2
|
||||
version: 3.24.3
|
||||
zod-validation-error:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0(zod@3.24.3)
|
||||
specifier: ^3.25.69
|
||||
version: 3.25.69
|
||||
devDependencies:
|
||||
'@discordjs/api-extractor':
|
||||
specifier: workspace:^
|
||||
@@ -13998,24 +13995,18 @@ packages:
|
||||
zlib-sync@0.1.10:
|
||||
resolution: {integrity: sha512-t7/pYg5tLBznL1RuhmbAt8rNp5tbhr+TSrJFnMkRtrGIaPJZ6Dc0uR4u3OoQI2d6cGlVI62E3Gy6gwkxyIqr/w==}
|
||||
|
||||
zod-validation-error@3.4.0:
|
||||
resolution: {integrity: sha512-ZOPR9SVY6Pb2qqO5XHt+MkkTRxGXb4EVtnjc9JpXUOtUB1T9Ru7mZOT361AN3MsetVe7R0a1KZshJDZdgp9miQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.18.0
|
||||
|
||||
zod-validation-error@3.4.1:
|
||||
resolution: {integrity: sha512-1KP64yqDPQ3rupxNv7oXhf7KdhHHgaqbKuspVoiN93TT0xrBjql+Svjkdjq/Qh/7GSMmgQs3AfvBT0heE35thw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
zod: ^3.24.4
|
||||
|
||||
zod@3.24.3:
|
||||
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
|
||||
|
||||
zod@3.25.39:
|
||||
resolution: {integrity: sha512-yrva2T2x4R+FMFTPBVD/YPS7ct8njqjnV93zNx/MlwqLAxcnxwRGbXWyWF63/nErl3rdRd8KARObon7BiWzabQ==}
|
||||
|
||||
zod@3.25.69:
|
||||
resolution: {integrity: sha512-cjUx+boz8dfYGssYKLGNTF5VUF6NETpcZCDTN3knhUUXQTAAvErzRhV3pgeCZm2YsL4HOwtEHPonlsshPu2I0A==}
|
||||
|
||||
zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
|
||||
@@ -29506,16 +29497,12 @@ snapshots:
|
||||
dependencies:
|
||||
nan: 2.22.0
|
||||
|
||||
zod-validation-error@3.4.0(zod@3.24.3):
|
||||
dependencies:
|
||||
zod: 3.24.3
|
||||
|
||||
zod-validation-error@3.4.1(zod@3.25.39):
|
||||
dependencies:
|
||||
zod: 3.25.39
|
||||
|
||||
zod@3.24.3: {}
|
||||
|
||||
zod@3.25.39: {}
|
||||
|
||||
zod@3.25.69: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
Reference in New Issue
Block a user