From 6609b8abe30f28b5540abafd57dd6ba84edc271d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 11:28:48 -0600 Subject: [PATCH 01/27] new endpoint for listing rooms and discussions for a team --- apps/meteor/app/api/server/v1/teams.ts | 24 ++ apps/meteor/eslint.txt | 0 apps/meteor/eslintfiles.txt | 0 apps/meteor/server/models/raw/Rooms.ts | 15 ++ apps/meteor/server/services/team/service.ts | 45 ++++ apps/meteor/tests/end-to-end/api/teams.ts | 221 ++++++++++++++++++ .../core-services/src/types/ITeamService.ts | 8 + .../model-typings/src/models/IRoomsModel.ts | 3 + .../v1/teams/TeamsListRoomsAndDiscussions.ts | 24 ++ packages/rest-typings/src/v1/teams/index.ts | 6 + 10 files changed, 346 insertions(+) create mode 100644 apps/meteor/eslint.txt create mode 100644 apps/meteor/eslintfiles.txt create mode 100644 packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index f64f8c820575..1a0b6b0db14b 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -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'; @@ -375,6 +376,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 { teamId, filter } = this.queryParams; + + const team = await getTeamByIdOrName({ teamId }); + 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 }, diff --git a/apps/meteor/eslint.txt b/apps/meteor/eslint.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/apps/meteor/eslintfiles.txt b/apps/meteor/eslintfiles.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 96cd5a3a3acf..1708cf58487c 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -1462,6 +1462,17 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.find({ uids: { $size: 2, $in: [uids] }, t: 'd' }); } + findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options: FindOptions = {}): FindPaginated> { + const query: Filter = { + _id: { + $in: ids, + }, + ...(filter && { $or: [{ name: new RegExp(escapeRegExp(filter), 'i') }, { fname: new RegExp(escapeRegExp(filter), 'i') }] }), + }; + + return this.findPaginated(query, options); + } + // UPDATE addImportIds(_id: IRoom['_id'], importIds: string[]): Promise { const query: Filter = { _id }; @@ -2059,4 +2070,8 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateMany(query, update); } + + findDiscussionsByPrid(prid: string, options?: FindOptions): FindCursor { + return this.find({ prid }, options); + } } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 27f7af1f1b1c..a592cc210743 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1078,4 +1078,49 @@ 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 listRoomsAndDiscussions( + userId: string, + team: ITeam, + filter?: string, + sort?: Record, + 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 = await Rooms.findDiscussionsByPrid(mainRoom._id, { projection: { _id: 1 } }) + .map(({ _id }) => _id) + .toArray(); + const teamRooms = await 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, + }; + } } diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index ca07d3e32679..ec5c2aa40a53 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2217,4 +2217,225 @@ 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); + + testTeam = ( + await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: teamName, + type: 0, + members: [testUser.username], + }) + ).body.team; + }); + + 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), + ]); + }); + + 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 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({ teamId: testTeam._id }) + .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); + }); + }); }); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 3caa6a2e97df..10a7f4f65638 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -129,4 +129,12 @@ export interface ITeamService { getRoomInfo( room: AtLeast, ): Promise<{ team?: Pick; parentRoom?: Pick }>; + listRoomsAndDiscussions( + userId: string, + team: ITeam, + filter?: string, + sort?: Record, + skip?: number, + limit?: number, + ): Promise<{ total: number; data: IRoom[] }>; } diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 498a3c6b4bbc..16fa8cdbc069 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -158,6 +158,8 @@ export interface IRoomsModel extends IBaseModel { findBiggestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise; findSmallestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise; + ; + findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options?: FindOptions): FindPaginated> countFederatedRooms(): Promise; incMsgCountById(rid: string, inc: number): Promise; @@ -281,4 +283,5 @@ export interface IRoomsModel extends IBaseModel { getSubscribedRoomIdsWithoutE2EKeys(uid: IUser['_id']): Promise; removeUsersFromE2EEQueueByRoomId(roomId: IRoom['_id'], uids: IUser['_id'][]): Promise; removeUserFromE2EEQueue(uid: IUser['_id']): Promise; + findDiscussionsByPrid(prid: string, options?: FindOptions): FindCursor; } diff --git a/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts b/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts new file mode 100644 index 000000000000..301c06351ee0 --- /dev/null +++ b/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts @@ -0,0 +1,24 @@ +import type { ITeam } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ajv } from '../Ajv'; + +export type TeamsListRoomsAndDiscussionsProps = PaginatedRequest<{ + teamId: ITeam['_id']; + filter?: string; +}>; + +const TeamsListRoomsAndDiscussionsPropsSchema = { + type: 'object', + properties: { + teamId: { type: 'string' }, + filter: { type: 'string' }, + offset: { type: 'number' }, + count: { type: 'number' }, + sort: { type: 'string' }, + }, + required: ['teamId'], + additionalProperties: false, +}; + +export const isTeamsListRoomsAndDiscussionsProps = ajv.compile(TeamsListRoomsAndDiscussionsPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/index.ts b/packages/rest-typings/src/v1/teams/index.ts index d63e6da8bd8a..9365c5961d32 100644 --- a/packages/rest-typings/src/v1/teams/index.ts +++ b/packages/rest-typings/src/v1/teams/index.ts @@ -6,6 +6,7 @@ import type { TeamsAddMembersProps } from './TeamsAddMembersProps'; import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; import type { TeamsDeleteProps } from './TeamsDeleteProps'; import type { TeamsLeaveProps } from './TeamsLeaveProps'; +import type { TeamsListRoomsAndDiscussionsProps } from './TeamsListRoomsAndDiscussions'; import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; @@ -19,6 +20,7 @@ export * from './TeamsRemoveMemberProps'; export * from './TeamsRemoveRoomProps'; export * from './TeamsUpdateMemberProps'; export * from './TeamsUpdateProps'; +export * from './TeamsListRoomsAndDiscussions'; type ITeamAutocompleteResult = Pick; @@ -184,4 +186,8 @@ export type TeamsEndpoints = { room: IRoom; }; }; + + '/v1/teams.listRoomsAndDiscussions': { + GET: (params: TeamsListRoomsAndDiscussionsProps) => PaginatedResult<{ data: IRoom[] }>; + }; }; From ff0775df532f5fb8d9c5f37d230a76929edd6811 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 11:36:26 -0600 Subject: [PATCH 02/27] dum --- packages/model-typings/src/models/IRoomsModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 16fa8cdbc069..4d116919c908 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -158,8 +158,8 @@ export interface IRoomsModel extends IBaseModel { findBiggestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise; findSmallestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise; - ; - findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options?: FindOptions): FindPaginated> + + findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options?: FindOptions): FindPaginated>; countFederatedRooms(): Promise; incMsgCountById(rid: string, inc: number): Promise; From c325f973d7468145f602bebf012882e444d94155 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 13:24:00 -0600 Subject: [PATCH 03/27] eslint could you please give me all errors at once --- apps/meteor/server/models/raw/Rooms.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 1708cf58487c..1a046b087c07 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -1462,7 +1462,11 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.find({ uids: { $size: 2, $in: [uids] }, t: 'd' }); } - findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options: FindOptions = {}): FindPaginated> { + findPaginatedByNameOrFnameInIds( + ids: IRoom['_id'][], + filter?: string, + options: FindOptions = {}, + ): FindPaginated> { const query: Filter = { _id: { $in: ids, From 7081a74888bf96a67503e21198a7f10ced4964b3 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 13:55:33 -0600 Subject: [PATCH 04/27] Delete apps/meteor/eslint.txt --- apps/meteor/eslint.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/meteor/eslint.txt diff --git a/apps/meteor/eslint.txt b/apps/meteor/eslint.txt deleted file mode 100644 index e69de29bb2d1..000000000000 From 6761d8db27a7dc53e948ef56b65d66db891fd965 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 13:55:48 -0600 Subject: [PATCH 05/27] Delete apps/meteor/eslintfiles.txt --- apps/meteor/eslintfiles.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 apps/meteor/eslintfiles.txt diff --git a/apps/meteor/eslintfiles.txt b/apps/meteor/eslintfiles.txt deleted file mode 100644 index e69de29bb2d1..000000000000 From 8f9f5b8fe372a7651b9ef24aacf5533daedc9d14 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 14:06:40 -0600 Subject: [PATCH 06/27] cr --- apps/meteor/server/models/raw/Rooms.ts | 3 ++- apps/meteor/server/services/team/service.ts | 11 +++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 1a046b087c07..784bd9f4e360 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -1467,11 +1467,12 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { filter?: string, options: FindOptions = {}, ): FindPaginated> { + const regxp = filter && new RegExp(escapeRegExp(filter), 'i'); const query: Filter = { _id: { $in: ids, }, - ...(filter && { $or: [{ name: new RegExp(escapeRegExp(filter), 'i') }, { fname: new RegExp(escapeRegExp(filter), 'i') }] }), + ...(regxp && { $or: [{ name: regxp }, { fname: regxp }] }), }; return this.findPaginated(query, options); diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index a592cc210743..bc70f1168ea2 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1094,12 +1094,11 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('error-invalid-team-no-main-room'); } - const discussionIds = await Rooms.findDiscussionsByPrid(mainRoom._id, { projection: { _id: 1 } }) - .map(({ _id }) => _id) - .toArray(); - const teamRooms = await Rooms.findByTeamId(team._id, { - projection: { _id: 1, t: 1 }, - }).toArray(); + 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 } }) From 1202a2f59fcc79a38f9181d59da4ddd5e45cf363 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 14:26:10 -0600 Subject: [PATCH 07/27] eslint --- apps/meteor/server/services/team/service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index bc70f1168ea2..9cb84c160ced 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1095,7 +1095,9 @@ export class TeamService extends ServiceClassInternal implements ITeamService { } const [discussionIds, teamRooms] = await Promise.all([ - Rooms.findDiscussionsByPrid(mainRoom._id, { projection: { _id: 1 } }).map(({ _id }) => _id).toArray(), + Rooms.findDiscussionsByPrid(mainRoom._id, { projection: { _id: 1 } }) + .map(({ _id }) => _id) + .toArray(), Rooms.findByTeamId(team._id, { projection: { _id: 1, t: 1 } }).toArray(), ]); From fb3819553ecf21571ad92b67cf52f76de0245df7 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 28 Aug 2024 17:48:00 -0600 Subject: [PATCH 08/27] Create soft-mirrors-remember.md --- .changeset/soft-mirrors-remember.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/soft-mirrors-remember.md diff --git a/.changeset/soft-mirrors-remember.md b/.changeset/soft-mirrors-remember.md new file mode 100644 index 000000000000..5d050ddc68ff --- /dev/null +++ b/.changeset/soft-mirrors-remember.md @@ -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.listRoomsAndDiscussions` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. From 00833ea39c0be975ebf35e3bc67b6f12071669c4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Thu, 29 Aug 2024 12:25:05 -0600 Subject: [PATCH 09/27] cr --- apps/meteor/app/api/server/v1/teams.ts | 4 +-- apps/meteor/tests/data/teams.helper.ts | 19 +++++++--- apps/meteor/tests/end-to-end/api/teams.ts | 35 +++++++++++++------ .../v1/teams/TeamsListRoomsAndDiscussions.ts | 13 ++++--- 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 1a0b6b0db14b..88853c3fd9d5 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -385,9 +385,9 @@ API.v1.addRoute( async get() { const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const { teamId, filter } = this.queryParams; + const { filter } = this.queryParams; - const team = await getTeamByIdOrName({ teamId }); + const team = await getTeamByIdOrName(this.queryParams); if (!team) { return API.v1.notFound(); } diff --git a/apps/meteor/tests/data/teams.helper.ts b/apps/meteor/tests/data/teams.helper.ts index 8fc60bd19fd4..f6cba25f86c9 100644 --- a/apps/meteor/tests/data/teams.helper.ts +++ b/apps/meteor/tests/data/teams.helper.ts @@ -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, teamName: string, type: TEAM_TYPE): Promise => { - const response = await request.post(api('teams.create')).set(credentials).send({ - name: teamName, - type, - }); +export const createTeam = async ( + credentials: Record, + teamName: string, + type: TEAM_TYPE, + members?: string[], +): Promise => { + const response = await request + .post(api('teams.create')) + .set(credentials) + .send({ + name: teamName, + type, + ...(members && { members }), + }); return response.body.team; }; diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index ec5c2aa40a53..69f76a4292e8 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2237,16 +2237,8 @@ describe('[Teams]', () => { testUser = await createUser(); testUserCredentials = await login(testUser.username, password); - testTeam = ( - await request - .post(api('teams.create')) - .set(credentials) - .send({ - name: teamName, - type: 0, - members: [testUser.username], - }) - ).body.team; + const members = testUser.username ? [testUser.username] : []; + testTeam = await createTeam(credentials, teamName, 0, members); }); before('make user owner', async () => { @@ -2330,6 +2322,7 @@ describe('[Teams]', () => { deleteRoom({ type: 'c', roomId: discussionOnPublicRoom._id }), deleteRoom({ type: 'c', roomId: discussionOnMainRoom._id }), deleteTeam(credentials, teamName), + deleteUser({ _id: testUser._id }), ]); }); @@ -2345,6 +2338,26 @@ describe('[Teams]', () => { 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); @@ -2380,7 +2393,7 @@ 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({ teamId: testTeam._id }) + .query({ teamName: testTeam.name }) .set(testUserCredentials) .expect(200); diff --git a/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts b/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts index 301c06351ee0..756ee466c4e5 100644 --- a/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts +++ b/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts @@ -3,22 +3,25 @@ import type { ITeam } from '@rocket.chat/core-typings'; import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import { ajv } from '../Ajv'; -export type TeamsListRoomsAndDiscussionsProps = PaginatedRequest<{ - teamId: ITeam['_id']; - filter?: string; -}>; +export type TeamsListRoomsAndDiscussionsProps = + | PaginatedRequest<{ + teamId: ITeam['_id']; + filter?: string; + }> + | PaginatedRequest<{ teamName: ITeam['name']; filter?: string }>; const TeamsListRoomsAndDiscussionsPropsSchema = { type: 'object', properties: { teamId: { type: 'string' }, + teamName: { type: 'string' }, filter: { type: 'string' }, offset: { type: 'number' }, count: { type: 'number' }, sort: { type: 'string' }, }, - required: ['teamId'], additionalProperties: false, + oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }], }; export const isTeamsListRoomsAndDiscussionsProps = ajv.compile(TeamsListRoomsAndDiscussionsPropsSchema); From 43d2534180d74942747ba90138bb914748a2c64f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 10:55:42 -0600 Subject: [PATCH 10/27] change 3 queries to use an aggregation --- apps/meteor/server/main.ts | 1 + apps/meteor/server/models/raw/Rooms.ts | 96 +++++++++++++++---- apps/meteor/server/services/team/service.ts | 26 +---- .../model-typings/src/models/IRoomsModel.ts | 10 +- 4 files changed, 90 insertions(+), 43 deletions(-) diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index 893a970578b0..facd1f494e29 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -1,5 +1,6 @@ import './models/startup'; + /** * ./settings uses top level await, in theory the settings creation * and the startup should be done in parallel diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 784bd9f4e360..eb70ecf7f7a6 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -1462,22 +1462,6 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.find({ uids: { $size: 2, $in: [uids] }, t: 'd' }); } - findPaginatedByNameOrFnameInIds( - ids: IRoom['_id'][], - filter?: string, - options: FindOptions = {}, - ): FindPaginated> { - const regxp = filter && new RegExp(escapeRegExp(filter), 'i'); - const query: Filter = { - _id: { - $in: ids, - }, - ...(regxp && { $or: [{ name: regxp }, { fname: regxp }] }), - }; - - return this.findPaginated(query, options); - } - // UPDATE addImportIds(_id: IRoom['_id'], importIds: string[]): Promise { const query: Filter = { _id }; @@ -2076,7 +2060,83 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateMany(query, update); } - findDiscussionsByPrid(prid: string, options?: FindOptions): FindCursor { - return this.find({ prid }, options); + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + options?: FindOptions, + ): 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 + ], + }, + }, + ]); } } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 9cb84c160ced..9857c7d94449 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1094,30 +1094,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, diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 4d116919c908..b0bbbfc35d6b 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -159,8 +159,6 @@ export interface IRoomsModel extends IBaseModel { findSmallestFederatedRoomInNumberOfUsers(options?: FindOptions): Promise; - findPaginatedByNameOrFnameInIds(ids: IRoom['_id'][], filter?: string, options?: FindOptions): FindPaginated>; - countFederatedRooms(): Promise; incMsgCountById(rid: string, inc: number): Promise; getIncMsgCountUpdateQuery(inc: number, roomUpdater: Updater): Updater; @@ -283,5 +281,11 @@ export interface IRoomsModel extends IBaseModel { getSubscribedRoomIdsWithoutE2EKeys(uid: IUser['_id']): Promise; removeUsersFromE2EEQueueByRoomId(roomId: IRoom['_id'], uids: IUser['_id'][]): Promise; removeUserFromE2EEQueue(uid: IUser['_id']): Promise; - findDiscussionsByPrid(prid: string, options?: FindOptions): FindCursor; + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + options?: FindOptions, + ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>; } From 6cbf0c905b0e91f3ffcd78f4d474211d021ea366 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 11:24:32 -0600 Subject: [PATCH 11/27] thanks notifylistener --- apps/meteor/server/main.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/server/main.ts b/apps/meteor/server/main.ts index facd1f494e29..893a970578b0 100644 --- a/apps/meteor/server/main.ts +++ b/apps/meteor/server/main.ts @@ -1,6 +1,5 @@ import './models/startup'; - /** * ./settings uses top level await, in theory the settings creation * and the startup should be done in parallel From fdc8eec73d83a41d38c91448172e1d0aa9f3be75 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 11:38:08 -0600 Subject: [PATCH 12/27] rename endpoint --- apps/meteor/app/api/server/v1/teams.ts | 8 +++---- apps/meteor/server/services/team/service.ts | 2 +- apps/meteor/tests/end-to-end/api/teams.ts | 24 +++++++++---------- .../core-services/src/types/ITeamService.ts | 2 +- ...AndDiscussions.ts => TeamsListChildren.ts} | 6 ++--- packages/rest-typings/src/v1/teams/index.ts | 8 +++---- 6 files changed, 25 insertions(+), 25 deletions(-) rename packages/rest-typings/src/v1/teams/{TeamsListRoomsAndDiscussions.ts => TeamsListChildren.ts} (72%) diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 88853c3fd9d5..50b93a0276e6 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -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'; @@ -379,8 +379,8 @@ 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 }, + 'teams.listChildren', + { authRequired: true, validateParams: isTeamsListChildrenProps }, { async get() { const { offset, count } = await getPaginationItems(this.queryParams); @@ -392,7 +392,7 @@ API.v1.addRoute( 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 }); }, diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 9857c7d94449..8004d6c66fc0 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1081,7 +1081,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, diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 69f76a4292e8..4ccac7b74512 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2218,7 +2218,7 @@ describe('[Teams]', () => { }); }); - describe('[teams.listRoomsAndDiscussions]', () => { + describe('[teams.listChildren]', () => { const teamName = `team-${Date.now()}`; let testTeam: ITeam; let testUser: IUser; @@ -2327,39 +2327,39 @@ 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')) + .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'); @@ -2392,7 +2392,7 @@ describe('[Teams]', () => { it('should return a valid list of rooms for non admin member too', async () => { const res = await request - .get(api('teams.listRoomsAndDiscussions')) + .get(api('teams.listChildren')) .query({ teamName: testTeam.name }) .set(testUserCredentials) .expect(200); @@ -2428,7 +2428,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); @@ -2441,7 +2441,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); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 10a7f4f65638..4bbe60de131e 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -129,7 +129,7 @@ export interface ITeamService { getRoomInfo( room: AtLeast, ): Promise<{ team?: Pick; parentRoom?: Pick }>; - listRoomsAndDiscussions( + listChildren( userId: string, team: ITeam, filter?: string, diff --git a/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts similarity index 72% rename from packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts rename to packages/rest-typings/src/v1/teams/TeamsListChildren.ts index 756ee466c4e5..df4a35b6b201 100644 --- a/packages/rest-typings/src/v1/teams/TeamsListRoomsAndDiscussions.ts +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -3,14 +3,14 @@ import type { ITeam } from '@rocket.chat/core-typings'; import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import { ajv } from '../Ajv'; -export type TeamsListRoomsAndDiscussionsProps = +export type TeamsListChildrenProps = | PaginatedRequest<{ teamId: ITeam['_id']; filter?: string; }> | PaginatedRequest<{ teamName: ITeam['name']; filter?: string }>; -const TeamsListRoomsAndDiscussionsPropsSchema = { +const TeamsListChildrenPropsSchema = { type: 'object', properties: { teamId: { type: 'string' }, @@ -24,4 +24,4 @@ const TeamsListRoomsAndDiscussionsPropsSchema = { oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }], }; -export const isTeamsListRoomsAndDiscussionsProps = ajv.compile(TeamsListRoomsAndDiscussionsPropsSchema); +export const isTeamsListChildrenProps = ajv.compile(TeamsListChildrenPropsSchema); diff --git a/packages/rest-typings/src/v1/teams/index.ts b/packages/rest-typings/src/v1/teams/index.ts index 9365c5961d32..a4ae6c7bca0f 100644 --- a/packages/rest-typings/src/v1/teams/index.ts +++ b/packages/rest-typings/src/v1/teams/index.ts @@ -6,7 +6,7 @@ import type { TeamsAddMembersProps } from './TeamsAddMembersProps'; import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; import type { TeamsDeleteProps } from './TeamsDeleteProps'; import type { TeamsLeaveProps } from './TeamsLeaveProps'; -import type { TeamsListRoomsAndDiscussionsProps } from './TeamsListRoomsAndDiscussions'; +import type { TeamsListChildrenProps } from './TeamsListChildren'; import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; @@ -20,7 +20,7 @@ export * from './TeamsRemoveMemberProps'; export * from './TeamsRemoveRoomProps'; export * from './TeamsUpdateMemberProps'; export * from './TeamsUpdateProps'; -export * from './TeamsListRoomsAndDiscussions'; +export * from './TeamsListChildren'; type ITeamAutocompleteResult = Pick; @@ -187,7 +187,7 @@ export type TeamsEndpoints = { }; }; - '/v1/teams.listRoomsAndDiscussions': { - GET: (params: TeamsListRoomsAndDiscussionsProps) => PaginatedResult<{ data: IRoom[] }>; + '/v1/teams.listChildren': { + GET: (params: TeamsListChildrenProps) => PaginatedResult<{ data: IRoom[] }>; }; }; From 868d7cb89b4a1ea5c190f6f29351f18457fd4007 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 11:38:41 -0600 Subject: [PATCH 13/27] Update .changeset/soft-mirrors-remember.md --- .changeset/soft-mirrors-remember.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/soft-mirrors-remember.md b/.changeset/soft-mirrors-remember.md index 5d050ddc68ff..78b005ee6b6e 100644 --- a/.changeset/soft-mirrors-remember.md +++ b/.changeset/soft-mirrors-remember.md @@ -5,4 +5,4 @@ "@rocket.chat/rest-typings": minor --- -New `teams.listRoomsAndDiscussions` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. +New `teams.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. From bb280f2809bee7e780c5303832f898aeb7fb299b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 12:09:20 -0600 Subject: [PATCH 14/27] lint --- apps/meteor/tests/end-to-end/api/teams.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 4ccac7b74512..79ea6177dd5d 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2343,11 +2343,7 @@ describe('[Teams]', () => { }); it('should fail if both properties are passed', async () => { - await request - .get(api('teams.listChildren')) - .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 () => { @@ -2391,11 +2387,7 @@ describe('[Teams]', () => { }); it('should return a valid list of rooms for non admin member too', async () => { - const res = await request - .get(api('teams.listChildren')) - .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'); From 13ffd240339506f624eb643c62f81f2adfc02117 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 13:10:47 -0600 Subject: [PATCH 15/27] filter by team main room id --- apps/meteor/app/api/server/v1/teams.ts | 17 +++++++++- apps/meteor/tests/end-to-end/api/teams.ts | 34 ++++++++++++++++++- .../src/v1/teams/TeamsListChildren.ts | 6 ++-- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 50b93a0276e6..cec8c393bee9 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -376,6 +376,21 @@ API.v1.addRoute( }, ); +const getTeamByIdOrNameOrParentRoom = async ( + params: { teamId: string } | { teamName: string } | { roomId: string }, +): Promise => { + if ('teamId' in params && params.teamId) { + return Team.getOneById(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( @@ -387,7 +402,7 @@ API.v1.addRoute( 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(); } diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 79ea6177dd5d..c9af0ba10d3c 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2218,7 +2218,7 @@ describe('[Teams]', () => { }); }); - describe('[teams.listChildren]', () => { + describe.only('[teams.listChildren]', () => { const teamName = `team-${Date.now()}`; let testTeam: ITeam; let testUser: IUser; @@ -2418,6 +2418,38 @@ describe('[Teams]', () => { 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'); + 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.listChildren')) diff --git a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts index df4a35b6b201..618cb3f3ac71 100644 --- a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -8,20 +8,22 @@ export type TeamsListChildrenProps = teamId: ITeam['_id']; filter?: string; }> - | PaginatedRequest<{ teamName: ITeam['name']; filter?: string }>; + | PaginatedRequest<{ teamName: ITeam['name']; filter?: string }> + | PaginatedRequest<{ roomId: ITeam['roomId']; filter?: string }>; const TeamsListChildrenPropsSchema = { type: 'object', properties: { teamId: { type: 'string' }, teamName: { type: 'string' }, + roomId: { type: 'string' }, filter: { type: 'string' }, offset: { type: 'number' }, count: { type: 'number' }, sort: { type: 'string' }, }, additionalProperties: false, - oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }], + oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }, { required: ['roomId'] }], }; export const isTeamsListChildrenProps = ajv.compile(TeamsListChildrenPropsSchema); From 3eb78c376d3c8d3c5a71d82832e7f86db75b626a Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 13:12:46 -0600 Subject: [PATCH 16/27] im dum --- apps/meteor/tests/end-to-end/api/teams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index c9af0ba10d3c..75a8525ec2b6 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2218,7 +2218,7 @@ describe('[Teams]', () => { }); }); - describe.only('[teams.listChildren]', () => { + describe('[teams.listChildren]', () => { const teamName = `team-${Date.now()}`; let testTeam: ITeam; let testUser: IUser; From 456b8e30f04652495b1c78073dd0a3ba76f5b5f2 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 2 Sep 2024 14:19:59 -0600 Subject: [PATCH 17/27] Update teams.ts --- apps/meteor/tests/end-to-end/api/teams.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 75a8525ec2b6..72371fce6661 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2354,6 +2354,14 @@ describe('[Teams]', () => { await request.get(api('teams.listChildren')).set(credentials).query({ teamName: 'invalid' }).expect(404); }); + it('should fail if roomId is empty', async () => { + await request.get(api('teams.listChildren')).set(credentials).query({ roomId: '' }).expect(404); + }); + + it('should fail if roomId is invalid', async () => { + 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.listChildren')).query({ teamId: testTeam._id }).set(credentials).expect(200); From 6020c6a4d89e010c482970e108c75eb8e9735c48 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 3 Sep 2024 09:13:38 -0600 Subject: [PATCH 18/27] add capability for filtering by type of channel or return both --- apps/meteor/app/api/server/v1/teams.ts | 4 +- apps/meteor/server/models/raw/Rooms.ts | 7 ++- apps/meteor/server/services/team/service.ts | 3 +- apps/meteor/tests/end-to-end/api/teams.ts | 48 +++++++++++++++++++ .../core-services/src/types/ITeamService.ts | 1 + .../model-typings/src/models/IRoomsModel.ts | 1 + .../src/v1/teams/TeamsListChildren.ts | 19 +++++--- 7 files changed, 70 insertions(+), 13 deletions(-) diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index cec8c393bee9..78772060ce4b 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -400,14 +400,14 @@ API.v1.addRoute( async get() { const { offset, count } = await getPaginationItems(this.queryParams); const { sort } = await this.parseJsonQuery(); - const { filter } = this.queryParams; + 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, sort, offset, count); + const data = await Team.listChildren(this.userId, team, filter, type, sort, offset, count); return API.v1.success({ ...data, offset, count }); }, diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index eb70ecf7f7a6..fde3a7ac4fc1 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2065,6 +2065,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { teamRoomId: string, userId: string, filter?: string, + type?: 'channel' | 'discussion', options?: FindOptions, ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }> { const nameFilter = filter ? new RegExp(escapeRegExp(filter), 'i') : undefined; @@ -2074,10 +2075,8 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { $and: [ { $or: [ - { - teamId, - }, - { prid: teamRoomId }, + ...(!type || type === 'channel' ? [{ teamId }] : []), + ...(!type || type === 'discussion' ? [{ prid: teamRoomId }] : []), ], }, ...(nameFilter ? [{ $or: [{ fname: nameFilter }, { name: nameFilter }] }] : []), diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 8004d6c66fc0..f96497006dfc 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1085,6 +1085,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { userId: string, team: ITeam, filter?: string, + type?: 'channel' | 'discussion', sort?: Record, skip = 0, limit = 10, @@ -1099,7 +1100,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { totalCount: [{ count: total }], paginatedResults: data, }, - ] = await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, { skip, limit, sort }).toArray(); + ] = await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, type, { skip, limit, sort }).toArray(); return { total, diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 72371fce6661..d9fb94f7ab5b 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2482,5 +2482,53 @@ describe('[Teams]', () => { expect(res.body).to.have.property('data').to.be.an('array'); expect(res.body.data).to.have.lengthOf(2); }); + + it('should return only items of type channel', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'channel' }) + .set(credentials) + .expect(200); + + expect(res.body).to.have.property('total').to.be.equal(4); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(4); + expect(res.body.data.some((room: IRoom) => !!room.prid)).to.be.false; + }); + + it('should return only items of type discussion', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'discussion' }) + .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).to.have.lengthOf(1); + expect(res.body.data.every((room: IRoom) => !!room.prid)).to.be.true; + }); + + it('should return both when type is not passed', async () => { + 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'); + expect(res.body.data).to.have.lengthOf(5); + expect(res.body.data.some((room: IRoom) => !!room.prid)).to.be.true; + expect(res.body.data.some((room: IRoom) => !room.prid)).to.be.true; + }); + + it('should fail if type is other than channel or discussion', async () => { + await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'other' }) + .set(credentials) + .expect(400); + }); }); }); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 4bbe60de131e..2f17c9017bb2 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -133,6 +133,7 @@ export interface ITeamService { userId: string, team: ITeam, filter?: string, + type?: 'channel' | 'discussion', sort?: Record, skip?: number, limit?: number, diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index b0bbbfc35d6b..9402d489600e 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -286,6 +286,7 @@ export interface IRoomsModel extends IBaseModel { teamRoomId: string, userId: string, filter?: string, + type?: 'channel' | 'discussion', options?: FindOptions, ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>; } diff --git a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts index 618cb3f3ac71..b971ce7b4c88 100644 --- a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -3,19 +3,26 @@ import type { ITeam } from '@rocket.chat/core-typings'; import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import { ajv } from '../Ajv'; +type GeneralProps = { + filter?: string; + type?: 'channel' | 'discussion'; +}; + export type TeamsListChildrenProps = - | PaginatedRequest<{ - teamId: ITeam['_id']; - filter?: string; - }> - | PaginatedRequest<{ teamName: ITeam['name']; filter?: string }> - | PaginatedRequest<{ roomId: ITeam['roomId']; filter?: string }>; + | PaginatedRequest< + { + teamId: ITeam['_id']; + } & GeneralProps + > + | PaginatedRequest<{ teamName: ITeam['name'] } & GeneralProps> + | PaginatedRequest<{ roomId: ITeam['roomId'] } & GeneralProps>; const TeamsListChildrenPropsSchema = { type: 'object', properties: { teamId: { type: 'string' }, teamName: { type: 'string' }, + type: { type: 'string', enum: ['channel', 'discussion'] }, roomId: { type: 'string' }, filter: { type: 'string' }, offset: { type: 'number' }, From e7c2c012c2d814de1ce3dfc3d866d1c9fb832f65 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 3 Sep 2024 09:52:14 -0600 Subject: [PATCH 19/27] lint --- apps/meteor/tests/end-to-end/api/teams.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index d9fb94f7ab5b..f2b01ab37dc8 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2510,11 +2510,7 @@ describe('[Teams]', () => { }); it('should return both when type is not passed', async () => { - const res = await request - .get(api('teams.listChildren')) - .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'); @@ -2524,11 +2520,7 @@ describe('[Teams]', () => { }); it('should fail if type is other than channel or discussion', async () => { - await request - .get(api('teams.listChildren')) - .query({ teamId: testTeam._id, type: 'other' }) - .set(credentials) - .expect(400); + await request.get(api('teams.listChildren')).query({ teamId: testTeam._id, type: 'other' }).set(credentials).expect(400); }); }); }); From 3ab5bc2d7c28b6f798e594207ffb92d66f9e8f88 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 3 Sep 2024 11:50:44 -0600 Subject: [PATCH 20/27] plural --- apps/meteor/server/services/team/service.ts | 4 ++-- apps/meteor/tests/end-to-end/api/teams.ts | 4 ++-- packages/core-services/src/types/ITeamService.ts | 2 +- packages/rest-typings/src/v1/teams/TeamsListChildren.ts | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index f96497006dfc..5ea8f64ef1c0 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1083,9 +1083,9 @@ export class TeamService extends ServiceClassInternal implements ITeamService { // 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: ITeam, + team: AtLeast, filter?: string, - type?: 'channel' | 'discussion', + type?: 'channels' | 'discussions', sort?: Record, skip = 0, limit = 10, diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index f2b01ab37dc8..3bc6b4122429 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2486,7 +2486,7 @@ describe('[Teams]', () => { it('should return only items of type channel', async () => { const res = await request .get(api('teams.listChildren')) - .query({ teamId: testTeam._id, type: 'channel' }) + .query({ teamId: testTeam._id, type: 'channels' }) .set(credentials) .expect(200); @@ -2499,7 +2499,7 @@ describe('[Teams]', () => { it('should return only items of type discussion', async () => { const res = await request .get(api('teams.listChildren')) - .query({ teamId: testTeam._id, type: 'discussion' }) + .query({ teamId: testTeam._id, type: 'discussions' }) .set(credentials) .expect(200); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 2f17c9017bb2..3be758c2db8f 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -133,7 +133,7 @@ export interface ITeamService { userId: string, team: ITeam, filter?: string, - type?: 'channel' | 'discussion', + type?: 'channels' | 'discussions', sort?: Record, skip?: number, limit?: number, diff --git a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts index b971ce7b4c88..41128e18a05f 100644 --- a/packages/rest-typings/src/v1/teams/TeamsListChildren.ts +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -5,7 +5,7 @@ import { ajv } from '../Ajv'; type GeneralProps = { filter?: string; - type?: 'channel' | 'discussion'; + type?: 'channels' | 'discussions'; }; export type TeamsListChildrenProps = @@ -22,7 +22,7 @@ const TeamsListChildrenPropsSchema = { properties: { teamId: { type: 'string' }, teamName: { type: 'string' }, - type: { type: 'string', enum: ['channel', 'discussion'] }, + type: { type: 'string', enum: ['channels', 'discussions'] }, roomId: { type: 'string' }, filter: { type: 'string' }, offset: { type: 'number' }, From e90194a0a65f8ad8b3a71884e409fa45f6a5f58c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 3 Sep 2024 12:23:47 -0600 Subject: [PATCH 21/27] cr --- apps/meteor/app/api/server/v1/teams.ts | 8 ++++---- apps/meteor/server/models/raw/Rooms.ts | 6 +++--- apps/meteor/server/services/team/service.ts | 8 ++++---- packages/core-services/src/types/ITeamService.ts | 4 ++-- packages/model-typings/src/models/IRoomsModel.ts | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 78772060ce4b..1e4ee8cd265b 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -378,15 +378,15 @@ API.v1.addRoute( const getTeamByIdOrNameOrParentRoom = async ( params: { teamId: string } | { teamName: string } | { roomId: string }, -): Promise => { +): Promise | null> => { if ('teamId' in params && params.teamId) { - return Team.getOneById(params.teamId); + return Team.getOneById(params.teamId, { projection: { type: 1, roomId: 1 } }); } if ('teamName' in params && params.teamName) { - return Team.getOneByName(params.teamName); + return Team.getOneByName(params.teamName, { projection: { type: 1, roomId: 1 } }); } if ('roomId' in params && params.roomId) { - return Team.getOneByRoomId(params.roomId); + return Team.getOneByRoomId(params.roomId, { projection: { type: 1, roomId: 1 } }); } return null; }; diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index fde3a7ac4fc1..7799b37cae35 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2065,7 +2065,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { teamRoomId: string, userId: string, filter?: string, - type?: 'channel' | 'discussion', + type?: 'channels' | 'discussions', options?: FindOptions, ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }> { const nameFilter = filter ? new RegExp(escapeRegExp(filter), 'i') : undefined; @@ -2075,8 +2075,8 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { $and: [ { $or: [ - ...(!type || type === 'channel' ? [{ teamId }] : []), - ...(!type || type === 'discussion' ? [{ prid: teamRoomId }] : []), + ...(!type || type === 'channels' ? [{ teamId }] : []), + ...(!type || type === 'discussions' ? [{ prid: teamRoomId }] : []), ], }, ...(nameFilter ? [{ $or: [{ fname: nameFilter }, { name: nameFilter }] }] : []), diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 5ea8f64ef1c0..9a41cb92078a 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -913,8 +913,8 @@ export class TeamService extends ServiceClassInternal implements ITeamService { }); } - async getOneByRoomId(roomId: string): Promise { - const room = await Rooms.findOneById(roomId); + async getOneByRoomId(roomId: string, options?: FindOptions): Promise { + const room = await Rooms.findOneById(roomId, { projection: { teamId: 1 } }); if (!room) { throw new Error('invalid-room'); @@ -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): Promise { @@ -1083,7 +1083,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { // 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, + team: AtLeast, filter?: string, type?: 'channels' | 'discussions', sort?: Record, diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 3be758c2db8f..132df89470ca 100644 --- a/packages/core-services/src/types/ITeamService.ts +++ b/packages/core-services/src/types/ITeamService.ts @@ -112,7 +112,7 @@ export interface ITeamService { getOneById

(teamId: string, options?: FindOptions

): Promise; getOneByName(teamName: string | RegExp, options?: FindOptions): Promise; getOneByMainRoomId(teamId: string): Promise | null>; - getOneByRoomId(teamId: string): Promise; + getOneByRoomId(teamId: string, options?: FindOptions): Promise; getMatchingTeamRooms(teamId: string, rids: Array): Promise>; autocomplete(uid: string, name: string): Promise; getAllPublicTeams(options?: FindOptions): Promise>; @@ -131,7 +131,7 @@ export interface ITeamService { ): Promise<{ team?: Pick; parentRoom?: Pick }>; listChildren( userId: string, - team: ITeam, + team: AtLeast, filter?: string, type?: 'channels' | 'discussions', sort?: Record, diff --git a/packages/model-typings/src/models/IRoomsModel.ts b/packages/model-typings/src/models/IRoomsModel.ts index 9402d489600e..bcf2382c7c3d 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -286,7 +286,7 @@ export interface IRoomsModel extends IBaseModel { teamRoomId: string, userId: string, filter?: string, - type?: 'channel' | 'discussion', + type?: 'channels' | 'discussions', options?: FindOptions, ): AggregationCursor<{ totalCount: { count: number }[]; paginatedResults: IRoom[] }>; } From 213c57799feb42659db49bb7be2ec79e66c08986 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 3 Sep 2024 13:55:56 -0600 Subject: [PATCH 22/27] lint --- apps/meteor/app/api/server/v1/teams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index 1e4ee8cd265b..acb6cba2bac7 100644 --- a/apps/meteor/app/api/server/v1/teams.ts +++ b/apps/meteor/app/api/server/v1/teams.ts @@ -378,7 +378,7 @@ API.v1.addRoute( const getTeamByIdOrNameOrParentRoom = async ( params: { teamId: string } | { teamName: string } | { roomId: string }, -): Promise | null> => { +): Promise | null> => { if ('teamId' in params && params.teamId) { return Team.getOneById(params.teamId, { projection: { type: 1, roomId: 1 } }); } From 9c049d6397ad7244627a3875e5f02d940dc4f973 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 4 Sep 2024 07:32:30 -0600 Subject: [PATCH 23/27] block non member usage --- apps/meteor/server/services/team/service.ts | 16 ++++++++++------ apps/meteor/tests/end-to-end/api/teams.ts | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 9a41cb92078a..f5218c88402a 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -1095,12 +1095,16 @@ export class TeamService extends ServiceClassInternal implements ITeamService { throw new Error('error-invalid-team-no-main-room'); } - const [ - { - totalCount: [{ count: total }], - paginatedResults: data, - }, - ] = await Rooms.findChildrenOfTeam(team._id, mainRoom._id, userId, filter, type, { skip, limit, sort }).toArray(); + 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, diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 3bc6b4122429..0ba8e9b12dfd 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2221,6 +2221,7 @@ describe('[Teams]', () => { describe('[teams.listChildren]', () => { const teamName = `team-${Date.now()}`; let testTeam: ITeam; + let testPrivateTeam: ITeam; let testUser: IUser; let testUserCredentials: Credentials; @@ -2239,6 +2240,7 @@ describe('[Teams]', () => { const members = testUser.username ? [testUser.username] : []; testTeam = await createTeam(credentials, teamName, 0, members); + testPrivateTeam = await createTeam(testUserCredentials, `${teamName}private`, 1, []); }); before('make user owner', async () => { @@ -2522,5 +2524,17 @@ describe('[Teams]', () => { it('should fail if type is other than channel or discussion', async () => { await request.get(api('teams.listChildren')).query({ teamId: testTeam._id, type: 'other' }).set(credentials).expect(400); }); + + it('should properly list children of a private team', async () => { + const res = await request.get(api('teams.listChildren')).query({ teamId: testPrivateTeam._id }).set(testUserCredentials).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).to.have.lengthOf(1); + }); + + it('should throw an error when a non member user tries to fetch info for team', async () => { + await request.get(api('teams.listChildren')).query({ teamId: testPrivateTeam._id }).set(credentials).expect(400); + }); }); }); From 53aa8f0a1d6722121dec118bf08ef73d366c149f Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 4 Sep 2024 08:12:14 -0600 Subject: [PATCH 24/27] Update apps/meteor/tests/end-to-end/api/teams.ts --- apps/meteor/tests/end-to-end/api/teams.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/tests/end-to-end/api/teams.ts b/apps/meteor/tests/end-to-end/api/teams.ts index 0ba8e9b12dfd..b630a97b1727 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2324,6 +2324,7 @@ describe('[Teams]', () => { deleteRoom({ type: 'c', roomId: discussionOnPublicRoom._id }), deleteRoom({ type: 'c', roomId: discussionOnMainRoom._id }), deleteTeam(credentials, teamName), + deleteTeam(credentials, testPrivateTeam.name), deleteUser({ _id: testUser._id }), ]); }); From 70aeebd66ecef1b972b4caf403dad61b1767c5ae Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 4 Sep 2024 08:19:24 -0600 Subject: [PATCH 25/27] Update apps/meteor/server/models/raw/Rooms.ts Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> --- apps/meteor/server/models/raw/Rooms.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 7799b37cae35..bf3b9f1cbcb4 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2131,8 +2131,8 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { $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 + { $skip: options?.skip || 0 }, + { $limit: options?.limit || 50 }, ], }, }, From b08814417896d171524c7c22ad02a87f7ef5896a Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 4 Sep 2024 08:22:25 -0600 Subject: [PATCH 26/27] cr --- apps/meteor/server/models/raw/Rooms.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index bf3b9f1cbcb4..4b3ae5d39326 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2103,6 +2103,11 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { $eq: ['$u._id', userId], }, }, + { + $expr: { + $ne: ['$t', 'c'], + }, + }, ], }, }, From 3fac30c8296d99a2c5cfc351e7671e72be9dcc5b Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 4 Sep 2024 08:54:01 -0600 Subject: [PATCH 27/27] eslint --- apps/meteor/server/models/raw/Rooms.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index 4b3ae5d39326..4c0b18528347 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2135,10 +2135,7 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { { $facet: { totalCount: [{ $count: 'count' }], - paginatedResults: [ - { $skip: options?.skip || 0 }, - { $limit: options?.limit || 50 }, - ], + paginatedResults: [{ $skip: options?.skip || 0 }, { $limit: options?.limit || 50 }], }, }, ]);