Skip to content

Commit

Permalink
feat: New endpoint for listing rooms & discussions from teams #33177
Browse files Browse the repository at this point in the history
…updated
  • Loading branch information
juliajforesti committed Sep 2, 2024
1 parent a62aaec commit 77f1eb2
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 92 deletions.
34 changes: 21 additions & 13 deletions apps/meteor/app/api/server/v1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
isTeamsDeleteProps,
isTeamsLeaveProps,
isTeamsUpdateProps,
isTeamsListRoomsAndDiscussionsProps,
isTeamsListChildrenProps,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
Expand Down Expand Up @@ -108,7 +108,7 @@ API.v1.addRoute(
},
);

const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string } | { roomId: string }): Promise<ITeam | null> => {
const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string }): Promise<ITeam | null> => {
if ('teamId' in params && params.teamId) {
return Team.getOneById<ITeam>(params.teamId);
}
Expand All @@ -117,10 +117,6 @@ const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string
return Team.getOneByName(params.teamName);
}

if ('roomId' in params && params.roomId) {
return Team.getOneByRoomId(params.roomId);
}

return null;
};

Expand Down Expand Up @@ -272,9 +268,6 @@ API.v1.addRoute(
Match.ObjectIncluding({
teamName: String,
}),
Match.ObjectIncluding({
roomId: String,
}),
),
);

Expand Down Expand Up @@ -383,23 +376,38 @@ API.v1.addRoute(
},
);

const getTeamByIdOrNameOrParentRoom = async (
params: { teamId: string } | { teamName: string } | { roomId: string },
): Promise<ITeam | null> => {
if ('teamId' in params && params.teamId) {
return Team.getOneById<ITeam>(params.teamId);
}
if ('teamName' in params && params.teamName) {
return Team.getOneByName(params.teamName);
}
if ('roomId' in params && params.roomId) {
return Team.getOneByRoomId(params.roomId);
}
return null;
};

// This should accept a teamId, filter (search by name on rooms collection) and sort/pagination
// should return a list of rooms/discussions from the team. the discussions will only be returned from the main room
API.v1.addRoute(
'teams.listRoomsAndDiscussions',
{ authRequired: true, validateParams: isTeamsListRoomsAndDiscussionsProps },
'teams.listChildren',
{ authRequired: true, validateParams: isTeamsListChildrenProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();
const { filter } = this.queryParams;

const team = await getTeamByIdOrName(this.queryParams);
const team = await getTeamByIdOrNameOrParentRoom(this.queryParams);
if (!team) {
return API.v1.notFound();
}

const data = await Team.listRoomsAndDiscussions(this.userId, team, filter, sort, offset, count);
const data = await Team.listChildren(this.userId, team, filter, sort, offset, count);

return API.v1.success({ ...data, offset, count });
},
Expand Down
96 changes: 78 additions & 18 deletions apps/meteor/server/models/raw/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,22 +1462,6 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
return this.find({ uids: { $size: 2, $in: [uids] }, t: 'd' });
}

findPaginatedByNameOrFnameInIds(
ids: IRoom['_id'][],
filter?: string,
options: FindOptions<IRoom> = {},
): FindPaginated<FindCursor<IRoom>> {
const regxp = filter && new RegExp(escapeRegExp(filter), 'i');
const query: Filter<IRoom> = {
_id: {
$in: ids,
},
...(regxp && { $or: [{ name: regxp }, { fname: regxp }] }),
};

return this.findPaginated(query, options);
}

