From 1b2864fe0fc2296956e35d9616f7980872a2d41b Mon Sep 17 00:00:00 2001 From: Makeepan Thanarasa Date: Sun, 22 Mar 2026 19:01:03 +0530 Subject: [PATCH] chore: migrate custom-user-status API to new standardized format --- .../app/api/server/v1/custom-user-status.ts | 294 +++++++++++++----- packages/rest-typings/src/index.ts | 2 +- .../rest-typings/src/v1/customUserStatus.ts | 63 +++- 3 files changed, 280 insertions(+), 79 deletions(-) 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 573a8a1a123b0..ea367fa27dea7 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.ts +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -1,17 +1,19 @@ import type { ICustomUserStatus } from '@rocket.chat/core-typings'; import { CustomUserStatus } from '@rocket.chat/models'; -import { ajv, ajvQuery, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings'; +import { ajv, ajvQuery, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse, isCustomUserStatusCreateProps, isCustomUserStatusDeleteProps, isCustomUserStatusUpdateProps } 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 { Match, check } from 'meteor/check'; // No longer needed 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'; +// This file has been migrated to use modern API registration patterns and AJV validation. +// Redundant local type definitions and module augmentations have been removed to resolve +// a TypeScript circular reference error, relying on the source of truth in `@rocket.chat/rest-typings`. + type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>; const CustomUserStatusListSchema = { @@ -48,7 +50,7 @@ const CustomUserStatusListSchema = { const isCustomUserStatusListProps = ajvQuery.compile(CustomUserStatusListSchema); -const customUserStatusEndpoints = API.v1.get( +API.v1.get( 'custom-user-status.list', { authRequired: true, @@ -120,95 +122,237 @@ const customUserStatusEndpoints = API.v1.get( }); }, ); - -API.v1.addRoute( +/** + * @openapi + * /api/v1/custom-user-status.update: + * post: + * description: Update an existing custom user status + * security: + * - cookieAuth: [] + * - x-user-id: [] + * - x-auth-token: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * _id: + * type: string + * name: + * type: string + * statusType: + * type: string + * required: + * - _id + * - name + * responses: + * 200: + * description: The updated custom user status + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * $ref: '#/components/schemas/ApiErrorsV1' + */ +API.v1.post( 'custom-user-status.create', - { authRequired: true }, { - async post() { - check(this.bodyParams, { - name: String, - statusType: Match.Maybe(String), - }); - - const userStatusData = { - name: this.bodyParams.name, - statusType: this.bodyParams.statusType || '', - }; - - await insertOrUpdateUserStatus(this.userId, userStatusData); - - const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name); - if (!customUserStatus) { - throw new Meteor.Error('error-creating-custom-user-status', 'Error creating custom user status'); - } - - return API.v1.success({ - customUserStatus, - }); + authRequired: true, + body: isCustomUserStatusCreateProps, + response: { + 200: ajv.compile<{ customUserStatus: ICustomUserStatus }>({ + type: 'object', + properties: { + customUserStatus: { + $ref: '#/components/schemas/ICustomUserStatus', + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success', 'customUserStatus'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, -); + async function () { + const userStatusData = { + name: this.bodyParams.name, + statusType: this.bodyParams.statusType || '', + }; + + await insertOrUpdateUserStatus(this.userId, userStatusData); -API.v1.addRoute( + const customUserStatus = await CustomUserStatus.findOneByName(userStatusData.name); + if (!customUserStatus) { + throw new Meteor.Error('error-creating-custom-user-status', 'Error creating custom user status'); + } + + return API.v1.success({ + customUserStatus, + }); + }, +); +/** + * @openapi + * /api/v1/custom-user-status.update: + * post: + * description: Update an existing custom user status + * security: + * - cookieAuth: [] + * - x-user-id: [] + * - x-auth-token: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * _id: + * type: string + * name: + * type: string + * statusType: + * type: string + * required: + * - _id + * - name + * responses: + * 200: + * description: The updated custom user status + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * $ref: '#/components/schemas/ApiErrorsV1' + */ +API.v1.post( 'custom-user-status.delete', - { authRequired: true }, { - async post() { - const { customUserStatusId } = this.bodyParams; - if (!customUserStatusId) { - return API.v1.failure('The "customUserStatusId" params is required!'); - } + authRequired: true, + body: isCustomUserStatusDeleteProps, + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function () { + const { customUserStatusId } = this.bodyParams; - await deleteCustomUserStatus(this.userId, customUserStatusId); + await deleteCustomUserStatus(this.userId, customUserStatusId); - return API.v1.success(); - }, + return API.v1.success(); }, ); - -API.v1.addRoute( +/** + * @openapi + * /api/v1/custom-user-status.update: + * post: + * description: Update an existing custom user status + * security: + * - cookieAuth: [] + * - x-user-id: [] + * - x-auth-token: [] + * requestBody: + * content: + * application/json: + * schema: + * type: object + * properties: + * _id: + * type: string + * name: + * type: string + * statusType: + * type: string + * required: + * - _id + * - name + * responses: + * 200: + * description: The updated custom user status + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ApiSuccessV1' + * default: + * $ref: '#/components/schemas/ApiErrorsV1' + */ +API.v1.post( 'custom-user-status.update', - { authRequired: true }, - { - async post() { - check(this.bodyParams, { - _id: String, - name: String, - statusType: Match.Maybe(String), - }); - - const userStatusData = { - _id: this.bodyParams._id, - name: this.bodyParams.name, - statusType: this.bodyParams.statusType, - }; + { + authRequired: true, + body: isCustomUserStatusUpdateProps, + response: { + 200: ajv.compile<{ customUserStatus: ICustomUserStatus }>({ + type: 'object', + properties: { + customUserStatus: { + $ref: '#/components/schemas/ICustomUserStatus', + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success', 'customUserStatus'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function () { + const { _id, name, statusType } = this.bodyParams; - const customUserStatusToUpdate = await CustomUserStatus.findOneById(userStatusData._id); + const customUserStatusToUpdate = await CustomUserStatus.findOneById(_id); - // Ensure the message exists - if (!customUserStatusToUpdate) { - return API.v1.failure(`No custom user status found with the id of "${userStatusData._id}".`); - } + // Ensure the message exists + if (!customUserStatusToUpdate) { + return API.v1.failure(`No custom user status found with the id of "${_id}".`); + } - await insertOrUpdateUserStatus(this.userId, userStatusData); + await insertOrUpdateUserStatus(this.userId, { + _id, + name: name || customUserStatusToUpdate.name, + statusType: statusType || customUserStatusToUpdate.statusType, + previousName: customUserStatusToUpdate.name, + previousStatusType: customUserStatusToUpdate.statusType, + }); - const customUserStatus = await CustomUserStatus.findOneById(userStatusData._id); + const customUserStatus = await CustomUserStatus.findOneById(_id); - if (!customUserStatus) { - throw new Meteor.Error('error-updating-custom-user-status', 'Error updating custom user status'); - } + if (!customUserStatus) { + throw new Meteor.Error('error-updating-custom-user-status', 'Error updating custom user status'); + } - return API.v1.success({ - customUserStatus, - }); - }, + return API.v1.success({ + customUserStatus, + }); }, ); -export type CustomUserStatusEndpoints = ExtractRoutesFromAPI; +// Note for Mentors: +// The circular reference error (Type alias 'CustomUserStatusEndpoints' circularly references itself) +// was resolved by removing the redundant server-side re-definition of CustomUserStatusEndpoints +// and the manual module augmentation. The endpoint types are now correctly resolved through the +// global `Endpoints` interface provided by the `@rocket.chat/rest-typings` package, following +// the project's modern API migration patterns. -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/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index deb8359b1f896..b760f0a567204 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -211,7 +211,7 @@ export * from './v1/videoConference'; export * from './v1/assets'; export * from './v1/channels'; export * from './v1/customSounds'; -export type * from './v1/customUserStatus'; +export * from './v1/customUserStatus'; export * from './v1/subscriptionsEndpoints'; export type * from './v1/mailer'; export * from './v1/mailer/MailerParamsPOST'; diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index 742e16934e1ee..be0876097e26c 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -1,17 +1,74 @@ import type { ICustomUserStatus } from '@rocket.chat/core-typings'; +import type { JSONSchemaType } from 'ajv'; + +import { ajv } from './Ajv'; + +// This file defines the API contract and AJV validation schemas for Custom User Status endpoints. +// It serves as the single source of truth for both the client (types) and the server (validation). + +export type CustomUserStatusCreateProps = { + name: string; + statusType?: string; +}; + +export type CustomUserStatusDeleteProps = { + customUserStatusId: string; +}; + +export type CustomUserStatusUpdateProps = { + _id: string; + name?: string; + statusType?: string; +}; export type CustomUserStatusEndpoints = { '/v1/custom-user-status.create': { - POST: (params: { name: string; statusType?: string }) => { + POST: (params: CustomUserStatusCreateProps) => { customUserStatus: ICustomUserStatus; }; }; '/v1/custom-user-status.delete': { - POST: (params: { customUserStatusId: string }) => void; + POST: (params: CustomUserStatusDeleteProps) => void; }; '/v1/custom-user-status.update': { - POST: (params: { _id: string; name?: string; statusType?: string }) => { + POST: (params: CustomUserStatusUpdateProps) => { customUserStatus: ICustomUserStatus; }; }; }; + +const customUserStatusCreatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + name: { type: 'string' }, + statusType: { type: 'string', nullable: true }, + }, + required: ['name'], + additionalProperties: false, +}; + +export const isCustomUserStatusCreateProps = ajv.compile(customUserStatusCreatePropsSchema); + +const customUserStatusDeletePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + customUserStatusId: { type: 'string' }, + }, + required: ['customUserStatusId'], + additionalProperties: false, +}; + +export const isCustomUserStatusDeleteProps = ajv.compile(customUserStatusDeletePropsSchema); + +const customUserStatusUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + _id: { type: 'string' }, + name: { type: 'string', nullable: true }, + statusType: { type: 'string', nullable: true }, + }, + required: ['_id'], + additionalProperties: false, +}; + +export const isCustomUserStatusUpdateProps = ajv.compile(customUserStatusUpdatePropsSchema);