-
Notifications
You must be signed in to change notification settings - Fork 13.8k
chore(api): migrate omnichannel single-route endpoints to typed HTTP methods #41356
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
base: develop
Are you sure you want to change the base?
Changes from all commits
178bda6
ec2f1a1
48ae19a
d473c64
5f773e5
2343cc9
8df24b8
4eb0579
8c941dc
7e5e071
a50e32e
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,5 +1,12 @@ | ||
| import type { ILivechatBusinessHour } from '@rocket.chat/core-typings'; | ||
| import type { PaginatedRequest } from '@rocket.chat/rest-typings'; | ||
| import { | ||
| ajv, | ||
| ajvQuery, | ||
| validateBadRequestErrorResponse, | ||
| validateForbiddenErrorResponse, | ||
| validateUnauthorizedErrorResponse, | ||
| } from '@rocket.chat/rest-typings'; | ||
|
|
||
| import { API } from '../../../../../server/api'; | ||
| import { getPaginationItems } from '../../../../../server/api/lib/getPaginationItems'; | ||
|
|
@@ -9,7 +16,7 @@ declare module '@rocket.chat/rest-typings' { | |
| // eslint-disable-next-line @typescript-eslint/naming-convention | ||
| interface Endpoints { | ||
| '/v1/livechat/business-hours': { | ||
| GET: (params: PaginatedRequest) => { | ||
| GET: (params: PaginatedRequest<{ name?: string }>) => { | ||
| businessHours: ILivechatBusinessHour[]; | ||
| count: number; | ||
| offset: number; | ||
|
|
@@ -19,26 +26,65 @@ declare module '@rocket.chat/rest-typings' { | |
| } | ||
| } | ||
|
|
||
| API.v1.addRoute( | ||
| const businessHoursQueryValidator = ajvQuery.compile<PaginatedRequest<{ name?: string }>>({ | ||
| type: 'object', | ||
| properties: { | ||
| count: { type: 'number' }, | ||
| offset: { type: 'number' }, | ||
| sort: { type: 'string' }, | ||
| query: { type: 'string' }, | ||
| name: { type: 'string' }, | ||
| }, | ||
| additionalProperties: false, | ||
| }); | ||
|
|
||
| const businessHoursResponseSchema = ajv.compile<{ | ||
| businessHours: ILivechatBusinessHour[]; | ||
| count: number; | ||
| offset: number; | ||
| total: number; | ||
| }>({ | ||
| type: 'object', | ||
| properties: { | ||
| businessHours: { type: 'array', items: { $ref: '#/components/schemas/ILivechatBusinessHour' } }, | ||
| count: { type: 'number' }, | ||
| offset: { type: 'number' }, | ||
| total: { type: 'number' }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['businessHours', 'count', 'offset', 'total', 'success'], | ||
| additionalProperties: false, | ||
| }); | ||
|
|
||
| API.v1.get( | ||
| 'livechat/business-hours', | ||
| { authRequired: true, permissionsRequired: ['view-livechat-business-hours'], license: ['livechat-enterprise'] }, | ||
| { | ||
| async get() { | ||
| const { offset, count } = await getPaginationItems(this.queryParams); | ||
| const { sort } = await this.parseJsonQuery(); | ||
| const { name } = this.queryParams; | ||
|
|
||
| return API.v1.success( | ||
| await findBusinessHours( | ||
| this.userId, | ||
| { | ||
| offset, | ||
| count, | ||
| sort, | ||
| }, | ||
| name, | ||
| ), | ||
| ); | ||
| authRequired: true, | ||
| permissionsRequired: ['view-livechat-business-hours'], | ||
| license: ['livechat-enterprise'], | ||
| query: businessHoursQueryValidator, | ||
| response: { | ||
| 200: businessHoursResponseSchema, | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const { offset, count } = await getPaginationItems(this.queryParams); | ||
| const { sort } = await this.parseJsonQuery(); | ||
| const { name } = this.queryParams; | ||
|
|
||
| return API.v1.success({ | ||
| ...(await findBusinessHours( | ||
| this.userId, | ||
| { | ||
| offset, | ||
| count, | ||
| sort, | ||
| }, | ||
| name, | ||
| )), | ||
| }); | ||
| }, | ||
|
Comment on lines
+59
to
+89
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if a pagination/name query schema already exists for reuse
rg -n "isGET.*BusinessHours.*Params|PaginatedRequest" packages/rest-typings/src/v1/omnichannel.ts | head -30Repository: RocketChat/Rocket.Chat Length of output: 2498 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== route file =="
sed -n '1,220p' apps/meteor/ee/server/api/v1/omnichannel/business-hours.ts
echo
echo "== sibling GET route =="
rg -n "livechat/business-hour|query:" apps/meteor/ee/server/api/v1/omnichannel -n
echo
echo "== possible query schema definitions =="
rg -n "isGET.*BusinessHour|BusinessHours.*Params|PaginatedRequest<.*name" apps packages -g '*.ts' | head -80Repository: RocketChat/Rocket.Chat Length of output: 5971 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,220p' apps/meteor/ee/server/api/v1/omnichannel/business-hours.tsRepository: RocketChat/Rocket.Chat Length of output: 2230 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1,120p' apps/meteor/server/api/v1/omnichannel/businessHours.ts
echo
sed -n '3230,3270p' packages/rest-typings/src/v1/omnichannel.tsRepository: RocketChat/Rocket.Chat Length of output: 3883 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '3230,3265p' packages/rest-typings/src/v1/omnichannel.tsRepository: RocketChat/Rocket.Chat Length of output: 855 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== getPaginationItems =="
sed -n '1,220p' server/api/lib/getPaginationItems.ts
echo
echo "== parseJsonQuery usage in API class =="
rg -n "parseJsonQuery\\(" server apps/meteor -g '*.ts' -g '*.js' | head -40Repository: RocketChat/Rocket.Chat Length of output: 265 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== locate getPaginationItems =="
fd -a "getPaginationItems.ts" apps packages server
echo
echo "== locate parseJsonQuery implementation =="
fd -a "ApiClass.ts" apps packages server | head -20
echo
echo "== locate API class files =="
fd -a "api.ts" apps packages server | head -40Repository: RocketChat/Rocket.Chat Length of output: 2009 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== getPaginationItems =="
sed -n '1,220p' apps/meteor/server/api/lib/getPaginationItems.ts
echo
echo "== parseJsonQuery in ApiClass =="
rg -n "parseJsonQuery|queryParams" apps/meteor/server/api/ApiClass.ts
sed -n '1,260p' apps/meteor/server/api/ApiClass.tsRepository: RocketChat/Rocket.Chat Length of output: 10173 Add the 🤖 Prompt for AI Agents |
||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,27 +1,53 @@ | ||
| import { | ||
| ajv, | ||
| validateBadRequestErrorResponse, | ||
| validateForbiddenErrorResponse, | ||
| validateUnauthorizedErrorResponse, | ||
| } from '@rocket.chat/rest-typings'; | ||
|
|
||
| import { setSLAToInquiry } from './lib/inquiries'; | ||
| import { API } from '../../../../../server/api'; | ||
|
|
||
| API.v1.addRoute( | ||
| const isPUTLivechatInquirySetSlaParams = ajv.compile<{ roomId: string; sla: string }>({ | ||
| type: 'object', | ||
| properties: { | ||
| roomId: { type: 'string' }, | ||
| sla: { type: 'string' }, | ||
| }, | ||
| required: ['roomId', 'sla'], | ||
| additionalProperties: false, | ||
| }); | ||
|
|
||
| const inquirySetSlaResponseSchema = ajv.compile<void>({ | ||
| type: 'object', | ||
| properties: { success: { type: 'boolean', enum: [true] } }, | ||
| required: ['success'], | ||
| additionalProperties: false, | ||
| }); | ||
|
|
||
| API.v1.put( | ||
| 'livechat/inquiry.setSLA', | ||
| { | ||
| authRequired: true, | ||
| permissionsRequired: { | ||
| PUT: { permissions: ['view-l-room', 'manage-livechat-sla'], operation: 'hasAny' }, | ||
| }, | ||
| license: ['livechat-enterprise'], | ||
| }, | ||
| { | ||
| async put() { | ||
| const { roomId, sla } = this.bodyParams; | ||
| if (!roomId) { | ||
| return API.v1.failure("The 'roomId' param is required"); | ||
| } | ||
| await setSLAToInquiry({ | ||
| userId: this.userId, | ||
| roomId, | ||
| sla, | ||
| }); | ||
| return API.v1.success(); | ||
| body: isPUTLivechatInquirySetSlaParams, | ||
| response: { | ||
| 200: inquirySetSlaResponseSchema, | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const { roomId, sla } = this.bodyParams; | ||
| await setSLAToInquiry({ | ||
| userId: this.userId, | ||
| roomId, | ||
| sla, | ||
| }); | ||
| return API.v1.success(); | ||
| }, | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,37 +1,62 @@ | ||
| import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; | ||
| import { LivechatRooms } from '@rocket.chat/models'; | ||
| import { | ||
| ajv, | ||
| validateBadRequestErrorResponse, | ||
| validateForbiddenErrorResponse, | ||
| validateUnauthorizedErrorResponse, | ||
| } from '@rocket.chat/rest-typings'; | ||
|
|
||
| import { API } from '../../../../../server/api'; | ||
| import type { ExtractRoutesFromAPI } from '../../../../../server/api/ApiClass'; | ||
| import { canAccessRoomAsync } from '../../../../../server/lib/authorization/canAccessRoom'; | ||
| import { requestPdfTranscript } from '../../../lib/omnichannel/requestPdfTranscript'; | ||
|
|
||
| API.v1.addRoute( | ||
| const requestTranscriptResponseSchema = ajv.compile<void>({ | ||
| type: 'object', | ||
| properties: { success: { type: 'boolean', enum: [true] } }, | ||
| required: ['success'], | ||
| additionalProperties: false, | ||
| }); | ||
|
|
||
| const requestTranscriptEndpoints = API.v1.post( | ||
| 'omnichannel/:rid/request-transcript', | ||
| { authRequired: true, permissionsRequired: ['request-pdf-transcript'], license: ['livechat-enterprise'] }, | ||
| { | ||
| async post() { | ||
| const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'open' | 'v' | 't' | 'pdfTranscriptFileId'>>( | ||
| this.urlParams.rid, | ||
| { | ||
| projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 }, | ||
| }, | ||
| ); | ||
| if (!room) { | ||
| throw new Error('error-invalid-room'); | ||
| } | ||
|
|
||
| if (!(await canAccessRoomAsync(room, { _id: this.userId }))) { | ||
| throw new Error('error-not-allowed'); | ||
| } | ||
|
|
||
| // Flow is as follows: | ||
| // 1. On Test Mode, call Transcript.workOnPdf directly | ||
| // 2. On Normal Mode, call QueueWorker.queueWork to queue the work | ||
| // 3. OmnichannelTranscript.workOnPdf will be called by the worker to generate the transcript | ||
| // 4. We be happy :) | ||
| await requestPdfTranscript(room, this.userId); | ||
|
|
||
| return API.v1.success(); | ||
| authRequired: true, | ||
| permissionsRequired: ['request-pdf-transcript'], | ||
| license: ['livechat-enterprise'], | ||
| body: ajv.compile<undefined>({ type: 'object', additionalProperties: false }), | ||
| response: { | ||
|
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| 200: requestTranscriptResponseSchema, | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const room = await LivechatRooms.findOneById<Pick<IOmnichannelRoom, '_id' | 'open' | 'v' | 't' | 'pdfTranscriptFileId'>>( | ||
| this.urlParams.rid, | ||
| { | ||
| projection: { _id: 1, open: 1, v: 1, t: 1, pdfTranscriptFileId: 1 }, | ||
| }, | ||
| ); | ||
| if (!room) { | ||
| return API.v1.failure('error-invalid-room'); | ||
| } | ||
|
|
||
| if (!(await canAccessRoomAsync(room, { _id: this.userId }))) { | ||
| return API.v1.failure('error-not-allowed'); | ||
| } | ||
|
|
||
| await requestPdfTranscript(room, this.userId); | ||
|
|
||
| return API.v1.success(); | ||
| }, | ||
| ); | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| type RequestTranscriptEndpoints = ExtractRoutesFromAPI<typeof requestTranscriptEndpoints>; | ||
|
|
||
| declare module '@rocket.chat/rest-typings' { | ||
| // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface | ||
| interface Endpoints extends RequestTranscriptEndpoints {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,56 @@ | ||
| import { isGETLivechatAgentsAgentIdDepartmentsParams } from '@rocket.chat/rest-typings'; | ||
| import type { ILivechatDepartmentAgents } from '@rocket.chat/core-typings'; | ||
| import { | ||
| ajv, | ||
| isGETLivechatAgentsAgentIdDepartmentsParams, | ||
| validateBadRequestErrorResponse, | ||
| validateForbiddenErrorResponse, | ||
| validateUnauthorizedErrorResponse, | ||
| } from '@rocket.chat/rest-typings'; | ||
|
|
||
| import { API } from '../..'; | ||
| import { findAgentDepartments } from './lib/agents'; | ||
|
|
||
| API.v1.addRoute( | ||
| const agentDepartmentsResponseSchema = ajv.compile<{ departments: (ILivechatDepartmentAgents & { departmentName: string })[] }>({ | ||
| type: 'object', | ||
| properties: { | ||
| departments: { | ||
| type: 'array', | ||
| items: { | ||
| allOf: [ | ||
| { $ref: '#/components/schemas/ILivechatDepartmentAgents' }, | ||
| { | ||
| type: 'object', | ||
| properties: { departmentName: { type: 'string' } }, | ||
| required: ['departmentName'], | ||
| }, | ||
| ], | ||
| }, | ||
| }, | ||
| success: { type: 'boolean', enum: [true] }, | ||
| }, | ||
| required: ['departments', 'success'], | ||
| additionalProperties: false, | ||
| }); | ||
|
|
||
| API.v1.get( | ||
| 'livechat/agents/:agentId/departments', | ||
| { authRequired: true, permissionsRequired: ['view-l-room'], validateParams: isGETLivechatAgentsAgentIdDepartmentsParams }, | ||
| { | ||
| async get() { | ||
| const departments = await findAgentDepartments({ | ||
| enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true', | ||
| agentId: this.urlParams.agentId, | ||
| }); | ||
|
|
||
| return API.v1.success(departments); | ||
| authRequired: true, | ||
| permissionsRequired: ['view-l-room'], | ||
| query: isGETLivechatAgentsAgentIdDepartmentsParams, | ||
| response: { | ||
| 200: agentDepartmentsResponseSchema, | ||
| 400: validateBadRequestErrorResponse, | ||
| 401: validateUnauthorizedErrorResponse, | ||
| 403: validateForbiddenErrorResponse, | ||
| }, | ||
| }, | ||
| async function action() { | ||
| const departments = await findAgentDepartments({ | ||
| enabledDepartmentsOnly: this.queryParams.enabledDepartmentsOnly && this.queryParams.enabledDepartmentsOnly === 'true', | ||
| agentId: this.urlParams.agentId, | ||
| }); | ||
|
|
||
| return API.v1.success(departments); | ||
| }, | ||
| ); |
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.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 2501
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 6592
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 50378
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 50378
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 752
🏁 Script executed:
Repository: RocketChat/Rocket.Chat
Length of output: 5860
Response schema rejects business-hours that include departments.
findBusinessHoursassignsdepartmentsfrom a{ _id: 1 }projection, but the 200 schema still uses fullILivechatBusinessHour/ILivechatDepartmentshapes. Any business hour with departments will fail response validation; either fetch full department docs or narrow the schema to the projected shape.🤖 Prompt for AI Agents