Skip to content

Commit ff2748b

Browse files
committed
using rolePriority value to sort
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
1 parent c3ad3cc commit ff2748b

File tree

11 files changed

+332
-167
lines changed

11 files changed

+332
-167
lines changed

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

+3-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Media, Team } from '@rocket.chat/core-services';
22
import type { IRoom, IUpload } from '@rocket.chat/core-typings';
3-
import { Messages, Rooms, Users, Uploads, Subscriptions, Roles } from '@rocket.chat/models';
3+
import { Messages, Rooms, Users, Uploads, Subscriptions } from '@rocket.chat/models';
44
import type { Notifications } from '@rocket.chat/rest-typings';
55
import {
66
isGETRoomsNameExists,
@@ -880,32 +880,15 @@ API.v1.addRoute(
880880
const { offset: skip, count: limit } = await getPaginationItems(this.queryParams);
881881
const { sort = {} } = await this.parseJsonQuery();
882882

883-
const { status, filter, rolesOrder } = this.queryParams;
884-
885-
if (rolesOrder) {
886-
const roles = await Promise.all(
887-
await Roles.find({
888-
scope: 'Subscriptions',
889-
_id: { $in: rolesOrder },
890-
}).toArray(),
891-
);
892-
893-
const rolesIds = roles.map(({ _id }) => _id);
894-
rolesOrder.forEach((providedRole) => {
895-
if (!rolesIds.includes(providedRole)) {
896-
throw new Error(`role "${providedRole}" not found`);
897-
}
898-
});
899-
}
883+
const { status, filter } = this.queryParams;
900884

901885
const { members, total } = await findUsersOfRoomOrderedByRole({
902886
rid: findResult._id,
903887
...(status && { status: { $in: status } }),
904888
skip,
905889
limit,
906890
filter,
907-
...(sort?.username && { sort: { username: sort.username } }),
908-
rolesInOrder: rolesOrder || ['owner', 'moderator'],
891+
sort: { ...(sort.rolePriority && { rolePriority: sort.rolePriority }), ...(sort.username && { username: sort.username }) },
909892
});
910893

911894
return API.v1.success({

apps/meteor/server/lib/findUsersOfRoomOrderedByRole.ts

+59-94
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { IUser, IRole } from '@rocket.chat/core-typings';
2-
import { Subscriptions } from '@rocket.chat/models';
1+
import { type IUser, type IRole, ROOM_ROLE_PRIORITY_MAP } from '@rocket.chat/core-typings';
2+
import { Subscriptions, Users } from '@rocket.chat/models';
33
import { escapeRegExp } from '@rocket.chat/string-helpers';
44
import type { Document, FilterOperators } from 'mongodb';
55

@@ -12,7 +12,6 @@ type FindUsersParam = {
1212
limit?: number;
1313
filter?: string;
1414
sort?: Record<string, any>;
15-
rolesInOrder?: IRole['_id'][];
1615
exceptions?: string[];
1716
extraQuery?: Document[];
1817
};
@@ -28,135 +27,101 @@ export async function findUsersOfRoomOrderedByRole({
2827
limit = 0,
2928
filter = '',
3029
sort,
31-
rolesInOrder = [],
3230
exceptions = [],
3331
extraQuery = [],
3432
}: FindUsersParam): Promise<{ members: UserWithRoleData[]; total: number }> {
3533
const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');
3634
const termRegex = new RegExp(escapeRegExp(filter), 'i');
3735
const orStmt = filter && searchFields.length ? searchFields.map((field) => ({ [field.trim()]: termRegex })) : [];
3836

39-
const useRealName = settings.get('UI_Use_Real_Name');
40-
const defaultSort = useRealName ? { name: 1 } : { username: 1 };
37+
const { rolePriority: rolePrioritySort, ...rest } = sort || {};
4138

4239
const sortCriteria = {
43-
rolePriority: 1,
40+
rolePriority: rolePrioritySort ?? 1,
4441
statusConnection: -1,
45-
...(sort || defaultSort),
42+
...(Object.keys(rest).length > 0
43+
? rest
44+
: {
45+
...(settings.get('UI_Use_Real_Name') ? { name: 1 } : { username: 1 }),
46+
}),
4647
};
4748

48-
const userLookupPipeline: Document[] = [
49-
{
50-
$match: {
51-
$and: [
52-
{
53-
$expr: { $eq: ['$_id', '$$userId'] },
54-
active: true,
55-
username: {
56-
$exists: true,
57-
...(exceptions.length > 0 && { $nin: exceptions }),
58-
},
59-
...(status && { status }),
60-
...(filter && orStmt.length > 0 && { $or: orStmt }),
61-
},
62-
...extraQuery,
63-
],
64-
},
65-
},
66-
];
67-
68-
const defaultPriority = rolesInOrder.length + 1;
69-
70-
const branches = rolesInOrder.map((role, index) => ({
71-
case: { $eq: ['$$this', role] },
72-
then: index + 1,
73-
}));
74-
75-
const filteredPipeline: Document[] = [
76-
{
77-
$lookup: {
78-
from: 'users',
79-
let: { userId: '$u._id' },
80-
pipeline: userLookupPipeline,
81-
as: 'userDetails',
49+
const matchUserFilter = {
50+
$and: [
51+
{
52+
__rooms: rid,
53+
active: true,
54+
username: {
55+
$exists: true,
56+
...(exceptions.length > 0 && { $nin: exceptions }),
57+
},
58+
...(status && { status }),
59+
...(filter && orStmt.length > 0 && { $or: orStmt }),
8260
},
83-
},
84-
{ $unwind: '$userDetails' },
85-
];
61+
...extraQuery,
62+
],
63+
};
8664

87-
const membersResult = Subscriptions.col.aggregate(
65+
const membersResult = Users.col.aggregate(
8866
[
89-
{ $match: { rid } },
90-
...filteredPipeline,
9167
{
92-
$addFields: {
93-
primaryRole: {
94-
$reduce: {
95-
input: '$roles',
96-
initialValue: { role: null, priority: defaultPriority },
97-
in: {
98-
$let: {
99-
vars: {
100-
currentPriority: {
101-
$switch: {
102-
branches,
103-
default: defaultPriority,
104-
},
105-
},
106-
},
107-
in: {
108-
$cond: [
109-
{
110-
$and: [{ $in: ['$$this', rolesInOrder] }, { $lt: ['$$currentPriority', '$$value.priority'] }],
111-
},
112-
{ role: '$$this', priority: '$$currentPriority' },
113-
'$$value',
114-
],
115-
},
68+
$match: matchUserFilter,
69+
},
70+
{
71+
$project: {
72+
_id: 1,
73+
name: 1,
74+
username: 1,
75+
nickname: 1,
76+
status: 1,
77+
avatarETag: 1,
78+
_updatedAt: 1,
79+
federated: 1,
80+
rolePriority: {
81+
$ifNull: [`$roomRolePriorities.${rid}`, ROOM_ROLE_PRIORITY_MAP.default],
82+
},
83+
},
84+
},
85+
{ $sort: sortCriteria },
86+
...(skip > 0 ? [{ $skip: skip }] : []),
87+
...(limit > 0 ? [{ $limit: limit }] : []),
88+
{
89+
$lookup: {
90+
from: Subscriptions.getCollectionName(),
91+
as: 'subscription',
92+
let: { userId: '$_id', roomId: rid },
93+
pipeline: [
94+
{
95+
$match: {
96+
$expr: {
97+
$and: [{ $eq: ['$rid', '$$roomId'] }, { $eq: ['$u._id', '$$userId'] }],
11698
},
11799
},
118100
},
119-
},
101+
{ $project: { roles: 1 } },
102+
],
120103
},
121104
},
122105
{
123106
$addFields: {
124-
rolePriority: { $ifNull: ['$primaryRole.priority', defaultPriority] },
107+
roles: { $arrayElemAt: ['$subscription.roles', 0] },
125108
},
126109
},
127110
{
128111
$project: {
129-
_id: '$userDetails._id',
130-
rid: 1,
131-
roles: 1,
132-
primaryRole: '$primaryRole.role',
133-
rolePriority: 1,
134-
username: '$userDetails.username',
135-
name: '$userDetails.name',
136-
nickname: '$userDetails.nickname',
137-
status: '$userDetails.status',
138-
avatarETag: '$userDetails.avatarETag',
139-
_updatedAt: '$userDetails._updatedAt',
140-
federated: '$userDetails.federated',
141-
statusConnection: '$userDetails.statusConnection',
112+
subscription: 0,
142113
},
143114
},
144-
{ $sort: sortCriteria },
145-
...(skip > 0 ? [{ $skip: skip }] : []),
146-
...(limit > 0 ? [{ $limit: limit }] : []),
147115
],
148116
{
149117
allowDiskUse: true,
150118
},
151119
);
152120

153-
const totalResult = Subscriptions.col.aggregate([{ $match: { rid } }, ...filteredPipeline, { $count: 'total' }], { allowDiskUse: true });
154-
155-
const [members, [{ totalCount }]] = await Promise.all([membersResult.toArray(), totalResult.toArray()]);
121+
const [members, totalCount] = await Promise.all([membersResult.toArray(), Users.countDocuments(matchUserFilter)]);
156122

157123
return {
158124
members: members.map((member: any) => {
159-
delete member.primaryRole;
160125
delete member.rolePriority;
161126
return member;
162127
}),

apps/meteor/server/startup/migrations/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ import './v315';
2424
import './v316';
2525
import './v317';
2626
import './v318';
27+
import './v320';
2728

2829
export * from './xrun';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import type { IRole, IUser } from '@rocket.chat/core-typings';
2+
import { ROOM_ROLE_PRIORITY_MAP } from '@rocket.chat/core-typings';
3+
import { Users, Subscriptions } from '@rocket.chat/models';
4+
5+
import { addMigration } from '../../lib/migrations';
6+
7+
export const getRoomRolePriorityForRole = (role: IRole['_id']) =>
8+
(ROOM_ROLE_PRIORITY_MAP as { [key: IRole['_id']]: number })[role] ?? ROOM_ROLE_PRIORITY_MAP.default;
9+
10+
export const calculateRoomRolePriorityFromRoles = (roles: IRole['_id'][]) => {
11+
return roles.reduce((acc, roleId) => Math.min(acc, getRoomRolePriorityForRole(roleId)), ROOM_ROLE_PRIORITY_MAP.default);
12+
};
13+
14+
const READ_BATCH_SIZE = 1000;
15+
const WRITE_BATCH_SIZE = 1000;
16+
17+
async function assignRoomRolePrioritiesFromMap(userIdAndroomRolePrioritiesMap: Map<IUser['_id'], IUser['roomRolePriorities']>) {
18+
const bulkOperations: any[] = [];
19+
let counter = 0;
20+
21+
const mapValues = userIdAndroomRolePrioritiesMap.entries();
22+
for (const [userId, roomRolePriorities] of mapValues) {
23+
userIdAndroomRolePrioritiesMap.delete(userId);
24+
if (roomRolePriorities) {
25+
bulkOperations.push({
26+
updateOne: {
27+
filter: { _id: userId },
28+
update: {
29+
$set: {
30+
...Object.entries(roomRolePriorities).reduce(
31+
(operations, roomPriority) => {
32+
const [rid, priority] = roomPriority;
33+
operations[`roomRolePriorities.${rid}`] = priority;
34+
return operations;
35+
},
36+
{} as Record<string, number>,
37+
),
38+
},
39+
},
40+
},
41+
});
42+
43+
counter++;
44+
45+
if (counter >= WRITE_BATCH_SIZE) {
46+
// eslint-disable-next-line no-await-in-loop
47+
await Users.col.bulkWrite(bulkOperations, { ordered: false });
48+
bulkOperations.length = 0;
49+
counter = 0;
50+
}
51+
}
52+
}
53+
54+
if (bulkOperations.length > 0) {
55+
await Users.col.bulkWrite(bulkOperations, { ordered: false });
56+
}
57+
58+
return userIdAndroomRolePrioritiesMap;
59+
}
60+
61+
addMigration({
62+
version: 320,
63+
name: 'Add "roomRolePriorities" field to users',
64+
async up() {
65+
let userIdAndroomRolePrioritiesMap = new Map<IUser['_id'], IUser['roomRolePriorities']>();
66+
67+
let skip = 0;
68+
69+
let hasMoreSubscriptions = true;
70+
do {
71+
const subscriptionsCursor = Subscriptions.find(
72+
{ roles: { $exists: true } },
73+
{
74+
projection: { 'rid': 1, 'roles': 1, 'u._id': 1 },
75+
sort: { _id: 1 },
76+
skip,
77+
limit: READ_BATCH_SIZE,
78+
},
79+
);
80+
81+
// eslint-disable-next-line no-await-in-loop
82+
const subscriptions = await subscriptionsCursor.toArray();
83+
84+
if (subscriptions.length === 0) {
85+
hasMoreSubscriptions = false;
86+
break;
87+
}
88+
89+
for (const sub of subscriptions) {
90+
if (!sub.roles?.length) {
91+
continue;
92+
}
93+
94+
const userId = sub.u._id;
95+
const roomId = sub.rid;
96+
const priority = calculateRoomRolePriorityFromRoles(sub.roles);
97+
98+
const existingPriorities = userIdAndroomRolePrioritiesMap.get(userId) || {};
99+
existingPriorities[roomId] = priority;
100+
userIdAndroomRolePrioritiesMap.set(userId, existingPriorities);
101+
}
102+
103+
skip += READ_BATCH_SIZE;
104+
} while (hasMoreSubscriptions);
105+
106+
userIdAndroomRolePrioritiesMap = await assignRoomRolePrioritiesFromMap(userIdAndroomRolePrioritiesMap);
107+
},
108+
});

0 commit comments

Comments
 (0)