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
  • Loading branch information
juliajforesti committed Aug 29, 2024
1 parent 309d5d7 commit 7ac83e0
Show file tree
Hide file tree
Showing 9 changed files with 384 additions and 7 deletions.
24 changes: 24 additions & 0 deletions apps/meteor/app/api/server/v1/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isTeamsDeleteProps,
isTeamsLeaveProps,
isTeamsUpdateProps,
isTeamsListRoomsAndDiscussionsProps,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
Expand Down Expand Up @@ -382,6 +383,29 @@ API.v1.addRoute(
},
);

// 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 },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();
const { filter } = this.queryParams;

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

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

return API.v1.success({ ...data, offset, count });
},
},
);

API.v1.addRoute(
'teams.members',
{ authRequired: true },
Expand Down
20 changes: 20 additions & 0 deletions apps/meteor/server/models/raw/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1462,6 +1462,22 @@ 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 @@ -2059,4 +2075,8 @@ 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);
}
}
46 changes: 46 additions & 0 deletions apps/meteor/server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,4 +1053,50 @@ export class TeamService extends ServiceClassInternal implements ITeamService {

return rooms;
}

// 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(
userId: string,
team: ITeam,
filter?: string,
sort?: Record<string, 1 | -1>,
skip = 0,
limit = 10,
): Promise<{ total: number; data: IRoom[] }> {
const mainRoom = await Rooms.findOneById(team.roomId, { projection: { _id: 1 } });
if (!mainRoom) {
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,
{
skip,
limit,
sort,
},
);

const [data, total] = await Promise.all([cursor.toArray(), totalCount]);

return {
total,
data,
};
}
}
19 changes: 14 additions & 5 deletions apps/meteor/tests/data/teams.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,20 @@ import type { ITeam, TEAM_TYPE } from '@rocket.chat/core-typings';

import { api, request } from './api-data';

export const createTeam = async (credentials: Record<string, any>, teamName: string, type: TEAM_TYPE): Promise<ITeam> => {
const response = await request.post(api('teams.create')).set(credentials).send({
name: teamName,
type,
});
export const createTeam = async (
credentials: Record<string, any>,
teamName: string,
type: TEAM_TYPE,
members?: string[],
): Promise<ITeam> => {
const response = await request
.post(api('teams.create'))
.set(credentials)
.send({
name: teamName,
type,
...(members && { members }),
});

return response.body.team;
};
Expand Down
234 changes: 234 additions & 0 deletions apps/meteor/tests/end-to-end/api/teams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2217,4 +2217,238 @@ describe('[Teams]', () => {
});
});
});

describe('[teams.listRoomsAndDiscussions]', () => {
const teamName = `team-${Date.now()}`;
let testTeam: ITeam;
let testUser: IUser;
let testUserCredentials: Credentials;

let privateRoom: IRoom;
let privateRoom2: IRoom;
let publicRoom: IRoom;
let publicRoom2: IRoom;

let discussionOnPrivateRoom: IRoom;
let discussionOnPublicRoom: IRoom;
let discussionOnMainRoom: IRoom;

before('Create test team', async () => {
testUser = await createUser();
testUserCredentials = await login(testUser.username, password);

const members = testUser.username ? [testUser.username] : [];
testTeam = await createTeam(credentials, teamName, 0, members);
});

before('make user owner', async () => {
await request
.post(api('teams.updateMember'))
.set(credentials)
.send({
teamName: testTeam.name,
member: {
userId: testUser._id,
roles: ['member', 'owner'],
},
})
.expect('Content-Type', 'application/json')
.expect(200);
});

before('create rooms', async () => {
privateRoom = (await createRoom({ type: 'p', name: `test-p-${Date.now()}` })).body.group;
privateRoom2 = (await createRoom({ type: 'p', name: `test-p2-${Date.now()}`, credentials: testUserCredentials })).body.group;
publicRoom = (await createRoom({ type: 'c', name: `test-c-${Date.now()}` })).body.channel;
publicRoom2 = (await createRoom({ type: 'c', name: `test-c2-${Date.now()}` })).body.channel;

await Promise.all([
request
.post(api('teams.addRooms'))
.set(credentials)
.send({
rooms: [privateRoom._id, publicRoom._id, publicRoom2._id],
teamId: testTeam._id,
})
.expect(200),
request
.post(api('teams.addRooms'))
.set(testUserCredentials)
.send({
rooms: [privateRoom2._id],
teamId: testTeam._id,
})
.expect(200),
]);
});

before('Create discussions', async () => {
discussionOnPrivateRoom = (
await request
.post(api('rooms.createDiscussion'))
.set(credentials)
.send({
prid: privateRoom._id,
t_name: `test-d-${Date.now()}`,
})
).body.discussion;
discussionOnPublicRoom = (
await request
.post(api('rooms.createDiscussion'))
.set(credentials)
.send({
prid: publicRoom._id,
t_name: `test-d-${Date.now()}`,
})
).body.discussion;
discussionOnMainRoom = (
await request
.post(api('rooms.createDiscussion'))
.set(credentials)
.send({
prid: testTeam.roomId,
t_name: `test-d-${Date.now()}`,
})
).body.discussion;
});

after(async () => {
await Promise.all([
deleteRoom({ type: 'p', roomId: privateRoom._id }),
deleteRoom({ type: 'p', roomId: privateRoom2._id }),
deleteRoom({ type: 'c', roomId: publicRoom._id }),
deleteRoom({ type: 'c', roomId: publicRoom2._id }),
deleteRoom({ type: 'p', roomId: discussionOnPrivateRoom._id }),
deleteRoom({ type: 'c', roomId: discussionOnPublicRoom._id }),
deleteRoom({ type: 'c', roomId: discussionOnMainRoom._id }),
deleteTeam(credentials, teamName),
deleteUser({ _id: testUser._id }),
]);
});

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

it('should fail if teamId is not passed as queryparam', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).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);
});

it('should fail if teamId is empty', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).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);
});

it('should fail if teamName is empty', async () => {
await request.get(api('teams.listRoomsAndDiscussions')).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);
});

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);

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.an('object');

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

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', async () => {
const res = await request
.get(api('teams.listRoomsAndDiscussions'))
.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 list of rooms filtered by name using the filter parameter', async () => {
const res = await request
.get(api('teams.listRoomsAndDiscussions'))
.query({ teamId: testTeam._id, filter: 'test-p' })
.set(credentials)
.expect(200);

expect(res.body).to.have.property('total').to.be.equal(1);
expect(res.body).to.have.property('data').to.be.an('array');
expect(res.body.data[0]._id).to.be.equal(privateRoom._id);
expect(res.body.data.find((room: IRoom) => room._id === privateRoom2._id)).to.be.undefined;
});

it('should paginate results', async () => {
const res = await request
.get(api('teams.listRoomsAndDiscussions'))
.query({ teamId: testTeam._id, offset: 1, count: 2 })
.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');
expect(res.body.data).to.have.lengthOf(2);
});
});
});
8 changes: 8 additions & 0 deletions packages/core-services/src/types/ITeamService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,12 @@ export interface ITeamService {
getStatistics(): Promise<ITeamStats>;
findBySubscribedUserIds(userId: string, callerId?: string): Promise<ITeam[]>;
addRolesToMember(teamId: string, userId: string, roles: Array<string>): Promise<boolean>;
listRoomsAndDiscussions(
userId: string,
team: ITeam,
filter?: string,
sort?: Record<string, 1 | -1>,
skip?: number,
limit?: number,
): Promise<{ total: number; data: IRoom[] }>;
}
Loading

0 comments on commit 7ac83e0

Please sign in to comment.