// UPDATE
addImportIds(_id: IRoom['_id'], importIds: string[]): Promise<UpdateResult> {
const query: Filter<IRoom> = { _id };
Expand Down Expand Up @@ -2076,7 +2060,83 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {
return this.updateMany(query, update);
}

findDiscussionsByPrid(prid: string, options?: FindOptions<IRoom>): FindCursor<IRoom> {
return this.find({ prid }, options);
findChildrenOfTeam(
teamId: string,
teamRoomId: string,
userId: string,
filter?: string,
options?: FindOptions<IRoom>,
): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }> {
const nameFilter = filter ? new RegExp(escapeRegExp(filter), 'i') : undefined;
return this.col.aggregate<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>([
{
$match: {
$and: [
{
$or: [
{
teamId,
},
{ prid: teamRoomId },
],
},
...(nameFilter ? [{ $or: [{ fname: nameFilter }, { name: nameFilter }] }] : []),
],
},
},
{
$lookup: {
from: 'rocketchat_subscription',
let: {
roomId: '$_id',
},
pipeline: [
{
$match: {
$and: [
{
$expr: {
$eq: ['$rid', '$$roomId'],
},
},
{
$expr: {
$eq: ['$u._id', userId],
},
},
],
},
},
{
$project: { _id: 1 },
},
],
as: 'subscription',
},
},
{
$match: {
$or: [
{ t: 'c' },
{
$expr: {
$ne: [{ $size: '$subscription' }, 0],
},
},
],
},
},
{ $project: { subscription: 0 } },
{ $sort: options?.sort || { ts: 1 } },
{
$facet: {
totalCount: [{ $count: 'count' }],
paginatedResults: [
{ $skip: options?.skip || 0 }, // Replace 0 with your skip value
{ $limit: options?.limit || 50 }, // Replace 10 with your limit value
],
},
},
]);
}
}
28 changes: 5 additions & 23 deletions apps/meteor/server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1056,7 +1056,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService {

// Returns the list of rooms and discussions a user has access to inside a team
// Rooms returned are a composition of the rooms the user is in + public rooms + discussions from the main room (if any)
async listRoomsAndDiscussions(
async listChildren(
userId: string,
team: ITeam,
filter?: string,
Expand All @@ -1069,30 +1069,12 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
throw new Error('error-invalid-team-no-main-room');
}

const [discussionIds, teamRooms] = await Promise.all([
Rooms.findDiscussionsByPrid(mainRoom._id, { projection: { _id: 1 } })
.map(({ _id }) => _id)
.toArray(),
Rooms.findByTeamId(team._id, { projection: { _id: 1, t: 1 } }).toArray(),
]);

const teamPublicIds = teamRooms.filter(({ t }) => t === 'c').map(({ _id }) => _id);
const teamRoomIds = teamRooms.map(({ _id }) => _id);
const roomIds = await Subscriptions.findByUserIdAndRoomIds(userId, teamRoomIds, { projection: { rid: 1 } })
.map(({ rid }) => rid)
.toArray();

const { cursor, totalCount } = Rooms.findPaginatedByNameOrFnameInIds(
[...new Set([mainRoom._id, ...roomIds, ...discussionIds, ...teamPublicIds])],
filter,
const [
{
skip,
limit,
sort,
totalCount: [{ count: total }],
paginatedResults: data,
},
);

const [data, total] = await Promise.all([cursor.toArray(), totalCount]);
] = await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, { skip, limit, sort }).toArray();

return {
total,
Expand Down
64 changes: 44 additions & 20 deletions apps/meteor/tests/end-to-end/api/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2218,7 +2218,7 @@ describe('[Teams]', () => {
});
});

describe('[teams.listRoomsAndDiscussions]', () => {
describe('[teams.listChildren]', () => {
const teamName = `team-${Date.now()}`;
let testTeam: ITeam;
let testUser: IUser;
Expand Down Expand Up @@ -2327,39 +2327,35 @@ describe('[Teams]', () => {
});

it('should fail if user is not logged in', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).expect(401);
await request.get(api('teams.listChildren')).expect(401);
});

it('should fail if teamId is not passed as queryparam', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).set(credentials).expect(400);
await request.get(api('teams.listChildren')).set(credentials).expect(400);
});

it('should fail if teamId is not valid', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).set(credentials).query({ teamId: 'invalid' }).expect(404);
await request.get(api('teams.listChildren')).set(credentials).query({ teamId: 'invalid' }).expect(404);
});

it('should fail if teamId is empty', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).set(credentials).query({ teamId: '' }).expect(404);
await request.get(api('teams.listChildren')).set(credentials).query({ teamId: '' }).expect(404);
});

it('should fail if both properties are passed', async () => {
await request
.get(api('teams.listRoomsAndDiscussions'))
.set(credentials)
.query({ teamId: testTeam._id, teamName: testTeam.name })
.expect(400);
await request.get(api('teams.listChildren')).set(credentials).query({ teamId: testTeam._id, teamName: testTeam.name }).expect(400);
});

it('should fail if teamName is empty', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).set(credentials).query({ teamName: '' }).expect(404);
await request.get(api('teams.listChildren')).set(credentials).query({ teamName: '' }).expect(404);
});

it('should fail if teamName is invalid', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).set(credentials).query({ teamName: 'invalid' }).expect(404);
await request.get(api('teams.listChildren')).set(credentials).query({ teamName: 'invalid' }).expect(404);
});

