Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/tricky-boxes-type.md
Original file line number Diff line number Diff line change
@@ -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.
143 changes: 95 additions & 48 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -945,6 +925,16 @@ API.v1.addRoute(
},
);

type RoomsFavorite =
| {
roomId: string;
favorite: boolean;
}
| {
roomName: string;
favorite: boolean;
};

const isRoomGetRolesPropsSchema = {
type: 'object',
properties: {
Expand All @@ -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<RoomsFavorite>(RoomsFavoriteSchema);

export const roomEndpoints = API.v1
.get(
'rooms.roles',
Expand Down Expand Up @@ -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<void>({
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<void>({
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) {
throw new Meteor.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<void>({
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<typeof roomEndpoints> & ExtractRoutesFromAPI<typeof roomInviteEndpoints>;
type RoomEndpoints = ExtractRoutesFromAPI<typeof roomEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
Expand Down
14 changes: 0 additions & 14 deletions packages/rest-typings/src/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading