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
KevLehman authored Sep 17, 2024
1 parent 636d32d commit 3a161c4
Show file tree
Hide file tree
Showing 10 changed files with 559 additions and 9 deletions.
8 changes: 8 additions & 0 deletions .changeset/soft-mirrors-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-services": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/rest-typings": minor
---

New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned.
39 changes: 39 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,
isTeamsListChildrenProps,
} from '@rocket.chat/rest-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Match, check } from 'meteor/check';
Expand Down Expand Up @@ -375,6 +376,44 @@ API.v1.addRoute(
},
);

const getTeamByIdOrNameOrParentRoom = async (
params: { teamId: string } | { teamName: string } | { roomId: string },
): Promise<Pick<ITeam, 'type' | 'roomId' | '_id'> | null> => {
if ('teamId' in params && params.teamId) {
return Team.getOneById<ITeam>(params.teamId, { projection: { type: 1, roomId: 1 } });
}
if ('teamName' in params && params.teamName) {
return Team.getOneByName(params.teamName, { projection: { type: 1, roomId: 1 } });
}
if ('roomId' in params && params.roomId) {
return Team.getOneByRoomId(params.roomId, { projection: { type: 1, roomId: 1 } });
}
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.listChildren',
{ authRequired: true, validateParams: isTeamsListChildrenProps },
{
async get() {
const { offset, count } = await getPaginationItems(this.queryParams);
const { sort } = await this.parseJsonQuery();
const { filter, type } = this.queryParams;

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

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

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

API.v1.addRoute(
'teams.members',
{ authRequired: true },
Expand Down
81 changes: 81 additions & 0 deletions apps/meteor/server/models/raw/Rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2063,4 +2063,85 @@ export class RoomsRaw extends BaseRaw<IRoom> implements IRoomsModel {

return this.updateMany(query, update);
}

findChildrenOfTeam(
teamId: string,
teamRoomId: string,
userId: string,
filter?: string,
type?: 'channels' | 'discussions',
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: [
...(!type || type === 'channels' ? [{ teamId }] : []),
...(!type || type === 'discussions' ? [{ 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],
},
},
{
$expr: {
$ne: ['$t', 'c'],
},
},
],
},
},
{
$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 }, { $limit: options?.limit || 50 }],
},
},
]);
}
}
39 changes: 36 additions & 3 deletions apps/meteor/server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -913,8 +913,8 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
});
}

async getOneByRoomId(roomId: string): Promise<ITeam | null> {
const room = await Rooms.findOneById(roomId);
async getOneByRoomId(roomId: string, options?: FindOptions<ITeam>): Promise<ITeam | null> {
const room = await Rooms.findOneById(roomId, { projection: { teamId: 1 } });

if (!room) {
throw new Error('invalid-room');
Expand All @@ -924,7 +924,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
throw new Error('room-not-on-team');
}

return Team.findOneById(room.teamId);
return Team.findOneById(room.teamId, options);
}

async addRolesToMember(teamId: string, userId: string, roles: Array<string>): Promise<boolean> {
Expand Down Expand Up @@ -1078,4 +1078,37 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
const parentRoom = await this.getParentRoom(team);
return { team, ...(parentRoom && { parentRoom }) };
}

// 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 listChildren(
userId: string,
team: AtLeast<ITeam, '_id' | 'roomId' | 'type'>,
filter?: string,
type?: 'channels' | 'discussions',
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 isMember = await TeamMember.findOneByUserIdAndTeamId(userId, team._id, {
projection: { _id: 1 },
});

if (!isMember) {
throw new Error('error-invalid-team-not-a-member');
}

const [{ totalCount: [{ count: total }] = [], paginatedResults: data = [] }] =
(await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, type, { skip, limit, sort }).toArray()) || [];

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
Loading

0 comments on commit 3a161c4

Please sign in to comment.