feat: file_types for builders (#11579)

* feat: `file_types`

* fix: .js extension

Co-authored-by: Almeida <github@almeidx.dev>

* fix: adding

* test: use `Reflect`

* fix: update deprecated usage

---------

Co-authored-by: Almeida <github@almeidx.dev>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Jiralite
2026-07-20 06:29:59 +01:00
committed by GitHub
parent c35a06663e
commit 2037593d8d
7 changed files with 173 additions and 7 deletions
@@ -1,4 +1,4 @@
import type { APIFileUploadComponent } from 'discord-api-types/v10';
import type { APIFileUploadComponent, FileUploadType } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import { FileUploadBuilder } from '../../src/components/fileUpload/FileUpload.js';
@@ -15,6 +15,16 @@ describe('File Upload Components', () => {
expect(() => {
fileUploadComponent().setCustomId('foobar').setMinValues(2).setMaxValues(9).toJSON();
}).not.toThrowError();
expect(() => {
fileUploadComponent().setCustomId('foobar').setFileTypes('audio', 'image', 'video', '.pdf').toJSON();
}).not.toThrow();
});
test('GIVEN file types THEN they can be added', () => {
expect(
fileUploadComponent().setCustomId('foobar').addFileTypes('image').addFileTypes(['.pdf']).toJSON().file_types,
).toEqual(['image', '.pdf']);
});
});
@@ -34,6 +44,30 @@ describe('File Upload Components', () => {
expect(() => {
fileUploadComponent().setCustomId('a').setMinValues(-1).toJSON();
}).toThrowError();
expect(() => {
fileUploadComponent()
.setCustomId('a')
.setFileTypes(Array.from({ length: 11 }, () => '.txt' as const))
.toJSON();
}).toThrow();
for (const invalidFileType of ['document', 'pdf', '.']) {
expect(() => {
fileUploadComponent()
.setCustomId('a')
.setFileTypes(invalidFileType as FileUploadType)
.toJSON();
}).toThrow();
}
expect(() => {
new FileUploadBuilder({
type: ComponentType.FileUpload,
custom_id: 'a',
file_types: ['document' as FileUploadType],
}).toJSON();
}).toThrow();
});
test('GIVEN valid input THEN valid JSON outputs are given', () => {
@@ -42,6 +76,7 @@ describe('File Upload Components', () => {
custom_id: 'custom id',
min_values: 5,
max_values: 6,
file_types: ['image', '.pdf'],
required: false,
} satisfies APIFileUploadComponent;
@@ -52,8 +87,11 @@ describe('File Upload Components', () => {
.setCustomId(fileUploadData.custom_id)
.setMaxValues(fileUploadData.max_values)
.setMinValues(fileUploadData.min_values)
.setFileTypes(fileUploadData.file_types)
.setRequired(fileUploadData.required)
.toJSON(),
).toEqual(fileUploadData);
expect(new FileUploadBuilder(fileUploadData).clearFileTypes().toJSON().file_types).toBeUndefined();
});
});
@@ -10,6 +10,7 @@ import {
type APIApplicationCommandRoleOption,
type APIApplicationCommandStringOption,
type APIApplicationCommandUserOption,
type FileUploadType,
} from 'discord-api-types/v10';
import { describe, test, expect } from 'vitest';
import {
@@ -222,11 +223,45 @@ describe('Application Command toJSON() results', () => {
});
test('GIVEN an attachment option THEN calling toJSON should return a valid JSON', () => {
expect(getAttachmentOption().toJSON()).toEqual<APIApplicationCommandAttachmentOption>({
expect(
getAttachmentOption().setFileTypes(['image', '.pdf']).toJSON(),
).toEqual<APIApplicationCommandAttachmentOption>({
name: 'attachment',
description: 'attachment',
type: ApplicationCommandOptionType.Attachment,
required: true,
file_types: ['image', '.pdf'],
});
});
test('GIVEN attachment option file types THEN they can be added', () => {
expect(
Reflect.get(getAttachmentOption().addFileTypes('image').addFileTypes(['.pdf']).toJSON(), 'file_types'),
).toEqual(['image', '.pdf']);
});
test('GIVEN attachment option file types THEN they can be cleared', () => {
expect(
Reflect.get(getAttachmentOption().setFileTypes('audio', '.ogg').clearFileTypes().toJSON(), 'file_types'),
).toBeUndefined();
});
test('GIVEN too many attachment option file types THEN calling toJSON should throw', () => {
expect(() =>
getAttachmentOption()
.setFileTypes(Array.from({ length: 11 }, () => '.txt' as const))
.toJSON(),
).toThrow();
});
test.each(['document', 'pdf', '.'])(
'GIVEN invalid attachment file type %s THEN calling toJSON should throw',
(fileType) => {
expect(() =>
getAttachmentOption()
.setFileTypes(fileType as FileUploadType)
.toJSON(),
).toThrow();
},
);
});
+5
View File
@@ -5,6 +5,11 @@ export const idPredicate = z.int().min(0).max(2_147_483_647).optional();
export const customIdPredicate = z.string().min(1).max(100);
export const snowflakePredicate = z.string().regex(/^(?:0|[1-9]\d*)$/);
export const fileUploadTypesPredicate = z
.union([z.enum(['audio', 'image', 'video']), z.string().min(2).startsWith('.')])
.array()
.max(10);
export const memberPermissionsPredicate = z.coerce.bigint();
export const localeMapPredicate = z.strictObject(
@@ -1,6 +1,6 @@
import { ComponentType } from 'discord-api-types/v10';
import { z } from 'zod';
import { customIdPredicate, idPredicate } from '../../Assertions';
import { customIdPredicate, fileUploadTypesPredicate, idPredicate } from '../../Assertions.js';
export const fileUploadPredicate = z.object({
type: z.literal(ComponentType.FileUpload),
@@ -8,5 +8,6 @@ export const fileUploadPredicate = z.object({
custom_id: customIdPredicate,
min_values: z.int().min(0).max(10).optional(),
max_values: z.int().min(1).max(10).optional(),
file_types: fileUploadTypesPredicate.optional(),
required: z.boolean().optional(),
});
@@ -1,5 +1,6 @@
import type { APIFileUploadComponent } from 'discord-api-types/v10';
import type { APIFileUploadComponent, FileUploadType } from 'discord-api-types/v10';
import { ComponentType } from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../util/normalizeArray.js';
import { validate } from '../../util/validation.js';
import { ComponentBuilder } from '../Component.js';
import { fileUploadPredicate } from './Assertions.js';
@@ -24,6 +25,7 @@ export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent>
* custom_id: "file_upload",
* min_values: 2,
* max_values: 5,
* file_types: ["image", ".pdf"],
* });
* ```
* @example
@@ -33,7 +35,9 @@ export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent>
* custom_id: "file_upload",
* min_values: 2,
* max_values: 5,
* }).setRequired();
* })
* .setFileTypes("image", ".pdf")
* .setRequired();
* ```
*/
public constructor(data: Partial<APIFileUploadComponent> = {}) {
@@ -87,6 +91,41 @@ export class FileUploadBuilder extends ComponentBuilder<APIFileUploadComponent>
return this;
}
/**
* Adds file types allowed in this file upload.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public addFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
this.data.file_types ??= [];
this.data.file_types.push(...normalizeArray(fileTypes));
return this;
}
/**
* Sets the file types allowed in this file upload.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public setFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
this.data.file_types = normalizeArray(fileTypes);
return this;
}
/**
* Clears the file types allowed in this file upload.
*/
public clearFileTypes() {
this.data.file_types = undefined;
return this;
}
/**
* Sets whether this file upload is required.
*
@@ -5,7 +5,7 @@ import {
ApplicationCommandType,
} from 'discord-api-types/v10';
import { z } from 'zod';
import { localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
import { fileUploadTypesPredicate, localeMapPredicate, memberPermissionsPredicate } from '../../../Assertions.js';
import { ApplicationCommandOptionAllowedChannelTypes } from './mixins/ApplicationCommandOptionChannelTypesMixin.js';
const namePredicate = z
@@ -77,6 +77,7 @@ export const baseBasicOptionPredicate = z.object({
export const attachmentOptionPredicate = z.object({
...baseBasicOptionPredicate.shape,
type: z.literal(ApplicationCommandOptionType.Attachment),
file_types: fileUploadTypesPredicate.optional(),
});
export const booleanOptionPredicate = z.object({
@@ -1,6 +1,12 @@
import { ApplicationCommandOptionType } from 'discord-api-types/v10';
import {
ApplicationCommandOptionType,
type APIApplicationCommandAttachmentOption,
type FileUploadType,
} from 'discord-api-types/v10';
import { normalizeArray, type RestOrArray } from '../../../../util/normalizeArray.js';
import { attachmentOptionPredicate } from '../Assertions.js';
import { ApplicationCommandOptionBase } from './ApplicationCommandOptionBase.js';
import type { ApplicationCommandOptionBaseData } from './ApplicationCommandOptionBase.js';
/**
* A chat input command attachment option.
@@ -11,10 +17,51 @@ export class ChatInputCommandAttachmentOption extends ApplicationCommandOptionBa
*/
protected static override readonly predicate = attachmentOptionPredicate;
/**
* @internal
*/
declare protected readonly data: ApplicationCommandOptionBaseData &
Partial<Pick<APIApplicationCommandAttachmentOption, 'file_types'>>;
/**
* Creates a new attachment option.
*/
public constructor() {
super(ApplicationCommandOptionType.Attachment);
}
/**
* Adds file types allowed for this attachment option.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public addFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
this.data.file_types ??= [];
this.data.file_types.push(...normalizeArray(fileTypes));
return this;
}
/**
* Sets the file types allowed for this attachment option.
*
* @remarks
* When specifying only extensions, include `.jpg` for image uploads and both `.mp4` and `.mov`
* for video uploads due to mobile platform limitations.
* @param fileTypes - The file groups or dot-prefixed extensions to allow
*/
public setFileTypes(...fileTypes: RestOrArray<FileUploadType>) {
this.data.file_types = normalizeArray(fileTypes);
return this;
}
/**
* Clears the file types allowed for this attachment option.
*/
public clearFileTypes() {
this.data.file_types = undefined;
return this;
}
}