From 6421f00a4167cc023eae3fe211943a66faea201c Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 2 Sep 2025 15:42:56 +0300 Subject: [PATCH 01/10] feat: add openapi support for integrations.get api --- apps/meteor/app/api/server/v1/integrations.ts | 87 ++++++++++++++----- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 5fb1781aa1411..ef02567129fe2 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -1,10 +1,12 @@ import type { IIntegration, INewIncomingIntegration, INewOutgoingIntegration } from '@rocket.chat/core-typings'; import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { + ajv, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, isIntegrationsCreateProps, isIntegrationsHistoryProps, isIntegrationsRemoveProps, - isIntegrationsGetProps, isIntegrationsUpdateProps, isIntegrationsListProps, } from '@rocket.chat/rest-typings'; @@ -22,10 +24,65 @@ import { updateIncomingIntegration } from '../../../integrations/server/methods/ import { addOutgoingIntegration } from '../../../integrations/server/methods/outgoing/addOutgoingIntegration'; import { deleteOutgoingIntegration } from '../../../integrations/server/methods/outgoing/deleteOutgoingIntegration'; import { updateOutgoingIntegration } from '../../../integrations/server/methods/outgoing/updateOutgoingIntegration'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { findOneIntegration } from '../lib/integrations'; +type IntegrationsGetProps = { integrationId: string; createdBy?: string }; + +const integrationsGetSchema = { + type: 'object', + properties: { + integrationId: { + type: 'string', + nullable: false, + }, + createdBy: { + type: 'string', + nullable: true, + }, + }, + required: ['integrationId'], +}; + +const isIntegrationsGetProps = ajv.compile(integrationsGetSchema); + +const integrationsEndpoints = API.v1.get( + 'integrations.get', + { + authRequired: true, + query: isIntegrationsGetProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ integration: IIntegration }>({ + type: 'object', + properties: { + integration: { $ref: '#/components/schemas/IIntegration' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['integration', 'success'], + }), + }, + }, + + async function action() { + const { integrationId, createdBy } = this.queryParams; + if (!integrationId) { + return API.v1.failure('The query parameter "integrationId" is required.'); + } + + return API.v1.success({ + integration: await findOneIntegration({ + userId: this.userId, + integrationId, + createdBy, + }), + }); + }, +); + API.v1.addRoute( 'integrations.create', { authRequired: true, validateParams: isIntegrationsCreateProps }, @@ -208,27 +265,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'integrations.get', - { authRequired: true, validateParams: isIntegrationsGetProps }, - { - async get() { - const { integrationId, createdBy } = this.queryParams; - if (!integrationId) { - return API.v1.failure('The query parameter "integrationId" is required.'); - } - - return API.v1.success({ - integration: await findOneIntegration({ - userId: this.userId, - integrationId, - createdBy, - }), - }); - }, - }, -); - API.v1.addRoute( 'integrations.update', { authRequired: true, validateParams: isIntegrationsUpdateProps }, @@ -272,3 +308,10 @@ API.v1.addRoute( }, }, ); + +export type IntegrationsEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends IntegrationsEndpoints {} +} From ba31d0a2628efbae40280e1196e397d6ab89f4a3 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 2 Sep 2025 15:43:27 +0300 Subject: [PATCH 02/10] refactor: remove unused integrations get from rest-typings --- .../v1/integrations/IntegrationsGetProps.ts | 22 ------------------- .../rest-typings/src/v1/integrations/index.ts | 1 - .../src/v1/integrations/integrations.ts | 5 ----- 3 files changed, 28 deletions(-) delete mode 100644 packages/rest-typings/src/v1/integrations/IntegrationsGetProps.ts diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsGetProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsGetProps.ts deleted file mode 100644 index 2ab243db58203..0000000000000 --- a/packages/rest-typings/src/v1/integrations/IntegrationsGetProps.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Ajv from 'ajv'; - -const ajv = new Ajv(); - -export type IntegrationsGetProps = { integrationId: string; createdBy?: string }; - -const integrationsGetSchema = { - type: 'object', - properties: { - integrationId: { - type: 'string', - nullable: false, - }, - createdBy: { - type: 'string', - nullable: true, - }, - }, - required: ['integrationId'], -}; - -export const isIntegrationsGetProps = ajv.compile(integrationsGetSchema); diff --git a/packages/rest-typings/src/v1/integrations/index.ts b/packages/rest-typings/src/v1/integrations/index.ts index a3ab979138548..78e61cf388884 100644 --- a/packages/rest-typings/src/v1/integrations/index.ts +++ b/packages/rest-typings/src/v1/integrations/index.ts @@ -3,7 +3,6 @@ export * from './integrations'; export * from './IntegrationsCreateProps'; export * from './IntegrationsHistoryProps'; export * from './IntegrationsRemoveProps'; -export * from './IntegrationsGetProps'; export * from './IntegrationsUpdateProps'; export * from './IntegrationsListProps'; export * from './hooks/IntegrationHooksAddProps'; diff --git a/packages/rest-typings/src/v1/integrations/integrations.ts b/packages/rest-typings/src/v1/integrations/integrations.ts index c2833f20bdbe0..db087f4fe990c 100644 --- a/packages/rest-typings/src/v1/integrations/integrations.ts +++ b/packages/rest-typings/src/v1/integrations/integrations.ts @@ -1,7 +1,6 @@ import type { IIntegration, IIntegrationHistory } from '@rocket.chat/core-typings'; import type { IntegrationsCreateProps } from './IntegrationsCreateProps'; -import type { IntegrationsGetProps } from './IntegrationsGetProps'; import type { IntegrationsHistoryProps } from './IntegrationsHistoryProps'; import type { IntegrationsListProps } from './IntegrationsListProps'; import type { IntegrationsRemoveProps } from './IntegrationsRemoveProps'; @@ -33,10 +32,6 @@ export type IntegrationsEndpoints = { }; }; - '/v1/integrations.get': { - GET: (params: IntegrationsGetProps) => { integration: IIntegration }; - }; - '/v1/integrations.update': { PUT: (params: IntegrationsUpdateProps) => { integration: IIntegration | null }; }; From 383f1d953c8f624b67dd4e2e82b97859d5584f18 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Tue, 2 Sep 2025 15:44:00 +0300 Subject: [PATCH 03/10] refactor: using typia --- packages/core-typings/src/Ajv.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index 8d828e041b1bd..716e757062804 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 { IIntegration } from './IIntegration'; 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 | IIntegration], + '3.0' +>(); From b3a23be7bf90fd5a30dceeb6b8e34c9289368883 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Tue, 2 Sep 2025 15:47:04 +0300 Subject: [PATCH 04/10] Create eighty-ways-care.md --- .changeset/eighty-ways-care.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/eighty-ways-care.md diff --git a/.changeset/eighty-ways-care.md b/.changeset/eighty-ways-care.md new file mode 100644 index 0000000000000..e058243a499f8 --- /dev/null +++ b/.changeset/eighty-ways-care.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 integrations.get 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. From ed905c5472eff6fd1e9f0e81824d6edbb62727fe Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Thu, 4 Sep 2025 17:55:47 +0300 Subject: [PATCH 05/10] refactor: switch from type to interface since Typia has issues with type aliases --- apps/meteor/app/api/server/v1/integrations.ts | 14 +++++++++++--- packages/core-typings/src/Ajv.ts | 4 ++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index ef02567129fe2..4f112b8772b47 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -1,4 +1,10 @@ -import type { IIntegration, INewIncomingIntegration, INewOutgoingIntegration } from '@rocket.chat/core-typings'; +import type { + IIntegration, + INewIncomingIntegration, + INewOutgoingIntegration, + IIncomingIntegration, + IOutgoingIntegration, +} from '@rocket.chat/core-typings'; import { Integrations, IntegrationHistory } from '@rocket.chat/models'; import { ajv, @@ -56,10 +62,12 @@ const integrationsEndpoints = API.v1.get( response: { 400: validateBadRequestErrorResponse, 401: validateUnauthorizedErrorResponse, - 200: ajv.compile<{ integration: IIntegration }>({ + 200: ajv.compile<{ integration: IIncomingIntegration | IOutgoingIntegration }>({ type: 'object', properties: { - integration: { $ref: '#/components/schemas/IIntegration' }, + integration: { + anyOf: [{ $ref: '#/components/schemas/IIncomingIntegration' }, { $ref: '#/components/schemas/IOutgoingIntegration' }], + }, success: { type: 'boolean', enum: [true] }, }, required: ['integration', 'success'], diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index 716e757062804..55246541116d4 100644 --- a/packages/core-typings/src/Ajv.ts +++ b/packages/core-typings/src/Ajv.ts @@ -1,7 +1,7 @@ import typia from 'typia'; import type { ICustomSound } from './ICustomSound'; -import type { IIntegration } from './IIntegration'; +import type { IIncomingIntegration, IOutgoingIntegration } from './IIntegration'; import type { IInvite } from './IInvite'; import type { IMessage } from './IMessage'; import type { IOAuthApps } from './IOAuthApps'; @@ -9,6 +9,6 @@ import type { IPermission } from './IPermission'; import type { ISubscription } from './ISubscription'; export const schemas = typia.json.schemas< - [ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IIntegration], + [ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IIncomingIntegration | IOutgoingIntegration], '3.0' >(); From 22b680b09f028672c04f94e6fdc191b5f8d0476b Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Thu, 4 Sep 2025 18:57:40 +0300 Subject: [PATCH 06/10] chore: update the test error for invalid params --- apps/meteor/tests/end-to-end/api/incoming-integrations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index bdc1691f79869..6b5230227b636 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -593,7 +593,8 @@ describe('[Incoming Integrations]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', `must have required property 'integrationId' [invalid-params]`); + expect(res.body).to.have.property('errorType', 'error-invalid-params'); + expect(res.body).to.have.property('error', `must have required property 'integrationId'`); }) .end(done); }); From 2631d63f164df9aa5aef8c5156bffbfe5a043e48 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Thu, 4 Sep 2025 20:14:35 +0300 Subject: [PATCH 07/10] refactor: change the script and event to optional in the integration interface --- packages/core-typings/src/IIntegration.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-typings/src/IIntegration.ts b/packages/core-typings/src/IIntegration.ts index 22fa520818757..c7bec04c43583 100644 --- a/packages/core-typings/src/IIntegration.ts +++ b/packages/core-typings/src/IIntegration.ts @@ -13,7 +13,7 @@ export interface IIncomingIntegration extends IRocketChatRecord { token: string; scriptEnabled: boolean; - script: string; + script?: string; scriptCompiled?: string; scriptError?: Pick; @@ -45,7 +45,7 @@ export interface IOutgoingIntegration extends IRocketChatRecord { username: string; channel: string[]; - event: OutgoingIntegrationEvent; + event?: OutgoingIntegrationEvent; targetRoom?: string; urls?: string[]; triggerWords?: string[]; @@ -53,7 +53,7 @@ export interface IOutgoingIntegration extends IRocketChatRecord { token: string; scriptEnabled: boolean; - script: string; + script?: string; scriptCompiled?: string; scriptError?: Pick; runOnEdits?: boolean; From 725bdfc7febb7d63cff5322b7751f3248576c564 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Fri, 5 Sep 2025 15:47:42 +0300 Subject: [PATCH 08/10] refactor: remove redundant null check for integration.script --- apps/meteor/app/api/server/v1/integrations.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/api/server/v1/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 4f112b8772b47..e436c36f95998 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -81,13 +81,16 @@ const integrationsEndpoints = API.v1.get( return API.v1.failure('The query parameter "integrationId" is required.'); } - return API.v1.success({ - integration: await findOneIntegration({ - userId: this.userId, - integrationId, - createdBy, - }), + const integration = await findOneIntegration({ + userId: this.userId, + integrationId, + createdBy, }); + + // TODO : remove this line if the database doesnt return null value for script. since script should never be null + if (!integration.script) integration.script = ''; + + return API.v1.success({ integration }); }, ); From bcb54553b8de77620432b3ea63792c8e6255409b Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Sat, 6 Sep 2025 15:58:51 +0300 Subject: [PATCH 09/10] refactor: remove redundant null check for integration.script --- packages/core-typings/src/IIntegration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-typings/src/IIntegration.ts b/packages/core-typings/src/IIntegration.ts index c7bec04c43583..853b6dc4d4f30 100644 --- a/packages/core-typings/src/IIntegration.ts +++ b/packages/core-typings/src/IIntegration.ts @@ -45,7 +45,7 @@ export interface IOutgoingIntegration extends IRocketChatRecord { username: string; channel: string[]; - event?: OutgoingIntegrationEvent; + event: OutgoingIntegrationEvent; targetRoom?: string; urls?: string[]; triggerWords?: string[]; From 9946d04247347d223b9e9a6aa5a8f0cc84ee68f3 Mon Sep 17 00:00:00 2001 From: ahmed-n-abdeltwab Date: Sat, 6 Sep 2025 16:39:07 +0300 Subject: [PATCH 10/10] chore: return the test as it was --- apps/meteor/tests/end-to-end/api/incoming-integrations.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts index 6b5230227b636..bdc1691f79869 100644 --- a/apps/meteor/tests/end-to-end/api/incoming-integrations.ts +++ b/apps/meteor/tests/end-to-end/api/incoming-integrations.ts @@ -593,8 +593,7 @@ describe('[Incoming Integrations]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-params'); - expect(res.body).to.have.property('error', `must have required property 'integrationId'`); + expect(res.body).to.have.property('error', `must have required property 'integrationId' [invalid-params]`); }) .end(done); });