Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/wet-roses-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/rest-typings": patch
---

Add OpenAPI support for the Rocket.Chat commands.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.
79 changes: 66 additions & 13 deletions apps/meteor/app/api/server/v1/commands.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,80 @@
import type { SlashCommand } from '@rocket.chat/core-typings';
import { Messages } from '@rocket.chat/models';
import { Random } from '@rocket.chat/random';
import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings';
import objectPath from 'object-path';

import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
import { executeSlashCommandPreview } from '../../../lib/server/methods/executeSlashCommandPreview';
import { getSlashCommandPreviews } from '../../../lib/server/methods/getSlashCommandPreviews';
import { slashCommands } from '../../../utils/server/slashCommand';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';
import { getLoggedInUser } from '../helpers/getLoggedInUser';
import { getPaginationItems } from '../helpers/getPaginationItems';

API.v1.addRoute(
type CommandsGetParams = { command: string };

const CommandsGetParamsSchema = {
type: 'object',
properties: {
command: { type: 'string' },
},
required: ['command'],
additionalProperties: false,
};

const isCommandsGetParams = ajv.compile<CommandsGetParams>(CommandsGetParamsSchema);

const commandsEndpoints = API.v1.get(
'commands.get',
{ authRequired: true },
{
get() {
const params = this.queryParams;
authRequired: true,
query: isCommandsGetParams,
response: {
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
200: ajv.compile<{
command: Pick<SlashCommand, 'clientOnly' | 'command' | 'description' | 'params' | 'providesPreview'>;
}>({
type: 'object',
properties: {
command: {
type: 'object',
properties: {
clientOnly: { type: 'boolean' },
command: { type: 'string' },
description: { type: 'string' },
params: { type: 'string' },
providesPreview: { type: 'boolean' },
},
required: ['command', 'providesPreview'],
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['command', 'success'],
additionalProperties: false,
}),
},
Comment on lines +37 to +61
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

200-response validator type omits success: true; tighten types and nested schema

The Ajv generic for 200 currently excludes success, while the schema requires it. This weakens extracted typings. Also consider freezing the nested command shape for cleaner OpenAPI docs.

Apply this diff:

-			200: ajv.compile<{
-				command: Pick<SlashCommand, 'clientOnly' | 'command' | 'description' | 'params' | 'providesPreview'>;
-			}>({
+			200: ajv.compile<{
+				command: Pick<SlashCommand, 'clientOnly' | 'command' | 'description' | 'params' | 'providesPreview'>;
+				success: true;
+			}>({
 				type: 'object',
 				properties: {
 					command: {
 						type: 'object',
 						properties: {
 							clientOnly: { type: 'boolean' },
 							command: { type: 'string' },
 							description: { type: 'string' },
 							params: { type: 'string' },
 							providesPreview: { type: 'boolean' },
 						},
 						required: ['command', 'providesPreview'],
+						additionalProperties: false,
 					},
 					success: {
 						type: 'boolean',
 						enum: [true],
 					},
 				},
 				required: ['command', 'success'],
 				additionalProperties: false,
 			}),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
200: ajv.compile<{
command: Pick<SlashCommand, 'clientOnly' | 'command' | 'description' | 'params' | 'providesPreview'>;
}>({
type: 'object',
properties: {
command: {
type: 'object',
properties: {
clientOnly: { type: 'boolean' },
command: { type: 'string' },
description: { type: 'string' },
params: { type: 'string' },
providesPreview: { type: 'boolean' },
},
required: ['command', 'providesPreview'],
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['command', 'success'],
additionalProperties: false,
}),
},
200: ajv.compile<{
command: Pick<SlashCommand, 'clientOnly' | 'command' | 'description' | 'params' | 'providesPreview'>;
success: true;
}>({
type: 'object',
properties: {
command: {
type: 'object',
properties: {
clientOnly: { type: 'boolean' },
command: { type: 'string' },
description: { type: 'string' },
params: { type: 'string' },
providesPreview: { type: 'boolean' },
},
required: ['command', 'providesPreview'],
additionalProperties: false,
},
success: {
type: 'boolean',
enum: [true],
},
},
required: ['command', 'success'],
additionalProperties: false,
}),
🤖 Prompt for AI Agents
In apps/meteor/app/api/server/v1/commands.ts around lines 37 to 61, the Ajv
generic for the 200 response omits the required success property and the nested
command type is not frozen; update the generic to include success: true (e.g.,
include success: true in the generic type) so the TypeScript type matches the
schema, and tighten/freeze the nested command shape (make the command property
readonly/explicitly typed or use a frozen/const-style typed structure) so the
nested command shape is exact for OpenAPI generation; ensure required fields
align between the generic and the schema and keep additionalProperties: false.

},

if (typeof params.command !== 'string') {
return API.v1.failure('The query param "command" must be provided.');
}
async function action() {
const params = this.queryParams;

const cmd = slashCommands.commands[params.command.toLowerCase()];
if (typeof params.command !== 'string') {
return API.v1.failure('The query param "command" must be provided.');
}

if (!cmd) {
return API.v1.failure(`There is no command in the system by the name of: ${params.command}`);
}
const cmd = slashCommands.commands[params.command.toLowerCase()];

return API.v1.success({ command: cmd });
},
if (!cmd) {
return API.v1.failure(`There is no command in the system by the name of: ${params.command}`);
}

return API.v1.success({ command: cmd });
},
);

Expand Down Expand Up @@ -335,3 +381,10 @@ API.v1.addRoute(
},
},
);

export type CommandsEndpoints = ExtractRoutesFromAPI<typeof commandsEndpoints>;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends CommandsEndpoints {}
}
2 changes: 1 addition & 1 deletion apps/meteor/tests/end-to-end/api/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('[Commands]', () => {
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body.error).to.be.equal('The query param "command" must be provided.');
expect(res.body.error).to.be.equal(`must have required property 'command'`);
})
.end(done);
});
Expand Down
5 changes: 0 additions & 5 deletions packages/rest-typings/src/v1/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@ import type { PaginatedRequest } from '../helpers/PaginatedRequest';
import type { PaginatedResult } from '../helpers/PaginatedResult';

export type CommandsEndpoints = {
'/v1/commands.get': {
GET: (params: { command: string }) => {
command: Pick<SlashCommand, 'clientOnly' | 'command' | 'description' | 'params' | 'providesPreview'>;
};
};
'/v1/commands.list': {
GET: (
params?: PaginatedRequest<{
Expand Down
Loading