it('should return a list of valid rooms for user', async () => {
const res = await request.get(api('teams.listRoomsAndDiscussions')).query({ teamId: testTeam._id }).set(credentials).expect(200);
const res = await request.get(api('teams.listChildren')).query({ teamId: testTeam._id }).set(credentials).expect(200);

expect(res.body).to.have.property('total').to.be.equal(5);
expect(res.body).to.have.property('data').to.be.an('array');
Expand Down Expand Up @@ -2391,11 +2387,39 @@ describe('[Teams]', () => {
});

it('should return a valid list of rooms for non admin member too', async () => {
const res = await request
.get(api('teams.listRoomsAndDiscussions'))
.query({ teamName: testTeam.name })
.set(testUserCredentials)
.expect(200);
const res = await request.get(api('teams.listChildren')).query({ teamName: testTeam.name }).set(testUserCredentials).expect(200);

expect(res.body).to.have.property('total').to.be.equal(5);
expect(res.body).to.have.property('data').to.be.an('array');
expect(res.body.data).to.have.lengthOf(5);

const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId);
expect(mainRoom).to.be.an('object');

const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id);
expect(publicChannel1).to.be.an('object');

const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id);
expect(publicChannel2).to.be.an('object');

const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id);
expect(privateChannel1).to.be.undefined;

const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id);
expect(privateChannel2).to.be.an('object');

const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id);
expect(discussionOnP).to.be.undefined;

const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id);
expect(discussionOnC).to.be.undefined;

const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id);
expect(mainDiscussion).to.be.an('object');
});

it('should return a valid list of rooms for non admin member too when filtering by teams main room id', async () => {
const res = await request.get(api('teams.listChildren')).query({ roomId: testTeam.roomId }).set(testUserCredentials).expect(200);

expect(res.body).to.have.property('total').to.be.equal(5);
expect(res.body).to.have.property('data').to.be.an('array');
Expand Down Expand Up @@ -2428,7 +2452,7 @@ describe('[Teams]', () => {

it('should return a list of rooms filtered by name using the filter parameter', async () => {
const res = await request
.get(api('teams.listRoomsAndDiscussions'))
.get(api('teams.listChildren'))
.query({ teamId: testTeam._id, filter: 'test-p' })
.set(credentials)
.expect(200);
Expand All @@ -2441,7 +2465,7 @@ describe('[Teams]', () => {

it('should paginate results', async () => {
const res = await request
.get(api('teams.listRoomsAndDiscussions'))
.get(api('teams.listChildren'))
.query({ teamId: testTeam._id, offset: 1, count: 2 })
.set(credentials)
.expect(200);
Expand Down
2 changes: 1 addition & 1 deletion packages/core-services/src/types/ITeamService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export interface ITeamService {
getStatistics(): Promise<ITeamStats>;
findBySubscribedUserIds(userId: string, callerId?: string): Promise<ITeam[]>;
addRolesToMember(teamId: string, userId: string, roles: Array<string>): Promise<boolean>;
listRoomsAndDiscussions(
listChildren(
userId: string,
team: ITeam,
filter?: string,
Expand Down
10 changes: 7 additions & 3 deletions packages/model-typings/src/models/IRoomsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,6 @@ export interface IRoomsModel extends IBaseModel<IRoom> {

findSmallestFederatedRoomInNumberOfUsers(options?: FindOptions<IRoom>): Promise<IRoom | undefined>;

findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options?: FindOptions<IRoom>): FindPaginated<FindCursor<IRoom>>;

countFederatedRooms(): Promise<number>;
incMsgCountById(rid: string, inc: number): Promise<UpdateResult>;
getIncMsgCountUpdateQuery(inc: number, roomUpdater: Updater<IRoom>): Updater<IRoom>;
Expand Down Expand Up @@ -283,5 +281,11 @@ export interface IRoomsModel extends IBaseModel<IRoom> {
getSubscribedRoomIdsWithoutE2EKeys(uid: IUser['_id']): Promise<IRoom['_id'][]>;
removeUsersFromE2EEQueueByRoomId(roomId: IRoom['_id'], uids: IUser['_id'][]): Promise<Document | UpdateResult>;
removeUserFromE2EEQueue(uid: IUser['_id']): Promise<Document | UpdateResult>;
findDiscussionsByPrid(prid: string, options?: FindOptions<IRoom>): FindCursor<IRoom>;
findChildrenOfTeam(
teamId: string,
teamRoomId: string,
userId: string,
filter?: string,
options?: FindOptions<IRoom>,
): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>;
}
Loading

0 comments on commit 77f1eb2

Please sign in to comment.