diff --git a/.changeset/sweet-terms-relax.md b/.changeset/sweet-terms-relax.md new file mode 100644 index 0000000000000..0ea8412c5eebb --- /dev/null +++ b/.changeset/sweet-terms-relax.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat custom-user-status.list 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/v1/custom-user-status.ts b/apps/meteor/app/api/server/v1/custom-user-status.ts index 037928cf1cdcf..4d4297cfe001c 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.ts +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -1,46 +1,124 @@ +import type { ICustomUserStatus } from '@rocket.chat/core-typings'; import { CustomUserStatus } from '@rocket.chat/models'; -import { isCustomUserStatusListProps } from '@rocket.chat/rest-typings'; +import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings'; +import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { deleteCustomUserStatus } from '../../../user-status/server/methods/deleteCustomUserStatus'; import { insertOrUpdateUserStatus } from '../../../user-status/server/methods/insertOrUpdateUserStatus'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -API.v1.addRoute( - 'custom-user-status.list', - { authRequired: true, validateParams: isCustomUserStatusListProps }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams as Record); - const { sort, query } = await this.parseJsonQuery(); - - const { name, _id } = this.queryParams; - - const filter = { - ...query, - ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), - ...(_id ? { _id } : {}), - }; +type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>; - const { cursor, totalCount } = CustomUserStatus.findPaginated(filter, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); +const CustomUserStatusListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + nullable: true, + }, + _id: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; - const [statuses, total] = await Promise.all([cursor.toArray(), totalCount]); +const isCustomUserStatusListProps = ajv.compile(CustomUserStatusListSchema); - return API.v1.success({ - statuses, - count: statuses.length, - offset, - total, - }); +const customUserStatusEndpoints = API.v1.get( + 'custom-user-status.list', + { + authRequired: true, + query: isCustomUserStatusListProps, + response: { + 200: ajv.compile< + PaginatedResult<{ + statuses: ICustomUserStatus[]; + }> + >({ + type: 'object', + properties: { + statuses: { + type: 'array', + items: { + $ref: '#/components/schemas/ICustomUserStatus', + }, + }, + count: { + type: 'number', + description: 'The number of custom user statuses returned in this response.', + }, + offset: { + type: 'number', + description: 'The number of custom user statuses that were skipped in this response.', + }, + total: { + type: 'number', + description: 'The total number of custom user statuses that match the query.', + }, + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['success', 'statuses', 'count', 'offset', 'total'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams as Record); + const { sort, query } = await this.parseJsonQuery(); + + const { name, _id } = this.queryParams; + + const filter = { + ...query, + ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), + ...(_id ? { _id } : {}), + }; + + const { cursor, totalCount } = CustomUserStatus.findPaginated(filter, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const [statuses, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + statuses, + count: statuses.length, + offset, + total, + }); + }, ); API.v1.addRoute( @@ -127,3 +205,10 @@ API.v1.addRoute( }, }, ); + +export type CustomUserStatusEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends CustomUserStatusEndpoints {} +} diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index 8d828e041b1bd..9188401b930ae 100644 --- a/packages/core-typings/src/Ajv.ts +++ b/packages/core-typings/src/Ajv.ts @@ -1,10 +1,14 @@ import typia from 'typia'; import type { ICustomSound } from './ICustomSound'; +import type { ICustomUserStatus } from './ICustomUserStatus'; import type { IInvite } from './IInvite'; import type { IMessage } from './IMessage'; import type { IOAuthApps } from './IOAuthApps'; import type { IPermission } from './IPermission'; import type { ISubscription } from './ISubscription'; -export const schemas = typia.json.schemas<[ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission], '3.0'>(); +export const schemas = typia.json.schemas< + [ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | ICustomUserStatus], + '3.0' +>(); diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index cf3e6801be17e..742e16934e1ee 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -1,55 +1,6 @@ -import type { ICustomUserStatus, IUserStatus } from '@rocket.chat/core-typings'; -import Ajv from 'ajv'; - -import type { PaginatedRequest } from '../helpers/PaginatedRequest'; -import type { PaginatedResult } from '../helpers/PaginatedResult'; - -const ajv = new Ajv({ - coerceTypes: true, -}); - -type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>; - -const CustomUserStatusListSchema = { - type: 'object', - properties: { - count: { - type: 'number', - nullable: true, - }, - offset: { - type: 'number', - nullable: true, - }, - sort: { - type: 'string', - nullable: true, - }, - name: { - type: 'string', - nullable: true, - }, - _id: { - type: 'string', - nullable: true, - }, - query: { - type: 'string', - nullable: true, - }, - }, - required: [], - additionalProperties: false, -}; - -export const isCustomUserStatusListProps = ajv.compile(CustomUserStatusListSchema); +import type { ICustomUserStatus } from '@rocket.chat/core-typings'; export type CustomUserStatusEndpoints = { - '/v1/custom-user-status.list': { - GET: (params: CustomUserStatusListProps) => PaginatedResult<{ - statuses: IUserStatus[]; - }>; - }; '/v1/custom-user-status.create': { POST: (params: { name: string; statusType?: string }) => { customUserStatus: ICustomUserStatus;