diff --git a/.changeset/flat-candles-walk.md b/.changeset/flat-candles-walk.md new file mode 100644 index 0000000000000..41e169237e6a0 --- /dev/null +++ b/.changeset/flat-candles-walk.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat subscriptions.getOne 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/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts index b6e406bbfc969..47233e60b7fae 100644 --- a/apps/meteor/app/api/server/v1/subscriptions.ts +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -1,7 +1,10 @@ -import { Rooms, Subscriptions } from '@rocket.chat/models'; +import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; +import { Subscriptions } from '@rocket.chat/models'; import { + ajv, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, isSubscriptionsGetProps, - isSubscriptionsGetOneProps, isSubscriptionsReadProps, isSubscriptionsUnreadProps, } from '@rocket.chat/rest-typings'; @@ -10,6 +13,7 @@ import { Meteor } from 'meteor/meteor'; import { readMessages } from '../../../../server/lib/readMessages'; import { getSubscriptions } from '../../../../server/publications/subscription'; import { unreadMessages } from '../../../message-mark-as-unread/server/unreadMessages'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; API.v1.addRoute( @@ -44,24 +48,162 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +type SubscriptionsGetOne = { roomId: IRoom['_id'] }; + +const SubscriptionsGetOneSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + minLength: 1, + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +const isSubscriptionsGetOneProps = ajv.compile(SubscriptionsGetOneSchema); + +const subscriptionsEndpoints = API.v1.get( 'subscriptions.getOne', { authRequired: true, - validateParams: isSubscriptionsGetOneProps, + query: isSubscriptionsGetOneProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ subscription: ISubscription | null }>({ + type: 'object', + properties: { + subscription: { + anyOf: [ + { type: 'null' }, + { + type: 'object', + properties: { + _id: { type: 'string' }, + open: { type: 'boolean' }, + alert: { type: 'boolean' }, + unread: { type: 'number' }, + userMentions: { type: 'number' }, + groupMentions: { type: 'number' }, + ts: { type: 'string' }, + rid: { type: 'string' }, + u: { + type: 'object', + properties: { + _id: { type: 'string' }, + username: { type: 'string' }, + name: { type: 'string' }, + }, + }, + v: { + type: 'object', + properties: { + _id: { type: 'string' }, + username: { type: 'string' }, + name: { type: 'string' }, + status: { type: 'string' }, + token: { type: 'string' }, + }, + }, + _updatedAt: { type: 'string' }, + ls: { type: 'string' }, + name: { type: 'string' }, + fname: { type: 'string' }, + t: { + type: 'string', + enum: ['c', 'd', 'p', 'l', 'v'], + description: "Type of room. 'c' = channel, 'd' = direct, 'p' = private, 'l' = livechat, 'v' = video or voice.", + }, + roles: { + type: 'array', + items: { type: 'string' }, + }, + lr: { type: 'string' }, + tunread: { + type: 'array', + items: { type: 'string' }, + }, + tunreadUser: { + type: 'array', + items: { type: 'string' }, + }, + tunreadGroup: { + type: 'array', + items: { type: 'string' }, + }, + f: { type: 'boolean' }, + hideUnreadStatus: { type: 'boolean', enum: [true] }, + hideMentionStatus: { type: 'boolean', enum: [true] }, + teamMain: { type: 'boolean' }, + teamId: { type: 'string' }, + broadcast: { type: 'boolean', enum: [true] }, + prid: { type: 'string' }, + onHold: { type: 'boolean' }, + encrypted: { type: 'boolean' }, + E2EKey: { type: 'string' }, + E2ESuggestedKey: { type: 'string' }, + unreadAlert: { type: 'string', enum: ['default', 'all', 'mentions', 'nothing'] }, + archived: { type: 'boolean' }, + code: { type: 'string' }, + audioNotificationValue: { type: 'string' }, + desktopNotifications: { type: 'string', enum: ['all', 'mentions', 'nothing'] }, + mobilePushNotifications: { type: 'string', enum: ['all', 'mentions', 'nothing'] }, + emailNotifications: { type: 'string', enum: ['all', 'mentions', 'nothing'] }, + userHighlights: { type: 'array', items: { type: 'string' } }, + blocked: { type: 'boolean' }, + blocker: { type: 'string' }, + autoTranslate: { type: 'boolean' }, + autoTranslateLanguage: { type: 'string' }, + disableNotifications: { type: 'boolean' }, + muteGroupMentions: { type: 'boolean' }, + ignored: { + type: 'array', + items: { type: 'string' }, + }, + department: {}, + desktopPrefOrigin: { type: 'string', enum: ['subscription', 'user'] }, + mobilePrefOrigin: { type: 'string', enum: ['subscription', 'user'] }, + emailPrefOrigin: { type: 'string', enum: ['subscription', 'user'] }, + customFields: {}, + oldRoomKeys: { + type: 'array', + items: { + type: 'object', + properties: { e2eKeyId: { type: 'string' }, ts: { type: 'string' }, E2EKey: { type: 'string' } }, + }, + }, + suggestedOldRoomKeys: { + type: 'array', + items: { + type: 'object', + properties: { e2eKeyId: { type: 'string' }, ts: { type: 'string' }, E2EKey: { type: 'string' } }, + }, + }, + }, + required: ['_id'], + additionalProperties: false, + }, + ], + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['subscription', 'success'], + additionalProperties: false, + }), + }, }, - { - async get() { - const { roomId } = this.queryParams; - if (!roomId) { - return API.v1.failure("The 'roomId' param is required"); - } + async function action() { + const { roomId } = this.queryParams; - return API.v1.success({ - subscription: await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId), - }); - }, + return API.v1.success({ + subscription: await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId), + }); }, ); @@ -115,3 +257,10 @@ API.v1.addRoute( }, }, ); + +export type SubscriptionsEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends SubscriptionsEndpoints {} +} diff --git a/apps/meteor/tests/end-to-end/api/subscriptions.ts b/apps/meteor/tests/end-to-end/api/subscriptions.ts index a03179569615a..d36d6739fcc0c 100644 --- a/apps/meteor/tests/end-to-end/api/subscriptions.ts +++ b/apps/meteor/tests/end-to-end/api/subscriptions.ts @@ -59,7 +59,8 @@ describe('[Subscriptions]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', "must have required property 'roomId' [invalid-params]"); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('error', "must have required property 'roomId'"); }) .end(done); }); diff --git a/packages/rest-typings/src/v1/subscriptionsEndpoints.ts b/packages/rest-typings/src/v1/subscriptionsEndpoints.ts index 9122c4c84ccac..1b6cfe2d654f8 100644 --- a/packages/rest-typings/src/v1/subscriptionsEndpoints.ts +++ b/packages/rest-typings/src/v1/subscriptionsEndpoints.ts @@ -3,8 +3,6 @@ import Ajv from 'ajv'; type SubscriptionsGet = { updatedSince?: string }; -type SubscriptionsGetOne = { roomId: IRoom['_id'] }; - type SubscriptionsRead = { rid: IRoom['_id']; readThreads?: boolean } | { roomId: IRoom['_id']; readThreads?: boolean }; type SubscriptionsUnread = { roomId: IRoom['_id'] } | { firstUnreadMessage: Pick }; @@ -27,19 +25,6 @@ const SubscriptionsGetSchema = { export const isSubscriptionsGetProps = ajv.compile(SubscriptionsGetSchema); -const SubscriptionsGetOneSchema = { - type: 'object', - properties: { - roomId: { - type: 'string', - }, - }, - required: ['roomId'], - additionalProperties: false, -}; - -export const isSubscriptionsGetOneProps = ajv.compile(SubscriptionsGetOneSchema); - const SubscriptionsReadSchema = { anyOf: [ { @@ -117,12 +102,6 @@ export type SubscriptionsEndpoints = { }; }; - '/v1/subscriptions.getOne': { - GET: (params: SubscriptionsGetOne) => { - subscription: ISubscription | null; - }; - }; - '/v1/subscriptions.read': { POST: (params: SubscriptionsRead) => void; };