From afcbf1c52e5d9813ccb5e745da56839294bcb68a Mon Sep 17 00:00:00 2001 From: Felipe <84182706+felipe-rod123@users.noreply.github.com> Date: Tue, 7 Jun 2022 17:12:01 -0300 Subject: [PATCH] Chore: convert invites, misc and subscriptions to TS and create definitions (#25350) --- apps/meteor/app/api/server/api.d.ts | 16 +- .../api/server/v1/{invites.js => invites.ts} | 54 ++-- .../app/api/server/v1/{misc.js => misc.ts} | 244 +++++++++++++----- .../meteor/app/api/server/v1/subscriptions.js | 99 ------- .../meteor/app/api/server/v1/subscriptions.ts | 101 ++++++++ .../server/functions/validateInviteToken.js | 5 +- .../meteor/client/views/invite/InvitePage.tsx | 8 +- .../externals/meteor/ddp-rate-limiter.d.ts | 10 + .../tests/end-to-end/api/00-miscellaneous.js | 6 + .../tests/end-to-end/api/04-direct-message.js | 2 + .../meteor/tests/end-to-end/api/23-invites.js | 2 +- .../meteor/tests/end-to-end/api/24-methods.js | 117 +++++++++ apps/meteor/tests/end-to-end/api/25-teams.js | 4 + packages/rest-typings/src/index.ts | 7 + packages/rest-typings/src/v1/invites.ts | 46 +++- packages/rest-typings/src/v1/misc.ts | 213 +++++++++++++++ .../src/v1/subscriptionsEndpoints.ts | 111 ++++++++ 17 files changed, 836 insertions(+), 209 deletions(-) rename apps/meteor/app/api/server/v1/{invites.js => invites.ts} (50%) rename apps/meteor/app/api/server/v1/{misc.js => misc.ts} (74%) delete mode 100644 apps/meteor/app/api/server/v1/subscriptions.js create mode 100644 apps/meteor/app/api/server/v1/subscriptions.ts create mode 100644 apps/meteor/definition/externals/meteor/ddp-rate-limiter.d.ts create mode 100644 packages/rest-typings/src/v1/subscriptionsEndpoints.ts diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index 98fcefe501e1..e8a8634e0615 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -82,6 +82,7 @@ type PartialThis = { }; type ActionThis = { + readonly requestIp: string; urlParams: UrlParams; // TODO make it unsafe readonly queryParams: TMethod extends 'GET' @@ -110,16 +111,29 @@ type ActionThis({ object, userId }: { object: { [key: string]: unknown }; userId: string }): { [key: string]: unknown } & T; composeRoomWithLastMessage(room: IRoom, userId: string): IRoom; } & (TOptions extends { authRequired: true } ? { readonly user: IUser; readonly userId: string; + readonly token: string; } : { readonly user: null; - readonly userId: null; + readonly userId: undefined; + readonly token?: string; }); export type ResultFor = diff --git a/apps/meteor/app/api/server/v1/invites.js b/apps/meteor/app/api/server/v1/invites.ts similarity index 50% rename from apps/meteor/app/api/server/v1/invites.js rename to apps/meteor/app/api/server/v1/invites.ts index 907585a818bc..8ac3a2b8eddb 100644 --- a/apps/meteor/app/api/server/v1/invites.js +++ b/apps/meteor/app/api/server/v1/invites.ts @@ -1,3 +1,7 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import { IInvite } from '@rocket.chat/core-typings'; +import { isFindOrCreateInviteParams, isUseInviteTokenProps, isValidateInviteTokenProps } from '@rocket.chat/rest-typings'; + import { API } from '../api'; import { findOrCreateInvite } from '../../../invites/server/functions/findOrCreateInvite'; import { removeInvite } from '../../../invites/server/functions/removeInvite'; @@ -7,10 +11,12 @@ import { validateInviteToken } from '../../../invites/server/functions/validateI API.v1.addRoute( 'listInvites', - { authRequired: true }, { - get() { - const result = Promise.await(listInvites(this.userId)); + authRequired: true, + }, + { + async get() { + const result = await listInvites(this.userId); return API.v1.success(result); }, }, @@ -18,13 +24,15 @@ API.v1.addRoute( API.v1.addRoute( 'findOrCreateInvite', - { authRequired: true }, { - post() { + authRequired: true, + validateParams: isFindOrCreateInviteParams, + }, + { + async post() { const { rid, days, maxUses } = this.bodyParams; - const result = Promise.await(findOrCreateInvite(this.userId, { rid, days, maxUses })); - return API.v1.success(result); + return API.v1.success((await findOrCreateInvite(this.userId, { rid, days, maxUses })) as IInvite); }, }, ); @@ -33,44 +41,44 @@ API.v1.addRoute( 'removeInvite/:_id', { authRequired: true }, { - delete() { + async delete() { const { _id } = this.urlParams; - const result = Promise.await(removeInvite(this.userId, { _id })); - return API.v1.success(result); + return API.v1.success(await removeInvite(this.userId, { _id })); }, }, ); API.v1.addRoute( 'useInviteToken', - { authRequired: true }, { - post() { + authRequired: true, + validateParams: isUseInviteTokenProps, + }, + { + async post() { const { token } = this.bodyParams; // eslint-disable-next-line react-hooks/rules-of-hooks - const result = Promise.await(useInviteToken(this.userId, token)); - return API.v1.success(result); + return API.v1.success(await useInviteToken(this.userId, token)); }, }, ); API.v1.addRoute( 'validateInviteToken', - { authRequired: false }, { - post() { + authRequired: false, + validateParams: isValidateInviteTokenProps, + }, + { + async post() { const { token } = this.bodyParams; - - let valid = true; try { - Promise.await(validateInviteToken(token)); - } catch (e) { - valid = false; + return API.v1.success({ valid: Boolean(await validateInviteToken(token)) }); + } catch (_) { + return API.v1.success({ valid: false }); } - - return API.v1.success({ valid }); }, }, ); diff --git a/apps/meteor/app/api/server/v1/misc.js b/apps/meteor/app/api/server/v1/misc.ts similarity index 74% rename from apps/meteor/app/api/server/v1/misc.js rename to apps/meteor/app/api/server/v1/misc.ts index 6d6d94438c6c..9a7b7135a56a 100644 --- a/apps/meteor/app/api/server/v1/misc.js +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -6,6 +6,14 @@ import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { EJSON } from 'meteor/ejson'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import { escapeHTML } from '@rocket.chat/string-helpers'; +import { + isShieldSvgProps, + isSpotlightProps, + isDirectoryProps, + isMethodCallProps, + isMethodCallAnonProps, + isMeteorCall, +} from '@rocket.chat/rest-typings'; import { hasPermission } from '../../../authorization/server'; import { Users } from '../../../models/server'; @@ -176,9 +184,17 @@ API.v1.addRoute( let onlineCache = 0; let onlineCacheDate = 0; const cacheInvalid = 60000; // 1 minute + API.v1.addRoute( 'shield.svg', - { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 60, intervalTimeInMS: 60000 } }, + { + authRequired: false, + rateLimiterOptions: { + numRequestsAllowed: 60, + intervalTimeInMS: 60000, + }, + validateParams: isShieldSvgProps, + }, { get() { const { type, icon } = this.queryParams; @@ -189,13 +205,13 @@ API.v1.addRoute( }); } - const types = settings.get('API_Shield_Types'); + const types = settings.get('API_Shield_Types'); if ( type && types !== '*' && !types .split(',') - .map((t) => t.trim()) + .map((t: string) => t.trim()) .includes(type) ) { throw new Meteor.Error('error-shield-disabled', 'This shield type is disabled', { @@ -297,23 +313,22 @@ API.v1.addRoute( ` .trim() .replace(/\>[\s]+\<'), - }; + } as any; }, }, ); API.v1.addRoute( 'spotlight', - { authRequired: true }, + { + authRequired: true, + validateParams: isSpotlightProps, + }, { get() { - check(this.queryParams, { - query: String, - }); - const { query } = this.queryParams; - const result = Meteor.runAsUser(this.userId, () => Meteor.call('spotlight', query)); + const result = Meteor.call('spotlight', query); return API.v1.success(result); }, @@ -322,7 +337,10 @@ API.v1.addRoute( API.v1.addRoute( 'directory', - { authRequired: true }, + { + authRequired: true, + validateParams: isDirectoryProps, + }, { get() { const { offset, count } = this.getPaginationItems(); @@ -336,17 +354,15 @@ API.v1.addRoute( const sortBy = sort ? Object.keys(sort)[0] : undefined; const sortDirection = sort && Object.values(sort)[0] === 1 ? 'asc' : 'desc'; - const result = Meteor.runAsUser(this.userId, () => - Meteor.call('browseChannels', { - text, - type, - workspace, - sortBy, - sortDirection, - offset: Math.max(0, offset), - limit: Math.max(0, count), - }), - ); + const result = Meteor.call('browseChannels', { + text, + type, + workspace, + sortBy, + sortDirection, + offset: Math.max(0, offset), + limit: Math.max(0, count), + }); if (!result) { return API.v1.failure('Please verify the parameters'); @@ -410,60 +426,154 @@ API.v1.addRoute( }, ); -const mountResult = ({ id, error, result }) => ({ +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/interface-name-prefix + interface Endpoints { + 'method.call/:method': { + POST: (params: { method: string; args: any[] }) => any; + }; + 'method.callAnon/:method': { + POST: (params: { method: string; args: any[] }) => any; + }; + } +} + +const mountResult = ({ + id, + error, + result, +}: { + id: string; + error?: unknown; + result?: unknown; +}): { + message: string; +} => ({ message: EJSON.stringify({ msg: 'result', id, - error, - result, + error: error as any, + result: result as any, }), }); -const methodCall = () => ({ - post() { - check(this.bodyParams, { - message: String, - }); - - const { method, params, id } = EJSON.parse(this.bodyParams.message); - - const connectionId = - this.token || - crypto - .createHash('md5') - .update(this.requestIp + this.request.headers['user-agent']) - .digest('hex'); - - const rateLimiterInput = { - userId: this.userId, - clientAddress: this.requestIp, - type: 'method', - name: method, - connectionId, - }; +// had to create two different endpoints for authenticated and non-authenticated calls +// because restivus does not provide 'this.userId' if 'authRequired: false' +API.v1.addRoute( + 'method.call/:method', + { + authRequired: true, + rateLimiterOptions: false, + validateParams: isMeteorCall, + }, + { + post() { + check(this.bodyParams, { + message: String, + }); - try { - DDPRateLimiter._increment(rateLimiterInput); - const rateLimitResult = DDPRateLimiter._check(rateLimiterInput); - if (!rateLimitResult.allowed) { - throw new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { - timeToReset: rateLimitResult.timeToReset, - }); + const data = EJSON.parse(this.bodyParams.message); + + if (!isMethodCallProps(data)) { + return API.v1.failure('Invalid method call'); } - const result = Meteor.call(method, ...params); - return API.v1.success(mountResult({ id, result })); - } catch (error) { - SystemLogger.error(`Exception while invoking method ${method}`, error.message); - if (settings.get('Log_Level') === '2') { - Meteor._debug(`Exception while invoking method ${method}`, error); + const { method, params, id } = data; + + const connectionId = + this.token || + crypto + .createHash('md5') + .update(this.requestIp + this.request.headers['user-agent']) + .digest('hex'); + + const rateLimiterInput = { + userId: this.userId, + clientAddress: this.requestIp, + type: 'method', + name: method, + connectionId, + }; + + try { + DDPRateLimiter._increment(rateLimiterInput); + const rateLimitResult = DDPRateLimiter._check(rateLimiterInput); + if (!rateLimitResult.allowed) { + throw new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { + timeToReset: rateLimitResult.timeToReset, + }); + } + + const result = Meteor.call(method, ...params); + return API.v1.success(mountResult({ id, result })); + } catch (error) { + if (error instanceof Error) SystemLogger.error(`Exception while invoking method ${method}`, error.message); + else SystemLogger.error(`Exception while invoking method ${method}`, error); + + if (settings.get('Log_Level') === '2') { + Meteor._debug(`Exception while invoking method ${method}`, error); + } + return API.v1.success(mountResult({ id, error })); } - return API.v1.success(mountResult({ id, error })); - } + }, }, -}); +); +API.v1.addRoute( + 'method.callAnon/:method', + { + authRequired: false, + rateLimiterOptions: false, + validateParams: isMeteorCall, + }, + { + post() { + check(this.bodyParams, { + message: String, + }); -// had to create two different endpoints for authenticated and non-authenticated calls -// because restivus does not provide 'this.userId' if 'authRequired: false' -API.v1.addRoute('method.call/:method', { authRequired: true, rateLimiterOptions: false }, methodCall()); -API.v1.addRoute('method.callAnon/:method', { authRequired: false, rateLimiterOptions: false }, methodCall()); + const data = EJSON.parse(this.bodyParams.message); + + if (!isMethodCallAnonProps(data)) { + return API.v1.failure('Invalid method call'); + } + + const { method, params, id } = data; + + const connectionId = + this.token || + crypto + .createHash('md5') + .update(this.requestIp + this.request.headers['user-agent']) + .digest('hex'); + + const rateLimiterInput = { + userId: this.userId || undefined, + clientAddress: this.requestIp, + type: 'method', + name: method, + connectionId, + }; + + try { + DDPRateLimiter._increment(rateLimiterInput); + const rateLimitResult = DDPRateLimiter._check(rateLimiterInput); + if (!rateLimitResult.allowed) { + throw new Meteor.Error('too-many-requests', DDPRateLimiter.getErrorMessage(rateLimitResult), { + timeToReset: rateLimitResult.timeToReset, + }); + } + + const result = Meteor.call(method, ...params); + return API.v1.success(mountResult({ id, result })); + } catch (error) { + if (error instanceof Error) SystemLogger.error(`Exception while invoking method ${method}`, error.message); + else SystemLogger.error(`Exception while invoking method ${method}`, error); + + if (settings.get('Log_Level') === '2') { + Meteor._debug(`Exception while invoking method ${method}`, error); + } + return API.v1.success(mountResult({ id, error })); + } + }, + }, +); diff --git a/apps/meteor/app/api/server/v1/subscriptions.js b/apps/meteor/app/api/server/v1/subscriptions.js deleted file mode 100644 index 6624ede0e6c4..000000000000 --- a/apps/meteor/app/api/server/v1/subscriptions.js +++ /dev/null @@ -1,99 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { Subscriptions } from '../../../models'; -import { API } from '../api'; - -API.v1.addRoute( - 'subscriptions.get', - { authRequired: true }, - { - get() { - const { updatedSince } = this.queryParams; - - let updatedSinceDate; - if (updatedSince) { - if (isNaN(Date.parse(updatedSince))) { - throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); - } else { - updatedSinceDate = new Date(updatedSince); - } - } - - let result; - Meteor.runAsUser(this.userId, () => { - result = Meteor.call('subscriptions/get', updatedSinceDate); - }); - - if (Array.isArray(result)) { - result = { - update: result, - remove: [], - }; - } - - return API.v1.success(result); - }, - }, -); - -API.v1.addRoute( - 'subscriptions.getOne', - { authRequired: true }, - { - get() { - const { roomId } = this.requestParams(); - - if (!roomId) { - return API.v1.failure("The 'roomId' param is required"); - } - - const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId); - - return API.v1.success({ - subscription, - }); - }, - }, -); - -/** - This API is suppose to mark any room as read. - - Method: POST - Route: api/v1/subscriptions.read - Params: - - rid: The rid of the room to be marked as read. - */ -API.v1.addRoute( - 'subscriptions.read', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - rid: String, - }); - - Meteor.runAsUser(this.userId, () => Meteor.call('readMessages', this.bodyParams.rid)); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'subscriptions.unread', - { authRequired: true }, - { - post() { - const { roomId, firstUnreadMessage } = this.bodyParams; - if (!roomId && firstUnreadMessage && !firstUnreadMessage._id) { - return API.v1.failure('At least one of "roomId" or "firstUnreadMessage._id" params is required'); - } - - Meteor.runAsUser(this.userId, () => Meteor.call('unreadMessages', firstUnreadMessage, roomId)); - - return API.v1.success(); - }, - }, -); diff --git a/apps/meteor/app/api/server/v1/subscriptions.ts b/apps/meteor/app/api/server/v1/subscriptions.ts new file mode 100644 index 000000000000..c042863eb1ec --- /dev/null +++ b/apps/meteor/app/api/server/v1/subscriptions.ts @@ -0,0 +1,101 @@ +import { Meteor } from 'meteor/meteor'; +import { + isSubscriptionsGetProps, + isSubscriptionsGetOneProps, + isSubscriptionsReadProps, + isSubscriptionsUnreadProps, +} from '@rocket.chat/rest-typings'; + +import { Subscriptions } from '../../../models/server/raw'; +import { API } from '../api'; + +API.v1.addRoute( + 'subscriptions.get', + { + authRequired: true, + validateParams: isSubscriptionsGetProps, + }, + { + async get() { + const { updatedSince } = this.queryParams; + + let updatedSinceDate: Date | undefined; + if (updatedSince) { + if (isNaN(Date.parse(updatedSince as string))) { + throw new Meteor.Error('error-roomId-param-invalid', 'The "lastUpdate" query parameter must be a valid date.'); + } + updatedSinceDate = new Date(updatedSince as string); + } + + const result = await Meteor.call('subscriptions/get', updatedSinceDate); + + return API.v1.success( + Array.isArray(result) + ? { + update: result, + remove: [], + } + : result, + ); + }, + }, +); + +API.v1.addRoute( + 'subscriptions.getOne', + { + authRequired: true, + validateParams: isSubscriptionsGetOneProps, + }, + { + async get() { + const { roomId }: { [roomId: string]: {} } | Record = this.queryParams; + + if (!roomId) { + return API.v1.failure("The 'roomId' param is required"); + } + + return API.v1.success({ + subscription: await Subscriptions.findOneByRoomIdAndUserId(roomId as string, this.userId), + }); + }, + }, +); + +/** + This API is suppose to mark any room as read. + + Method: POST + Route: api/v1/subscriptions.read + Params: + - rid: The rid of the room to be marked as read. + */ +API.v1.addRoute( + 'subscriptions.read', + { + authRequired: true, + validateParams: isSubscriptionsReadProps, + }, + { + post() { + Meteor.call('readMessages', this.bodyParams.rid); + + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'subscriptions.unread', + { + authRequired: true, + validateParams: isSubscriptionsUnreadProps, + }, + { + post() { + Meteor.call('unreadMessages', (this.bodyParams as any).firstUnreadMessage, (this.bodyParams as any).roomId); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/app/invites/server/functions/validateInviteToken.js b/apps/meteor/app/invites/server/functions/validateInviteToken.js index 385d55ca0ee1..99603999f5ae 100644 --- a/apps/meteor/app/invites/server/functions/validateInviteToken.js +++ b/apps/meteor/app/invites/server/functions/validateInviteToken.js @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { Rooms } from '../../../models'; -import { Invites } from '../../../models/server/raw'; +import { Rooms, Invites } from '../../../models/server/raw'; export const validateInviteToken = async (token) => { if (!token || typeof token !== 'string') { @@ -19,7 +18,7 @@ export const validateInviteToken = async (token) => { }); } - const room = Rooms.findOneById(inviteData.rid); + const room = await Rooms.findOneById(inviteData.rid); if (!room) { throw new Meteor.Error('error-invalid-room', 'The invite token is invalid.', { method: 'validateInviteToken', diff --git a/apps/meteor/client/views/invite/InvitePage.tsx b/apps/meteor/client/views/invite/InvitePage.tsx index 98e3448dbe82..319f997ba089 100644 --- a/apps/meteor/client/views/invite/InvitePage.tsx +++ b/apps/meteor/client/views/invite/InvitePage.tsx @@ -36,9 +36,9 @@ const InvitePage = (): ReactElement => { try { const { valid } = await APIClient.v1.post< - OperationParams<'POST', '/v1/validateInviteToken'>, + OperationParams<'POST', 'validateInviteToken'>, never, - OperationResult<'POST', '/v1/validateInviteToken'> + OperationResult<'POST', 'validateInviteToken'> >('validateInviteToken', { token }); return valid; @@ -65,9 +65,9 @@ const InvitePage = (): ReactElement => { try { const result = await APIClient.v1.post< - OperationParams<'POST', '/v1/useInviteToken'>, + OperationParams<'POST', 'useInviteToken'>, never, - OperationResult<'POST', '/v1/useInviteToken'> + OperationResult<'POST', 'useInviteToken'> >('useInviteToken', { token }); if (!result?.room.name) { dispatchToastMessage({ type: 'error', message: t('Failed_to_activate_invite_token') }); diff --git a/apps/meteor/definition/externals/meteor/ddp-rate-limiter.d.ts b/apps/meteor/definition/externals/meteor/ddp-rate-limiter.d.ts new file mode 100644 index 000000000000..784abd6def11 --- /dev/null +++ b/apps/meteor/definition/externals/meteor/ddp-rate-limiter.d.ts @@ -0,0 +1,10 @@ +declare module 'meteor/ddp-rate-limiter' { + namespace DDPRateLimiter { + function _increment(number: DDPRateLimiter.Matcher): void; + function _check(number: DDPRateLimiter.Matcher): { + allowed: boolean; + timeToReset: number; + }; + function getErrorMessage(result: { allowed: boolean }): string; + } +} diff --git a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js index 674ddccdf96a..35057ab54b3a 100644 --- a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js +++ b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js @@ -564,6 +564,12 @@ describe('miscellaneous', function () { updateSetting('API_Enable_Shields', false).then(() => { request .get(api('shield.svg')) + .query({ + type: 'online', + icon: true, + channel: 'general', + name: 'Rocket.Chat', + }) .expect('Content-Type', 'application/json') .expect(400) .expect((res) => { diff --git a/apps/meteor/tests/end-to-end/api/04-direct-message.js b/apps/meteor/tests/end-to-end/api/04-direct-message.js index 9eb450539ab9..3610b073c34b 100644 --- a/apps/meteor/tests/end-to-end/api/04-direct-message.js +++ b/apps/meteor/tests/end-to-end/api/04-direct-message.js @@ -617,6 +617,8 @@ describe('[Direct Messages]', function () { .set(userCredentials) .send({ message: JSON.stringify({ + id: 'id', + msg: 'method', method: 'saveUserPreferences', params: [{ emailNotificationMode: 'nothing' }], }), diff --git a/apps/meteor/tests/end-to-end/api/23-invites.js b/apps/meteor/tests/end-to-end/api/23-invites.js index 0b2cbc7e6c15..1adcfc5d99e9 100644 --- a/apps/meteor/tests/end-to-end/api/23-invites.js +++ b/apps/meteor/tests/end-to-end/api/23-invites.js @@ -142,7 +142,7 @@ describe('Invites', function () { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-invalid-token'); + expect(res.body).to.have.property('errorType', 'invalid-params'); }) .end(done); }); diff --git a/apps/meteor/tests/end-to-end/api/24-methods.js b/apps/meteor/tests/end-to-end/api/24-methods.js index 65c74c18f17e..67d8f8253c87 100644 --- a/apps/meteor/tests/end-to-end/api/24-methods.js +++ b/apps/meteor/tests/end-to-end/api/24-methods.js @@ -80,6 +80,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getThreadMessages', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -99,6 +101,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getThreadMessages', params: [{ tmid: firstMessage._id }], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -188,6 +192,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getMessages', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -207,6 +213,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getMessages', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -231,6 +239,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getMessages', params: [[firstMessage._id]], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -254,6 +264,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getMessages', params: [[firstMessage._id, lastMessage._id]], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -343,6 +355,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadHistory', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -362,6 +376,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadHistory', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -384,6 +400,8 @@ describe('Meteor.methods', function () { .set(credentials) .send({ message: JSON.stringify({ + id: 'id', + msg: 'method', method: 'loadHistory', params: [rid], }), @@ -410,6 +428,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadHistory', params: [rid, postMessageDate], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -434,6 +454,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadHistory', params: [rid, { $date: new Date().getTime() }, 1], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -458,6 +480,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadHistory', params: [rid, null, 20, lastMessage], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -547,6 +571,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadNextMessages', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -566,6 +592,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadNextMessages', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -590,6 +618,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadNextMessages', params: [rid], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -614,6 +644,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadNextMessages', params: [rid, postMessageDate], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -638,6 +670,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadNextMessages', params: [rid, startDate, 1], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -715,6 +749,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getUsersOfRoom', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -734,6 +770,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getUsersOfRoom', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -757,6 +795,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getUsersOfRoom', params: [rid], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -781,6 +821,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getUserRoles', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -800,6 +842,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getUserRoles', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -823,6 +867,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'listCustomUserStatus', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -842,6 +888,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'listCustomUserStatus', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -869,6 +917,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'permissions/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -888,6 +938,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'permissions/get', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -911,6 +963,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'permissions/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1000,6 +1054,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadMissedMessages', params: [rid, date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1019,6 +1075,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadMissedMessages', params: ['', date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1038,6 +1096,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadMissedMessages', params: [rid], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1057,6 +1117,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadMissedMessages', params: [rid, { $date: new Date().getTime() }], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1080,6 +1142,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadMissedMessages', params: [rid, date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1103,6 +1167,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'loadMissedMessages', params: [rid, postMessageDate], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1131,6 +1197,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'public-settings/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1150,6 +1218,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'public-settings/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1177,6 +1247,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'private-settings/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1200,6 +1272,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'private-settings/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1225,6 +1299,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'private-settings/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1254,6 +1330,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'private-settings/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1284,6 +1362,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'subscriptions/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1303,6 +1383,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'subscriptions/get', params: [], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1326,6 +1408,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'subscriptions/get', params: [date], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1376,6 +1460,7 @@ describe('Meteor.methods', function () { method: 'sendMessage', params: [{ _id: `${Date.now() + Math.random()}`, rid, msg: 'test message' }], id: 1000, + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1405,6 +1490,8 @@ describe('Meteor.methods', function () { msg: 'test message with https://github.com', }, ], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1463,6 +1550,8 @@ describe('Meteor.methods', function () { msg: 'test message with https://github.com', }, ], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1494,6 +1583,8 @@ describe('Meteor.methods', function () { msg: 'test message with ```https://github.com```', }, ], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1517,6 +1608,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'updateMessage', params: [{ _id: messageId, rid, msg: 'https://github.com updated' }], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1544,6 +1637,8 @@ describe('Meteor.methods', function () { msg: 'test message with ```https://github.com``` updated', }, ], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1629,6 +1724,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'createDirectMessage', params: [testUser.username], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1649,6 +1746,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'createDirectMessage', params: [testUser2.username], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1669,6 +1768,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'setUserActiveStatus', params: [testUser._id, false, false], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1687,6 +1788,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'setUserActiveStatus', params: [testUser2._id, false, false], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1705,6 +1808,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getRoomByTypeAndName', params: ['d', dmId], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1723,6 +1828,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'setUserActiveStatus', params: [testUser._id, true, false], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1741,6 +1848,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getRoomByTypeAndName', params: ['d', dmId], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1772,6 +1881,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getRoomByTypeAndName', params: ['d', dmTestId], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1792,6 +1903,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'setUserActiveStatus', params: [testUser2._id, true, false], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1810,6 +1923,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getRoomByTypeAndName', params: ['d', dmTestId], + id: 'id', + msg: 'method', }), }) .end((err, res) => { @@ -1828,6 +1943,8 @@ describe('Meteor.methods', function () { message: JSON.stringify({ method: 'getRoomByTypeAndName', params: ['d', dmTestId], + id: 'id', + msg: 'method', }), }) .end((err, res) => { diff --git a/apps/meteor/tests/end-to-end/api/25-teams.js b/apps/meteor/tests/end-to-end/api/25-teams.js index e549e761c6c8..4b45f73ef5da 100644 --- a/apps/meteor/tests/end-to-end/api/25-teams.js +++ b/apps/meteor/tests/end-to-end/api/25-teams.js @@ -1230,6 +1230,8 @@ describe('[Teams]', () => { message: JSON.stringify({ method: 'addUsersToRoom', params: [{ rid: privateRoom3._id, users: [testUser.username] }], + id: 'id', + msg: 'method', }), }) .expect('Content-Type', 'application/json') @@ -1584,6 +1586,8 @@ describe('[Teams]', () => { message: JSON.stringify({ method: 'saveUserPreferences', params: [{ emailNotificationMode: 'nothing' }], + id: 'id', + msg: 'method', }), }) .expect(200); diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 0a9dbb202fe3..5c2b9fa56e4d 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -35,6 +35,7 @@ import type { VoipEndpoints } from './v1/voip'; import type { EmailInboxEndpoints } from './v1/email-inbox'; import type { WebdavEndpoints } from './v1/webdav'; import type { OAuthAppsEndpoint } from './v1/oauthapps'; +import type { SubscriptionsEndpoints } from './v1/subscriptionsEndpoints'; import type { CommandsEndpoints } from './v1/commands'; // eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/interface-name-prefix @@ -73,6 +74,7 @@ export interface Endpoints EmailInboxEndpoints, WebdavEndpoints, OAuthAppsEndpoint, + SubscriptionsEndpoints, AutoTranslateEndpoints {} type OperationsByPathPattern = TPathPattern extends any @@ -157,6 +159,11 @@ export * from './v1/channels/ChannelsModeratorsProps'; export * from './v1/channels/ChannelsConvertToTeamProps'; export * from './v1/channels/ChannelsSetReadOnlyProps'; export * from './v1/channels/ChannelsDeleteProps'; + +export * from './v1/subscriptionsEndpoints'; +export * from './v1/misc'; +export * from './v1/invites'; + export * from './v1/dm'; export * from './v1/dm/DmHistoryProps'; export * from './v1/integrations'; diff --git a/packages/rest-typings/src/v1/invites.ts b/packages/rest-typings/src/v1/invites.ts index fda658af2267..d123c3d18df7 100644 --- a/packages/rest-typings/src/v1/invites.ts +++ b/packages/rest-typings/src/v1/invites.ts @@ -5,11 +5,11 @@ const ajv = new Ajv({ coerceTypes: true, }); -type v1UseInviteTokenProps = { +type UseInviteTokenProps = { token: string; }; -const v1UseInviteTokenPropsSchema = { +const UseInviteTokenPropsSchema = { type: 'object', properties: { token: { @@ -20,13 +20,13 @@ const v1UseInviteTokenPropsSchema = { additionalProperties: false, }; -export const isV1UseInviteTokenProps = ajv.compile(v1UseInviteTokenPropsSchema); +export const isUseInviteTokenProps = ajv.compile(UseInviteTokenPropsSchema); -type v1ValidateInviteTokenProps = { +type ValidateInviteTokenProps = { token: string; }; -const v1ValidateInviteTokenPropsSchema = { +const ValidateInviteTokenPropsSchema = { type: 'object', properties: { token: { @@ -37,17 +37,38 @@ const v1ValidateInviteTokenPropsSchema = { additionalProperties: false, }; -export const isV1ValidateInviteTokenProps = ajv.compile(v1ValidateInviteTokenPropsSchema); +export const isValidateInviteTokenProps = ajv.compile(ValidateInviteTokenPropsSchema); + +type FindOrCreateInviteParams = { rid: IRoom['_id']; days: number; maxUses: number }; + +const FindOrCreateInviteParamsSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + days: { + type: 'integer', + }, + maxUses: { + type: 'integer', + }, + }, + required: ['rid', 'days', 'maxUses'], + additionalProperties: false, +}; + +export const isFindOrCreateInviteParams = ajv.compile(FindOrCreateInviteParamsSchema); export type InvitesEndpoints = { 'listInvites': { GET: () => Array; }; 'removeInvite/:_id': { - DELETE: () => void; + DELETE: () => boolean; }; - '/v1/useInviteToken': { - POST: (params: v1UseInviteTokenProps) => { + 'useInviteToken': { + POST: (params: UseInviteTokenProps) => { room: { rid: IRoom['_id']; prid: IRoom['prid']; @@ -57,7 +78,10 @@ export type InvitesEndpoints = { }; }; }; - '/v1/validateInviteToken': { - POST: (params: v1ValidateInviteTokenProps) => { valid: boolean }; + 'validateInviteToken': { + POST: (params: ValidateInviteTokenProps) => { valid: boolean }; + }; + 'findOrCreateInvite': { + POST: (params: FindOrCreateInviteParams) => IInvite; }; }; diff --git a/packages/rest-typings/src/v1/misc.ts b/packages/rest-typings/src/v1/misc.ts index 06d59cd754b6..f41ffe905860 100644 --- a/packages/rest-typings/src/v1/misc.ts +++ b/packages/rest-typings/src/v1/misc.ts @@ -1,3 +1,175 @@ +import type { IRoom, ITeam, IUser } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../helpers/PaginatedRequest'; +import type { PaginatedResult } from '../helpers/PaginatedResult'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +type ShieldSvg = { + type?: string; + icon?: 'true' | 'false'; + channel: string; + name: string; +}; + +const ShieldSvgSchema = { + type: 'object', + properties: { + type: { + type: 'string', + nullable: true, + }, + icon: { + type: 'string', + enum: ['true', 'false'], + nullable: true, + }, + channel: { + type: 'string', + }, + name: { + type: 'string', + }, + }, + required: ['name', 'channel'], + additionalProperties: false, +}; + +export const isShieldSvgProps = ajv.compile(ShieldSvgSchema); + +type Spotlight = { query: string; limit: number; offset: number }; + +const SpotlightSchema = { + type: 'object', + properties: { + query: { + type: 'string', + }, + limit: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + }, + required: ['query'], + additionalProperties: false, +}; + +export const isSpotlightProps = ajv.compile(SpotlightSchema); + +type Directory = PaginatedRequest<{ + text: string; + type: string; + workspace: string; +}>; + +const DirectorySchema = { + type: 'object', + properties: { + text: { + type: 'string', + nullable: true, + }, + type: { + type: 'string', + nullable: true, + }, + workspace: { + type: 'string', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isDirectoryProps = ajv.compile(DirectorySchema); + +type MethodCall = { method: string; params: unknown[]; id: string; msg: 'string' }; + +const MethodCallSchema = { + type: 'object', + properties: { + method: { + type: 'string', + }, + params: { + type: 'array', + }, + id: { + type: 'string', + }, + msg: { + type: 'string', + enum: ['method'], + }, + }, + required: ['method', 'params', 'id', 'msg'], + additionalProperties: false, +}; + +export const isMethodCallProps = ajv.compile(MethodCallSchema); + +export const isMeteorCall = ajv.compile<{ + message: string; +}>({ + type: 'object', + properties: { + message: { + type: 'string', + }, + }, + required: ['message'], + additionalProperties: false, +}); + +type MethodCallAnon = { method: string; params: unknown[]; id: string; msg: 'method' }; + +const MethodCallAnonSchema = { + type: 'object', + properties: { + method: { + type: 'string', + }, + params: { + type: 'array', + }, + id: { + type: 'string', + }, + msg: { + type: 'string', + enum: ['method'], + }, + }, + required: ['method', 'params', 'id', 'msg'], + additionalProperties: false, +}; + +export const isMethodCallAnonProps = ajv.compile(MethodCallAnonSchema); + export type MiscEndpoints = { 'stdout.queue': { GET: () => { @@ -8,4 +180,45 @@ export type MiscEndpoints = { }[]; }; }; + 'me': { + GET: (params: { fields: { [k: string]: number }; user: IUser }) => IUser & { + email?: string; + settings: { + profile: {}; + preferences: unknown; + }; + avatarUrl: string; + }; + }; + + 'shield.svg': { + GET: (params: ShieldSvg) => { + svg: string; + }; + }; + + 'spotlight': { + GET: (params: Spotlight) => { + users: Pick[]; + rooms: IRoom[]; + }; + }; + + 'directory': { + GET: (params: Directory) => PaginatedResult<{ + result: (IUser | IRoom | ITeam)[]; + }>; + }; + + 'method.call': { + POST: (params: MethodCall) => { + result: unknown; + }; + }; + + 'method.callAnon': { + POST: (params: MethodCallAnon) => { + result: unknown; + }; + }; }; diff --git a/packages/rest-typings/src/v1/subscriptionsEndpoints.ts b/packages/rest-typings/src/v1/subscriptionsEndpoints.ts new file mode 100644 index 000000000000..d913fb8ebc1d --- /dev/null +++ b/packages/rest-typings/src/v1/subscriptionsEndpoints.ts @@ -0,0 +1,111 @@ +import type { ISubscription, IMessage, IRoom } from '@rocket.chat/core-typings'; +import Ajv from 'ajv'; + +type SubscriptionsGet = { updatedSince?: string }; + +type SubscriptionsGetOne = { roomId: IRoom['_id'] }; + +type SubscriptionsRead = { rid: IRoom['_id'] }; + +type SubscriptionsUnread = { roomId: IRoom['_id'] } | { firstUnreadMessage: Pick }; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +const SubscriptionsGetSchema = { + type: 'object', + properties: { + updatedSince: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; + +export const isSubscriptionsGetProps = ajv.compile(SubscriptionsGetSchema); + +const SubscriptionsGetOneSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, +}; + +export const isSubscriptionsGetOneProps = ajv.compile(SubscriptionsGetOneSchema); + +const SubscriptionsReadSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isSubscriptionsReadProps = ajv.compile(SubscriptionsReadSchema); + +const SubscriptionsUnreadSchema = { + anyOf: [ + { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + }, + required: ['roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + firstUnreadMessage: { + type: 'object', + properties: { + _id: { + type: 'string', + }, + }, + required: ['_id'], + additionalProperties: false, + }, + }, + required: ['firstUnreadMessage'], + additionalProperties: false, + }, + ], +}; + +export const isSubscriptionsUnreadProps = ajv.compile(SubscriptionsUnreadSchema); + +export type SubscriptionsEndpoints = { + 'subscriptions.get': { + GET: (params: SubscriptionsGet) => { + update: ISubscription[]; + remove: (Pick & { _deletedAt: Date })[]; + }; + }; + + 'subscriptions.getOne': { + GET: (params: SubscriptionsGetOne) => { + subscription: ISubscription | null; + }; + }; + + 'subscriptions.read': { + POST: (params: SubscriptionsRead) => void; + }; + + 'subscriptions.unread': { + POST: (params: SubscriptionsUnread) => void; + }; +};