diff --git a/.changeset/red-squids-develop.md b/.changeset/red-squids-develop.md new file mode 100644 index 0000000000000..8455fac859900 --- /dev/null +++ b/.changeset/red-squids-develop.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat integrations.history 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/integrations.ts b/apps/meteor/app/api/server/v1/integrations.ts index 5fb1781aa1411..97f577db220df 100644 --- a/apps/meteor/app/api/server/v1/integrations.ts +++ b/apps/meteor/app/api/server/v1/integrations.ts @@ -1,8 +1,9 @@ -import type { IIntegration, INewIncomingIntegration, INewOutgoingIntegration } from '@rocket.chat/core-typings'; +import type { IIntegration, INewIncomingIntegration, INewOutgoingIntegration, IIntegrationHistory } from '@rocket.chat/core-typings'; import { Integrations, IntegrationHistory } from '@rocket.chat/models'; +import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; import { + ajv, isIntegrationsCreateProps, - isIntegrationsHistoryProps, isIntegrationsRemoveProps, isIntegrationsGetProps, isIntegrationsUpdateProps, @@ -22,6 +23,7 @@ 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'; @@ -43,45 +45,118 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +type IntegrationsHistoryProps = PaginatedRequest<{ id: string }>; + +const integrationsHistorySchema = { + type: 'object', + properties: { + id: { type: 'string', nullable: false, minLength: 1 }, + offset: { type: 'number', nullable: true }, + count: { type: 'number', nullable: true }, + sort: { type: 'string', nullable: true }, + query: { type: 'string', nullable: true }, + }, + required: ['id'], + additionalProperties: false, +}; + +const isIntegrationsHistoryProps = ajv.compile(integrationsHistorySchema); + +const integrationsHistoryEndpoints = API.v1.get( 'integrations.history', { authRequired: true, validateParams: isIntegrationsHistoryProps, + query: isIntegrationsHistoryProps, permissionsRequired: { GET: { permissions: ['manage-outgoing-integrations', 'manage-own-outgoing-integrations'], operation: 'hasAny' }, }, + response: { + 400: ajv.compile<{ + error?: string; + errorType?: string; + stack?: string; + details?: string; + }>({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + stack: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + details: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + 401: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + status: { type: 'string' }, + message: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + 403: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + status: { type: 'string' }, + message: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + 200: ajv.compile>({ + type: 'object', + properties: { + history: { type: 'array' }, + offset: { type: 'string' }, + items: { type: 'integer' }, + count: { type: 'integer' }, + total: { type: 'integer' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + }, }, - { - async get() { - const { userId, queryParams } = this; - - if (!queryParams.id || queryParams.id.trim() === '') { - return API.v1.failure('Invalid integration id.'); - } - const { id } = queryParams; - const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields: projection, query } = await this.parseJsonQuery(); - const ourQuery = Object.assign(await mountIntegrationHistoryQueryBasedOnPermissions(userId, id), query); - - const { cursor, totalCount } = IntegrationHistory.findPaginated(ourQuery, { - sort: sort || { _updatedAt: -1 }, - skip: offset, - limit: count, - projection, - }); - - const [history, total] = await Promise.all([cursor.toArray(), totalCount]); - - return API.v1.success({ - history, - offset, - items: history.length, - count: history.length, - total, - }); - }, + async function action() { + const { userId, queryParams } = this; + + if (!queryParams.id || queryParams.id.trim() === '') { + return API.v1.failure('Invalid integration id.'); + } + + const { id } = queryParams; + const { offset, count } = await getPaginationItems(this.queryParams); + const { sort, fields: projection, query } = await this.parseJsonQuery(); + const ourQuery = Object.assign(await mountIntegrationHistoryQueryBasedOnPermissions(userId, id), query); + + const { cursor, totalCount } = IntegrationHistory.findPaginated(ourQuery, { + sort: sort || { _updatedAt: -1 }, + skip: offset, + limit: count, + projection, + }); + + const [history, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + history, + offset, + items: history.length, + count: history.length, + total, + }); }, ); @@ -272,3 +347,12 @@ API.v1.addRoute( }, }, ); + +type IntegrationsHistoryEndpoints = ExtractRoutesFromAPI; + +export type IntegrationsEndpoints = IntegrationsHistoryEndpoints; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends IntegrationsHistoryEndpoints {} +} diff --git a/packages/rest-typings/src/v1/integrations/IntegrationsHistoryProps.ts b/packages/rest-typings/src/v1/integrations/IntegrationsHistoryProps.ts deleted file mode 100644 index 8009d2c5f01a1..0000000000000 --- a/packages/rest-typings/src/v1/integrations/IntegrationsHistoryProps.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Ajv from 'ajv'; - -import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; - -const ajv = new Ajv({ coerceTypes: true }); - -export type IntegrationsHistoryProps = PaginatedRequest<{ id: string }>; - -const integrationsHistorySchema = { - type: 'object', - properties: { - id: { type: 'string', nullable: false, minLength: 1 }, - offset: { type: 'number', nullable: true }, - count: { type: 'number', nullable: true }, - sort: { type: 'string', nullable: true }, - query: { type: 'string', nullable: true }, - }, - required: ['id'], - additionalProperties: false, -}; - -export const isIntegrationsHistoryProps = ajv.compile(integrationsHistorySchema); diff --git a/packages/rest-typings/src/v1/integrations/index.ts b/packages/rest-typings/src/v1/integrations/index.ts index a3ab979138548..b886bdd85172f 100644 --- a/packages/rest-typings/src/v1/integrations/index.ts +++ b/packages/rest-typings/src/v1/integrations/index.ts @@ -1,7 +1,6 @@ export * from './integrations'; export * from './IntegrationsCreateProps'; -export * from './IntegrationsHistoryProps'; export * from './IntegrationsRemoveProps'; export * from './IntegrationsGetProps'; export * from './IntegrationsUpdateProps'; diff --git a/packages/rest-typings/src/v1/integrations/integrations.ts b/packages/rest-typings/src/v1/integrations/integrations.ts index c2833f20bdbe0..2f13815880e7a 100644 --- a/packages/rest-typings/src/v1/integrations/integrations.ts +++ b/packages/rest-typings/src/v1/integrations/integrations.ts @@ -1,8 +1,7 @@ -import type { IIntegration, IIntegrationHistory } from '@rocket.chat/core-typings'; +import type { IIntegration } 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'; import type { IntegrationsUpdateProps } from './IntegrationsUpdateProps'; @@ -13,13 +12,6 @@ export type IntegrationsEndpoints = { POST: (params: IntegrationsCreateProps) => { integration: IIntegration }; }; - '/v1/integrations.history': { - GET: (params: IntegrationsHistoryProps) => PaginatedResult<{ - history: IIntegrationHistory[]; - items: number; - }>; - }; - '/v1/integrations.list': { GET: (params: IntegrationsListProps) => PaginatedResult<{ integrations: IIntegration[];