Skip to content

Commit f94d258

Browse files
feat: Add OpenAPI Support to subscriptions.getOne API
Key Changes: - Implemented the new pattern and added AJV-based JSON schema validation for API. - Uses the ExtractRoutesFromAPI utility from the TypeScript definitions to dynamically derive the routes from the endpoint specifications. - Enabled Swagger UI integration for this API. - Route Methods Chaining for the endpoints. - This does not introduce any breaking changes to the endpoint logic.
1 parent 11e1c51 commit f94d258

File tree

5 files changed

+71
-37
lines changed

5 files changed

+71
-37
lines changed

.changeset/flat-candles-walk.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
"@rocket.chat/rest-typings": patch
4+
---
5+
6+
Add OpenAPI support for the Rocket.Chat subscriptions.getOne 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.

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

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import type { IRoom, ISubscription } from '@rocket.chat/core-typings';
12
import { Rooms, Subscriptions } from '@rocket.chat/models';
23
import {
4+
ajv,
5+
validateUnauthorizedErrorResponse,
6+
validateBadRequestErrorResponse,
37
isSubscriptionsGetProps,
4-
isSubscriptionsGetOneProps,
58
isSubscriptionsReadProps,
69
isSubscriptionsUnreadProps,
710
} from '@rocket.chat/rest-typings';
@@ -10,6 +13,7 @@ import { Meteor } from 'meteor/meteor';
1013
import { readMessages } from '../../../../server/lib/readMessages';
1114
import { getSubscriptions } from '../../../../server/publications/subscription';
1215
import { unreadMessages } from '../../../message-mark-as-unread/server/unreadMessages';
16+
import type { ExtractRoutesFromAPI } from '../ApiClass';
1317
import { API } from '../api';
1418

1519
API.v1.addRoute(
@@ -44,24 +48,53 @@ API.v1.addRoute(
4448
},
4549
);
4650

47-
API.v1.addRoute(
51+
type SubscriptionsGetOne = { roomId: IRoom['_id'] };
52+
53+
const SubscriptionsGetOneSchema = {
54+
type: 'object',
55+
properties: {
56+
roomId: {
57+
type: 'string',
58+
minLength: 1,
59+
},
60+
},
61+
required: ['roomId'],
62+
additionalProperties: false,
63+
};
64+
65+
const isSubscriptionsGetOneProps = ajv.compile<SubscriptionsGetOne>(SubscriptionsGetOneSchema);
66+
67+
const subscriptionsEndpoints = API.v1.get(
4868
'subscriptions.getOne',
4969
{
5070
authRequired: true,
51-
validateParams: isSubscriptionsGetOneProps,
71+
query: isSubscriptionsGetOneProps,
72+
response: {
73+
400: validateBadRequestErrorResponse,
74+
401: validateUnauthorizedErrorResponse,
75+
200: ajv.compile<{ subscription: ISubscription | null }>({
76+
type: 'object',
77+
properties: {
78+
subscription: {
79+
anyOf: [{ type: 'null' }, { $ref: '#/components/schemas/ISubscription' }],
80+
},
81+
success: {
82+
type: 'boolean',
83+
enum: [true],
84+
},
85+
},
86+
required: ['subscription', 'success'],
87+
additionalProperties: false,
88+
}),
89+
},
5290
},
53-
{
54-
async get() {
55-
const { roomId } = this.queryParams;
5691

57-
if (!roomId) {
58-
return API.v1.failure("The 'roomId' param is required");
59-
}
92+
async function action() {
93+
const { roomId } = this.queryParams;
6094

61-
return API.v1.success({
62-
subscription: await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId),
63-
});
64-
},
95+
return API.v1.success({
96+
subscription: await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId),
97+
});
6598
},
6699
);
67100

@@ -115,3 +148,10 @@ API.v1.addRoute(
115148
},
116149
},
117150
);
151+
152+
export type SubscriptionsEndpoints = ExtractRoutesFromAPI<typeof subscriptionsEndpoints>;
153+
154+
declare module '@rocket.chat/rest-typings' {
155+
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
156+
interface Endpoints extends SubscriptionsEndpoints {}
157+
}

