diff --git a/.changeset/soft-mirrors-remember.md b/.changeset/soft-mirrors-remember.md new file mode 100644 index 000000000000..78b005ee6b6e --- /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.listChildren` endpoint that allows users listing rooms & discussions from teams. Only the discussions from the team's main room are returned. diff --git a/apps/meteor/app/api/server/v1/teams.ts b/apps/meteor/app/api/server/v1/teams.ts index f64f8c820575..acb6cba2bac7 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, + isTeamsListChildrenProps, } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; @@ -375,6 +376,44 @@ API.v1.addRoute( }, ); +const getTeamByIdOrNameOrParentRoom = async ( + params: { teamId: string } | { teamName: string } | { roomId: string }, +): Promise | null> => { + if ('teamId' in params && params.teamId) { + return Team.getOneById(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 }, diff --git a/apps/meteor/server/models/raw/Rooms.ts b/apps/meteor/server/models/raw/Rooms.ts index ec3bd6fe8d40..7d4a0a54dedf 100644 --- a/apps/meteor/server/models/raw/Rooms.ts +++ b/apps/meteor/server/models/raw/Rooms.ts @@ -2063,4 +2063,85 @@ export class RoomsRaw extends BaseRaw implements IRoomsModel { return this.updateMany(query, update); } + + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + type?: 'channels' | 'discussions', + 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: [ + ...(!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 }], + }, + }, + ]); + } } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 27f7af1f1b1c..f5218c88402a 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 { @@ -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, + filter?: string, + type?: 'channels' | 'discussions', + 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 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, + }; + } } 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 ca07d3e32679..b630a97b1727 100644 --- a/apps/meteor/tests/end-to-end/api/teams.ts +++ b/apps/meteor/tests/end-to-end/api/teams.ts @@ -2217,4 +2217,325 @@ describe('[Teams]', () => { }); }); }); + + describe('[teams.listChildren]', () => { + const teamName = `team-${Date.now()}`; + let testTeam: ITeam; + let testPrivateTeam: 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); + testPrivateTeam = await createTeam(testUserCredentials, `${teamName}private`, 1, []); + }); + + 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), + deleteTeam(credentials, testPrivateTeam.name), + deleteUser({ _id: testUser._id }), + ]); + }); + + it('should fail if user is not logged in', async () => { + await request.get(api('teams.listChildren')).expect(401); + }); + + it('should fail if teamId is not passed as queryparam', async () => { + await request.get(api('teams.listChildren')).set(credentials).expect(400); + }); + + it('should fail if teamId is not valid', async () => { + 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.listChildren')).set(credentials).query({ teamId: '' }).expect(404); + }); + + 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); + }); + + it('should fail if teamName is empty', async () => { + 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.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); + + 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.listChildren')).query({ teamName: testTeam.name }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + expect(res.body.data).to.have.lengthOf(5); + + const mainRoom = res.body.data.find((room: IRoom) => room._id === testTeam.roomId); + expect(mainRoom).to.be.an('object'); + + const publicChannel1 = res.body.data.find((room: IRoom) => room._id === publicRoom._id); + expect(publicChannel1).to.be.an('object'); + + const publicChannel2 = res.body.data.find((room: IRoom) => room._id === publicRoom2._id); + expect(publicChannel2).to.be.an('object'); + + const privateChannel1 = res.body.data.find((room: IRoom) => room._id === privateRoom._id); + expect(privateChannel1).to.be.undefined; + + const privateChannel2 = res.body.data.find((room: IRoom) => room._id === privateRoom2._id); + expect(privateChannel2).to.be.an('object'); + + const discussionOnP = res.body.data.find((room: IRoom) => room._id === discussionOnPrivateRoom._id); + expect(discussionOnP).to.be.undefined; + + const discussionOnC = res.body.data.find((room: IRoom) => room._id === discussionOnPublicRoom._id); + expect(discussionOnC).to.be.undefined; + + const mainDiscussion = res.body.data.find((room: IRoom) => room._id === discussionOnMainRoom._id); + expect(mainDiscussion).to.be.an('object'); + }); + + it('should return a valid list of rooms for non admin member too when filtering by teams main room id', async () => { + const res = await request.get(api('teams.listChildren')).query({ roomId: testTeam.roomId }).set(testUserCredentials).expect(200); + + expect(res.body).to.have.property('total').to.be.equal(5); + expect(res.body).to.have.property('data').to.be.an('array'); + 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')) + .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.listChildren')) + .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); + }); + + it('should return only items of type channel', async () => { + const res = await request + .get(api('teams.listChildren')) + .query({ teamId: testTeam._id, type: 'channels' }) + .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: 'discussions' }) + .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); + }); + + 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); + }); + }); }); diff --git a/packages/core-services/src/types/ITeamService.ts b/packages/core-services/src/types/ITeamService.ts index 3caa6a2e97df..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>; @@ -129,4 +129,13 @@ export interface ITeamService { getRoomInfo( room: AtLeast, ): Promise<{ team?: Pick; parentRoom?: Pick }>; + listChildren( + userId: string, + team: AtLeast, + filter?: string, + type?: 'channels' | 'discussions', + 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 9097a89c1413..91802f836719 100644 --- a/packages/model-typings/src/models/IRoomsModel.ts +++ b/packages/model-typings/src/models/IRoomsModel.ts @@ -282,4 +282,12 @@ export interface IRoomsModel extends IBaseModel { getSubscribedRoomIdsWithoutE2EKeys(uid: IUser['_id']): Promise; removeUsersFromE2EEQueueByRoomId(roomId: IRoom['_id'], uids: IUser['_id'][]): Promise; removeUserFromE2EEQueue(uid: IUser['_id']): Promise; + findChildrenOfTeam( + teamId: string, + teamRoomId: string, + userId: string, + filter?: string, + type?: 'channels' | 'discussions', + 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 new file mode 100644 index 000000000000..41128e18a05f --- /dev/null +++ b/packages/rest-typings/src/v1/teams/TeamsListChildren.ts @@ -0,0 +1,36 @@ +import type { ITeam } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ajv } from '../Ajv'; + +type GeneralProps = { + filter?: string; + type?: 'channels' | 'discussions'; +}; + +export type TeamsListChildrenProps = + | 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: ['channels', 'discussions'] }, + roomId: { type: 'string' }, + filter: { type: 'string' }, + offset: { type: 'number' }, + count: { type: 'number' }, + sort: { type: 'string' }, + }, + additionalProperties: false, + oneOf: [{ required: ['teamId'] }, { required: ['teamName'] }, { required: ['roomId'] }], +}; + +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 d63e6da8bd8a..a4ae6c7bca0f 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 { TeamsListChildrenProps } from './TeamsListChildren'; 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 './TeamsListChildren'; type ITeamAutocompleteResult = Pick; @@ -184,4 +186,8 @@ export type TeamsEndpoints = { room: IRoom; }; }; + + '/v1/teams.listChildren': { + GET: (params: TeamsListChildrenProps) => PaginatedResult<{ data: IRoom[] }>; + }; };