diff --git a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts index d9b69056519b2..34d4ab0446817 100644 --- a/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts +++ b/apps/meteor/app/user-status/server/methods/insertOrUpdateUserStatus.ts @@ -52,15 +52,11 @@ export const insertOrUpdateUserStatus = async (userId: string, userStatusData: I }); } - let matchingResults = []; + const conflictingUserStatus = userStatusData._id + ? await CustomUserStatus.findOneByNameExceptId(userStatusData.name, userStatusData._id, { projection: { _id: 1 } }) + : await CustomUserStatus.findOneByName(userStatusData.name, { projection: { _id: 1 } }); - if (userStatusData._id) { - matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); - } else { - matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); - } - - if (matchingResults.length > 0) { + if (conflictingUserStatus) { throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', { method: 'insertOrUpdateUserStatus', }); diff --git a/apps/meteor/server/api/v1/omnichannel/message.ts b/apps/meteor/server/api/v1/omnichannel/message.ts index 5b63071339aaa..be2ea18193043 100644 --- a/apps/meteor/server/api/v1/omnichannel/message.ts +++ b/apps/meteor/server/api/v1/omnichannel/message.ts @@ -257,12 +257,8 @@ API.v1.addRoute( let rid: string; if (visitor) { const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId }); - const rooms = await LivechatRooms.findOpenByVisitorToken(visitorToken, {}, extraQuery).toArray(); - if (rooms && rooms.length > 0) { - rid = rooms[0]._id; - } else { - rid = Random.id(); - } + const room = await LivechatRooms.findOneOpenByVisitorToken(visitorToken, { projection: { _id: 1 } }, extraQuery); + rid = room?._id ?? Random.id(); } else { rid = Random.id(); diff --git a/apps/meteor/server/api/v1/permissions.ts b/apps/meteor/server/api/v1/permissions.ts index 4dd4dad5d9c10..6ae14594ca590 100644 --- a/apps/meteor/server/api/v1/permissions.ts +++ b/apps/meteor/server/api/v1/permissions.ts @@ -180,17 +180,17 @@ const permissionsEndpoints = API.v1 } const permissionKeys = bodyParams.permissions.map(({ _id }) => _id); - const permissions = await Permissions.find({ _id: { $in: permissionKeys } }).toArray(); + const permissionsCount = await Permissions.countDocuments({ _id: { $in: permissionKeys } }); - if (permissions.length !== bodyParams.permissions.length) { + if (permissionsCount !== bodyParams.permissions.length) { return API.v1.failure('Invalid permission', 'error-invalid-permission'); } const roleKeys = [...new Set(bodyParams.permissions.flatMap((p) => p.roles))]; - const roles = await Roles.find({ _id: { $in: roleKeys } }).toArray(); + const rolesCount = await Roles.countDocuments({ _id: { $in: roleKeys } }); - if (roles.length !== roleKeys.length) { + if (rolesCount !== roleKeys.length) { return API.v1.failure('Invalid role', 'error-invalid-role'); } diff --git a/apps/meteor/server/lib/media/custom-sounds/lib/insertOrUpdateSound.ts b/apps/meteor/server/lib/media/custom-sounds/lib/insertOrUpdateSound.ts index e56b0ae233bee..b20eddb9c4adf 100644 --- a/apps/meteor/server/lib/media/custom-sounds/lib/insertOrUpdateSound.ts +++ b/apps/meteor/server/lib/media/custom-sounds/lib/insertOrUpdateSound.ts @@ -28,9 +28,9 @@ export const insertOrUpdateSound = async (soundData: ICustomSoundData): Promise< }); } - const matchingResults = await CustomSounds.findByName(soundData.name, soundData._id).toArray(); + const conflictingSound = await CustomSounds.findOneByName(soundData.name, soundData._id, { projection: { _id: 1 } }); - if (matchingResults.length > 0) { + if (conflictingSound) { throw new Meteor.Error('Custom_Sound_Error_Name_Already_In_Use', 'The custom sound name is already in use', { method: 'insertOrUpdateSound', }); diff --git a/apps/meteor/server/lib/media/emoji-custom/lib/insertOrUpdateEmoji.ts b/apps/meteor/server/lib/media/emoji-custom/lib/insertOrUpdateEmoji.ts index c2f4102f41831..ecb5d13e10157 100644 --- a/apps/meteor/server/lib/media/emoji-custom/lib/insertOrUpdateEmoji.ts +++ b/apps/meteor/server/lib/media/emoji-custom/lib/insertOrUpdateEmoji.ts @@ -68,21 +68,11 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension; - let matchingResults = []; + const conflictingEmoji = await EmojiCustom.findOneByNamesOrAliases([emojiData.name, ...aliases], emojiData._id, { + projection: { _id: 1 }, + }); - if (emojiData._id) { - matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray(); - for (const alias of aliases) { - matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray()); - } - } else { - matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray(); - for (const alias of aliases) { - matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray()); - } - } - - if (matchingResults.length > 0) { + if (conflictingEmoji) { throw new Meteor.Error('Custom_Emoji_Error_Name_Or_Alias_Already_In_Use', 'The custom emoji or one of its aliases is already in use', { method: 'insertOrUpdateEmoji', }); diff --git a/apps/meteor/server/lib/saml/lib/SAML.ts b/apps/meteor/server/lib/saml/lib/SAML.ts index 7fc20a0f6af8e..0fb1729ea2803 100644 --- a/apps/meteor/server/lib/saml/lib/SAML.ts +++ b/apps/meteor/server/lib/saml/lib/SAML.ts @@ -328,7 +328,11 @@ export class SAML { }, 5000); try { - const loggedOutUsers = await Users.findBySAMLNameIdOrIdpSession(result.nameID, result.idpSession).toArray(); + // limit 2: only need to know whether the match is missing, unique, or ambiguous + const loggedOutUsers = await Users.findBySAMLNameIdOrIdpSession(result.nameID, result.idpSession, { + projection: { _id: 1 }, + limit: 2, + }).toArray(); if (loggedOutUsers.length > 1) { throw new Meteor.Error('Found multiple users matching SAML session'); } @@ -380,7 +384,8 @@ export class SAML { const logOutUser = async (inResponseTo: string): Promise => { SAMLUtils.log({ msg: 'Processing logout for inResponseTo', inResponseTo }); - const loggedOutUsers = await Users.findBySAMLInResponseTo(inResponseTo).toArray(); + // limit 2: only need to know whether the match is missing, unique, or ambiguous + const loggedOutUsers = await Users.findBySAMLInResponseTo(inResponseTo, { projection: { _id: 1 }, limit: 2 }).toArray(); if (loggedOutUsers.length > 1) { throw new Meteor.Error('Found multiple users matching SAML inResponseTo fields'); } diff --git a/apps/meteor/server/lib/statistics/lib/statistics.ts b/apps/meteor/server/lib/statistics/lib/statistics.ts index 3fb332d5c689d..6f6052baf228b 100644 --- a/apps/meteor/server/lib/statistics/lib/statistics.ts +++ b/apps/meteor/server/lib/statistics/lib/statistics.ts @@ -447,35 +447,9 @@ export const statistics = { ); statsPms.push( - Integrations.find( - {}, - { - projection: { - _id: 0, - type: 1, - enabled: 1, - scriptEnabled: 1, - }, - readPreference, - }, - ) - .toArray() - .then((found) => { - const integrations = found; - - statistics.integrations = { - totalIntegrations: integrations.length, - totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length, - totalIncomingActive: integrations.filter( - (integration) => integration.enabled === true && integration.type === 'webhook-incoming', - ).length, - totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length, - totalOutgoingActive: integrations.filter( - (integration) => integration.enabled === true && integration.type === 'webhook-outgoing', - ).length, - totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length, - }; - }), + Integrations.getStatistics({ readPreference }).then((integrations) => { + statistics.integrations = integrations; + }), ); statsPms.push( diff --git a/packages/model-typings/src/models/IBaseModel.ts b/packages/model-typings/src/models/IBaseModel.ts index 93a513d17cbfb..247461b962cf6 100644 --- a/packages/model-typings/src/models/IBaseModel.ts +++ b/packages/model-typings/src/models/IBaseModel.ts @@ -4,6 +4,7 @@ import type { ChangeStream, ClientSession, Collection, + CountDocumentsOptions, DeleteOptions, DeleteResult, Document, @@ -128,6 +129,6 @@ export interface IBaseModel< ): FindPaginated>>; watch(pipeline?: object[]): ChangeStream; - countDocuments(query: Filter): Promise; + countDocuments(query: Filter, options?: CountDocumentsOptions): Promise; estimatedDocumentCount(): Promise; } diff --git a/packages/model-typings/src/models/ICustomSoundsModel.ts b/packages/model-typings/src/models/ICustomSoundsModel.ts index ec0412297fe34..b30a5104895bb 100644 --- a/packages/model-typings/src/models/ICustomSoundsModel.ts +++ b/packages/model-typings/src/models/ICustomSoundsModel.ts @@ -5,6 +5,7 @@ import type { IBaseModel } from './IBaseModel'; export interface ICustomSoundsModel extends IBaseModel { findByName(name: string, exceptId?: string, options?: FindOptions): FindCursor; + findOneByName(name: string, exceptId?: string, options?: FindOptions): Promise; create(data: Omit): Promise>>; updateById(_id: string, data: Partial>): Promise; } diff --git a/packages/model-typings/src/models/ICustomUserStatusModel.ts b/packages/model-typings/src/models/ICustomUserStatusModel.ts index 552d8bfe57f1a..4e55a29992f1c 100644 --- a/packages/model-typings/src/models/ICustomUserStatusModel.ts +++ b/packages/model-typings/src/models/ICustomUserStatusModel.ts @@ -6,6 +6,7 @@ import type { IBaseModel, InsertionModel } from './IBaseModel'; export interface ICustomUserStatusModel extends IBaseModel { findOneByName(name: string, options?: undefined): Promise; findOneByName(name: string, options?: FindOptions): Promise; + findOneByNameExceptId(name: string, except: string, options?: FindOptions): Promise; findByName(name: string, options?: FindOptions): FindCursor; findByNameExceptId(name: string, except: string, options?: FindOptions): FindCursor; setName(_id: string, name: string): Promise; diff --git a/packages/model-typings/src/models/IEmojiCustomModel.ts b/packages/model-typings/src/models/IEmojiCustomModel.ts index 0ed54f36ab393..e41bedd14fc57 100644 --- a/packages/model-typings/src/models/IEmojiCustomModel.ts +++ b/packages/model-typings/src/models/IEmojiCustomModel.ts @@ -5,6 +5,7 @@ import type { IBaseModel, InsertionModel } from './IBaseModel'; export interface IEmojiCustomModel extends IBaseModel { findByNameOrAlias(emojiName: string, options?: FindOptions): FindCursor; + findOneByNamesOrAliases(names: string[], exceptId?: string, options?: FindOptions): Promise; findByNameOrAliasExceptID(name: string, except: string, options?: FindOptions): FindCursor; setName(_id: string, name: string): Promise; setAliases(_id: string, aliases: string[]): Promise; diff --git a/packages/model-typings/src/models/IIntegrationsModel.ts b/packages/model-typings/src/models/IIntegrationsModel.ts index 8eb4888e16b06..4fc3e12765ecf 100644 --- a/packages/model-typings/src/models/IIntegrationsModel.ts +++ b/packages/model-typings/src/models/IIntegrationsModel.ts @@ -1,8 +1,17 @@ import type { IIntegration, IUser } from '@rocket.chat/core-typings'; -import type { FindCursor, FindOptions } from 'mongodb'; +import type { AggregateOptions, FindCursor, FindOptions } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; +export type IntegrationsStatistics = { + totalIntegrations: number; + totalIncoming: number; + totalIncomingActive: number; + totalOutgoing: number; + totalOutgoingActive: number; + totalWithScriptEnabled: number; +}; + export interface IIntegrationsModel extends IBaseModel { disableByUserId(userId: IIntegration['userId']): ReturnType['updateMany']>; findByChannels(channels: IIntegration['channel']): FindCursor; @@ -16,4 +25,5 @@ export interface IIntegrationsModel extends IBaseModel { token: string, options?: FindOptions

, ): Promise

; + getStatistics(options?: AggregateOptions): Promise; } diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 205c110427f56..5753262e1490e 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -173,7 +173,11 @@ export interface ILivechatRoomsModel extends IBaseModel { association: ILivechatContactVisitorAssociation, options?: FindOptions, ): Promise; - findOneOpenByVisitorToken(visitorToken: string, options?: FindOptions): Promise; + findOneOpenByVisitorToken( + visitorToken: string, + options?: FindOptions, + extraQuery?: Filter, + ): Promise; findOneOpenByVisitorTokenAndDepartmentIdAndSource( visitorToken: string, departmentId?: string, diff --git a/packages/models/src/models/CustomSounds.ts b/packages/models/src/models/CustomSounds.ts index 7c3fbe121ba8e..98781868f0c81 100644 --- a/packages/models/src/models/CustomSounds.ts +++ b/packages/models/src/models/CustomSounds.ts @@ -23,6 +23,15 @@ export class CustomSoundsRaw extends BaseRaw implements ICustomSou return this.find(query, options); } + findOneByName(name: string, exceptId?: string, options?: FindOptions): Promise { + const query = { + name, + ...(exceptId && { _id: { $nin: [exceptId] } }), + }; + + return this.findOne(query, options); + } + // INSERT create(data: Omit): Promise>> { return this.insertOne(data); diff --git a/packages/models/src/models/CustomUserStatus.ts b/packages/models/src/models/CustomUserStatus.ts index 0518e4aac58b4..c0bcbfee0e778 100644 --- a/packages/models/src/models/CustomUserStatus.ts +++ b/packages/models/src/models/CustomUserStatus.ts @@ -21,6 +21,15 @@ export class CustomUserStatusRaw extends BaseRaw implements I return options ? this.findOne({ name }, options) : this.findOne({ name }); } + findOneByNameExceptId(name: string, except: string, options?: FindOptions): Promise { + const query = { + _id: { $nin: [except] }, + name, + }; + + return this.findOne(query, options); + } + // find findByName(name: string, options?: FindOptions): FindCursor { const query = { diff --git a/packages/models/src/models/EmojiCustom.ts b/packages/models/src/models/EmojiCustom.ts index d6e3167a848c0..011ccbe623310 100644 --- a/packages/models/src/models/EmojiCustom.ts +++ b/packages/models/src/models/EmojiCustom.ts @@ -1,6 +1,6 @@ import type { IEmojiCustom, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IEmojiCustomModel, InsertionModel } from '@rocket.chat/model-typings'; -import type { Collection, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb'; +import type { Collection, Filter, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -28,6 +28,15 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto return this.find(query, options); } + findOneByNamesOrAliases(names: string[], exceptId?: string, options?: FindOptions): Promise { + const query: Filter = { + ...(exceptId && { _id: { $nin: [exceptId] } }), + $or: [{ name: { $in: names } }, { aliases: { $in: names } }], + }; + + return this.findOne(query, options); + } + findByNameOrAliasExceptID(name: string, except: string, options?: FindOptions): FindCursor { const query = { _id: { $nin: [except] }, diff --git a/packages/models/src/models/Integrations.ts b/packages/models/src/models/Integrations.ts index 8ae0c87037b5d..d680c57db4262 100644 --- a/packages/models/src/models/Integrations.ts +++ b/packages/models/src/models/Integrations.ts @@ -1,6 +1,6 @@ import type { IIntegration, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; -import type { IBaseModel, IIntegrationsModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, FindCursor, FindOptions, IndexDescription } from 'mongodb'; +import type { IBaseModel, IIntegrationsModel, IntegrationsStatistics } from '@rocket.chat/model-typings'; +import type { AggregateOptions, Collection, Db, FindCursor, FindOptions, IndexDescription } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -72,4 +72,46 @@ export class IntegrationsRaw extends BaseRaw implements IIntegrati ): Promise

