Skip to content

Commit

Permalink
Replace aggregation by 3 finds
Browse files Browse the repository at this point in the history
  • Loading branch information
matheusbsilva137 committed Sep 28, 2023
1 parent 432ac79 commit 9627c99
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 146 deletions.
11 changes: 1 addition & 10 deletions apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,23 +1063,14 @@ API.v1.addRoute(
const { sort = {} } = await this.parseJsonQuery();
const { status, filter } = this.queryParams;

const cursor = await findUsersOfRoomByHighestRole({
const { members, total } = await findUsersOfRoomByHighestRole({
rid: findResult._id,
...(status && { status: { $in: status } }),
limit,
filter,
...(sort?.username && { sort: { username: sort.username } }),
});

const result = await cursor.toArray();
if (!result?.[0]) {
return API.v1.failure('No user info could be found for the channel provided');
}
const {
members,
totalCount: [{ total } = { total: 0 }],
} = result[0];

return API.v1.success({
members,
count: members.length,
Expand Down
11 changes: 1 addition & 10 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,23 +757,14 @@ API.v1.addRoute(
const { sort = {} } = await this.parseJsonQuery();
const { status, filter } = this.queryParams;

const cursor = await findUsersOfRoomByHighestRole({
const { members, total } = await findUsersOfRoomByHighestRole({
rid: findResult.rid,
...(status && { status: { $in: status } }),
limit,
filter,
...(sort?.username && { sort: { username: sort.username } }),
});

const result = await cursor.toArray();
if (!result?.[0]) {
return API.v1.failure('No user info could be found for the group provided');
}
const {
members,
totalCount: [{ total } = { total: 0 }],
} = result[0];

return API.v1.success({
members,
count: members.length,
Expand Down
102 changes: 87 additions & 15 deletions apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IUserWithRoleInfo, ISubscription } from '@rocket.chat/core-typings';
import type { IUserWithRoleInfo, ISubscription, IUser } from '@rocket.chat/core-typings';
import { Users, Subscriptions } from '@rocket.chat/models';
import type { AggregationCursor, FilterOperators } from 'mongodb';
import type { FilterOperators, FindOptions } from 'mongodb';

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

Expand All @@ -13,14 +13,62 @@ type FindUsersParam = {
sort?: Record<string, any>;
};

async function findUsersWithRolesOfRoom(
{ rid, status, limit = 0, filter = '' }: FindUsersParam,
options: FindOptions<IUser>,
): Promise<{ members: IUserWithRoleInfo[]; totalCount: number; allMembersIds: string[] }> {
// Sort roles in descending order so that owners are listed before moderators
// This could possibly cause issues with custom roles, but that's intended to improve performance
const highestRolesMembersSubscriptions = await Subscriptions.findByRoomIdAndRoles(rid, ['owner', 'moderator'], {
projection: { 'u._id': 1, 'roles': 1 },
sort: { roles: -1 },
}).toArray();

const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');

const totalCount = highestRolesMembersSubscriptions.length;
const allMembersIds = highestRolesMembersSubscriptions.map((s: ISubscription) => s.u?._id);
const highestRolesMembersIdsToFind = allMembersIds.slice(0, limit);

const { cursor } = Users.findPaginatedActiveUsersByIds(filter, searchFields, highestRolesMembersIdsToFind, options, [
{
__rooms: rid,
...(status && { status }),
},
]);
const highestRolesMembers = await cursor.toArray();

// maps for efficient lookup of highest roles and sort order
const ordering: Record<string, number> = {};
const highestRoleById: Record<string, string> = {};

const limitedHighestRolesMembersSubs = highestRolesMembersSubscriptions.slice(0, limit);
limitedHighestRolesMembersSubs.forEach((sub, index) => {
ordering[sub.u._id] = index;
highestRoleById[sub.u._id] = sub.roles?.includes('owner') ? 'owner' : 'moderator';
});

highestRolesMembers.sort((a, b) => ordering[a._id] - ordering[b._id]);
const membersWithHighestRoles = highestRolesMembers.map(
(member): IUserWithRoleInfo => ({
...member,
highestRole: {
role: highestRoleById[member._id],
level: highestRoleById[member._id] === 'owner' ? 0 : 1,
},
}),
);
return { members: membersWithHighestRoles, totalCount, allMembersIds };
}

export async function findUsersOfRoomByHighestRole({
rid,
status,
limit = 0,
filter = '',
sort,
}: FindUsersParam): Promise<AggregationCursor<{ members: IUserWithRoleInfo[]; totalCount: { total: number }[] }>> {
const options = {
}: FindUsersParam): Promise<{ members: IUserWithRoleInfo[]; total: number }> {
const options: FindOptions<IUser> = {
projection: {
name: 1,
username: 1,
Expand All @@ -37,18 +85,42 @@ export async function findUsersOfRoomByHighestRole({
},
limit,
};

const extraQuery = {
__rooms: rid,
...(status && { status }),
};
const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');

const ownersIds = (await Subscriptions.findByRoomIdAndHighestRole(rid, 'owner', { projection: { 'u._id': 1 } }).toArray()).map(
(s: ISubscription) => s.u?._id,
);
const moderatorsIds = (await Subscriptions.findByRoomIdAndHighestRole(rid, 'moderator', { projection: { 'u._id': 1 } }).toArray()).map(
(s: ISubscription) => s.u?._id,
);
return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, searchFields, ownersIds, moderatorsIds, options, [
{
...(status && { status }),
},
// Find highest roles members (owners and moderator)
const {
members: highestRolesMembers,
totalCount: totalMembersWithRoles,
allMembersIds: highestRolesMembersIds,
} = await findUsersWithRolesOfRoom({ rid, status, limit, filter }, options);

if (limit <= highestRolesMembers.length) {
const totalMembersCount = await Users.countActiveUsersExcept(filter, highestRolesMembersIds, searchFields, [extraQuery]);
return { members: highestRolesMembers, total: totalMembersWithRoles + totalMembersCount };
}
if (options.limit) {
options.limit -= highestRolesMembers.length;
}

// Find average members
const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(filter, highestRolesMembersIds, searchFields, options, [
extraQuery,
]);
const [members, totalMembersCount] = await Promise.all([await cursor.toArray(), totalCount]);
const membersWithHighestRoles = members.map(
(member): IUserWithRoleInfo => ({
...member,
highestRole: {
role: 'member',
level: 2,
},
}),
);

const allMembers = highestRolesMembers.concat(membersWithHighestRoles);
return { members: allMembers, total: totalMembersWithRoles + totalMembersCount };
}
14 changes: 0 additions & 14 deletions apps/meteor/server/models/raw/Subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,20 +943,6 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
return this.find(query, options);
}

findByRoomIdAndHighestRole(roomId: string, role: string, options?: FindOptions<ISubscription>): FindCursor<ISubscription> {
// roles ordered from lowest to highest
const mainRoles = ['moderator', 'owner'];

const roleIndex = mainRoles.indexOf(role || '');
const rolesToExclude = mainRoles.slice(roleIndex + 1);
const query = {
rid: roomId,
roles: { $eq: role, $nin: rolesToExclude },
};

return this.find(query, options);
}

countByRoomIdAndRoles(roomId: string, roles: string[]): Promise<number> {
roles = ([] as string[]).concat(roles);
const query = {
Expand Down
123 changes: 44 additions & 79 deletions apps/meteor/server/models/raw/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,8 @@ export class UsersRaw extends BaseRaw {
$and: [
{
active: true,
username: {
$exists: true,
...(exceptions.length && { $nin: exceptions }),
},
...(exceptions.length && { _id: { $nin: exceptions } }),
username: { $exists: true },
// if the search term is empty, don't need to have the $or statement (because it would be an empty regex)
...(searchTerm && orStmt.length && { $or: orStmt }),
},
Expand All @@ -325,12 +323,39 @@ export class UsersRaw extends BaseRaw {
return this.findPaginated(query, options);
}

findPaginatedActiveUsersByRoomIdWithHighestRole(
countActiveUsersExcept(searchTerm, exceptions, searchFields, extraQuery = [], { startsWith = false, endsWith = false } = {}) {
if (exceptions == null) {
exceptions = [];
}
if (!Array.isArray(exceptions)) {
exceptions = [exceptions];
}

const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : '');
const termRegex = new RegExp(regexString, 'i');

const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex }));

const query = {
$and: [
{
active: true,
...(exceptions.length && { _id: { $nin: exceptions } }),
username: { $exists: true },
// if the search term is empty, don't need to have the $or statement (because it would be an empty regex)
...(searchTerm && orStmt.length && { $or: orStmt }),
},
...extraQuery,
],
};

return this.col.countDocuments(query);
}

findPaginatedActiveUsersByIds(
searchTerm,
rid,
searchFields,
ownersIds,
moderatorsIds,
ids = [],
options = {},
extraQuery = [],
{ startsWith = false, endsWith = false } = {},
Expand All @@ -339,81 +364,21 @@ export class UsersRaw extends BaseRaw {
const termRegex = new RegExp(regexString, 'i');

const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex }));
const userSearchConditions = [
{
active: true,
__rooms: rid,
username: { $exists: true },
// if the search term is empty, don't need to have the $or statement (because it would be an empty regex)
...(searchTerm && orStmt.length && { $or: orStmt }),
},
...extraQuery,
];

const ownerQuery = { $and: [...userSearchConditions, { _id: { $in: ownersIds } }] };
const moderatorQuery = { $and: [...userSearchConditions, { _id: { $in: moderatorsIds } }] };
const memberQuery = { $and: [...userSearchConditions, { _id: { $nin: [...ownersIds, ...moderatorsIds] } }] };

const skip =
options.skip !== 0
? [
{
$skip: options.skip,
},
]
: [];

const limit =
options.limit !== 0
? [
{
$limit: options.limit,
},
]
: [];

return this.col.aggregate(
[
{
$facet: {
owners: [
{ $match: ownerQuery },
{ $project: options.projection },
{ $addFields: { highestRole: { role: 'owner', level: 0 } } },
],
moderators: [
{ $match: moderatorQuery },
{ $project: options.projection },
{ $addFields: { highestRole: { role: 'moderator', level: 1 } } },
],
members: [
{ $match: memberQuery },
{ $project: options.projection },
{ $addFields: { highestRole: { role: 'member', level: 2 } } },
],
},
},
{ $project: { allMembers: { $concatArrays: ['$owners', '$moderators', '$members'] } } },
{ $unwind: '$allMembers' },
{ $replaceRoot: { newRoot: '$allMembers' } },
const query = {
$and: [
{
$facet: {
members: [
{
$sort: {
'highestRole.level': 1,
...options.sort,
},
},
...skip,
...limit,
],
totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }],
},
active: true,
...(ids.length && { _id: { $in: ids } }),
username: { $exists: true },
// if the search term is empty, don't need to have the $or statement (because it would be an empty regex)
...(searchTerm && orStmt.length && { $or: orStmt }),
},
...extraQuery,
],
{ allowDiskUse: true },
);
};

return this.findPaginated(query, options);
}

findPaginatedByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) {
Expand Down
1 change: 0 additions & 1 deletion packages/model-typings/src/models/ISubscriptionsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {
options?: FindOptions<ISubscription>,
): FindCursor<ISubscription>;
findByRoomIdAndRoles(roomId: string, roles: string[], options?: FindOptions<ISubscription>): FindCursor<ISubscription>;
findByRoomIdAndHighestRole(roomId: string, role: string, options?: FindOptions<ISubscription>): FindCursor<ISubscription>;
findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions<ISubscription>): FindCursor<ISubscription>;
findByUserIdUpdatedAfter(userId: string, updatedAt: Date, options?: FindOptions<ISubscription>): FindCursor<ISubscription>;

Expand Down
Loading

0 comments on commit 9627c99

Please sign in to comment.