Skip to content

Commit 066395a

Browse files
committed
fix(groups.create): migrate groups.create with AJV validation and schema types
- add AJV compilation for request and response validation - define TypeScript schema types for groups.create endpoint - align schema with API contracts - add yarn changeset for versioning
1 parent 4f88165 commit 066395a

File tree

5 files changed

+146
-129
lines changed

5 files changed

+146
-129
lines changed

.changeset/eleven-hounds-brake.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/rest-typings': minor
3+
'@rocket.chat/meteor': minor
4+
---
5+
6+
- Add migration of groups.create to support OpenAPI documentation

apps/meteor/app/api/server/v1/groups.ts

Lines changed: 140 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { Team, isMeteorError } from '@rocket.chat/core-services';
22
import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings';
33
import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models';
4-
import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsFilesProps } from '@rocket.chat/rest-typings';
4+
import {
5+
isGroupsOnlineProps,
6+
isGroupsMessagesProps,
7+
isGroupsFilesProps,
8+
ajv,
9+
validateBadRequestErrorResponse,
10+
validateUnauthorizedErrorResponse,
11+
validateForbiddenErrorResponse,
12+
} from '@rocket.chat/rest-typings';
513
import { isTruthy } from '@rocket.chat/tools';
614
import { check, Match } from 'meteor/check';
7-
import { Meteor } from 'meteor/meteor';
815
import type { Filter } from 'mongodb';
916

1017
import { eraseRoom } from '../../../../server/lib/eraseRoom';
@@ -31,12 +38,136 @@ import { executeGetRoomRoles } from '../../../lib/server/methods/getRoomRoles';
3138
import { leaveRoomMethod } from '../../../lib/server/methods/leaveRoom';
3239
import { executeUnarchiveRoom } from '../../../lib/server/methods/unarchiveRoom';
3340
import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser';
41+
import type { ExtractRoutesFromAPI } from '../ApiClass';
3442
import { API } from '../api';
3543
import { addUserToFileObj } from '../helpers/addUserToFileObj';
3644
import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage';
3745
import { getPaginationItems } from '../helpers/getPaginationItems';
3846
import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams';
3947

