From 2eab7bd4d30343827e34b78e221e2058bef1a599 Mon Sep 17 00:00:00 2001 From: amitb0ra Date: Sun, 1 Mar 2026 05:32:48 +0000 Subject: [PATCH] chore: migrate mailer endpoints to new OpenAPI pattern with AJV validation - Migrated mailer and mailer.unsubscribe POST endpoints from addRoute to chainable .post() pattern - Added inline AJV response schemas (200/400/401/403) for OpenAPI 3.0.3 doc generation - Moved type definitions and validators from rest-typings into handler file - Removed packages/rest-typings/src/v1/mailer.ts and mailer/ directory - Updated packages/rest-typings/src/index.ts to remove old imports and re-exports - Uses ExtractRoutesFromAPI + module augmentation for type extraction --- .changeset/migrate-mailer-openapi.md | 6 + apps/meteor/app/api/server/v1/mailer.ts | 116 ++++++++++++++---- packages/rest-typings/src/index.ts | 5 - packages/rest-typings/src/v1/mailer.ts | 12 -- .../src/v1/mailer/MailerParamsPOST.ts | 36 ------ .../v1/mailer/MailerUnsubscribeParamsPOST.ts | 22 ---- 6 files changed, 99 insertions(+), 98 deletions(-) create mode 100644 .changeset/migrate-mailer-openapi.md delete mode 100644 packages/rest-typings/src/v1/mailer.ts delete mode 100644 packages/rest-typings/src/v1/mailer/MailerParamsPOST.ts delete mode 100644 packages/rest-typings/src/v1/mailer/MailerUnsubscribeParamsPOST.ts diff --git a/.changeset/migrate-mailer-openapi.md b/.changeset/migrate-mailer-openapi.md new file mode 100644 index 0000000000000..de6e28b2c0d1e --- /dev/null +++ b/.changeset/migrate-mailer-openapi.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': patch +'@rocket.chat/rest-typings': patch +--- + +Migrated `mailer` and `mailer.unsubscribe` REST API endpoints from deprecated `addRoute` pattern to new chainable `.post()` pattern with inline AJV response schemas, enabling automatic OpenAPI 3.0.3 documentation generation. diff --git a/apps/meteor/app/api/server/v1/mailer.ts b/apps/meteor/app/api/server/v1/mailer.ts index 57fa62e85a6c3..cfebfa3897965 100644 --- a/apps/meteor/app/api/server/v1/mailer.ts +++ b/apps/meteor/app/api/server/v1/mailer.ts @@ -1,41 +1,111 @@ -import { isMailerProps, isMailerUnsubscribeProps } from '@rocket.chat/rest-typings'; +import { + ajv, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; import { sendMail } from '../../../mail-messages/server/functions/sendMail'; import { Mailer } from '../../../mail-messages/server/lib/Mailer'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; -API.v1.addRoute( - 'mailer', - { - authRequired: true, - validateParams: isMailerProps, - permissionsRequired: ['send-mail'], +type MailerProps = { + from: string; + subject: string; + body: string; + dryrun?: boolean; + query?: string; +}; + +const isMailerProps = ajv.compile({ + type: 'object', + properties: { + from: { type: 'string' }, + subject: { type: 'string' }, + body: { type: 'string' }, + dryrun: { type: 'boolean', nullable: true }, + query: { type: 'string', nullable: true }, + }, + required: ['from', 'subject', 'body'], + additionalProperties: false, +}); + +type MailerUnsubscribeProps = { + _id: string; + createdAt: string; +}; + +const isMailerUnsubscribeProps = ajv.compile({ + type: 'object', + properties: { + _id: { type: 'string' }, + createdAt: { type: 'string' }, }, - { - async post() { + required: ['_id', 'createdAt'], + additionalProperties: false, +}); + +const mailerEndpoints = API.v1 + .post( + 'mailer', + { + authRequired: true, + body: isMailerProps, + permissionsRequired: ['send-mail'], + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { const { from, subject, body, dryrun, query } = this.bodyParams; const result = await sendMail({ from, subject, body, dryrun: Boolean(dryrun), query }); return API.v1.success(result); }, - }, -); - -API.v1.addRoute( - 'mailer.unsubscribe', - { - authRequired: true, - validateParams: isMailerUnsubscribeProps, - rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 1 }, - }, - { - async post() { + ) + .post( + 'mailer.unsubscribe', + { + authRequired: true, + body: isMailerUnsubscribeProps, + rateLimiterOptions: { intervalTimeInMS: 60000, numRequestsAllowed: 1 }, + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { const { _id, createdAt } = this.bodyParams; await Mailer.unsubscribe(_id, createdAt); return API.v1.success(); }, - }, -); + ); + +export type MailerEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends MailerEndpoints {} +} diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index b0e2dacff7a85..f2539362f1b63 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -27,7 +27,6 @@ import type { IntegrationHooksEndpoints } from './v1/integrations/hooks'; import type { InvitesEndpoints } from './v1/invites'; import type { LDAPEndpoints } from './v1/ldap'; import type { LicensesEndpoints } from './v1/licenses'; -import type { MailerEndpoints } from './v1/mailer'; import type { MeEndpoints } from './v1/me'; import type { MiscEndpoints } from './v1/misc'; import type { ModerationEndpoints } from './v1/moderation'; @@ -81,7 +80,6 @@ export interface Endpoints E2eEndpoints, AssetsEndpoints, EmailInboxEndpoints, - MailerEndpoints, SubscriptionsEndpoints, AutoTranslateEndpoints, ImportEndpoints, @@ -213,9 +211,6 @@ export * from './v1/channels'; export * from './v1/customSounds'; export * from './v1/customUserStatus'; export * from './v1/subscriptionsEndpoints'; -export * from './v1/mailer'; -export * from './v1/mailer/MailerParamsPOST'; -export * from './v1/mailer/MailerUnsubscribeParamsPOST'; export * from './v1/misc'; export * from './v1/invites'; export * from './v1/dm'; diff --git a/packages/rest-typings/src/v1/mailer.ts b/packages/rest-typings/src/v1/mailer.ts deleted file mode 100644 index d084ad1bad902..0000000000000 --- a/packages/rest-typings/src/v1/mailer.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { MailerProps } from './mailer/MailerParamsPOST'; -import type { MailerUnsubscribeProps } from './mailer/MailerUnsubscribeParamsPOST'; - -export type MailerEndpoints = { - '/v1/mailer': { - POST: (params: MailerProps) => void; - }; - - '/v1/mailer.unsubscribe': { - POST: (params: MailerUnsubscribeProps) => void; - }; -}; diff --git a/packages/rest-typings/src/v1/mailer/MailerParamsPOST.ts b/packages/rest-typings/src/v1/mailer/MailerParamsPOST.ts deleted file mode 100644 index d022ccda930ff..0000000000000 --- a/packages/rest-typings/src/v1/mailer/MailerParamsPOST.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ajv } from '../Ajv'; - -export type MailerProps = { - from: string; - subject: string; - body: string; - dryrun?: boolean; - query?: string; -}; - -const MailerPropsSchema = { - type: 'object', - properties: { - from: { - type: 'string', - }, - subject: { - type: 'string', - }, - body: { - type: 'string', - }, - dryrun: { - type: 'boolean', - nullable: true, - }, - query: { - type: 'string', - nullable: true, - }, - }, - required: ['from', 'subject', 'body'], - additionalProperties: false, -}; - -export const isMailerProps = ajv.compile(MailerPropsSchema); diff --git a/packages/rest-typings/src/v1/mailer/MailerUnsubscribeParamsPOST.ts b/packages/rest-typings/src/v1/mailer/MailerUnsubscribeParamsPOST.ts deleted file mode 100644 index 225cd3860afc8..0000000000000 --- a/packages/rest-typings/src/v1/mailer/MailerUnsubscribeParamsPOST.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ajv } from '../Ajv'; - -export type MailerUnsubscribeProps = { - _id: string; - createdAt: string; -}; - -const MailerUnsubscribePropsSchema = { - type: 'object', - properties: { - _id: { - type: 'string', - }, - createdAt: { - type: 'string', - }, - }, - required: ['_id', 'createdAt'], - additionalProperties: false, -}; - -export const isMailerUnsubscribeProps = ajv.compile(MailerUnsubscribePropsSchema);