packages/http-router/src/Router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ export class Router<
215215
{
216216
success: false,
217217
errorType: 'error-invalid-params',
218-
error: validatorFn.errors?.map((error: any) => error.message).join('\n '),
218+
error: `${validatorFn.errors?.map((error: any) => error.message).join('\n ')} [invalid-params]`,
219219
},
220220
400,
221221
);

packages/models/src/models/Subscriptions.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1848,13 +1848,18 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
18481848

18491849
// INSERT
18501850
async createWithRoomAndUser(room: IRoom, user: IUser, extraData: Partial<ISubscription> = {}): Promise<InsertOneResult<ISubscription>> {
1851+
const now = new Date();
18511852
const subscription = {
18521853
open: false,
18531854
alert: false,
18541855
unread: 0,
18551856
userMentions: 0,
18561857
groupMentions: 0,
1857-
ts: room.ts,
1858+
ts: room.ts ?? now,
1859+
// last read / last seen default to the room timestamp when creating the subscription
1860+
ls: extraData.ls ?? extraData.lr ?? room.ts ?? now,
1861+
lr: extraData.lr ?? extraData.ls ?? room.ts ?? now,
1862+
_updatedAt: extraData._updatedAt ?? now,
18581863
rid: room._id,
18591864
name: room.name,
18601865
fname: room.fname,
@@ -1885,13 +1890,17 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
18851890
room: IRoom,
18861891
users: { user: AtLeast<IUser, '_id' | 'username' | 'name' | 'settings'>; extraData: Record<string, any> }[] = [],
18871892
): Promise<InsertManyResult<ISubscription>> {
1893+
const now = new Date();
18881894
const subscriptions = users.map(({ user, extraData }) => ({
18891895
open: false,
18901896
alert: false,
18911897
unread: 0,
18921898
userMentions: 0,
18931899
groupMentions: 0,
1894-
ts: room.ts,
1900+
ts: room.ts ?? now,
1901+
ls: extraData.ls ?? extraData.lr ?? room.ts ?? now,
1902+
lr: extraData.lr ?? extraData.ls ?? room.ts ?? now,
1903+
_updatedAt: extraData._updatedAt ?? now,
18951904
rid: room._id,
18961905
name: room.name,
18971906
fname: room.fname,

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

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import { ajv } from './Ajv';
44

55
type SubscriptionsGet = { updatedSince?: string };
66

7-
type SubscriptionsGetOne = { roomId: IRoom['_id'] };
8-
97
type SubscriptionsRead = { rid: IRoom['_id']; readThreads?: boolean } | { roomId: IRoom['_id']; readThreads?: boolean };
108

119
type SubscriptionsUnread = { roomId: IRoom['_id'] } | { firstUnreadMessage: Pick<IMessage, '_id'> };
@@ -24,19 +22,6 @@ const SubscriptionsGetSchema = {
2422

2523
export const isSubscriptionsGetProps = ajv.compile<SubscriptionsGet>(SubscriptionsGetSchema);
2624

27-
const SubscriptionsGetOneSchema = {
28-
type: 'object',
29-
properties: {
30-
roomId: {
31-
type: 'string',
32-
},
33-
},
34-
required: ['roomId'],
35-
additionalProperties: false,
36-
};
37-
38-
export const isSubscriptionsGetOneProps = ajv.compile<SubscriptionsGetOne>(SubscriptionsGetOneSchema);
39-
4025
const SubscriptionsReadSchema = {
4126
anyOf: [
4227
{
@@ -114,12 +99,6 @@ export type SubscriptionsEndpoints = {
11499
};
115100
};
116101

117-
'/v1/subscriptions.getOne': {
118-
GET: (params: SubscriptionsGetOne) => {
119-
subscription: ISubscription | null;
120-
};
121-
};
122-
123102
'/v1/subscriptions.read': {
124103
POST: (params: SubscriptionsRead) => void;
125104
};

0 commit comments

Comments
 (0)