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/weak-terms-shave.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 emoji-custom.create 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.
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ export type TypedThis<TOptions extends TypedOptions, TPath extends string = ''>
query: Record<string, unknown>;
}>;
bodyParams: TOptions['body'] extends ValidateFunction<infer Body> ? Body : never;

request: Request;
requestIp?: string;
route: string;
response: Response;
Expand Down
100 changes: 69 additions & 31 deletions apps/meteor/app/api/server/v1/emoji-custom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Media } from '@rocket.chat/core-services';
import type { IEmojiCustom } from '@rocket.chat/core-typings';
import { EmojiCustom } from '@rocket.chat/models';
import { isEmojiCustomList } from '@rocket.chat/rest-typings';
import { ajv, isEmojiCustomList } from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';

Expand All @@ -11,6 +11,7 @@ import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUp
import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom';
import { deleteEmojiCustom } from '../../../emoji-custom/server/methods/deleteEmojiCustom';
import { settings } from '../../../settings/server';
import type { ExtractRoutesFromAPI } from '../ApiClass';
import { API } from '../api';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { findEmojisCustom } from '../lib/emoji-custom';
Expand Down Expand Up @@ -103,45 +104,73 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
const emojiCustomCreateEndpoints = API.v1.post(
'emoji-custom.create',
{ authRequired: true },
{
async post() {
const emoji = await getUploadFormData(
{
request: this.request,
authRequired: true,
response: {
400: ajv.compile({
type: 'object',
properties: {
success: { type: 'boolean', enum: [false] },
stack: { type: 'string' },
error: { type: 'string' },
errorType: { type: 'string' },
details: { type: 'string' },
},
{ field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize') },
);
required: ['success'],
additionalProperties: false,
}),
200: ajv.compile<void>({
type: 'object',
properties: {
success: {
type: 'boolean',
enum: [true],
},
},
required: ['success'],
additionalProperties: false,
}),
},
Comment on lines +111 to +135
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Verify 400 response schema matches API.v1.failure() output.

The 400 response schema defines optional fields (stack, error, errorType, details), but API.v1.failure() at line 170 is called without arguments. Ensure the schema accurately reflects all possible 400 responses from this endpoint.

Run this script to check the API.v1.failure() implementation:


🏁 Script executed:

#!/bin/bash
# Description: Check API.v1.failure() signature and return shape

# Search for the failure method definition
ast-grep --pattern $'failure($$$) {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Locate API.v1.failure definition and its return shape

echo "Searching for .failure( occurrences across .ts files"
rg -nP '\.failure\(' -C3 -g '*.ts' || echo "No .failure calls found"

echo -e "\nSearching for class API definition"
rg -nP 'class\s+API\b' -C3 -g '*.ts' || echo "No class API found"

echo -e "\nSearching for exported failure helper"
rg -nP 'export\s+(const|function)\s+failure' -C3 -g '*.ts' || echo "No exported failure helper found"

Length of output: 201931


🏁 Script executed:

#!/bin/bash
# Locate ApiClass declaration and failure method definition

echo "ApiClass declaration:"
rg -nP 'class\s+ApiClass\b' -n apps/meteor/app/api/server/ApiClass.ts || echo "Class ApiClass not found"

echo -e "\nFailure method signature and context:"
rg -nP '^\s*(public|private|protected)\s+failure\s*\(' -n -C3 apps/meteor/app/api/server/ApiClass.ts || echo "failure method definition not found"

Length of output: 121


🏁 Script executed:

#!/bin/bash
# Locate the ApiClass.ts file
fd --extension ts ApiClass.ts

# Search for the ApiClass declaration
rg -nP 'class\s+ApiClass\b' -g 'apps/meteor/app/api/server/ApiClass.ts'

# Extract the failure method implementation from ApiClass.ts
sed -n '900,950p' apps/meteor/app/api/server/ApiClass.ts | rg -C3 'failure\('

Length of output: 472


🏁 Script executed:

# Print the first 200 lines of ApiClass.ts to locate the failure method definition
sed -n '1,200p' apps/meteor/app/api/server/ApiClass.ts

Length of output: 6409


🏁 Script executed:

#!/bin/bash
# Search for public success and failure method definitions
echo "-- public success occurrences --"
rg -nP 'public\s+success\(' -C3 apps/meteor/app/api/server/ApiClass.ts || echo "No public success found"

echo -e "\n-- public failure occurrences --"
rg -nP 'public\s+failure\(' -C3 apps/meteor/app/api/server/ApiClass.ts || echo "No public failure found"

echo -e "\n-- Context around where success and failure are called --"
sed -n '600,700p' apps/meteor/app/api/server/ApiClass.ts | rg -C3 '\.failure\(' || echo "No failure calls in this segment"
sed -n '600,700p' apps/meteor/app/api/server/ApiClass.ts | rg -C3 '\.success\(' || echo "No success calls in this segment"

Length of output: 343


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' apps/meteor/app/api/server/definition.ts

Length of output: 6259


Add missing message field to 400 schema
The default FailureResult body may include an optional message?: string, but your AJV schema forbids any properties beyond success, stack, error, errorType, and details. In apps/meteor/app/api/server/v1/emoji-custom.ts (lines 111–135), under the 400 schema’s properties, add

message: { type: 'string' },

so the schema aligns with API.v1.failure()’s possible output.

🤖 Prompt for AI Agents
In apps/meteor/app/api/server/v1/emoji-custom.ts around lines 111 to 135 the 400
AJV response schema omits the optional message field present in the default
FailureResult, causing valid error responses to be rejected; add a message
property with type 'string' to the 400 schema's properties so the schema allows
the optional message (i.e., include message: { type: 'string' } alongside stack,
error, errorType, details) while keeping required/additionalProperties as-is.

},
async function action() {
const emoji = await getUploadFormData(
{
request: this.request,
},
{
field: 'emoji',
sizeLimit: settings.get('FileUpload_MaxFileSize'),
},
);

const { fields, fileBuffer, mimetype } = emoji;
const { fields, fileBuffer, mimetype } = emoji;

const isUploadable = await Media.isImage(fileBuffer);
if (!isUploadable) {
throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image");
}
const isUploadable = await Media.isImage(fileBuffer);
if (!isUploadable) {
throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image");
}

const [, extension] = mimetype.split('/');
fields.extension = extension;
const [, extension] = mimetype.split('/');
fields.extension = extension;

try {
const emojiData = await insertOrUpdateEmoji(this.userId, {
...fields,
newFile: true,
aliases: fields.aliases || '',
name: fields.name,
extension: fields.extension,
});
try {
const emojiData = await insertOrUpdateEmoji(this.userId, {
...fields,
newFile: true,
aliases: fields.aliases || '',
name: fields.name,
extension: fields.extension,
});

await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData);
} catch (e) {
SystemLogger.error(e);
return API.v1.failure();
}
await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData);
} catch (e) {
SystemLogger.error(e);
return API.v1.failure();
}

return API.v1.success();
},
return API.v1.success();
},
);

Expand Down Expand Up @@ -219,3 +248,12 @@ API.v1.addRoute(
},
},
);

type EmojiCustomCreateEndpoints = ExtractRoutesFromAPI<typeof emojiCustomCreateEndpoints>;

export type EmojiCustomEndpoints = EmojiCustomCreateEndpoints;

declare module '@rocket.chat/rest-typings' {
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
interface Endpoints extends EmojiCustomCreateEndpoints {}
}
3 changes: 0 additions & 3 deletions packages/rest-typings/src/v1/emojiCustom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,6 @@ export type EmojiCustomEndpoints = {
'/v1/emoji-custom.delete': {
POST: (params: emojiCustomDeleteProps) => void;
};
'/v1/emoji-custom.create': {
POST: (params: { emoji: ICustomEmojiDescriptor }) => void;
};
'/v1/emoji-custom.update': {
POST: (params: { emoji: ICustomEmojiDescriptor }) => void;
};
Expand Down
Loading