diff --git a/.changeset/weak-terms-shave.md b/.changeset/weak-terms-shave.md new file mode 100644 index 0000000000000..1813edcdb2b5b --- /dev/null +++ b/.changeset/weak-terms-shave.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat emoji-custom.create API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 029c3ac223391..63db70e14e4a6 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -301,7 +301,7 @@ export type TypedThis query: Record; }>; bodyParams: TOptions['body'] extends ValidateFunction ? Body : never; - + request: Request; requestIp?: string; route: string; response: Response; diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index fa84d3cc6b995..c7fcfa7f698d9 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -1,7 +1,7 @@ import { Media } from '@rocket.chat/core-services'; import type { IEmojiCustom } from '@rocket.chat/core-typings'; import { EmojiCustom } from '@rocket.chat/models'; -import { isEmojiCustomList } from '@rocket.chat/rest-typings'; +import { ajv, isEmojiCustomList } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -11,6 +11,7 @@ import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUp import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom'; import { deleteEmojiCustom } from '../../../emoji-custom/server/methods/deleteEmojiCustom'; import { settings } from '../../../settings/server'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { findEmojisCustom } from '../lib/emoji-custom'; @@ -103,45 +104,73 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +const emojiCustomCreateEndpoints = API.v1.post( 'emoji-custom.create', - { authRequired: true }, { - async post() { - const emoji = await getUploadFormData( - { - request: this.request, + authRequired: true, + response: { + 400: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + stack: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + details: { type: 'string' }, }, - { field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize') }, - ); + required: ['success'], + additionalProperties: false, + }), + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const emoji = await getUploadFormData( + { + request: this.request, + }, + { + field: 'emoji', + sizeLimit: settings.get('FileUpload_MaxFileSize'), + }, + ); - const { fields, fileBuffer, mimetype } = emoji; + const { fields, fileBuffer, mimetype } = emoji; - const isUploadable = await Media.isImage(fileBuffer); - if (!isUploadable) { - throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); - } + const isUploadable = await Media.isImage(fileBuffer); + if (!isUploadable) { + throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); + } - const [, extension] = mimetype.split('/'); - fields.extension = extension; + const [, extension] = mimetype.split('/'); + fields.extension = extension; - try { - const emojiData = await insertOrUpdateEmoji(this.userId, { - ...fields, - newFile: true, - aliases: fields.aliases || '', - name: fields.name, - extension: fields.extension, - }); + try { + const emojiData = await insertOrUpdateEmoji(this.userId, { + ...fields, + newFile: true, + aliases: fields.aliases || '', + name: fields.name, + extension: fields.extension, + }); - await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); - } catch (e) { - SystemLogger.error(e); - return API.v1.failure(); - } + await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); + } catch (e) { + SystemLogger.error(e); + return API.v1.failure(); + } - return API.v1.success(); - }, + return API.v1.success(); }, ); @@ -219,3 +248,12 @@ API.v1.addRoute( }, }, ); + +type EmojiCustomCreateEndpoints = ExtractRoutesFromAPI; + +export type EmojiCustomEndpoints = EmojiCustomCreateEndpoints; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends EmojiCustomCreateEndpoints {} +} diff --git a/packages/rest-typings/src/v1/emojiCustom.ts b/packages/rest-typings/src/v1/emojiCustom.ts index ad67bbf7d0e9b..dda81acbcb3fa 100644 --- a/packages/rest-typings/src/v1/emojiCustom.ts +++ b/packages/rest-typings/src/v1/emojiCustom.ts @@ -67,9 +67,6 @@ export type EmojiCustomEndpoints = { '/v1/emoji-custom.delete': { POST: (params: emojiCustomDeleteProps) => void; }; - '/v1/emoji-custom.create': { - POST: (params: { emoji: ICustomEmojiDescriptor }) => void; - }; '/v1/emoji-custom.update': { POST: (params: { emoji: ICustomEmojiDescriptor }) => void; };