{ return this.findOne

({ _id: id, token }, options); } + + async getStatistics(options?: AggregateOptions): Promise { + const results = await this.col + .aggregate<{ _id: IIntegration['type']; total: number; active: number; scriptEnabled: number }>( + [ + { + $group: { + _id: '$type', + total: { $sum: 1 }, + active: { $sum: { $cond: ['$enabled', 1, 0] } }, + scriptEnabled: { $sum: { $cond: ['$scriptEnabled', 1, 0] } }, + }, + }, + ], + options, + ) + .toArray(); + + const statistics: IntegrationsStatistics = { + totalIntegrations: 0, + totalIncoming: 0, + totalIncomingActive: 0, + totalOutgoing: 0, + totalOutgoingActive: 0, + totalWithScriptEnabled: 0, + }; + + for (const { _id, total, active, scriptEnabled } of results) { + statistics.totalIntegrations += total; + statistics.totalWithScriptEnabled += scriptEnabled; + + if (_id === 'webhook-incoming') { + statistics.totalIncoming += total; + statistics.totalIncomingActive += active; + } else if (_id === 'webhook-outgoing') { + statistics.totalOutgoing += total; + statistics.totalOutgoingActive += active; + } + } + + return statistics; + } } diff --git a/packages/models/src/models/LivechatRooms.ts b/packages/models/src/models/LivechatRooms.ts index b77a766701832..6d4eedcbb94d5 100644 --- a/packages/models/src/models/LivechatRooms.ts +++ b/packages/models/src/models/LivechatRooms.ts @@ -1896,11 +1896,12 @@ export class LivechatRoomsRaw extends BaseRaw implements ILive return this.findOne(query, options); } - findOneOpenByVisitorToken(visitorToken: string, options: FindOptions = {}) { + findOneOpenByVisitorToken(visitorToken: string, options: FindOptions = {}, extraQuery: Filter = {}) { const query: Filter = { 't': 'l', 'open': true, 'v.token': visitorToken, + ...extraQuery, }; return this.findOne(query, options); diff --git a/packages/models/src/models/Messages.ts b/packages/models/src/models/Messages.ts index 29c50d6e74a5a..78e61d502dc21 100644 --- a/packages/models/src/models/Messages.ts +++ b/packages/models/src/models/Messages.ts @@ -1397,20 +1397,13 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { query.tcount = { $exists: false }; } - const notCountedMessages = ( - await this.find( - { - ...query, - $or: [{ _hidden: true }, { editedAt: { $exists: true }, editedBy: { $exists: true }, t: 'rm' }], - }, - { - projection: { - _id: 1, - }, - limit, - }, - ).toArray() - ).length; + const notCountedMessages = await this.countDocuments( + { + ...query, + $or: [{ _hidden: true }, { editedAt: { $exists: true }, editedBy: { $exists: true }, t: 'rm' }], + }, + { ...(limit ? { limit } : {}) }, + ); if (!limit) { const count = (await this.deleteMany(query)).deletedCount - notCountedMessages;