Files
discord.js/packages/builders/src/components/Assertions.ts
Almeida a5bd4cfe73 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>
2025-07-03 01:02:45 +01:00

160 lines
4.7 KiB
TypeScript

import { ButtonStyle, ChannelType, ComponentType, SelectMenuDefaultValueType } from 'discord-api-types/v10';
import { z } from 'zod/v4';
import { customIdPredicate } from '../Assertions.js';
const labelPredicate = z.string().min(1).max(80);
export const emojiPredicate = z
.strictObject({
id: z.string().optional(),
name: z.string().min(2).max(32).optional(),
animated: z.boolean().optional(),
})
.refine((data) => data.id !== undefined || data.name !== undefined, {
error: "Either 'id' or 'name' must be provided",
});
const buttonPredicateBase = z.strictObject({
type: z.literal(ComponentType.Button),
disabled: z.boolean().optional(),
});
const buttonCustomIdPredicateBase = buttonPredicateBase.extend({
custom_id: customIdPredicate,
emoji: emojiPredicate.optional(),
label: labelPredicate,
});
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.url({ protocol: /^(?:https?|discord)$/ }),
emoji: emojiPredicate.optional(),
label: labelPredicate,
});
const buttonPremiumPredicate = buttonPredicateBase.extend({
style: z.literal(ButtonStyle.Premium),
sku_id: z.string(),
});
export const buttonPredicate = z.discriminatedUnion('style', [
buttonLinkPredicate,
buttonPrimaryPredicate,
buttonSecondaryPredicate,
buttonSuccessPredicate,
buttonDangerPredicate,
buttonPremiumPredicate,
]);
const selectMenuBasePredicate = z.object({
placeholder: z.string().max(150).optional(),
min_values: z.number().min(0).max(25).optional(),
max_values: z.number().min(0).max(25).optional(),
custom_id: customIdPredicate,
disabled: z.boolean().optional(),
});
export const selectMenuChannelPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.ChannelSelect),
channel_types: z.enum(ChannelType).array().optional(),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Channel) })
.array()
.max(25)
.optional(),
});
export const selectMenuMentionablePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.MentionableSelect),
default_values: z
.object({
id: z.string(),
type: z.literal([SelectMenuDefaultValueType.Role, SelectMenuDefaultValueType.User]),
})
.array()
.max(25)
.optional(),
});
export const selectMenuRolePredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.RoleSelect),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.Role) })
.array()
.max(25)
.optional(),
});
export const selectMenuStringOptionPredicate = z.object({
label: labelPredicate,
value: z.string().min(1).max(100),
description: z.string().min(1).max(100).optional(),
emoji: emojiPredicate.optional(),
default: z.boolean().optional(),
});
export const selectMenuStringPredicate = selectMenuBasePredicate
.extend({
type: z.literal(ComponentType.StringSelect),
options: selectMenuStringOptionPredicate.array().min(1).max(25),
})
.check((ctx) => {
const addIssue = (name: string, minimum: number) =>
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 (ctx.value.max_values !== undefined && ctx.value.options.length < ctx.value.max_values) {
addIssue('max_values', ctx.value.max_values);
}
if (ctx.value.min_values !== undefined && ctx.value.options.length < ctx.value.min_values) {
addIssue('min_values', ctx.value.min_values);
}
});
export const selectMenuUserPredicate = selectMenuBasePredicate.extend({
type: z.literal(ComponentType.UserSelect),
default_values: z
.object({ id: z.string(), type: z.literal(SelectMenuDefaultValueType.User) })
.array()
.max(25)
.optional(),
});
export const actionRowPredicate = z.object({
type: z.literal(ComponentType.ActionRow),
components: z.union([
z
.object({ type: z.literal(ComponentType.Button) })
.array()
.min(1)
.max(5),
z
.object({
type: z.literal([
ComponentType.ChannelSelect,
ComponentType.MentionableSelect,
ComponentType.StringSelect,
ComponentType.RoleSelect,
ComponentType.TextInput,
ComponentType.UserSelect,
]),
})
.array()
.length(1),
]),
});