From 85e4a5b8a619f2c4c1ead6453c33a8e909c928b9 Mon Sep 17 00:00:00 2001 From: CSantosM <4a.santos@gmail.com> Date: Thu, 5 Feb 2026 13:52:24 +0100 Subject: [PATCH] Adds expandable properties to room responses Implements expandable properties for room responses to reduce payload size. Introduces an `expand` query parameter to control which complex properties, like `config`, are included in the response. By default, these properties are replaced with a stub containing a HATEOAS link to fetch the full data. This change optimizes network bandwidth and improves API performance by preventing unnecessary data transfer, especially when clients only need a subset of room details. --- .../openapi/components/parameters/expand.yaml | 16 ++ .../components/parameters/room-fields.yaml | 2 +- .../responses/success-get-room.yaml | 47 +++--- .../responses/success-get-rooms.yaml | 158 ++++++++++++++---- .../components/schemas/expandable-stub.yaml | 24 +++ .../openapi/components/schemas/meet-room.yaml | 12 +- meet-ce/backend/openapi/paths/rooms.yaml | 6 + .../src/controllers/room.controller.ts | 7 +- meet-ce/backend/src/helpers/room.helper.ts | 32 ++++ .../room-validator.middleware.ts | 12 ++ .../src/models/zod-schemas/room.schema.ts | 34 ++++ meet-ce/backend/src/routes/room.routes.ts | 2 + meet-ce/backend/src/services/room.service.ts | 22 ++- .../tests/helpers/assertion-helpers.ts | 43 +++-- .../backend/tests/helpers/request-helpers.ts | 11 +- .../integration/api/rooms/create-room.test.ts | 45 ++++- .../integration/api/rooms/get-room.test.ts | 26 ++- .../integration/api/rooms/get-rooms.test.ts | 140 ++++++++++++++-- meet-ce/typings/src/expand.ts | 17 ++ meet-ce/typings/src/room.ts | 16 +- 20 files changed, 552 insertions(+), 120 deletions(-) create mode 100644 meet-ce/backend/openapi/components/parameters/expand.yaml create mode 100644 meet-ce/backend/openapi/components/schemas/expandable-stub.yaml create mode 100644 meet-ce/typings/src/expand.ts diff --git a/meet-ce/backend/openapi/components/parameters/expand.yaml b/meet-ce/backend/openapi/components/parameters/expand.yaml new file mode 100644 index 00000000..59f30503 --- /dev/null +++ b/meet-ce/backend/openapi/components/parameters/expand.yaml @@ -0,0 +1,16 @@ +name: expand +in: query +description: > + Specifies which complex properties to include in the response. + + By default, certain large or nested properties are excluded to optimize payload size + and reduce network bandwidth. +
+ Provide a comma-separated list of property names to include their full data in the response. +required: false +schema: + type: string +examples: + config: + value: 'expand=config' + summary: Expand room configuration diff --git a/meet-ce/backend/openapi/components/parameters/room-fields.yaml b/meet-ce/backend/openapi/components/parameters/room-fields.yaml index 21bcb8fd..08d5f149 100644 --- a/meet-ce/backend/openapi/components/parameters/room-fields.yaml +++ b/meet-ce/backend/openapi/components/parameters/room-fields.yaml @@ -6,4 +6,4 @@ description: > required: false schema: type: string -example: 'roomId,moderatorUrl' +example: 'fields=roomId,moderatorUrl' diff --git a/meet-ce/backend/openapi/components/responses/success-get-room.yaml b/meet-ce/backend/openapi/components/responses/success-get-room.yaml index ead4bb77..61e28aa0 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-room.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-room.yaml @@ -4,7 +4,7 @@ content: schema: $ref: '../schemas/meet-room.yaml' examples: - complete_room_details: + default_room_details: summary: Full room details response value: roomId: 'room-123' @@ -16,18 +16,8 @@ content: withMeeting: when_meeting_ends withRecordings: close config: - recording: - enabled: false - layout: grid - encoding: H264_720P_30 - chat: - enabled: true - virtualBackground: - enabled: true - e2ee: - enabled: false - captions: - enabled: true + _expandable: true + _href: '/api/v1/rooms/room-123?expand=config' roles: moderator: permissions: @@ -80,25 +70,30 @@ content: creationDate: 1620000000000 autoDeletionDate: 1900000000000 config: - recording: - enabled: false - layout: grid - encoding: - video: - width: 1920 - height: 1080 - framerate: 30 - codec: H264_MAIN - audio: - codec: OPUS - bitrate: 128 + _expandable: true + _href: '/api/v1/rooms/room-123?expand=config' + + expand=config: + summary: Room details with expanded config + value: + roomId: 'room-123' + roomName: 'room' + creationDate: 1620000000000 + autoDeletionDate: 1900000000000 + config: chat: enabled: true + saveChat: true + recording: + enabled: true + layout: grid + encoding: H264_720P_30 virtualBackground: enabled: true e2ee: enabled: false - + captions: + enabled: true fields=anonymous: summary: Response containing only anonymous access configuration value: diff --git a/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml b/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml index a169e7a8..d610b2ff 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml @@ -12,8 +12,132 @@ content: $ref: '../schemas/meet-pagination.yaml' examples: - complete_room_details: + default_room_details: summary: Full room details response with multiple rooms + value: + rooms: + - roomId: 'room-123' + roomName: 'room' + owner: 'admin' + creationDate: 1620000000000 + autoDeletionDate: 1900000000000 + autoDeletionPolicy: + withMeeting: when_meeting_ends + withRecordings: close + config: + _expandable: true + _href: '/api/v1/rooms/room-123?expand=config' + roles: + moderator: + permissions: + canRecord: true + canRetrieveRecordings: true + canDeleteRecordings: true + canJoinMeeting: true + canShareAccessLinks: true + canMakeModerator: true + canKickParticipants: true + canEndMeeting: true + canPublishVideo: true + canPublishAudio: true + canShareScreen: true + canReadChat: true + canWriteChat: true + canChangeVirtualBackground: true + speaker: + permissions: + canRecord: false + canRetrieveRecordings: true + canDeleteRecordings: false + canJoinMeeting: true + canShareAccessLinks: false + canMakeModerator: false + canKickParticipants: false + canEndMeeting: false + canPublishVideo: true + canPublishAudio: true + canShareScreen: true + canReadChat: true + canWriteChat: true + canChangeVirtualBackground: true + anonymous: + moderator: + enabled: true + accessUrl: 'http://localhost:6080/room/room-123?secret=123456' + speaker: + enabled: true + accessUrl: 'http://localhost:6080/room/room-123?secret=654321' + accessUrl: 'http://localhost:6080/room/room-123' + status: open + meetingEndAction: none + - roomId: 'room-456' + roomName: 'room' + owner: 'alice_smith' + creationDate: 1620001000000 + autoDeletionDate: 1900000000000 + autoDeletionPolicy: + withMeeting: when_meeting_ends + withRecordings: close + config: + _expandable: true + _href: '/api/v1/rooms/room-456?expand=config' + roles: + moderator: + permissions: + canRecord: true + canRetrieveRecordings: true + canDeleteRecordings: false + canJoinMeeting: true + canShareAccessLinks: true + canMakeModerator: false + canKickParticipants: true + canEndMeeting: true + canPublishVideo: true + canPublishAudio: true + canShareScreen: true + canReadChat: true + canWriteChat: true + canChangeVirtualBackground: true + speaker: + permissions: + canRecord: true + canRetrieveRecordings: true + canDeleteRecordings: false + canJoinMeeting: true + canShareAccessLinks: false + canMakeModerator: false + canKickParticipants: false + canEndMeeting: false + canPublishVideo: true + canPublishAudio: true + canShareScreen: false + canReadChat: true + canWriteChat: true + canChangeVirtualBackground: true + anonymous: + moderator: + enabled: false + accessUrl: 'http://localhost:6080/room/room-456?secret=789012' + speaker: + enabled: true + accessUrl: 'http://localhost:6080/room/room-456?secret=210987' + accessUrl: 'http://localhost:6080/room/room-456' + status: open + meetingEndAction: none + pagination: + isTruncated: false + maxItems: 10 + fields=roomId: + summary: Response with only roomId for each room + value: + rooms: + - roomId: 'room-123' + - roomId: 'room-456' + pagination: + isTruncated: false + maxItems: 10 + expand=config: + summary: Room details with expanded config value: rooms: - roomId: 'room-123' @@ -153,16 +277,6 @@ content: pagination: isTruncated: false maxItems: 10 - fields=roomId: - summary: Response with only roomId for each room - value: - rooms: - - roomId: 'room-123' - - roomId: 'room-456' - pagination: - isTruncated: false - maxItems: 10 - fields=roomId,roomName,creationDate,autoDeletionDate,config: summary: Room details including config but no URLs value: @@ -172,29 +286,15 @@ content: creationDate: 1620000000000 autoDeletionDate: 1900000000000 config: - recording: - enabled: false - layout: grid - chat: - enabled: true - virtualBackground: - enabled: true - e2ee: - enabled: false + _expandable: true + _href: '/api/v1/rooms/room-123?expand=config' - roomId: 'room-456' roomName: 'room' creationDate: 1620001000000 autoDeletionDate: 1900000000000 config: - recording: - enabled: true - layout: grid - chat: - enabled: false - virtualBackground: - enabled: false - e2ee: - enabled: false + _expandable: true + _href: '/api/v1/rooms/room-456?expand=config' pagination: isTruncated: true nextPageToken: 'abc123' diff --git a/meet-ce/backend/openapi/components/schemas/expandable-stub.yaml b/meet-ce/backend/openapi/components/schemas/expandable-stub.yaml new file mode 100644 index 00000000..b3f167d9 --- /dev/null +++ b/meet-ce/backend/openapi/components/schemas/expandable-stub.yaml @@ -0,0 +1,24 @@ +ExpandableStub: + type: object + description: > + Marker object that replaces expandable properties when they are not expanded. + + Indicates that a property can be included in full by using the `expand` query parameter. + Follows HATEOAS principles by providing a hypermedia link to expand the property. + properties: + _expandable: + type: boolean + const: true + description: > + Indicates that this property can be expanded using the expand query parameter. + Always `true` for stub objects. + example: true + _href: + type: string + format: uri + description: > + Hypermedia link (HATEOAS) to fetch the resource with this property expanded. + + The URL includes the current expand parameters plus the new property to maintain state. + example: "/api/v1/rooms/room-123?expand=config" + additionalProperties: false diff --git a/meet-ce/backend/openapi/components/schemas/meet-room.yaml b/meet-ce/backend/openapi/components/schemas/meet-room.yaml index f0e9b671..9b3f4b16 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room.yaml @@ -30,7 +30,7 @@ properties: The timestamp (in milliseconds since the Unix epoch) specifying when the room will be automatically deleted. This must be at least one hour in the future. - After this time, the room is closed to new participants and scheduled for deletion. + After this time, the room is closed to new participants and scheduled for deletion. It will be removed after the last participant leaves (graceful deletion). If not set, the room remains active until manually deleted. @@ -63,8 +63,14 @@ properties: - force: The room and its recordings will be deleted. - close: The room will be closed instead of deleted, maintaining its recordings. config: - $ref: meet-room-config.yaml#/MeetRoomConfig - description: The config for the room. + description: > + The configuration for the room (chat, recording, virtual background, e2ee, captions). + + By default, this property is excluded from responses to reduce payload size. + It is replaced with an expandable stub. Use `?expand=config` to include the full configuration. + oneOf: + - $ref: expandable-stub.yaml#/ExpandableStub + - $ref: meet-room-config.yaml#/MeetRoomConfig # maxParticipants: # type: integer # example: 10 diff --git a/meet-ce/backend/openapi/paths/rooms.yaml b/meet-ce/backend/openapi/paths/rooms.yaml index d2b13dc7..70b63e78 100644 --- a/meet-ce/backend/openapi/paths/rooms.yaml +++ b/meet-ce/backend/openapi/paths/rooms.yaml @@ -43,6 +43,7 @@ - $ref: '../components/parameters/room-name.yaml' - $ref: '../components/parameters/room-status.yaml' - $ref: '../components/parameters/room-fields.yaml' + - $ref: '../components/parameters/expand.yaml' - $ref: '../components/parameters/max-items.yaml' - $ref: '../components/parameters/next-page-token.yaml' - $ref: '../components/parameters/sort-field.yaml' @@ -100,6 +101,10 @@ summary: Get a room description: > Retrieves the details of an OpenVidu Meet room with the specified room ID. +
+ By default, certain large properties like `config` are excluded from the response + to reduce payload size. These properties are replaced with an expandable stub. + Use the `expand` parameter to include these properties when needed. tags: - OpenVidu Meet - Rooms security: @@ -109,6 +114,7 @@ parameters: - $ref: '../components/parameters/room-id-path.yaml' - $ref: '../components/parameters/room-fields.yaml' + - $ref: '../components/parameters/expand.yaml' responses: '200': $ref: '../components/responses/success-get-room.yaml' diff --git a/meet-ce/backend/src/controllers/room.controller.ts b/meet-ce/backend/src/controllers/room.controller.ts index 1580e209..2d3df7e6 100644 --- a/meet-ce/backend/src/controllers/room.controller.ts +++ b/meet-ce/backend/src/controllers/room.controller.ts @@ -34,7 +34,7 @@ export const getRooms = async (req: Request, res: Response) => { const roomService = container.get(RoomService); const queryParams = req.query as MeetRoomFilters; - logger.verbose('Getting all rooms'); + logger.verbose(`Getting all rooms with expand: ${queryParams.expand || 'none'}`); try { const { rooms, isTruncated, nextPageToken } = await roomService.getAllMeetRooms(queryParams); @@ -50,12 +50,13 @@ export const getRoom = async (req: Request, res: Response) => { const { roomId } = req.params; const fields = req.query.fields as string | undefined; + const expand = req.query.expand as string | undefined; try { - logger.verbose(`Getting room '${roomId}'`); + logger.verbose(`Getting room '${roomId}' with expand: ${expand || 'none'}`); const roomService = container.get(RoomService); - const room = await roomService.getMeetRoom(roomId, fields, true); + const room = await roomService.getMeetRoom(roomId, fields, expand, true); return res.status(200).json(room); } catch (error) { diff --git a/meet-ce/backend/src/helpers/room.helper.ts b/meet-ce/backend/src/helpers/room.helper.ts index f6cac38d..40f28e13 100644 --- a/meet-ce/backend/src/helpers/room.helper.ts +++ b/meet-ce/backend/src/helpers/room.helper.ts @@ -1,4 +1,5 @@ import { MeetRoom, MeetRoomOptions } from '@openvidu-meet/typings'; +import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_ENV } from '../environment.js'; export class MeetRoomHelper { @@ -115,4 +116,35 @@ export class MeetRoomHelper { return false; } } + + /** + * Processes a room to replace non-expanded properties with stubs. + * + * @example + * ``` + * { + * config: { + * _expandable: true, + * _href: '/api/rooms/123?expand=config' + * } + * } + * ``` + */ + static processRoomExpandProperties(room: MeetRoom, expand?: string): MeetRoom { + const expandProps = expand ? expand.split(',').map((p) => p.trim()) : []; + const processed = { ...room }; + const { roomId } = room; + const baseUrl = `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId || room.roomId}`; + + // Replace config with stub if not expanded + if (!expandProps.includes('config')) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (processed as any).config = { + _expandable: true, + _href: `${baseUrl}?expand=config` + }; + } + + return processed; + } } diff --git a/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts b/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts index 36bc90f7..d2a4be55 100644 --- a/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts +++ b/meet-ce/backend/src/middlewares/request-validators/room-validator.middleware.ts @@ -3,6 +3,7 @@ import { rejectUnprocessableRequest } from '../../models/error.model.js'; import { BulkDeleteRoomsReqSchema, DeleteRoomReqSchema, + GetRoomQuerySchema, nonEmptySanitizedRoomId, RoomFiltersSchema, RoomOptionsSchema, @@ -60,6 +61,17 @@ export const withValidRoomId = (req: Request, res: Response, next: NextFunction) next(); }; +export const validateGetRoomReq = (req: Request, res: Response, next: NextFunction) => { + const { success, error, data } = GetRoomQuerySchema.safeParse(req.query); + + if (!success) { + return rejectUnprocessableRequest(res, error); + } + + req.query = data; + next(); +}; + export const validateDeleteRoomReq = (req: Request, res: Response, next: NextFunction) => { const roomIdResult = nonEmptySanitizedRoomId('roomId').safeParse(req.params.roomId); diff --git a/meet-ce/backend/src/models/zod-schemas/room.schema.ts b/meet-ce/backend/src/models/zod-schemas/room.schema.ts index 4dffef23..9a4647da 100644 --- a/meet-ce/backend/src/models/zod-schemas/room.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/room.schema.ts @@ -366,10 +366,39 @@ export const RoomOptionsSchema: z.ZodType = z.object({ // .default(null) }); +// Shared expand validation schema for Room entity +const expandSchema = z + .string() + .optional() + .refine( + (value) => { + if (!value) return true; + + const allowed = ['config']; + const requested = value.split(',').map((p) => p.trim()); + + return requested.every((p) => allowed.includes(p)); + }, + { + message: 'Invalid expand properties. Valid options: config' + } + ) + .transform((value) => { + // Filter and clean expand values + if (!value) return undefined; + + const allowed = ['config']; + const requested = value.split(',').map((p) => p.trim()); + const valid = requested.filter((p) => allowed.includes(p)); + + return valid.length > 0 ? valid.join(',') : undefined; + }); + export const RoomFiltersSchema: z.ZodType = z.object({ roomName: z.string().optional(), status: z.nativeEnum(MeetRoomStatus).optional(), fields: z.string().optional(), + expand: expandSchema, maxItems: z.coerce .number() .positive('maxItems must be a positive number') @@ -385,6 +414,11 @@ export const RoomFiltersSchema: z.ZodType = z.object({ sortOrder: z.enum(['asc', 'desc']).optional().default('desc') }); +export const GetRoomQuerySchema = z.object({ + fields: z.string().optional(), + expand: expandSchema +}); + export const DeleteRoomReqSchema = z.object({ withMeeting: RoomDeletionPolicyWithMeetingSchema.optional().default(MeetRoomDeletionPolicyWithMeeting.FAIL), withRecordings: RoomDeletionPolicyWithRecordingsSchema.optional().default(MeetRoomDeletionPolicyWithRecordings.FAIL) diff --git a/meet-ce/backend/src/routes/room.routes.ts b/meet-ce/backend/src/routes/room.routes.ts index f81e30d4..1994166e 100644 --- a/meet-ce/backend/src/routes/room.routes.ts +++ b/meet-ce/backend/src/routes/room.routes.ts @@ -20,6 +20,7 @@ import { validateBulkDeleteRoomsReq, validateCreateRoomReq, validateDeleteRoomReq, + validateGetRoomReq, validateGetRoomsReq, validateUpdateRoomAnonymousReq, validateUpdateRoomConfigReq, @@ -66,6 +67,7 @@ roomRouter.get( accessTokenValidator(MeetUserRole.ADMIN, MeetUserRole.USER, MeetUserRole.ROOM_MEMBER) ), withValidRoomId, + validateGetRoomReq, authorizeRoomAccess, roomCtrl.getRoom ); diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index b1823922..701ac84c 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -159,7 +159,10 @@ export class RoomService { rolesUpdatedAt: now, meetingEndAction: MeetingEndAction.NONE }; - return await this.roomRepository.create(meetRoom); + const room = await this.roomRepository.create(meetRoom); + + // Avoid include full config in payload + return MeetRoomHelper.processRoomExpandProperties(room, ''); } /** @@ -316,7 +319,7 @@ export class RoomService { * - USER: Can see rooms they own or are members of * - ROOM_MEMBER: Can see rooms they are members of * - * @param filters - Filtering, pagination and sorting options + * @param filters - Filtering, pagination and sorting options (including expand) * @returns A Promise that resolves to paginated room list * @throws If there was an error retrieving the rooms */ @@ -340,7 +343,12 @@ export class RoomService { } } - return await this.roomRepository.find(queryOptions); + const response = await this.roomRepository.find(queryOptions); + + // Process rooms with expand logic + response.rooms = response.rooms.map((room) => MeetRoomHelper.processRoomExpandProperties(room, filters.expand)); + + return response; } /** @@ -401,10 +409,11 @@ export class RoomService { * * @param roomId - The name of the room to retrieve. * @param fields - Optional fields to retrieve from the room. + * @param expand - Optional comma-separated list of properties to expand. * @param checkPermissions - Whether to check permissions and remove sensitive properties. Defaults to false. - * @returns A promise that resolves to an {@link MeetRoom} object. + * @returns A promise that resolves to an {@link MeetRoom} object (with expandable properties as stubs when not expanded). */ - async getMeetRoom(roomId: string, fields?: string, checkPermissions = false): Promise { + async getMeetRoom(roomId: string, fields?: string, expand?: string, checkPermissions = false): Promise { const room = await this.roomRepository.findByRoomId(roomId, fields); if (!room) { @@ -421,7 +430,8 @@ export class RoomService { } } - return room; + // Process expand + return MeetRoomHelper.processRoomExpandProperties(room, expand); } /** diff --git a/meet-ce/backend/tests/helpers/assertion-helpers.ts b/meet-ce/backend/tests/helpers/assertion-helpers.ts index 78836240..19ede058 100644 --- a/meet-ce/backend/tests/helpers/assertion-helpers.ts +++ b/meet-ce/backend/tests/helpers/assertion-helpers.ts @@ -101,7 +101,7 @@ export const expectSuccessRoomResponse = ( roomName: string, roomIdPrefix?: string, autoDeletionDate?: number, - config?: MeetRoomConfig + config?: MeetRoomConfig | 'expandable' ) => { expect(response.status).toBe(200); expectValidRoom(response.body, roomName, roomIdPrefix, config, autoDeletionDate); @@ -113,11 +113,31 @@ export const expectSuccessRoomConfigResponse = (response: Response, config: Meet expect(response.body).toEqual(config); }; +/** + * Validates if a property is an expandable stub + */ +export const expectExpandableStub = (property: any, roomId: string, propertyName: string) => { + expect(property).toBeDefined(); + expect(property._expandable).toBe(true); + expect(property._href).toBeDefined(); + expect(property._href).toContain(`/rooms/${roomId}`); + expect(property._href).toContain(`expand=${propertyName}`); +}; + +/** + * Validates that a property is NOT an expandable stub (i.e., it's the actual expanded value) + */ +export const expectExpandedProperty = (property: any) => { + expect(property).toBeDefined(); + expect(property._expandable).toBeUndefined(); + expect(property._href).toBeUndefined(); +}; + export const expectValidRoom = ( room: MeetRoom, name: string, roomIdPrefix?: string, - config?: MeetRoomConfig, + config?: MeetRoomConfig | 'expandable', autoDeletionDate?: number, autoDeletionPolicy?: MeetRoomAutoDeletionPolicy, status?: MeetRoomStatus, @@ -151,21 +171,14 @@ export const expectValidRoom = ( expect(room.config).toBeDefined(); - if (config !== undefined) { + // Check if config should be an expandable stub + if (config === 'expandable' || config === undefined) { + expectExpandableStub(room.config, room.roomId, 'config'); + } else { + // Validate it's NOT an expandable stub (it's expanded) + expectExpandedProperty(room.config); // Use toMatchObject to allow encoding defaults to be added without breaking tests expect(room.config).toMatchObject(config as any); - } else { - expect(room.config).toEqual({ - recording: { - enabled: true, - layout: DEFAULT_RECORDING_LAYOUT, - encoding: DEFAULT_RECORDING_ENCODING_PRESET - }, - chat: { enabled: true }, - virtualBackground: { enabled: true }, - e2ee: { enabled: false }, - captions: { enabled: true } - }); } expect(room.owner).toBeDefined(); diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index baad91a8..5c2ec9cf 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -419,16 +419,23 @@ export const getRooms = async (query: Record = {}) => { * * @param roomId - The unique identifier of the room to retrieve * @param fields - Optional fields to filter in the response + * @param expand - Optional expand parameter to include additional data (e.g., 'config') * @param roomMemberToken - Optional room member token for authentication * @returns A Promise that resolves to the room data * @throws Error if the app instance is not defined */ -export const getRoom = async (roomId: string, fields?: string, roomMemberToken?: string) => { +export const getRoom = async (roomId: string, fields?: string, expand?: string, roomMemberToken?: string) => { checkAppIsRunning(); + const queryParams: Record = {}; + + if (fields) queryParams.fields = fields; + + if (expand) queryParams.expand = expand; + const req = request(app) .get(getFullPath(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`)) - .query({ fields }); + .query(queryParams); if (roomMemberToken) { req.set(INTERNAL_CONFIG.ROOM_MEMBER_TOKEN_HEADER, roomMemberToken); diff --git a/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts index 717ba096..abe90e45 100644 --- a/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/create-room.test.ts @@ -19,7 +19,7 @@ import { expectValidRoom, expectValidationError } from '../../../helpers/assertion-helpers.js'; -import { createRoom, deleteAllRooms, getFullPath, startTestServer } from '../../../helpers/request-helpers.js'; +import { createRoom, deleteAllRooms, getFullPath, getRoom, startTestServer } from '../../../helpers/request-helpers.js'; const ROOMS_PATH = getFullPath(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms`); @@ -85,7 +85,7 @@ describe('Room API Tests', () => { room, 'Example Room', 'example_room', - payload.config, + 'expandable', validAutoDeletionDate, payload.autoDeletionPolicy ); @@ -118,7 +118,16 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, // Default value captions: { enabled: true } }; - expectValidRoom(room, 'Partial Config Room', 'partial_config_room', expectedConfig, validAutoDeletionDate); + expectValidRoom(room, 'Partial Config Room', 'partial_config_room', 'expandable', validAutoDeletionDate); + + const response = await getRoom(room.roomId, undefined, 'config'); + expectValidRoom( + response.body, + 'Partial Config Room', + 'partial_config_room', + expectedConfig, + validAutoDeletionDate + ); }); it('Should create a room when sending partial config with two fields', async () => { @@ -148,7 +157,16 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, // Default value captions: { enabled: true } // Default value }; - expectValidRoom(room, 'Partial Config Room', 'partial_config_room', expectedConfig, validAutoDeletionDate); + expectValidRoom(room, 'Partial Config Room', 'partial_config_room', 'expandable', validAutoDeletionDate); + + const response = await getRoom(room.roomId, undefined, 'config'); + expectValidRoom( + response.body, + 'Partial Config Room', + 'partial_config_room', + expectedConfig, + validAutoDeletionDate + ); }); }); @@ -312,14 +330,17 @@ describe('Room API Tests', () => { recording: { enabled: true, layout: DEFAULT_RECORDING_LAYOUT, - encoding: DEFAULT_RECORDING_ENCODING_PRESET // Default value + encoding: DEFAULT_RECORDING_ENCODING_PRESET }, chat: { enabled: true }, virtualBackground: { enabled: true }, e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, 'Room without encoding', 'room_without_encoding', expectedConfig); + expectValidRoom(room, 'Room without encoding', 'room_without_encoding', 'expandable'); + + const response = await getRoom(room.roomId, undefined, 'config'); + expectValidRoom(response.body, 'Room without encoding', 'room_without_encoding', expectedConfig); }); it('Should create a room with H264_1080P_30 encoding preset', async () => { @@ -346,7 +367,9 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, '1080p Preset Room', '1080p_preset_room', expectedConfig); + expectValidRoom(room, '1080p Preset Room', '1080p_preset_room', 'expandable'); + const response = await getRoom(room.roomId, undefined, 'config'); + expectValidRoom(response.body, '1080p Preset Room', '1080p_preset_room', expectedConfig); }); it('Should create a room with PORTRAIT_H264_720P_30 encoding preset', async () => { @@ -373,7 +396,9 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, 'Portrait 720p Room', 'portrait_720p_room', expectedConfig); + expectValidRoom(room, 'Portrait 720p Room', 'portrait_720p_room', 'expandable'); + const response = await getRoom(room.roomId, undefined, 'config'); + expectValidRoom(response.body, 'Portrait 720p Room', 'portrait_720p_room', expectedConfig); }); it('Should create a room with advanced encoding options - both video and audio', async () => { @@ -430,7 +455,9 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', expectedConfig); + expectValidRoom(room, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', 'expandable'); + const response = await getRoom(room.roomId, undefined, 'config'); + expectValidRoom(response.body, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', expectedConfig); }); }); diff --git a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts index f38844d1..c1a40b23 100644 --- a/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/get-room.test.ts @@ -31,10 +31,11 @@ describe('Room API Tests', () => { roomName: 'test-room' }); - expectValidRoom(createdRoom, 'test-room'); + expectValidRoom(createdRoom, 'test-room', 'test_room', 'expandable'); + // Get room without expand - should return expandable stub const response = await getRoom(createdRoom.roomId); - expectSuccessRoomResponse(response, 'test-room', 'test_room'); + expectSuccessRoomResponse(response, 'test-room', 'test_room', undefined, 'expandable'); }); it('should retrieve a room with custom config', async () => { @@ -55,8 +56,8 @@ describe('Room API Tests', () => { // Create a room with custom config const { roomId } = await createRoom(payload); - // Retrieve the room by its ID - const response = await getRoom(roomId); + // Retrieve the room by its ID with expand=config + const response = await getRoom(roomId, undefined, 'config'); expectSuccessRoomResponse(response, 'custom-config', 'custom_config', undefined, payload.config); }); @@ -107,7 +108,7 @@ describe('Room API Tests', () => { it('should retrieve a room without moderatorUrl when participant is speaker', async () => { const roomData = await setupSingleRoom(); - const response = await getRoom(roomData.room.roomId, undefined, roomData.speakerToken); + const response = await getRoom(roomData.room.roomId, undefined, undefined, roomData.speakerToken); expect(response.status).toBe(200); expect(response.body.moderatorUrl).toBeUndefined(); }); @@ -123,7 +124,7 @@ describe('Room API Tests', () => { } }); - const response = await getRoom(createdRoom.roomId); + const response = await getRoom(createdRoom.roomId, undefined, 'config'); expect(response.status).toBe(200); expect(response.body.config.recording.encoding).toBe(MeetRecordingEncodingPreset.H264_1080P_60); }); @@ -156,7 +157,7 @@ describe('Room API Tests', () => { } }); - const response = await getRoom(createdRoom.roomId); + const response = await getRoom(createdRoom.roomId, undefined, 'config'); expect(response.status).toBe(200); expect(response.body.config.recording.encoding).toMatchObject(advancedEncoding); }); @@ -175,5 +176,16 @@ describe('Room API Tests', () => { expectValidationError(response, 'roomId', 'cannot be empty after sanitization'); }); + + it('should fail when expand has invalid values', async () => { + const createdRoom = await createRoom({ + roomName: 'invalid-expand-test' + }); + + // Get room with invalid expand values + const response = await getRoom(createdRoom.roomId, undefined, 'invalid,wrongparam'); + + expectValidationError(response, 'expand', 'Invalid expand properties. Valid options: config'); + }); }); }); diff --git a/meet-ce/backend/tests/integration/api/rooms/get-rooms.test.ts b/meet-ce/backend/tests/integration/api/rooms/get-rooms.test.ts index 2003708d..bd215967 100644 --- a/meet-ce/backend/tests/integration/api/rooms/get-rooms.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/get-rooms.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; -import { MeetRoom, MeetRoomStatus } from '@openvidu-meet/typings'; +import { MeetRecordingEncodingPreset, MeetRecordingLayout, MeetRoom, MeetRoomStatus } from '@openvidu-meet/typings'; import ms from 'ms'; import { expectSuccessRoomsResponse, @@ -37,7 +37,7 @@ describe('Room API Tests', () => { const response = await getRooms(); expectSuccessRoomsResponse(response, 1, 10, false, false); - expectValidRoom(response.body.rooms[0], 'test-room'); + expectValidRoom(response.body.rooms[0], 'test-room', 'test_room', 'expandable'); }); it('should return a list of rooms applying fields filter', async () => { @@ -55,12 +55,14 @@ describe('Room API Tests', () => { }); it('should return a list of rooms applying roomName filter', async () => { - await createRoom({ - roomName: 'test-room' - }); - await createRoom({ - roomName: 'other-room' - }); + await Promise.all([ + createRoom({ + roomName: 'test-room' + }), + createRoom({ + roomName: 'other-room' + }) + ]); const response = await getRooms({ roomName: 'test-room' }); const { rooms } = response.body; @@ -81,14 +83,20 @@ describe('Room API Tests', () => { }); it('should return a list of rooms with pagination', async () => { + const promises = []; + // Create rooms sequentially to ensure different creation dates for (let i = 0; i < 6; i++) { - await createRoom({ - roomName: `test-room-${i}`, - autoDeletionDate: validAutoDeletionDate - }); + promises.push( + createRoom({ + roomName: `test-room-${i}`, + autoDeletionDate: validAutoDeletionDate + }) + ); } + await Promise.all(promises); + let response = await getRooms({ maxItems: 3 }); let { pagination, rooms } = response.body; @@ -120,9 +128,11 @@ describe('Room API Tests', () => { }); it('should sort rooms by roomName ascending and descending', async () => { - await createRoom({ roomName: 'zebra-room' }); - await createRoom({ roomName: 'alpha-room' }); - await createRoom({ roomName: 'beta-room' }); + await Promise.all([ + createRoom({ roomName: 'zebra-room' }), + createRoom({ roomName: 'alpha-room' }), + createRoom({ roomName: 'beta-room' }) + ]); // Test ascending let response = await getRooms({ sortField: 'roomName', sortOrder: 'asc' }); @@ -172,9 +182,11 @@ describe('Room API Tests', () => { const date1 = now + ms('2h'); const date2 = now + ms('3h'); - await createRoom({ roomName: 'room-3h', autoDeletionDate: date2 }); - await createRoom({ roomName: 'room-2h', autoDeletionDate: date1 }); - await createRoom({ roomName: 'room-without-date' }); // Room without autoDeletionDate + await Promise.all([ + createRoom({ roomName: 'room-3h', autoDeletionDate: date2 }), + createRoom({ roomName: 'room-2h', autoDeletionDate: date1 }), + createRoom({ roomName: 'room-without-date' }) // Room without autoDeletionDate + ]); // Test ascending let response = await getRooms({ sortField: 'autoDeletionDate', sortOrder: 'asc' }); @@ -232,4 +244,96 @@ describe('Room API Tests', () => { expectValidationError(response, 'status', 'Invalid enum value'); }); }); + + describe('List Rooms with Expand Parameter Tests', () => { + it('should return rooms with config as expandable stub when expand parameter is not provided', async () => { + await createRoom({ + roomName: 'no-expand-list-test' + }); + + const response = await getRooms(); + expectSuccessRoomsResponse(response, 1, 10, false, false); + + const room = response.body.rooms[0]; + expectValidRoom(room, 'no-expand-list-test', 'no_expand_list_test', 'expandable'); + }); + + it('should return rooms with full config when using expand=config', async () => { + const customConfig = { + recording: { + enabled: false, + layout: MeetRecordingLayout.SPEAKER, + encoding: MeetRecordingEncodingPreset.H264_1080P_30 + }, + chat: { enabled: false }, + virtualBackground: { enabled: false }, + e2ee: { enabled: true }, + captions: { enabled: false } + }; + + await createRoom({ + roomName: 'expand-list-test', + config: customConfig + }); + + const response = await getRooms({ expand: 'config' }); + expectSuccessRoomsResponse(response, 1, 10, false, false); + + const room = response.body.rooms[0]; + expectValidRoom(room, 'expand-list-test', 'expand_list_test', customConfig); + }); + + it('should fail when expand has invalid values', async () => { + await createRoom({ + roomName: 'invalid-expand-list' + }); + + const response = await getRooms({ expand: 'invalid,wrongparam' }); + expectValidationError(response, 'expand', 'Invalid expand properties. Valid options: config'); + }); + + it('should return multiple rooms with full config when using expand=config', async () => { + const config1 = { + recording: { + enabled: true, + layout: MeetRecordingLayout.GRID, + encoding: MeetRecordingEncodingPreset.H264_720P_30 + }, + chat: { enabled: true }, + virtualBackground: { enabled: true }, + e2ee: { enabled: false }, + captions: { enabled: true } + }; + + const config2 = { + recording: { + enabled: false, + layout: MeetRecordingLayout.SPEAKER, + encoding: MeetRecordingEncodingPreset.H264_1080P_30 + }, + chat: { enabled: false }, + virtualBackground: { enabled: false }, + e2ee: { enabled: true }, + captions: { enabled: false } + }; + + await Promise.all([ + createRoom({ roomName: 'multi-expand-1', config: config1 }), + createRoom({ roomName: 'multi-expand-2', config: config2 }) + ]); + + const response = await getRooms({ expand: 'config' }); + expectSuccessRoomsResponse(response, 2, 10, false, false); + + const rooms = response.body.rooms; + const room1 = rooms.find((r: MeetRoom) => r.roomName === 'multi-expand-1'); + const room2 = rooms.find((r: MeetRoom) => r.roomName === 'multi-expand-2'); + + expect(room1).toBeDefined(); + expect(room2).toBeDefined(); + + expectValidRoom(room1, 'multi-expand-1', 'multi_expand_1', config1); + expectValidRoom(room2, 'multi-expand-2', 'multi_expand_2', config2); + }); + }); }); diff --git a/meet-ce/typings/src/expand.ts b/meet-ce/typings/src/expand.ts new file mode 100644 index 00000000..073810ec --- /dev/null +++ b/meet-ce/typings/src/expand.ts @@ -0,0 +1,17 @@ +/** + * Stub that indicates a property can be expanded. + */ +export interface ExpandableStub { + _expandable: true; + _href: string; +} + +/** + * Adds expand query parameter support to filters. + */ +export interface ExpandableFilters { + /** + * Comma-separated properties to expand (e.g., "config,autoDeletionPolicy"). + */ + expand?: string; +} diff --git a/meet-ce/typings/src/room.ts b/meet-ce/typings/src/room.ts index df5ebc41..edc8a9c4 100644 --- a/meet-ce/typings/src/room.ts +++ b/meet-ce/typings/src/room.ts @@ -1,3 +1,4 @@ +import { ExpandableFilters } from './expand.js'; import { MeetRoomMemberPermissions } from './permissions/meet-permissions.js'; import { MeetRoomConfig } from './room-config.js'; import { SortAndPagination } from './sort-pagination.js'; @@ -233,10 +234,23 @@ export enum MeetRoomDeletionPolicyWithRecordings { FAIL = 'fail' } -export interface MeetRoomFilters extends SortAndPagination { +/** + * Filters for querying rooms with pagination, sorting, field selection, and expand support. + */ +export interface MeetRoomFilters extends SortAndPagination, ExpandableFilters { + /** + * Filter rooms by name (case-insensitive partial match) + */ roomName?: string; + /** + * Filter rooms by status + */ status?: MeetRoomStatus; + /** + * Comma-separated list of fields to include in the response + */ fields?: string; + // expand inherited from ExpandableFilters } export enum MeetRoomDeletionSuccessCode {