-
Notifications
You must be signed in to change notification settings - Fork 13.6k
refactor(api): migrate integrations.* endpoints from addRoute to new chained router API #39374
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,19 @@ | ||
| import type { IIntegration, INewIncomingIntegration, INewOutgoingIntegration } from '@rocket.chat/core-typings'; | ||
| import type { IIntegration, IIntegrationHistory, INewIncomingIntegration, INewOutgoingIntegration } from '@rocket.chat/core-typings'; | ||
| import { Integrations, IntegrationHistory } from '@rocket.chat/models'; | ||
| import type { PaginatedResult } from '@rocket.chat/rest-typings'; | ||
| import { | ||
| ajv, | ||
| isIntegrationsCreateProps, | ||
| isIntegrationsHistoryProps, | ||
| isIntegrationsRemoveProps, | ||
| isIntegrationsGetProps, | ||
| isIntegrationsUpdateProps, | ||
| isIntegrationsListProps, | ||
| validateUnauthorizedErrorResponse, | ||
| validateBadRequestErrorResponse, | ||
| validateForbiddenErrorResponse, | ||
| } from '@rocket.chat/rest-typings'; | ||
| import { escapeRegExp } from '@rocket.chat/string-helpers'; | ||
| import { Match, check } from 'meteor/check'; | ||
| import type { Filter } from 'mongodb'; | ||
|
|
||
| import { | ||
|
|
@@ -22,10 +26,314 @@ 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'; | ||
|
|
||
| const integrationResponseSchema = { | ||
| type: 'object', | ||
| properties: { | ||
| integration: { type: 'object', additionalProperties: true }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['integration', 'success'], | ||
| additionalProperties: false, | ||
| } as const; | ||
|
|
||
| const integrationsEndpoints = API.v1 | ||
| .post( | ||
| 'integrations.create', | ||
| { | ||
| authRequired: true, | ||
| body: isIntegrationsCreateProps, | ||
| response: { | ||
| 200: ajv.compile<{ integration: IIntegration }>(integrationResponseSchema), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| switch (this.bodyParams.type) { | ||
| case 'webhook-outgoing': | ||
| return API.v1.success({ integration: await addOutgoingIntegration(this.userId, this.bodyParams as INewOutgoingIntegration) }); | ||
| case 'webhook-incoming': | ||
| return API.v1.success({ integration: await addIncomingIntegration(this.userId, this.bodyParams as INewIncomingIntegration) }); | ||
| default: | ||
| return API.v1.failure('Invalid integration type.'); | ||
| } | ||
| }, | ||
| ) | ||
| .get( | ||
| 'integrations.history', | ||
| { | ||
| authRequired: true, | ||
| query: isIntegrationsHistoryProps, | ||
| permissionsRequired: { permissions: ['manage-outgoing-integrations', 'manage-own-outgoing-integrations'], operation: 'hasAny' }, | ||
| response: { | ||
| 200: ajv.compile<PaginatedResult<{ history: IIntegrationHistory[]; items: number }>>({ | ||
| type: 'object', | ||
| properties: { | ||
| history: { type: 'array', items: { type: 'object', additionalProperties: true } }, | ||
| offset: { type: 'number' }, | ||
| count: { type: 'number' }, | ||
| items: { type: 'number' }, | ||
| total: { type: 'number' }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['history', 'offset', 'count', 'items', 'total', 'success'], | ||
| additionalProperties: false, | ||
| }), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| 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, | ||
| }); | ||
| }, | ||
| ) | ||
| .get( | ||
| 'integrations.list', | ||
| { | ||
| authRequired: true, | ||
| query: isIntegrationsListProps, | ||
| permissionsRequired: { | ||
| permissions: [ | ||
| 'manage-outgoing-integrations', | ||
| 'manage-own-outgoing-integrations', | ||
| 'manage-incoming-integrations', | ||
| 'manage-own-incoming-integrations', | ||
| ], | ||
| operation: 'hasAny', | ||
| }, | ||
| response: { | ||
| 200: ajv.compile<PaginatedResult<{ integrations: IIntegration[]; items: number }>>({ | ||
| type: 'object', | ||
| properties: { | ||
| integrations: { type: 'array', items: { type: 'object', additionalProperties: true } }, | ||
| offset: { type: 'number' }, | ||
| count: { type: 'number' }, | ||
| items: { type: 'number' }, | ||
| total: { type: 'number' }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['integrations', 'offset', 'count', 'items', 'total', 'success'], | ||
| additionalProperties: false, | ||
| }), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const { offset, count } = await getPaginationItems(this.queryParams); | ||
| const { sort, fields, query } = await this.parseJsonQuery(); | ||
| const { name, type } = this.queryParams; | ||
|
|
||
| const filter = { | ||
| ...query, | ||
| ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), | ||
| ...(type ? { type } : {}), | ||
| }; | ||
|
|
||
| const ourQuery = Object.assign(await mountIntegrationQueryBasedOnPermissions(this.userId), filter) as Filter<IIntegration>; | ||
|
|
||
| const { cursor, totalCount } = Integrations.findPaginated(ourQuery, { | ||
| sort: sort || { ts: -1 }, | ||
| skip: offset, | ||
| limit: count, | ||
| projection: fields, | ||
| }); | ||
|
|
||
| const [integrations, total] = await Promise.all([cursor.toArray(), totalCount]); | ||
|
|
||
| return API.v1.success({ | ||
| integrations, | ||
| offset, | ||
| items: integrations.length, | ||
| count: integrations.length, | ||
| total, | ||
| }); | ||
| }, | ||
| ) | ||
| .post( | ||
| 'integrations.remove', | ||
| { | ||
| authRequired: true, | ||
| body: isIntegrationsRemoveProps, | ||
| permissionsRequired: { | ||
| permissions: [ | ||
| 'manage-outgoing-integrations', | ||
| 'manage-own-outgoing-integrations', | ||
| 'manage-incoming-integrations', | ||
| 'manage-own-incoming-integrations', | ||
| ], | ||
| operation: 'hasAny', | ||
| }, | ||
| response: { | ||
| 200: ajv.compile<{ integration: IIntegration }>(integrationResponseSchema), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const { bodyParams } = this; | ||
|
|
||
| let integration: IIntegration | null = null; | ||
| switch (bodyParams.type) { | ||
| case 'webhook-outgoing': | ||
| if (!bodyParams.target_url && !bodyParams.integrationId) { | ||
| return API.v1.failure('An integrationId or target_url needs to be provided.'); | ||
| } | ||
|
|
||
| if (bodyParams.target_url) { | ||
| integration = await Integrations.findOne({ urls: bodyParams.target_url }); | ||
| } else if (bodyParams.integrationId) { | ||
| integration = await Integrations.findOne({ _id: bodyParams.integrationId }); | ||
| } | ||
|
|
||
| if (!integration) { | ||
| return API.v1.failure('No integration found.'); | ||
| } | ||
|
|
||
| await deleteOutgoingIntegration(integration._id, this.userId); | ||
|
|
||
| return API.v1.success({ | ||
| integration, | ||
| }); | ||
| case 'webhook-incoming': | ||
| if (!bodyParams.integrationId) { | ||
| return API.v1.failure('An integrationId needs to be provided.'); | ||
| } | ||
|
|
||
| integration = await Integrations.findOne({ _id: bodyParams.integrationId }); | ||
|
|
||
| if (!integration) { | ||
| return API.v1.failure('No integration found.'); | ||
| } | ||
|
|
||
| await deleteIncomingIntegration(integration._id, this.userId); | ||
|
|
||
| return API.v1.success({ | ||
| integration, | ||
| }); | ||
| default: | ||
| return API.v1.failure('Invalid integration type.'); | ||
| } | ||
| }, | ||
| ) | ||
| .get( | ||
| 'integrations.get', | ||
| { | ||
| authRequired: true, | ||
| query: isIntegrationsGetProps, | ||
| response: { | ||
| 200: ajv.compile<{ integration: IIntegration }>(integrationResponseSchema), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| }, | ||
| }, | ||
| 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, | ||
| }), | ||
| }); | ||
| }, | ||
| ) | ||
| .put( | ||
| 'integrations.update', | ||
| { | ||
| authRequired: true, | ||
| body: isIntegrationsUpdateProps, | ||
| response: { | ||
| 200: ajv.compile<{ integration: IIntegration | null }>(integrationResponseSchema), | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const { bodyParams } = this; | ||
|
|
||
| let integration; | ||
| switch (bodyParams.type) { | ||
| case 'webhook-outgoing': | ||
| if (bodyParams.target_url) { | ||
| integration = await Integrations.findOne({ urls: bodyParams.target_url }); | ||
| } else if (bodyParams.integrationId) { | ||
| integration = await Integrations.findOne({ _id: bodyParams.integrationId }); | ||
| } | ||
|
|
||
| if (!integration) { | ||
| return API.v1.failure('No integration found.'); | ||
| } | ||
|
|
||
| await updateOutgoingIntegration(this.userId, integration._id, bodyParams); | ||
|
|
||
| return API.v1.success({ | ||
| integration: await Integrations.findOne({ _id: integration._id }), | ||
| }); | ||
| case 'webhook-incoming': | ||
| integration = await Integrations.findOne({ _id: bodyParams.integrationId }); | ||
|
|
||
| if (!integration) { | ||
| return API.v1.failure('No integration found.'); | ||
| } | ||
|
|
||
| await updateIncomingIntegration(this.userId, integration._id, bodyParams); | ||
|
|
||
| return API.v1.success({ | ||
| integration: await Integrations.findOne({ _id: integration._id }), | ||
| }); | ||
| default: | ||
| return API.v1.failure('Invalid integration type.'); | ||
| } | ||
| }, | ||
| ); | ||
|
Comment on lines
+44
to
+327
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove legacy The new chained routes are added here, but the legacy registrations for the same paths still exist starting at Line 337. This creates duplicate route definitions ( 🤖 Prompt for AI Agents |
||
|
|
||
| export type IntegrationsEndpointsNew = ExtractRoutesFromAPI<typeof integrationsEndpoints>; | ||
|
|
||
| declare module '@rocket.chat/rest-typings' { | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface | ||
| interface Endpoints extends IntegrationsEndpointsNew {} | ||
| } | ||
|
|
||
|
|
||
| API.v1.addRoute( | ||
| 'integrations.create', | ||
| { authRequired: true, validateParams: isIntegrationsCreateProps }, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: The integrations.* routes are registered twice: the new chained API definitions were added, but the legacy API.v1.addRoute blocks for the same endpoints still remain below. This risks duplicate-route errors or handler overrides and should be cleaned up during the migration.
Prompt for AI agents