Skip to content

Commit ebbc00c

Browse files
Replace owner/moderator finds by aggregation
1 parent caa78a4 commit ebbc00c

File tree

3 files changed

+124
-61
lines changed

3 files changed

+124
-61
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,18 @@
1-
import type { IUserWithRoleInfo, ISubscription, IUser } from '@rocket.chat/core-typings';
1+
import type { IUserWithRoleInfo, IUser, IRoom, UserStatus } from '@rocket.chat/core-typings';
22
import { Users, Subscriptions } from '@rocket.chat/models';
3-
import type { FilterOperators, FindOptions } from 'mongodb';
3+
import type { Filter, FilterOperators, FindOptions } from 'mongodb';
44

55
import { settings } from '../../app/settings/server';
66

77
type FindUsersParam = {
88
rid: string;
9-
status?: FilterOperators<string>;
9+
status?: FilterOperators<UserStatus>;
1010
skip?: number;
1111
limit?: number;
1212
filter?: string;
1313
sort?: Record<string, any>;
1414
};
1515

16-
async function findUsersWithRolesOfRoom(
17-
{ rid, status, limit = 0, filter = '' }: FindUsersParam,
18-
options: FindOptions<IUser>,
19-
): Promise<{ members: IUserWithRoleInfo[]; totalCount: number; allMembersIds: string[] }> {
20-
// Sort roles in descending order so that owners are listed before moderators
21-
// This could possibly cause issues with custom roles, but that's intended to improve performance
22-
const highestRolesMembersSubscriptions = await Subscriptions.findByRoomIdAndRoles(rid, ['owner', 'moderator'], {
23-
projection: { 'u._id': 1, 'roles': 1 },
24-
sort: { roles: -1 },
25-
}).toArray();
26-
27-
const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');
28-
29-
const totalCount = highestRolesMembersSubscriptions.length;
30-
const allMembersIds = highestRolesMembersSubscriptions.map((s: ISubscription) => s.u?._id);
31-
const highestRolesMembersIdsToFind = allMembersIds.slice(0, limit);
32-
33-
const { cursor } = Users.findPaginatedActiveUsersByIds(filter, searchFields, highestRolesMembersIdsToFind, options, [
34-
{
35-
__rooms: rid,
36-
...(status && { status }),
37-
},
38-
]);
39-
const highestRolesMembers = await cursor.toArray();
40-
41-
// maps for efficient lookup of highest roles and sort order
42-
const ordering: Record<string, number> = {};
43-
const highestRoleById: Record<string, string> = {};
44-
45-
const limitedHighestRolesMembersSubs = highestRolesMembersSubscriptions.slice(0, limit);
46-
limitedHighestRolesMembersSubs.forEach((sub, index) => {
47-
ordering[sub.u._id] = index;
48-
highestRoleById[sub.u._id] = sub.roles?.includes('owner') ? 'owner' : 'moderator';
49-
});
50-
51-
highestRolesMembers.sort((a, b) => ordering[a._id] - ordering[b._id]);
52-
const membersWithHighestRoles = highestRolesMembers.map(
53-
(member): IUserWithRoleInfo => ({
54-
...member,
55-
highestRole: {
56-
role: highestRoleById[member._id],
57-
level: highestRoleById[member._id] === 'owner' ? 0 : 1,
58-
},
59-
}),
60-
);
61-
return { members: membersWithHighestRoles, totalCount, allMembersIds };
62-
}
63-
6416
export async function findUsersOfRoomByHighestRole({
6517
rid,
6618
status,
@@ -85,22 +37,25 @@ export async function findUsersOfRoomByHighestRole({
8537
},
8638
limit,
8739
};
88-
const extraQuery = {
40+
const extraQuery: Filter<IUser & { __rooms: IRoom['_id'][] }> = {
8941
__rooms: rid,
9042
...(status && { status }),
9143
};
9244
const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');
9345

94-
// Find highest roles members (owners and moderator)
46+
// Find highest roles members (owners and moderators)
47+
const result = await Subscriptions.findPaginatedActiveHighestRoleUsers(filter, rid, searchFields, options, extraQuery);
9548
const {
96-
members: highestRolesMembers,
97-
totalCount: totalMembersWithRoles,
98-
allMembersIds: highestRolesMembersIds,
99-
} = await findUsersWithRolesOfRoom({ rid, status, limit, filter }, options);
49+
members: highestRolesMembers = [],
50+
totalCount: totalMembersWithRoles = { total: 0 },
51+
ids = { allMembersIds: [] },
52+
} = result[0] || {};
53+
const { total: totalMembersWithRolesCount } = totalMembersWithRoles;
54+
const { allMembersIds: highestRolesMembersIds } = ids;
10055

10156
if (limit <= highestRolesMembers.length) {
10257
const totalMembersCount = await Users.countActiveUsersExcept(filter, highestRolesMembersIds, searchFields, [extraQuery]);
103-
return { members: highestRolesMembers, total: totalMembersWithRoles + totalMembersCount };
58+
return { members: highestRolesMembers, total: totalMembersWithRolesCount + totalMembersCount };
10459
}
10560
if (options.limit) {
10661
options.limit -= highestRolesMembers.length;
@@ -122,5 +77,5 @@ export async function findUsersOfRoomByHighestRole({
12277
);
12378

12479
const allMembers = highestRolesMembers.concat(membersWithHighestRoles);
125-
return { members: allMembers, total: totalMembersWithRoles + totalMembersCount };
80+
return { members: allMembers, total: totalMembersWithRolesCount + totalMembersCount };
12681
}

apps/meteor/server/models/raw/Subscriptions.ts

+100-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType, SpotlightUser } from '@rocket.chat/core-typings';
1+
import type {
2+
IRole,
3+
IRoom,
4+
ISubscription,
5+
IUser,
6+
IUserWithRoleInfo,
7+
RocketChatRecordDeleted,
8+
RoomType,
9+
SpotlightUser,
10+
} from '@rocket.chat/core-typings';
211
import type { ISubscriptionsModel } from '@rocket.chat/model-typings';
312
import { Rooms, Users } from '@rocket.chat/models';
413
import { escapeRegExp } from '@rocket.chat/string-helpers';
@@ -450,6 +459,96 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
450459
.toArray();
451460
}
452461

462+
async findPaginatedActiveHighestRoleUsers(
463+
searchTerm: string,
464+
rid: IRoom['_id'],
465+
searchFields: string[],
466+
options: FindOptions<IUser> = {},
467+
extraQuery?: Filter<IUser>,
468+
{ startsWith = false, endsWith = false }: { startsWith?: string | false; endsWith?: string | false } = {},
469+
): Promise<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }[]> {
470+
const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i');
471+
const orStatement = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })) as { [x: string]: RegExp }[];
472+
473+
const limit =
474+
options.limit !== 0
475+
? [
476+
{
477+
$limit: options.limit,
478+
},
479+
]
480+
: [];
481+
482+
return this.col
483+
.aggregate<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }>(
484+
[
485+
{
486+
$match: {
487+
rid,
488+
roles: { $in: ['owner', 'moderator'] },
489+
},
490+
},
491+
{
492+
$lookup: {
493+
from: 'users',
494+
as: 'user',
495+
let: { id: '$u._id' },
496+
pipeline: [
497+
{
498+
$match: {
499+
$expr: { $eq: ['$_id', '$$id'] },
500+
username: { $exists: true },
501+
active: true,
502+
...(searchTerm && orStatement.length > 0 && { $or: orStatement }),
503+
...extraQuery,
504+
},
505+
},
506+
{ $project: options.projection },
507+
],
508+
},
509+
},
510+
{
511+
$unwind: {
512+
path: '$user',
513+
},
514+
},
515+
{
516+
$addFields: {
517+
'user.highestRole': {
518+
$cond: [{ $in: ['owner', '$roles'] }, { role: 'owner', level: 0 }, { role: 'moderator', level: 1 }],
519+
},
520+
},
521+
},
522+
{
523+
$replaceRoot: { newRoot: '$user' },
524+
},
525+
{
526+
$facet: {
527+
members: [
528+
{
529+
$sort: {
530+
'highestRole.level': 1,
531+
...(options.sort as object),
532+
},
533+
},
534+
...limit,
535+
],
536+
ids: [{ $group: { _id: null, allMembersIds: { $push: '$_id' } } }],
537+
totalCount: [{ $count: 'total' }],
538+
},
539+
},
540+
{
541+
$unwind: { path: '$totalCount' },
542+
},
543+
{
544+
$unwind: { path: '$ids' },
545+
},
546+
],
547+
{ allowDiskUse: true },
548+
)
549+
.toArray();
550+
}
551+
453552
incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise<UpdateResult | Document> {
454553
if (inc == null) {
455554
inc = 1;

packages/model-typings/src/models/ISubscriptionsModel.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings';
1+
import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser, IUserWithRoleInfo } from '@rocket.chat/core-typings';
22
import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter, InsertOneResult } from 'mongodb';
33

44
import type { IBaseModel } from './IBaseModel';
@@ -76,6 +76,15 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {
7676
options?: AggregateOptions,
7777
): Promise<SpotlightUser[]>;
7878

79+
findPaginatedActiveHighestRoleUsers(
80+
searchTerm: string,
81+
rid: IRoom['_id'],
82+
searchFields: string[],
83+
options?: FindOptions<IUser>,
84+
extraQuery?: Filter<IUser & { __rooms: IRoom['_id'][] }>,
85+
{ startsWith = false, endsWith = false }?: { startsWith?: string | false; endsWith?: string | false },
86+
): Promise<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }[]>;
87+
7988
incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise<UpdateResult | Document>;
8089

8190
setAlertForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise<UpdateResult | Document>;

0 commit comments

Comments
 (0)