diff --git a/.changeset/tricky-boxes-type.md b/.changeset/tricky-boxes-type.md new file mode 100644 index 0000000000000..084f3f79fe242 --- /dev/null +++ b/.changeset/tricky-boxes-type.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat rooms.favorite APIs 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/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 686f76a7476c6..988e60002eef9 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -311,26 +311,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'rooms.favorite', - { authRequired: true }, - { - async post() { - const { favorite } = this.bodyParams; - - if (!this.bodyParams.hasOwnProperty('favorite')) { - return API.v1.failure("The 'favorite' param is required"); - } - - const room = await findRoomByIdOrName({ params: this.bodyParams }); - - await toggleFavoriteMethod(this.userId, room._id, favorite); - - return API.v1.success(); - }, - }, -); - API.v1.addRoute( 'rooms.cleanHistory', { authRequired: true, validateParams: isRoomsCleanHistoryProps }, @@ -945,6 +925,16 @@ API.v1.addRoute( }, ); +type RoomsFavorite = + | { + roomId: string; + favorite: boolean; + } + | { + roomName: string; + favorite: boolean; + }; + const isRoomGetRolesPropsSchema = { type: 'object', properties: { @@ -953,6 +943,32 @@ const isRoomGetRolesPropsSchema = { additionalProperties: false, required: ['rid'], }; + +const RoomsFavoriteSchema = { + anyOf: [ + { + type: 'object', + properties: { + favorite: { type: 'boolean' }, + roomName: { type: 'string' }, + }, + required: ['roomName', 'favorite'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + favorite: { type: 'boolean' }, + roomId: { type: 'string' }, + }, + required: ['roomId', 'favorite'], + additionalProperties: false, + }, + ], +}; + +const isRoomsFavoriteProps = ajv.compile(RoomsFavoriteSchema); + export const roomEndpoints = API.v1 .get( 'rooms.roles', @@ -1066,39 +1082,70 @@ export const roomEndpoints = API.v1 total, }); }, - ); + ) + .post( + 'rooms.invite', + { + authRequired: true, + body: isRoomsInviteProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const { roomId, action } = this.bodyParams; -const roomInviteEndpoints = API.v1.post( - 'rooms.invite', - { - authRequired: true, - body: isRoomsInviteProps, - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), + try { + await FederationMatrix.handleInvite(roomId, this.userId, action); + return API.v1.success(); + } catch (error) { + return API.v1.failure({ error: `Failed to handle invite: ${error instanceof Error ? error.message : String(error)}` }); + } }, - }, - async function action() { - const { roomId, action } = this.bodyParams; + ) + .post( + 'rooms.favorite', + { + authRequired: true, + body: isRoomsFavoriteProps, + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { favorite } = this.bodyParams; + + const room = await findRoomByIdOrName({ params: this.bodyParams }); + + await toggleFavoriteMethod(this.userId, room._id, favorite); - try { - await FederationMatrix.handleInvite(roomId, this.userId, action); return API.v1.success(); - } catch (error) { - return API.v1.failure({ error: `Failed to handle invite: ${error instanceof Error ? error.message : String(error)}` }); - } - }, -); + }, + ); -type RoomEndpoints = ExtractRoutesFromAPI & ExtractRoutesFromAPI; +type RoomEndpoints = ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index c8d77253ecf5c..18c4574f7bb23 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -805,20 +805,6 @@ export type RoomsEndpoints = { POST: (params: { roomId: string; notifications: Notifications }) => void; }; - '/v1/rooms.favorite': { - POST: ( - params: - | { - roomId: string; - favorite: boolean; - } - | { - roomName: string; - favorite: boolean; - }, - ) => void; - }; - '/v1/rooms.nameExists': { GET: (params: { roomName: string }) => { exists: boolean;