48+
type GroupsCreateProps = {
49+
name: string;
50+
members?: string[];
51+
customFields?: Record<string, any>;
52+
readOnly?: boolean;
53+
extraData?: {
54+
broadcast: boolean;
55+
encrypted: boolean;
56+
federated?: boolean;
57+
topic?: string;
58+
teamId?: string;
59+
};
60+
excludeSelf?: boolean;
61+
};
62+
63+
const GroupsCreatePropsSchema = {
64+
type: 'object',
65+
properties: {
66+
name: {
67+
type: 'string',
68+
},
69+
members: {
70+
type: 'array',
71+
items: { type: 'string' },
72+
},
73+
readOnly: {
74+
type: 'boolean',
75+
},
76+
customFields: {
77+
type: 'object',
78+
additionalProperties: true,
79+
},
80+
extraData: {
81+
type: 'object',
82+
properties: {
83+
broadcast: {
84+
type: 'boolean',
85+
},
86+
encrypted: {
87+
type: 'boolean',
88+
},
89+
federated: {
90+
type: 'boolean',
91+
},
92+
teamId: {
93+
type: 'string',
94+
},
95+
topic: {
96+
type: 'string',
97+
},
98+
},
99+
additionalProperties: false,
100+
},
101+
excludeSelf: {
102+
type: 'boolean',
103+
},
104+
},
105+
required: ['name'],
106+
additionalProperties: false,
107+
};
108+
109+
const isGroupsCreateProps = ajv.compile<GroupsCreateProps>(GroupsCreatePropsSchema);
110+
111+
const isGroupsCreateResponseSchema = ajv.compile({
112+
type: 'object',
113+
properties: {
114+
success: {
115+
type: 'boolean',
116+
enum: [true],
117+
},
118+
group: {
119+
$ref: '#/components/schemas/IRoom',
120+
},
121+
},
122+
required: ['success', 'group'],
123+
additionalProperties: false,
124+
});
125+
126+
const groupsEndpoints = API.v1
127+
//Creates private group
128+
.post(
129+
'groups.create',
130+
{
131+
authRequired: true,
132+
body: isGroupsCreateProps,
133+
response: {
134+
200: isGroupsCreateResponseSchema,
135+
400: validateBadRequestErrorResponse,
136+
401: validateUnauthorizedErrorResponse,
137+
403: validateForbiddenErrorResponse,
138+
},
139+
},
140+
async function action() {
141+
const readOnly = this.bodyParams.readOnly ?? false;
142+
143+
try {
144+
const result = await createPrivateGroupMethod(
145+
this.user,
146+
this.bodyParams.name,
147+
this.bodyParams.members ? this.bodyParams.members : [],
148+
readOnly,
149+
this.bodyParams.customFields,
150+
this.bodyParams.extraData,
151+
this.bodyParams.excludeSelf ?? false,
152+
);
153+
154+
const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude });
155+
if (!room) {
156+
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
157+
}
158+
159+
return API.v1.success({
160+
group: await composeRoomWithLastMessage(room, this.userId),
161+
});
162+
} catch (error: unknown) {
163+
if (isMeteorError(error) && error.reason === 'error-not-allowed') {
164+
throw new Meteor.Error('error-not-allowed', 'Not allowed');
165+
}
166+
throw error;
167+
}
168+
},
169+
);
170+
40171
async function getRoomFromParams(params: { roomId?: string } | { roomName?: string }): Promise<IRoom> {
41172
if (
42173
(!('roomId' in params) && !('roomName' in params)) ||
@@ -316,58 +447,6 @@ API.v1.addRoute(
316447
},
317448
);
318449

319-
// Create Private Group
320-
API.v1.addRoute(
321-
'groups.create',
322-
{ authRequired: true },
323-
{
324-
async post() {
325-
if (!this.bodyParams.name) {
326-
return API.v1.failure('Body param "name" is required');
327-
}
328-
329-
if (this.bodyParams.members && !Array.isArray(this.bodyParams.members)) {
330-
return API.v1.failure('Body param "members" must be an array if provided');
331-
}
332-
333-
if (this.bodyParams.customFields && !(typeof this.bodyParams.customFields === 'object')) {
334-
return API.v1.failure('Body param "customFields" must be an object if provided');
335-
}
336-
if (this.bodyParams.extraData && !(typeof this.bodyParams.extraData === 'object')) {
337-
return API.v1.failure('Body param "extraData" must be an object if provided');
338-
}
339-
340-
const readOnly = typeof this.bodyParams.readOnly !== 'undefined' ? this.bodyParams.readOnly : false;
341-
342-
try {
343-
const result = await createPrivateGroupMethod(
344-
this.user,
345-
this.bodyParams.name,
346-
this.bodyParams.members ? this.bodyParams.members : [],
347-
readOnly,
348-
this.bodyParams.customFields,
349-
this.bodyParams.extraData,
350-
this.bodyParams.excludeSelf ?? false,
351-
);
352-
353-
const room = await Rooms.findOneById(result.rid, { projection: API.v1.defaultFieldsToExclude });
354-
if (!room) {
355-
throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "roomName" param provided does not match any group');
356-
}
357-
358-
return API.v1.success({
359-
group: await composeRoomWithLastMessage(room, this.userId),
360-
});
361-
} catch (error: unknown) {
362-
if (isMeteorError(error) && error.reason === 'error-not-allowed') {
363-
return API.v1.forbidden();
364-
}
365-
throw error;
366-
}
367-
},
368-
},
369-
);
370-
371450
API.v1.addRoute(
372451
'groups.delete',
373452
{ authRequired: true },
@@ -1300,3 +1379,10 @@ API.v1.addRoute(
13001379
},
13011380
},
13021381
);
1382+
1383+
export type GroupsEndpoints = ExtractRoutesFromAPI<typeof groupsEndpoints>;
1384+
1385+
declare module '@rocket.chat/rest-typings' {
1386+
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
1387+
interface Endpoints extends GroupsEndpoints {}
1388+
}

packages/rest-typings/src/v1/groups/GroupsCreateProps.ts

Lines changed: 0 additions & 68 deletions
This file was deleted.

packages/rest-typings/src/v1/groups/groups.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import type { GroupsArchiveProps } from './GroupsArchiveProps';
88
import type { GroupsCloseProps } from './GroupsCloseProps';
99
import type { GroupsConvertToTeamProps } from './GroupsConvertToTeamProps';
1010
import type { GroupsCountersProps } from './GroupsCountersProps';
11-
import type { GroupsCreateProps } from './GroupsCreateProps';
1211
import type { GroupsDeleteProps } from './GroupsDeleteProps';
1312
import type { GroupsFilesProps } from './GroupsFilesProps';
1413
import type { GroupsGetIntegrationsProps } from './GroupsGetIntegrationsProps';
@@ -65,11 +64,6 @@ export type GroupsEndpoints = {
6564
'/v1/groups.unarchive': {
6665
POST: (params: GroupsUnarchiveProps) => void;
6766
};
68-
'/v1/groups.create': {
69-
POST: (params: GroupsCreateProps) => {
70-
group: Omit<IRoom, 'joinCode' | 'members' | 'importIds' | 'e2e'>;
71-
};
72-
};
7367
'/v1/groups.convertToTeam': {
7468
POST: (params: GroupsConvertToTeamProps) => { team: ITeam };
7569
};

packages/rest-typings/src/v1/groups/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ export type * from './groups';
33
export * from './GroupsArchiveProps';
44
export * from './GroupsCloseProps';
55
export * from './GroupsConvertToTeamProps';
6-
export * from './GroupsCreateProps';
76
export * from './GroupsCountersProps';
87
export * from './GroupsDeleteProps';
98
export * from './GroupsFilesProps';

0 commit comments

Comments
 (0)