From b8e7baf7056e1ad1a599377bf7be580e0f5e4347 Mon Sep 17 00:00:00 2001 From: CSantosM <4a.santos@gmail.com> Date: Wed, 11 Feb 2026 17:20:16 +0100 Subject: [PATCH] Renames expand to extraFields in room API Updates the room API to use `extraFields` instead of `expand` for including additional data in responses. This change improves clarity and consistency in the API design. It also simplifies the filtering logic by explicitly differentiating between base fields (controlled by `fields`) and extra fields (controlled by `extraFields`). The changes include: - Renaming the query parameter and header - Updating the validation schemas - Adjusting the filtering logic in the controller and service layers - Updating the frontend components and services --- .../{expand.yaml => extraFields.yaml} | 10 +- ...-header.yaml => x-extrafields-header.yaml} | 13 +- .../responses/success-bulk-delete-rooms.yaml | 21 +- .../responses/success-create-room.yaml | 12 +- .../responses/success-get-room.yaml | 29 ++- .../responses/success-get-rooms.yaml | 63 ++++-- .../success-room-process-deletion.yaml | 17 +- .../success-room-schedule-deletion.yaml | 18 +- .../components/schemas/expandable-stub.yaml | 24 --- .../schemas/meet-extra-field-metadata.yaml | 2 + .../components/schemas/meet-room-config.yaml | 3 +- .../openapi/components/schemas/meet-room.yaml | 13 +- .../backend/openapi/openvidu-meet-api.yaml | 2 +- meet-ce/backend/openapi/paths/rooms.yaml | 35 ++-- .../src/controllers/room.controller.ts | 71 ++++--- .../src/helpers/field-filter.helper.ts | 185 ++++++++++++++++++ meet-ce/backend/src/helpers/room.helper.ts | 137 +++++-------- meet-ce/backend/src/models/room-response.ts | 23 --- .../src/models/zod-schemas/room.schema.ts | 49 +++-- .../src/services/room-member.service.ts | 14 +- meet-ce/backend/src/services/room.service.ts | 56 ++---- .../tests/helpers/assertion-helpers.ts | 39 ++-- .../backend/tests/helpers/request-helpers.ts | 18 +- .../integration/api/rooms/create-room.test.ts | 99 ++++++---- .../integration/api/rooms/delete-room.test.ts | 2 +- .../api/rooms/e2ee-room-config.test.ts | 10 +- .../integration/api/rooms/get-room.test.ts | 19 +- .../integration/api/rooms/get-rooms.test.ts | 46 ++--- .../recording-layout-room-config.test.ts | 6 +- .../api/rooms/update-room-config.test.ts | 4 +- .../meeting/services/meeting-lobby.service.ts | 2 +- .../lib/domains/rooms/models/room-request.ts | 8 +- .../room-wizard/room-wizard.component.ts | 2 +- .../domains/rooms/services/room.service.ts | 6 +- meet-ce/typings/src/room-response.ts | 33 +--- 35 files changed, 644 insertions(+), 447 deletions(-) rename meet-ce/backend/openapi/components/parameters/{expand.yaml => extraFields.yaml} (61%) rename meet-ce/backend/openapi/components/parameters/{x-expand-header.yaml => x-extrafields-header.yaml} (52%) delete mode 100644 meet-ce/backend/openapi/components/schemas/expandable-stub.yaml create mode 100644 meet-ce/backend/openapi/components/schemas/meet-extra-field-metadata.yaml create mode 100644 meet-ce/backend/src/helpers/field-filter.helper.ts delete mode 100644 meet-ce/backend/src/models/room-response.ts diff --git a/meet-ce/backend/openapi/components/parameters/expand.yaml b/meet-ce/backend/openapi/components/parameters/extraFields.yaml similarity index 61% rename from meet-ce/backend/openapi/components/parameters/expand.yaml rename to meet-ce/backend/openapi/components/parameters/extraFields.yaml index 59f30503..3b240abd 100644 --- a/meet-ce/backend/openapi/components/parameters/expand.yaml +++ b/meet-ce/backend/openapi/components/parameters/extraFields.yaml @@ -1,16 +1,14 @@ -name: expand +name: extraFields in: query description: > - Specifies which complex properties to include in the response. + Specifies which extra fields 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. + + Note: Extra fields specified here will be included even if not specified in the `fields` parameter. required: false schema: type: string -examples: - config: - value: 'expand=config' - summary: Expand room configuration diff --git a/meet-ce/backend/openapi/components/parameters/x-expand-header.yaml b/meet-ce/backend/openapi/components/parameters/x-extrafields-header.yaml similarity index 52% rename from meet-ce/backend/openapi/components/parameters/x-expand-header.yaml rename to meet-ce/backend/openapi/components/parameters/x-extrafields-header.yaml index b85ad096..1c51af1a 100644 --- a/meet-ce/backend/openapi/components/parameters/x-expand-header.yaml +++ b/meet-ce/backend/openapi/components/parameters/x-extrafields-header.yaml @@ -1,15 +1,17 @@ -name: X-Expand +name: X-ExtraFields in: header description: > - Specifies which complex properties to include fully expanded in the response. + Specifies which extra fields to include fully in the response. - By default, certain large or nested properties (like `config`) are replaced with expandable stubs + By default, certain large or nested properties (like `config`) are excluded to optimize payload size and reduce network bandwidth. Use this header to include the full data of these properties in the creation response, avoiding the need for a subsequent GET request. - Provide a comma-separated list of property names to expand. + Provide a comma-separated list of property names to include. + + Note: Extra fields specified here will be included even if not specified in the `X-Fields` header. required: false schema: type: string @@ -17,3 +19,6 @@ examples: config: value: 'config' summary: Include full room configuration in response + combined: + value: 'config' + summary: 'Use with X-Fields header for union behavior (X-Fields ∪ X-ExtraFields)' diff --git a/meet-ce/backend/openapi/components/responses/success-bulk-delete-rooms.yaml b/meet-ce/backend/openapi/components/responses/success-bulk-delete-rooms.yaml index a96bccc6..6a49d196 100644 --- a/meet-ce/backend/openapi/components/responses/success-bulk-delete-rooms.yaml +++ b/meet-ce/backend/openapi/components/responses/success-bulk-delete-rooms.yaml @@ -28,14 +28,15 @@ content: type: string description: A message providing additional context about the success room: - $ref: '../schemas/meet-room.yaml' + allOf: + - $ref: '../schemas/meet-room.yaml' + - type: object + properties: + _extraFields: + type: array + description: > + List of extra fields that can be included in the response based on the `X-ExtraFields` header or `extraFields` query parameter. + items: + type: string + example: config description: List of rooms that were successfully processed for deletion - example: - message: 'All rooms successfully processed for deletion' - successful: - - roomId: room-123 - successCode: room_deleted - message: Room 'room-123' deleted successfully - - roomId: room-456 - successCode: room_with_active_meeting_deleted - message: Room 'room-456' with active meeting deleted successfully diff --git a/meet-ce/backend/openapi/components/responses/success-create-room.yaml b/meet-ce/backend/openapi/components/responses/success-create-room.yaml index d746f6c3..c91f33fd 100644 --- a/meet-ce/backend/openapi/components/responses/success-create-room.yaml +++ b/meet-ce/backend/openapi/components/responses/success-create-room.yaml @@ -2,7 +2,17 @@ description: Room created successfully content: application/json: schema: - $ref: '../schemas/meet-room.yaml' + allOf: + - $ref: '../schemas/meet-room.yaml' + - type: object + properties: + _extraFields: + type: array + description: > + List of extra fields that can be included in the response based on the `X-ExtraFields` header. + items: + type: string + example: config headers: Location: description: URL of the newly created room 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 61e28aa0..61f79a94 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-room.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-room.yaml @@ -2,7 +2,17 @@ description: Success response for retrieving a room content: application/json: schema: - $ref: '../schemas/meet-room.yaml' + allOf: + - $ref: '../schemas/meet-room.yaml' + - type: object + properties: + _extraFields: + type: array + description: > + List of extra fields that can be included in the response based on the `X-ExtraFields` header or `extraFields` query parameter. + items: + type: string + example: config examples: default_room_details: summary: Full room details response @@ -15,9 +25,6 @@ content: autoDeletionPolicy: withMeeting: when_meeting_ends withRecordings: close - config: - _expandable: true - _href: '/api/v1/rooms/room-123?expand=config' roles: moderator: permissions: @@ -61,6 +68,8 @@ content: accessUrl: 'http://localhost:6080/room/room-123' status: open meetingEndAction: none + _extraFields: + - config fields=roomId,roomName,creationDate,autoDeletionDate,config: summary: Room details with roomId, roomName, creationDate, autoDeletionDate, and config @@ -69,11 +78,10 @@ content: roomName: 'room' creationDate: 1620000000000 autoDeletionDate: 1900000000000 - config: - _expandable: true - _href: '/api/v1/rooms/room-123?expand=config' + _extraFields: + - config - expand=config: + extraFields=config: summary: Room details with expanded config value: roomId: 'room-123' @@ -83,7 +91,6 @@ content: config: chat: enabled: true - saveChat: true recording: enabled: true layout: grid @@ -94,6 +101,8 @@ content: enabled: false captions: enabled: true + _extraFields: + - config fields=anonymous: summary: Response containing only anonymous access configuration value: @@ -104,3 +113,5 @@ content: speaker: enabled: true accessUrl: 'http://localhost:6080/room/room-123?secret=654321' + _extraFields: + - config 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 d610b2ff..1e6762a0 100644 --- a/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml +++ b/meet-ce/backend/openapi/components/responses/success-get-rooms.yaml @@ -8,6 +8,13 @@ content: type: array items: $ref: '../schemas/meet-room.yaml' + _extraFields: + type: array + description: > + List of extra fields that can be included in the response based on the `X-ExtraFields` header or `extraFields` query parameter. + items: + type: string + example: config pagination: $ref: '../schemas/meet-pagination.yaml' @@ -24,9 +31,6 @@ content: autoDeletionPolicy: withMeeting: when_meeting_ends withRecordings: close - config: - _expandable: true - _href: '/api/v1/rooms/room-123?expand=config' roles: moderator: permissions: @@ -78,9 +82,6 @@ content: autoDeletionPolicy: withMeeting: when_meeting_ends withRecordings: close - config: - _expandable: true - _href: '/api/v1/rooms/room-456?expand=config' roles: moderator: permissions: @@ -124,6 +125,8 @@ content: accessUrl: 'http://localhost:6080/room/room-456' status: open meetingEndAction: none + _extraFields: + - config pagination: isTruncated: false maxItems: 10 @@ -133,10 +136,12 @@ content: rooms: - roomId: 'room-123' - roomId: 'room-456' + _extraFields: + - config pagination: isTruncated: false maxItems: 10 - expand=config: + extraFields=config: summary: Room details with expanded config value: rooms: @@ -222,9 +227,13 @@ content: height: 720 framerate: 60 codec: H264_HIGH + bitrate: 2500 + keyFrameInterval: 2 + depth: 2 audio: codec: AAC bitrate: 192 + frequency: 48000 chat: enabled: false virtualBackground: @@ -274,27 +283,43 @@ content: accessUrl: 'http://localhost:6080/room/room-456' status: open meetingEndAction: none + _extraFields: + - config pagination: isTruncated: false maxItems: 10 - fields=roomId,roomName,creationDate,autoDeletionDate,config: - summary: Room details including config but no URLs + fields=roomId;extraFields=config: + summary: Room details including config value: rooms: - roomId: 'room-123' - roomName: 'room' - creationDate: 1620000000000 - autoDeletionDate: 1900000000000 config: - _expandable: true - _href: '/api/v1/rooms/room-123?expand=config' + recording: + enabled: false + layout: grid + encoding: H264_720P_30 + chat: + enabled: true + virtualBackground: + enabled: true + e2ee: + enabled: false + captions: + enabled: true - roomId: 'room-456' - roomName: 'room' - creationDate: 1620001000000 - autoDeletionDate: 1900000000000 config: - _expandable: true - _href: '/api/v1/rooms/room-456?expand=config' + recording: + enabled: true + layout: grid + encoding: H264_720P_30 + chat: + enabled: false + virtualBackground: + enabled: false + e2ee: + enabled: false + _extraFields: + - config pagination: isTruncated: true nextPageToken: 'abc123' diff --git a/meet-ce/backend/openapi/components/responses/success-room-process-deletion.yaml b/meet-ce/backend/openapi/components/responses/success-room-process-deletion.yaml index 53e5ec2d..12d2f957 100644 --- a/meet-ce/backend/openapi/components/responses/success-room-process-deletion.yaml +++ b/meet-ce/backend/openapi/components/responses/success-room-process-deletion.yaml @@ -18,7 +18,18 @@ content: type: string description: A message providing additional context about the success room: - $ref: '../schemas/meet-room.yaml' + allOf: + - $ref: '../schemas/meet-room.yaml' + - type: object + properties: + _extraFields: + type: array + description: > + List of extra fields that can be included in the response based on the `X-ExtraFields` header or `extraFields` query parameter. + items: + type: string + example: config + examples: room_deleted: value: @@ -94,6 +105,8 @@ content: accessUrl: 'http://localhost:6080/room/room-123' status: closed meetingEndAction: none + _extraFields: + - config room_with_active_meeting_and_recordings_deleted: value: successCode: room_with_active_meeting_and_recordings_deleted @@ -160,3 +173,5 @@ content: accessUrl: 'http://localhost:6080/room/room-123' status: active_meeting meetingEndAction: close + _extraFields: + - config diff --git a/meet-ce/backend/openapi/components/responses/success-room-schedule-deletion.yaml b/meet-ce/backend/openapi/components/responses/success-room-schedule-deletion.yaml index 69fccbe2..f5a75943 100644 --- a/meet-ce/backend/openapi/components/responses/success-room-schedule-deletion.yaml +++ b/meet-ce/backend/openapi/components/responses/success-room-schedule-deletion.yaml @@ -15,7 +15,17 @@ content: type: string description: A message providing additional context about the success room: - $ref: '../schemas/meet-room.yaml' + allOf: + - $ref: '../schemas/meet-room.yaml' + - type: object + properties: + _extraFields: + type: array + description: > + List of extra fields that can be included in the response based on the `X-ExtraFields` header or `extraFields` query parameter. + items: + type: string + example: config examples: room_with_active_meeting_scheduled_to_be_deleted: value: @@ -79,6 +89,8 @@ content: accessUrl: 'http://localhost:6080/room/room-123' status: active_meeting meetingEndAction: delete + _extraFields: + - config room_with_active_meeting_and_recordings_scheduled_to_be_deleted: value: successCode: room_with_active_meeting_and_recordings_scheduled_to_be_deleted @@ -141,6 +153,8 @@ content: accessUrl: 'http://localhost:6080/room/room-123' status: active_meeting meetingEndAction: delete + _extraFields: + - config room_with_active_meeting_scheduled_to_be_closed: value: successCode: room_with_active_meeting_scheduled_to_be_closed @@ -203,3 +217,5 @@ content: accessUrl: 'http://localhost:6080/room/room-123' status: active_meeting meetingEndAction: close + _extraFields: + - config diff --git a/meet-ce/backend/openapi/components/schemas/expandable-stub.yaml b/meet-ce/backend/openapi/components/schemas/expandable-stub.yaml deleted file mode 100644 index b3f167d9..00000000 --- a/meet-ce/backend/openapi/components/schemas/expandable-stub.yaml +++ /dev/null @@ -1,24 +0,0 @@ -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-extra-field-metadata.yaml b/meet-ce/backend/openapi/components/schemas/meet-extra-field-metadata.yaml new file mode 100644 index 00000000..1906b855 --- /dev/null +++ b/meet-ce/backend/openapi/components/schemas/meet-extra-field-metadata.yaml @@ -0,0 +1,2 @@ +type: string +description: Extra field that can be included in the response if specified in the `X-ExtraFields` header or `extraFields` query parameter. diff --git a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml index 4f41b40f..212eef4c 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room-config.yaml @@ -175,7 +175,7 @@ MeetRecordingVideoEncodingOptions: example: 4500 description: | Video bitrate in kbps - keyframeInterval: + keyFrameInterval: type: number minimum: 0 example: 4 @@ -240,6 +240,7 @@ MeetRecordingEncodingOptions: codec: H264_MAIN bitrate: 3000 keyFrameInterval: 4 + depth: 24 audio: codec: OPUS bitrate: 128 diff --git a/meet-ce/backend/openapi/components/schemas/meet-room.yaml b/meet-ce/backend/openapi/components/schemas/meet-room.yaml index 9b3f4b16..a0d8d8ff 100644 --- a/meet-ce/backend/openapi/components/schemas/meet-room.yaml +++ b/meet-ce/backend/openapi/components/schemas/meet-room.yaml @@ -63,14 +63,15 @@ properties: - force: The room and its recordings will be deleted. - close: The room will be closed instead of deleted, maintaining its recordings. config: - description: > + 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 + By default, **this property is excluded from responses** to reduce payload size and optimize performance. + + To include this property in the response: + - For GET requests: use the `extraFields=config` query parameter + - For POST requests: use the `X-ExtraFields: config` header + $ref: meet-room-config.yaml#/MeetRoomConfig # maxParticipants: # type: integer # example: 10 diff --git a/meet-ce/backend/openapi/openvidu-meet-api.yaml b/meet-ce/backend/openapi/openvidu-meet-api.yaml index 97df5ca9..795406f4 100644 --- a/meet-ce/backend/openapi/openvidu-meet-api.yaml +++ b/meet-ce/backend/openapi/openvidu-meet-api.yaml @@ -2,7 +2,7 @@ openapi: 3.1.0 info: $ref: './info/info.yaml' servers: - - url: /api/v1 + - url: meet/api/v1 description: OpenVidu Meet API tags: $ref: './tags/tags.yaml' diff --git a/meet-ce/backend/openapi/paths/rooms.yaml b/meet-ce/backend/openapi/paths/rooms.yaml index 77c67ffc..9df191c4 100644 --- a/meet-ce/backend/openapi/paths/rooms.yaml +++ b/meet-ce/backend/openapi/paths/rooms.yaml @@ -2,15 +2,18 @@ post: operationId: createRoom summary: Create a room - description: > + description: | Creates a new OpenVidu Meet room. The room will be available for participants to join using the generated URLs. -
+ + **Response Customization:** + You can control the response format using custom headers: - - - `X-Expand`: Include expanded properties (e.g., `config`) instead of stubs - + - `X-ExtraFields`: Include extra fields (e.g., `config`) that are excluded by default - `X-Fields`: Filter which fields to include in the response for efficiency + + > **Note:** POST operations use headers instead of query parameters to avoid sensitive data appearing in URL logs. + > GET operations use query parameters for easier caching and bookmarking. tags: - OpenVidu Meet - Rooms security: @@ -18,7 +21,7 @@ - accessTokenHeader: [] parameters: - $ref: '../components/parameters/x-fields-header.yaml' - - $ref: '../components/parameters/x-expand-header.yaml' + - $ref: '../components/parameters/x-extrafields-header.yaml' requestBody: $ref: '../components/requestBodies/create-room-request.yaml' responses: @@ -41,6 +44,10 @@ By default, the rooms are sorted by creation date in descending order (newest first). + **Field Filtering:** + - Use `fields` to specify which base properties to include (whitelist approach) + - Use `extraFields` to include additional properties that are excluded by default (e.g., `config`) + > **Note:** If this endpoint is called using the `accessTokenHeader` authentication method, > only rooms the authenticated user has access to will be returned. tags: @@ -52,7 +59,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/extraFields.yaml' - $ref: '../components/parameters/max-items.yaml' - $ref: '../components/parameters/next-page-token.yaml' - $ref: '../components/parameters/sort-field.yaml' @@ -108,12 +115,16 @@ get: operationId: getRoom summary: Get a room - description: > + 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. + to reduce payload size and optimize performance. + Use the `extraFields` query parameter to include these properties when needed. + + **Field Filtering:** + - Use `fields` to specify which base properties to include (whitelist approach) + - Use `extraFields` to include additional properties that are excluded by default tags: - OpenVidu Meet - Rooms security: @@ -123,7 +134,7 @@ parameters: - $ref: '../components/parameters/room-id-path.yaml' - $ref: '../components/parameters/room-fields.yaml' - - $ref: '../components/parameters/expand.yaml' + - $ref: '../components/parameters/extraFields.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 3001ecfb..724f33d2 100644 --- a/meet-ce/backend/src/controllers/room.controller.ts +++ b/meet-ce/backend/src/controllers/room.controller.ts @@ -2,7 +2,7 @@ import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, - MeetRoomExpandableProperties, + MeetRoomExtraField, MeetRoomField, MeetRoomFilters, MeetRoomOptions @@ -19,7 +19,7 @@ import { getBaseUrl } from '../utils/url.utils.js'; interface RequestWithValidatedHeaders extends Request { validatedHeaders?: { 'x-fields'?: MeetRoomField[]; - 'x-expand'?: MeetRoomExpandableProperties[]; + 'x-extrafields'?: MeetRoomExtraField[]; }; } @@ -29,16 +29,16 @@ export const createRoom = async (req: Request, res: Response) => { const options: MeetRoomOptions = req.body; const { validatedHeaders } = req as RequestWithValidatedHeaders; const fields = validatedHeaders?.['x-fields']; - const expand = validatedHeaders?.['x-expand']; + const extraFields = validatedHeaders?.['x-extrafields']; try { logger.verbose(`Creating room with options '${JSON.stringify(options)}'`); // Pass response options to service for consistent handling - const room = await roomService.createMeetRoom(options, { - fields, - collapse: MeetRoomHelper.toCollapseProperties(expand) - }); + let room = await roomService.createMeetRoom(options); + + room = MeetRoomHelper.applyFieldFilters(room, fields, extraFields); + room = MeetRoomHelper.addResponseMetadata(room); res.set('Location', `${getBaseUrl()}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}`); return res.status(201).json(room); @@ -52,12 +52,19 @@ export const getRooms = async (req: Request, res: Response) => { const roomService = container.get(RoomService); const queryParams = req.query as MeetRoomFilters; - logger.verbose(`Getting all rooms with expand: ${queryParams.expand || 'none'}`); + logger.verbose(`Getting all rooms with filters: ${JSON.stringify(queryParams)}`); try { - const { rooms, isTruncated, nextPageToken } = await roomService.getAllMeetRooms(queryParams); + const fieldsForQuery = MeetRoomHelper.computeFieldsForRoomQuery(queryParams.fields, queryParams.extraFields); + const optimizedQueryParams = { ...queryParams, fields: fieldsForQuery }; + + const { rooms, isTruncated, nextPageToken } = await roomService.getAllMeetRooms(optimizedQueryParams); const maxItems = Number(queryParams.maxItems); - return res.status(200).json({ rooms, pagination: { isTruncated, nextPageToken, maxItems } }); + + // Add metadata at response root level (multiple rooms strategy) + let response = { rooms, pagination: { isTruncated, nextPageToken, maxItems } }; + response = MeetRoomHelper.addResponseMetadata(response); + return res.status(200).json(response); } catch (error) { handleError(res, error, 'getting rooms'); } @@ -68,21 +75,24 @@ export const getRoom = async (req: Request, res: Response) => { const { roomId } = req.params; // Zod already validated and transformed to typed arrays - const { fields, expand } = req.query as { + const { fields, extraFields } = req.query as { fields?: MeetRoomField[]; - expand?: MeetRoomExpandableProperties[]; + extraFields?: MeetRoomExtraField[]; }; try { - logger.verbose(`Getting room '${roomId}' with expand: ${expand?.join(',') || 'none'}`); + logger.verbose(`Getting room '${roomId}' with filters: ${JSON.stringify({ fields, extraFields })}`); const roomService = container.get(RoomService); - const collapse = MeetRoomHelper.toCollapseProperties(expand); - const room = await roomService.getMeetRoom(roomId, { - fields, - collapse, - applyPermissionFiltering: true - }); + const fieldsForQuery = MeetRoomHelper.computeFieldsForRoomQuery(fields, extraFields); + + let room = await roomService.getMeetRoom(roomId, fieldsForQuery); + + // Apply permission filtering to the room based on the authenticated user's permissions + const permissions = await roomService.getAuthenticatedRoomMemberPermissions(roomId); + room = MeetRoomHelper.applyPermissionFiltering(room, permissions); + + room = MeetRoomHelper.addResponseMetadata(room); return res.status(200).json(room); } catch (error) { @@ -104,6 +114,11 @@ export const deleteRoom = async (req: Request, res: Response) => { logger.verbose(`Deleting room '${roomId}'`); const response = await roomService.deleteMeetRoom(roomId, withMeeting, withRecordings); + // Add metadata to room if present in response + if (response.room) { + response.room = MeetRoomHelper.addResponseMetadata(response.room); + } + // Determine the status code based on the success code // If the room action is scheduled, return 202. Otherwise, return 200. const scheduledSuccessCodes = [ @@ -134,6 +149,13 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => { logger.verbose(`Deleting rooms: ${roomIds}`); const { successful, failed } = await roomService.bulkDeleteMeetRooms(roomIds, withMeeting, withRecordings); + // Add metadata to each room object in successful/failed arrays + successful.forEach((item) => { + if (item.room) { + item.room = MeetRoomHelper.addResponseMetadata(item.room); + } + }); + logger.info( `Bulk delete operation - Successfully processed rooms: ${successful.length}, failed to process: ${failed.length}` ); @@ -143,9 +165,12 @@ export const bulkDeleteRooms = async (req: Request, res: Response) => { return res.status(200).json({ message: 'All rooms successfully processed for deletion', successful }); } else { // Some rooms failed to process - return res - .status(400) - .json({ message: `${failed.length} room(s) failed to process while deleting`, successful, failed }); + const response = { + message: `${failed.length} room(s) failed to process while deleting`, + successful, + failed + }; + return res.status(400).json(response); } } catch (error) { handleError(res, error, `deleting rooms`); @@ -160,7 +185,7 @@ export const getRoomConfig = async (req: Request, res: Response) => { logger.verbose(`Getting room config for room '${roomId}'`); try { - const { config } = await roomService.getMeetRoom(roomId, { fields: ['config'] }); + const { config } = await roomService.getMeetRoom(roomId, ['config']); return res.status(200).json(config); } catch (error) { handleError(res, error, `getting room config for room '${roomId}'`); diff --git a/meet-ce/backend/src/helpers/field-filter.helper.ts b/meet-ce/backend/src/helpers/field-filter.helper.ts new file mode 100644 index 00000000..dc702abd --- /dev/null +++ b/meet-ce/backend/src/helpers/field-filter.helper.ts @@ -0,0 +1,185 @@ +/** + * Generic helper for managing field filtering in a two-layer approach: + * 1. Database query optimization (what fields to retrieve from DB) + * 2. HTTP response filtering (what fields to include in the API response) + * + * This helper is designed to be reusable across different entities (Room, Recording, User, etc.) + * + * Key concepts: + * - Base fields: Standard fields included by default + * - Extra fields: Fields excluded by default, must be explicitly requested via extraFields parameter + * - Union logic: Final fields = fields ∪ extraFields + */ + +/** + * Calculates the optimal set of fields to request from the database. + * This minimizes data transfer and processing by excluding unnecessary extra fields. + * + * Logic: + * - If `fields` is specified: return fields ∪ extraFields (explicit selection) + * - If only `extraFields` is specified: return all base fields + requested extra fields + * - If neither is specified: return all base fields (exclude all extra fields from DB query) + * + * @param fields - Explicitly requested fields (e.g., ['roomId', 'roomName']) + * @param extraFields - Extra fields to include (e.g., ['config']) + * @param allFields - Complete list of all possible fields for this entity + * @param extraFieldsList - List of fields that are considered "extra" (excluded by default) + * @returns Array of fields to request from database, or undefined if all fields should be retrieved + * + * @example + * ```typescript + * // No filters → retrieve all base fields only (efficient!) + * buildFieldsForDbQuery(undefined, undefined, MEET_ROOM_FIELDS, MEET_ROOM_EXTRA_FIELDS) + * // Returns: ['roomId', 'roomName', 'owner', ...] (without 'config') + * + * // Only extraFields → retrieve base fields + requested extras + * buildFieldsForDbQuery(undefined, ['config'], MEET_ROOM_FIELDS, MEET_ROOM_EXTRA_FIELDS) + * // Returns: ['roomId', 'roomName', 'owner', ..., 'config'] + * + * // Both fields and extraFields → retrieve union + * buildFieldsForDbQuery(['roomId'], ['config'], MEET_ROOM_FIELDS, MEET_ROOM_EXTRA_FIELDS) + * // Returns: ['roomId', 'config'] + * ``` + */ +export function buildFieldsForDbQuery( + fields: readonly TField[] | undefined, + extraFields: readonly TExtraField[] | undefined, + allFields: readonly TField[], + extraFieldsList: readonly TExtraField[] +): TField[] | undefined { + // Case 1: fields is explicitly specified + // Return the union of fields and extraFields for precise DB query + if (fields && fields.length > 0) { + const union = new Set([...fields, ...(extraFields || [])]); + return Array.from(union); + } + + // Case 2: Only extraFields specified (no fields) + // Include all base fields + requested extra fields + if (extraFields && extraFields.length > 0) { + // All fields except extra fields that are NOT requested + const baseFields = allFields.filter((field) => !extraFieldsList.includes(field as TExtraField)); + const union = new Set([...baseFields, ...extraFields]); + return Array.from(union); + } + + // Case 3: Neither fields nor extraFields specified + // Return only base fields (exclude all extra fields) + const baseFields = allFields.filter((field) => !extraFieldsList.includes(field as TExtraField)); + return baseFields as TField[]; +} + +/** + * Applies HTTP-level field filtering to an entity object. + * This is the final transformation before sending the response to the client. + * + * The logic follows the union principle: final allowed fields = fields ∪ extraFields + * + * Behavior: + * - If neither fields nor extraFields are specified: removes all extra fields from the response + * - If only fields is specified: includes only those fields (removing extra fields unless in the list) + * - If only extraFields is specified: includes all base fields + specified extra fields + * - If both are specified: includes the union of both sets (fields ∪ extraFields) + * + * This unified approach prevents bugs from chaining destructive filters on the same object. + * + * @param entity - The entity object to filter + * @param fields - Optional array of field names to include + * @param extraFields - Optional array of extra field names to include + * @param extraFieldsList - List of fields that are considered "extra" (excluded by default) + * @returns The filtered entity object + * + * @example + * ```typescript + * // No filters - removes extra fields only: + * applyHttpFieldFiltering(room, undefined, undefined, MEET_ROOM_EXTRA_FIELDS) + * // Result: room without 'config' property + * + * // Only fields specified - includes only those fields: + * applyHttpFieldFiltering(room, ['roomId', 'roomName'], undefined, MEET_ROOM_EXTRA_FIELDS) + * // Result: { roomId: '123', roomName: 'My Room' } + * + * // Only extraFields specified - includes base fields + extra fields: + * applyHttpFieldFiltering(room, undefined, ['config'], MEET_ROOM_EXTRA_FIELDS) + * // Result: room with all base fields and 'config' property + * + * // Both specified - includes union of both: + * applyHttpFieldFiltering(room, ['roomId'], ['config'], MEET_ROOM_EXTRA_FIELDS) + * // Result: { roomId: '123', config: {...} } + * ``` + */ +export function applyHttpFieldFiltering( + entity: TEntity, + fields: readonly string[] | undefined, + extraFields: readonly TExtraField[] | undefined, + extraFieldsList: readonly TExtraField[] +): TEntity { + if (!entity) { + return entity; + } + + // Case 1: No filters specified - remove extra fields only + if ((!fields || fields.length === 0) && (!extraFields || extraFields.length === 0)) { + const processedEntity = { ...entity } as Record; + extraFieldsList.forEach((field) => { + delete processedEntity[field]; + }); + return processedEntity as TEntity; + } + + // Case 2: Only extraFields specified - include all base fields + specified extra fields + if (!fields || fields.length === 0) { + const processedEntity = { ...entity } as Record; + // Remove extra fields that are NOT in the extraFields list + extraFieldsList.forEach((field) => { + if (!extraFields!.includes(field)) { + delete processedEntity[field]; + } + }); + return processedEntity as TEntity; + } + + // Case 3: fields is specified (with or without extraFields) + // Create the union: fields ∪ extraFields + const allowedFields = new Set([...fields, ...(extraFields || [])]); + + const filteredEntity = {} as Record; + const entityAsRecord = entity as Record; + + for (const key of Object.keys(entityAsRecord)) { + if (allowedFields.has(key)) { + filteredEntity[key] = entityAsRecord[key]; + } + } + + return filteredEntity as TEntity; +} + +/** + * Adds metadata to the response indicating which extra fields are available. + * This allows API consumers to discover available extra fields without consulting documentation. + * + * @param obj - The object to enhance with metadata (can be a single entity or a response object) + * @param extraFieldsList - List of available extra fields + * @returns The object with _extraFields metadata added + * + * @example + * ```typescript + * // Single entity + * addResponseMetadata(room, MEET_ROOM_EXTRA_FIELDS) + * // Result: { ...room, _extraFields: ['config'] } + * + * // Response object + * addResponseMetadata({ rooms: [...] }, MEET_ROOM_EXTRA_FIELDS) + * // Result: { rooms: [...], _extraFields: ['config'] } + * ``` + */ +export function addHttpResponseMetadata( + obj: T, + extraFieldsList: readonly TExtraField[] +): T & { _extraFields: TExtraField[] } { + return { + ...obj, + _extraFields: [...extraFieldsList] + }; +} diff --git a/meet-ce/backend/src/helpers/room.helper.ts b/meet-ce/backend/src/helpers/room.helper.ts index ad957107..c83c2ea9 100644 --- a/meet-ce/backend/src/helpers/room.helper.ts +++ b/meet-ce/backend/src/helpers/room.helper.ts @@ -1,16 +1,16 @@ import { - MEET_ROOM_EXPANDABLE_FIELDS, + MEET_ROOM_EXTRA_FIELDS, + MEET_ROOM_FIELDS, MeetRoom, - MeetRoomCollapsibleProperties, - MeetRoomExpandableProperties, + MeetRoomExtraField, MeetRoomField, MeetRoomMemberPermissions, MeetRoomOptions, SENSITIVE_ROOM_FIELDS_ENTRIES } from '@openvidu-meet/typings'; -import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_ENV } from '../environment.js'; import { getBasePath } from '../utils/html-dynamic-base-path.utils.js'; +import { addHttpResponseMetadata, applyHttpFieldFiltering, buildFieldsForDbQuery } from './field-filter.helper.js'; export class MeetRoomHelper { private constructor() { @@ -128,103 +128,51 @@ export class MeetRoomHelper { } /** - * Determines which properties of a MeetRoom should be collapsed into stubs based on the provided expandable properties. - * By default, if no expandable properties are specified, the 'config' property will be collapsed. - * @param expandableProps - * @returns An array of MeetRoomCollapsibleProperties that should be collapsed into stubs when returning a MeetRoom object. + * Calculates optimal fields to request from database for Room queries. + * Minimizes data transfer by excluding unnecessary extra fields. + * + * @param fields - Explicitly requested fields + * @param extraFields - Extra fields to include + * @returns Array of fields to request from database */ - static toCollapseProperties(expand?: MeetRoomExpandableProperties[]): MeetRoomCollapsibleProperties[] { - // If not expand provided, collapse all collapsible properties by default - if (!expand || expand.length === 0) { - return [...MEET_ROOM_EXPANDABLE_FIELDS]; - } - - // Return the properties that are not included in the expand array, but only those that are actually expandable - return MEET_ROOM_EXPANDABLE_FIELDS.filter((prop) => !expand.includes(prop)); + static computeFieldsForRoomQuery( + fields?: MeetRoomField[], + extraFields?: MeetRoomExtraField[] + ): MeetRoomField[] | undefined { + return buildFieldsForDbQuery(fields, extraFields, MEET_ROOM_FIELDS, MEET_ROOM_EXTRA_FIELDS); } /** - * Processes a room to collapse specified properties into stubs. - * By default, returns the full room object. - * Only collapses properties when explicitly specified in the collapse parameter. + * Applies HTTP-level field filtering to a MeetRoom object. + * This is the final transformation before sending the response to the client. + * + * The logic follows the union principle: final allowed fields = fields ∪ extraFields * * @param room - The room object to process - * @param props - Optional list of properties to collapse (e.g., ['config']) + * @param fields - Optional array of field names to include (e.g., ['roomId', 'roomName']) + * @param extraFields - Optional array of extra field names to include (e.g., ['config']) + * @returns A MeetRoom object with fields filtered according to the union of both parameters * @example * ``` - * // Collapse config: - * { - * config: { - * _expandable: true, - * _href: '/api/rooms/123?expand=config' - * } - * } - * ``` - */ - static applyCollapseProperties(room: MeetRoom, props?: MeetRoomCollapsibleProperties[]): MeetRoom { - // If no collapse specified, return the full room - if (!room || !props || props.length === 0) { - return room; - } - - // Filter the props to only those that exist in the room object and are not undefined - const existingProps = props.filter( - (prop) => Object.prototype.hasOwnProperty.call(room, prop) && room[prop] !== undefined - ); - - // If none of the specified props exist in the room, return the full room without modification - if (existingProps.length === 0) { - return room; - } - - const collapsedRoom = { ...room }; - const { roomId } = room; - - // Append the base path (without trailing slash) - const basePath = getBasePath().slice(0, -1); - const baseUrl = `${basePath}${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`; - - existingProps.forEach((prop) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (collapsedRoom as any)[prop] = { - _expandable: true, - _href: `${baseUrl}?expand=${prop}` - }; - }); - - return collapsedRoom; - } - - /** - * Filters a MeetRoom object to include only the specified fields. - * By default, returns the full room object. - * Only filters when fields are explicitly specified. + * // No filters - removes extra fields only: + * const room = applyFieldFilters(fullRoom); + * // Result: room without 'config' property * - * @param room - The room object to filter - * @param fields - Optional list of fields to include (e.g., ['roomId', 'roomName']) - * @returns A MeetRoom object containing only the specified fields, or the full room if no fields are specified - * @example - * ``` - * // Filter to only roomId and roomName: - * const filtered = MeetRoomHelper.applyFieldsFilter(room, ['roomId', 'roomName']); + * // Only fields specified - includes only those fields: + * const room = applyFieldFilters(fullRoom, ['roomId', 'roomName']); * // Result: { roomId: '123', roomName: 'My Room' } + * + * // Only extraFields specified - includes base fields + extra fields: + * const room = applyFieldFilters(fullRoom, undefined, ['config']); + * // Result: room with all base fields and 'config' property + * + * // Both specified - includes union of both: + * const room = applyFieldFilters(fullRoom, ['roomId'], ['config']); + * // Result: { roomId: '123', config: {...} } * ``` */ - static applyFieldsFilter(room: MeetRoom, fields?: MeetRoomField[]): MeetRoom { - // If no fields specified, return the full room - if (!room || !fields || fields.length === 0) { - return room; - } - - const filteredRoom = {} as Record; - - for (const key of Object.keys(room)) { - if (fields.includes(key as MeetRoomField)) { - filteredRoom[key] = room[key as keyof MeetRoom]; - } - } - - return filteredRoom as unknown as MeetRoom; + static applyFieldFilters(room: MeetRoom, fields?: MeetRoomField[], extraFields?: MeetRoomExtraField[]): MeetRoom { + return applyHttpFieldFiltering(room, fields, extraFields, MEET_ROOM_EXTRA_FIELDS); } /** @@ -260,4 +208,15 @@ export class MeetRoomHelper { return filteredRoom ?? room; } + + /** + * Adds metadata to the room response indicating which extra fields are available. + * This allows API consumers to discover available extra fields without consulting documentation. + * + * @param obj - The object to enhance with metadata + * @returns The object with _extraFields metadata added + */ + static addResponseMetadata(obj: T): T & { _extraFields: MeetRoomExtraField[] } { + return addHttpResponseMetadata(obj, MEET_ROOM_EXTRA_FIELDS); + } } diff --git a/meet-ce/backend/src/models/room-response.ts b/meet-ce/backend/src/models/room-response.ts deleted file mode 100644 index 8ef25d15..00000000 --- a/meet-ce/backend/src/models/room-response.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { MeetRoomCollapsibleProperties, MeetRoomField } from '@openvidu-meet/typings'; - -/** - * Options for configuring the response MeetRoom REST API object - */ -export interface MeetRoomServerResponseOptions { - /** - * Array of fields to include in the response. - * If not specified, all fields are included. - */ - fields?: MeetRoomField[]; - /** - * Array of collapsed properties to expand in the response. - * If not specified, no collapsed properties are expanded. - * - */ - collapse?: MeetRoomCollapsibleProperties[]; - /** - * Whether to check permissions for the room. - * If true, sensitive properties will be removed from the response if the requester does not have permission to view them. - */ - applyPermissionFiltering?: boolean; -} 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 788c08de..e433f368 100644 --- a/meet-ce/backend/src/models/zod-schemas/room.schema.ts +++ b/meet-ce/backend/src/models/zod-schemas/room.schema.ts @@ -1,5 +1,5 @@ import { - MEET_ROOM_EXPANDABLE_FIELDS, + MEET_ROOM_EXTRA_FIELDS, MEET_ROOM_FIELDS, MeetAppearanceConfig, MeetChatConfig, @@ -16,7 +16,7 @@ import { MeetRoomConfig, MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, - MeetRoomExpandableProperties, + MeetRoomExtraField, MeetRoomField, MeetRoomOptions, MeetRoomRolesConfig, @@ -369,38 +369,40 @@ export const RoomOptionsSchema: z.ZodType = z.object({ // .default(null) }); -// Shared expand validation schema for Room entity +// Shared extraFields validation schema for Room entity // Validates and transforms comma-separated string to typed array -const expandSchema = z +const extraFieldsSchema = z .string() .optional() .refine( (value) => { if (!value) return true; - const allowed = MEET_ROOM_EXPANDABLE_FIELDS; + const allowed = MEET_ROOM_EXTRA_FIELDS; const requested = value.split(',').map((p) => p.trim()); - return requested.every((p) => allowed.includes(p as MeetRoomExpandableProperties)); + return requested.every((p) => allowed.includes(p as MeetRoomExtraField)); }, { - message: `Invalid expand properties. Valid options: ${MEET_ROOM_EXPANDABLE_FIELDS.join(', ')}` + message: `Invalid extraFields. Valid options: ${MEET_ROOM_EXTRA_FIELDS.join(', ')}` } ) .transform((value) => { - // Transform to typed array of MeetRoomExpandableProperties + // Transform to typed array of MeetRoomExtraField if (!value) return undefined; - const allowed = MEET_ROOM_EXPANDABLE_FIELDS; + const allowed = MEET_ROOM_EXTRA_FIELDS; const requested = value.split(',').map((p) => p.trim()); - const valid = requested.filter((p) => allowed.includes(p as MeetRoomExpandableProperties)); + const valid = requested.filter((p) => allowed.includes(p as MeetRoomExtraField)); - return valid.length > 0 ? (valid as MeetRoomExpandableProperties[]) : undefined; + return valid.length > 0 ? (valid as MeetRoomExtraField[]) : undefined; }); // Shared fields validation schema for Room entity // Validates and transforms comma-separated string to typed array -// Only allows fields that exist in MEET_ROOM_FIELDS +// IMPORTANT: Only allows BASE fields (non-extra fields) in the 'fields' parameter. +// Any extra fields included in 'fields' will be automatically filtered out. +// Extra fields MUST be requested via the 'extraFields' parameter. const fieldsSchema = z .string() .optional() @@ -412,22 +414,27 @@ const fieldsSchema = z .map((field) => field.trim()) .filter((field) => field !== ''); - // Filter: only keep valid fields that exist in MeetRoom - const validFields = requested.filter((field) => - MEET_ROOM_FIELDS.includes(field as MeetRoomField) - ) as MeetRoomField[]; + // Filter: only keep valid BASE fields (exclude extra fields) + // This ensures 'fields' parameter can ONLY contain base fields + const validBaseFields = requested.filter((field) => { + // Must be a valid field AND NOT an extra field + return ( + MEET_ROOM_FIELDS.includes(field as MeetRoomField) && + !MEET_ROOM_EXTRA_FIELDS.includes(field as MeetRoomExtraField) + ); + }) as MeetRoomField[]; // Deduplicate - const unique = Array.from(new Set(validFields)); + const unique = Array.from(new Set(validBaseFields)); - return unique.length > 0 ? unique : []; + return unique.length > 0 ? unique : undefined; }); export const RoomFiltersSchema = z.object({ roomName: z.string().optional(), status: z.nativeEnum(MeetRoomStatus).optional(), fields: fieldsSchema, - expand: expandSchema, + extraFields: extraFieldsSchema, maxItems: z.coerce .number() .positive('maxItems must be a positive number') @@ -445,12 +452,12 @@ export const RoomFiltersSchema = z.object({ export const GetRoomQuerySchema = z.object({ fields: fieldsSchema, - expand: expandSchema + extraFields: extraFieldsSchema }); export const CreateRoomHeadersSchema = z.object({ 'x-fields': fieldsSchema, - 'x-expand': expandSchema + 'x-extrafields': extraFieldsSchema }); export const DeleteRoomReqSchema = z.object({ diff --git a/meet-ce/backend/src/services/room-member.service.ts b/meet-ce/backend/src/services/room-member.service.ts index a816f891..784ed649 100644 --- a/meet-ce/backend/src/services/room-member.service.ts +++ b/meet-ce/backend/src/services/room-member.service.ts @@ -109,7 +109,7 @@ export class RoomMemberService { } // Compute effective permissions - const room = await this.roomService.getMeetRoom(roomId, { fields: ['roles'] }); + const room = await this.roomService.getMeetRoom(roomId, ['roles']); const effectivePermissions = this.computeEffectivePermissions(room.roles, baseRole, customPermissions); const now = Date.now(); @@ -154,7 +154,7 @@ export class RoomMemberService { */ async isRoomMember(roomId: string, memberId: string): Promise { // Verify room exists first - await this.roomService.getMeetRoom(roomId, { fields: ['roomId'] }); + await this.roomService.getMeetRoom(roomId, ['roomId']); const member = await this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId); return !!member; } @@ -219,7 +219,7 @@ export class RoomMemberService { } // Recompute effective permissions - const room = await this.roomService.getMeetRoom(roomId, { fields: ['roles'] }); + const room = await this.roomService.getMeetRoom(roomId, ['roles']); member.effectivePermissions = this.computeEffectivePermissions( room.roles, member.baseRole, @@ -420,7 +420,7 @@ export class RoomMemberService { } else { // If secret matches anonymous access URL secret, assign role and permissions based on it baseRole = await this.getRoomMemberRoleBySecret(roomId, secret); - const room = await this.roomService.getMeetRoom(roomId, { fields: ['roles', 'anonymous'] }); + const room = await this.roomService.getMeetRoom(roomId, ['roles', 'anonymous']); // Check that anonymous access is enabled for the role if (!room.anonymous[baseRole].enabled) { @@ -492,7 +492,7 @@ export class RoomMemberService { userId?: string ): Promise { // Check that room is open - const room = await this.roomService.getMeetRoom(roomId, { fields: ['status', 'config'] }); + const room = await this.roomService.getMeetRoom(roomId, ['status', 'config']); if (room.status === MeetRoomStatus.CLOSED) { throw errorRoomClosed(roomId); @@ -611,7 +611,7 @@ export class RoomMemberService { * @throws Error if the provided secret doesn't match any of the room's secrets (unauthorized) */ protected async getRoomMemberRoleBySecret(roomId: string, secret: string): Promise { - const room = await this.roomService.getMeetRoom(roomId, { fields: ['roomId', 'anonymous'] }); + const room = await this.roomService.getMeetRoom(roomId, ['roomId', 'anonymous']); const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); switch (secret) { @@ -780,7 +780,7 @@ export class RoomMemberService { newRole: MeetRoomMemberRole ): Promise { try { - const meetRoom = await this.roomService.getMeetRoom(roomId, { fields: ['roles', 'anonymous'] }); + const meetRoom = await this.roomService.getMeetRoom(roomId, ['roles', 'anonymous']); const participant = await this.getParticipantFromMeeting(roomId, participantIdentity); const metadata: MeetRoomMemberTokenMetadata = this.tokenService.parseRoomMemberTokenMetadata( participant.metadata diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 0e11130e..cb3da895 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -8,6 +8,7 @@ import { MeetRoomDeletionPolicyWithMeeting, MeetRoomDeletionPolicyWithRecordings, MeetRoomDeletionSuccessCode, + MeetRoomField, MeetRoomFilters, MeetRoomMemberPermissions, MeetRoomOptions, @@ -36,7 +37,6 @@ import { OpenViduMeetError } from '../models/error.model.js'; -import { MeetRoomServerResponseOptions } from '../models/room-response.js'; import { RoomMemberRepository } from '../repositories/room-member.repository.js'; import { RoomRepository } from '../repositories/room.repository.js'; import { FrontendEventService } from './frontend-event.service.js'; @@ -73,15 +73,14 @@ export class RoomService { * Creates an OpenVidu Meet room with the specified options. * * @param {MeetRoomOptions} roomOptions - The options for creating the OpenVidu room. - * @param {MeetRoomServerResponseOptions} responseOpts - Options for controlling the response format (fields, collapse) + * @param {MeetRoomServerResponseOptions} responseOpts - Options for controlling the response format (fields, extraFields) * @returns {Promise} A promise that resolves to the created OpenVidu room. * * @throws {Error} If the room creation fails. * */ - async createMeetRoom(roomOptions: MeetRoomOptions, responseOpts?: MeetRoomServerResponseOptions): Promise { + async createMeetRoom(roomOptions: MeetRoomOptions): Promise { const { roomName, autoDeletionDate, autoDeletionPolicy, config, roles, anonymous } = roomOptions; - const { collapse, fields } = responseOpts || {}; // Generate a unique room ID based on the room name const roomIdPrefix = MeetRoomHelper.createRoomIdPrefixFromRoomName(roomName!) || 'room'; @@ -163,12 +162,7 @@ export class RoomService { rolesUpdatedAt: now, meetingEndAction: MeetingEndAction.NONE }; - let room = await this.roomRepository.create(meetRoom); - - room = MeetRoomHelper.applyCollapseProperties(room, collapse); - room = MeetRoomHelper.applyFieldsFilter(room, fields); - - return room; + return this.roomRepository.create(meetRoom); } /** @@ -327,8 +321,8 @@ 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 (including expand) - * @returns A Promise that resolves to paginated room list + * @param filters - Filtering, pagination and sorting options (fields used for DB query optimization) + * @returns A Promise that resolves to paginated room list (with DB-optimized fields, but no HTTP filtering) * @throws If there was an error retrieving the rooms */ async getAllMeetRooms(filters: MeetRoomFilters): Promise<{ @@ -339,6 +333,8 @@ export class RoomService { const queryOptions: MeetRoomFilters & { roomIds?: string[]; owner?: string } = { ...filters }; const user = this.requestSessionService.getAuthenticatedUser(); + // TODO: This logic may move to a controller because it is related to access control for HTTP requests, + // TODO: while this service is also used in non-HTTP contexts (e.g., scheduler for auto-deletion, background jobs for recording management, etc). // Admin can see all rooms - no additional filters needed if (user && user.role !== MeetUserRole.ADMIN) { // For USER and ROOM_MEMBER roles, get the list of room IDs they are members of @@ -351,12 +347,7 @@ export class RoomService { } } - const response = await this.roomRepository.find(queryOptions); - - const collapse = MeetRoomHelper.toCollapseProperties(filters.expand); - response.rooms = response.rooms.map((room) => MeetRoomHelper.applyCollapseProperties(room, collapse)); - - return response; + return this.roomRepository.find(queryOptions); } /** @@ -416,27 +407,18 @@ export class RoomService { * Retrieves a specific meeting room by its unique identifier. * * @param roomId - The name of the room to retrieve. - * @param responseOpts - Optional parameters for retrieving the room: - * - fields: Array of fields to retrieve from the room - * - collapse: {@link MeetRoomCollapsible} list of properties to collapse into {@link ExpandableStub} - * - applyPermissionFiltering: Whether to check permissions for the room and remove sensitive properties if the requester doesn't have access - * @returns A promise that resolves to an {@link MeetRoom} object + * @param fields - Array of fields to retrieve from database (for query optimization) + * @returns A promise that resolves to an {@link MeetRoom} object if found, or rejects with an error if not found. */ - async getMeetRoom(roomId: string, responseOpts?: MeetRoomServerResponseOptions): Promise { - const { collapse, applyPermissionFiltering, fields } = responseOpts || {}; - let room = await this.roomRepository.findByRoomId(roomId, fields); + async getMeetRoom(roomId: string, fields?: MeetRoomField[]): Promise { + const room = await this.roomRepository.findByRoomId(roomId, fields); if (!room) { this.logger.error(`Meet room with ID ${roomId} not found.`); throw errorRoomNotFound(roomId); } - if (applyPermissionFiltering) { - const permissions = await this.getAuthenticatedRoomMemberPermissions(roomId); - room = MeetRoomHelper.applyPermissionFiltering(room, permissions); - } - - return MeetRoomHelper.applyCollapseProperties(room, collapse); + return room; } /** @@ -463,7 +445,7 @@ export class RoomService { ); // Check if there's an active meeting in the room and/or if it has recordings associated - const room = await this.getMeetRoom(roomId, { fields: ['status'] }); + const room = await this.getMeetRoom(roomId, ['status']); const hasActiveMeeting = room.status === MeetRoomStatus.ACTIVE_MEETING; const hasRecordings = await this.recordingService.hasRoomRecordings(roomId); @@ -484,7 +466,7 @@ export class RoomService { hasRecordings, withMeeting, withRecordings, - MeetRoomHelper.applyCollapseProperties(updatedRoom!, ['config']) + MeetRoomHelper.applyFieldFilters(updatedRoom!, undefined, []) ); } catch (error) { this.logger.error(`Error deleting room '${roomId}': ${error}`); @@ -856,7 +838,7 @@ export class RoomService { * @throws Error if room not found */ async isRoomOwner(roomId: string, userId: string): Promise { - const room = await this.getMeetRoom(roomId, { fields: ['owner'] }); + const room = await this.getMeetRoom(roomId, ['owner']); return room.owner === userId; } @@ -869,7 +851,7 @@ export class RoomService { * @throws Error if room not found */ async isValidRoomSecret(roomId: string, secret: string): Promise { - const room = await this.getMeetRoom(roomId, { fields: ['anonymous'] }); + const room = await this.getMeetRoom(roomId, ['anonymous']); const { moderatorSecret, speakerSecret } = MeetRoomHelper.extractSecretsFromRoom(room); return secret === moderatorSecret || secret === speakerSecret; } @@ -933,7 +915,7 @@ export class RoomService { */ async canUserAccessRoom(roomId: string, user: MeetUser): Promise { // Verify room exists first (throws 404 if not found) - const room = await this.getMeetRoom(roomId, { fields: ['owner'] }); + const room = await this.getMeetRoom(roomId, ['owner']); if (user.role === MeetUserRole.ADMIN) { // Admins can access all rooms diff --git a/meet-ce/backend/tests/helpers/assertion-helpers.ts b/meet-ce/backend/tests/helpers/assertion-helpers.ts index 68cdb97d..8af7da8e 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 | 'expandable' + config?: MeetRoomConfig ) => { expect(response.status).toBe(200); expectValidRoom(response.body, roomName, roomIdPrefix, config, autoDeletionDate); @@ -113,31 +113,16 @@ 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 expectExtraFieldsInResponse = (room: MeetRoom) => { + expect((room as any)._extraFields).toBeDefined(); + expect((room as any)._extraFields).toContain('config'); }; export const expectValidRoom = ( room: MeetRoom, name: string, roomIdPrefix?: string, - config?: MeetRoomConfig | 'expandable', + config?: MeetRoomConfig, autoDeletionDate?: number, autoDeletionPolicy?: MeetRoomAutoDeletionPolicy, status?: MeetRoomStatus, @@ -169,15 +154,15 @@ export const expectValidRoom = ( expect(room.autoDeletionPolicy).toEqual(autoDeletionPolicy); } - expect(room.config).toBeDefined(); - - // Check if config should be an expandable stub - if (config === 'expandable' || config === undefined) { - expectExpandableStub(room.config, room.roomId, 'config'); + // Validate config based on parameter: + // - If config is provided: verify it exists and matches the expected value + // - If config is undefined: verify the property does not exist + if (config === undefined) { + expect(room.config).toBeUndefined(); } else { - // Validate it's NOT an expandable stub (it's expanded) - expectExpandedProperty(room.config); + expect(room.config).toBeDefined(); // Use toMatchObject to allow encoding defaults to be added without breaking tests + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect(room.config).toMatchObject(config as any); } diff --git a/meet-ce/backend/tests/helpers/request-helpers.ts b/meet-ce/backend/tests/helpers/request-helpers.ts index 12f14ce3..2a078ece 100644 --- a/meet-ce/backend/tests/helpers/request-helpers.ts +++ b/meet-ce/backend/tests/helpers/request-helpers.ts @@ -394,7 +394,7 @@ export const deleteAllUsers = async () => { * @param accessToken - Optional access token for authentication (uses API key if not provided) * @param headers - Optional headers object supporting: * - xFields: Comma-separated list of fields to include (e.g., 'roomId,roomName') - * - xExpand: Comma-separated list of properties to expand (e.g., 'config') + * - xExtraFields: Comma-separated list of extra fields to include (e.g., 'config') * @returns A Promise that resolves to the created MeetRoom * @example * ``` @@ -404,14 +404,14 @@ export const deleteAllUsers = async () => { * // Create room with specific fields only * const room = await createRoom({ roomName: 'Test' }, undefined, { xFields: 'roomId,roomName' }); * - * // Create room with expanded config - * const room = await createRoom({ roomName: 'Test' }, undefined, { xExpand: 'config' }); + * // Create room with extra fields included + * const room = await createRoom({ roomName: 'Test' }, undefined, { xExtraFields: 'config' }); * ``` */ export const createRoom = async ( options: MeetRoomOptions = {}, accessToken?: string, - headers?: { xFields?: string; xExpand?: string } + headers?: { xFields?: string; xExtraFields?: string } ): Promise => { checkAppIsRunning(); @@ -431,8 +431,8 @@ export const createRoom = async ( req.set('x-fields', headers.xFields); } - if (headers?.xExpand) { - req.set('x-expand', headers.xExpand); + if (headers?.xExtraFields) { + req.set('x-extrafields', headers.xExtraFields); } const response = await req; @@ -453,19 +453,19 @@ 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 extraFields - Optional extraFields 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, expand?: string, roomMemberToken?: string) => { +export const getRoom = async (roomId: string, fields?: string, extraFields?: string, roomMemberToken?: string) => { checkAppIsRunning(); const queryParams: Record = {}; if (fields) queryParams.fields = fields; - if (expand) queryParams.expand = expand; + if (extraFields) queryParams.extraFields = extraFields; const req = request(app) .get(getFullPath(`${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${roomId}`)) 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 4a61042e..2ef290fd 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 @@ -17,6 +17,7 @@ import { MEET_ENV } from '../../../../src/environment.js'; import { DEFAULT_RECORDING_ENCODING_PRESET, DEFAULT_RECORDING_LAYOUT, + expectExtraFieldsInResponse, expectValidRoom, expectValidationError } from '../../../helpers/assertion-helpers.js'; @@ -41,6 +42,7 @@ describe('Room API Tests', () => { it('Should create a room with default name when roomName is omitted', async () => { const room = await createRoom(); expectValidRoom(room, 'Room'); + expectExtraFieldsInResponse(room); }); it('Should create a room without autoDeletionDate (default behavior)', async () => { @@ -48,6 +50,7 @@ describe('Room API Tests', () => { roomName: 'Test Room' }); expectValidRoom(room, 'Test Room'); + expectExtraFieldsInResponse(room); }); it('Should create a room with a valid autoDeletionDate', async () => { @@ -57,6 +60,7 @@ describe('Room API Tests', () => { }); expectValidRoom(room, 'Room', 'room', undefined, validAutoDeletionDate); + expectExtraFieldsInResponse(room); }); it('Should create a room when sending full valid payload', async () => { @@ -86,10 +90,11 @@ describe('Room API Tests', () => { room, 'Example Room', 'example_room', - 'expandable', + undefined, validAutoDeletionDate, payload.autoDeletionPolicy ); + expectExtraFieldsInResponse(room); }); it('Should create a room when sending partial config', async () => { @@ -119,8 +124,8 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, // Default value captions: { enabled: true } }; - expectValidRoom(room, 'Partial Config Room', 'partial_config_room', 'expandable', validAutoDeletionDate); - + expectValidRoom(room, 'Partial Config Room', 'partial_config_room', undefined, validAutoDeletionDate); + expectExtraFieldsInResponse(room); const response = await getRoom(room.roomId, undefined, 'config'); expectValidRoom( response.body, @@ -158,8 +163,8 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, // Default value captions: { enabled: true } // Default value }; - expectValidRoom(room, 'Partial Config Room', 'partial_config_room', 'expandable', validAutoDeletionDate); - + expectValidRoom(room, 'Partial Config Room', 'partial_config_room', undefined, validAutoDeletionDate); + expectExtraFieldsInResponse(room); const response = await getRoom(room.roomId, undefined, 'config'); expectValidRoom( response.body, @@ -170,55 +175,64 @@ describe('Room API Tests', () => { ); }); - it('should create a room with collapsed config by default', async () => { + it('should not include config property by default (extraFields not specified)', async () => { const room = await createRoom({ - roomName: 'Collapsed Config Room' + roomName: 'Default Room' }); - expect(room.config).toBeDefined(); - expect((room.config as any)._expandable).toBe(true); - expect((room.config as any)._href).toBe( - `${INTERNAL_CONFIG.API_BASE_PATH_V1}/rooms/${room.roomId}?expand=config` - ); + // Config should not be in the response by default + expect(room.config).toBeUndefined(); + // But _extraFields metadata should be present + expect((room as any)._extraFields).toBeDefined(); + expect((room as any)._extraFields).toContain('config'); + expect((room as any)._extraFields.length).toBe(1); }); - it('should expand config when x-Expand header is provided', async () => { + it('should include extra fields when X-ExtraFields header is provided', async () => { const room = await createRoom( { - roomName: 'Collapsed Config Room' + roomName: 'Extra Fields Room' }, undefined, - { xExpand: 'config' } + { xExtraFields: 'config' } ); + // Config should be present when requested via extraFields expect(room.config).toBeDefined(); - expect((room.config as any)._expandable).toBeUndefined(); - expect((room.config as any)._href).toBeUndefined(); expect(room.config.recording.layout).toBe(DEFAULT_RECORDING_LAYOUT); + expect((room as any)._extraFields).toContain('config'); + expect((room as any)._extraFields.length).toBe(1); }); - it('should filter fields when x-Field header is provided', async () => { + it('should filter fields when x-Fields header is provided', async () => { const room = await createRoom(undefined, undefined, { xFields: 'roomName' }); - expect(Object.keys(room).length).toBe(1); - expect(room.roomName).toBeDefined(); - }); - - it('should filter fields and expand config when both xFields and xExpand are provided', async () => { - const room = await createRoom(undefined, undefined, { xFields: 'config', xExpand: 'config' }); - - expect(Object.keys(room).length).toBe(1); - expect(room.config).toBeDefined(); - expect((room.config as any)._expandable).toBeUndefined(); - expect((room.config as any)._href).toBeUndefined(); - }); - - it('should not includes config if filter fields are provided without config', async () => { - const room = await createRoom(undefined, undefined, { xFields: 'roomName', xExpand: 'config' }); - + expect(Object.keys(room).length).toBe(2); // roomName + _extraFields expect(room.roomName).toBeDefined(); + expect((room as any)._extraFields).toBeDefined(); + // Config should not be present even though we're filtering fields expect(room.config).toBeUndefined(); }); + + it('should include extra fields even when fields filter is applied', async () => { + const room = await createRoom(undefined, undefined, { xFields: 'roomId,config', xExtraFields: 'config' }); + + // Should only have roomId, config, and _extraFields + expect(Object.keys(room).length).toBe(3); + expect(room.roomId).toBeDefined(); + expect(room.config).toBeDefined(); + expect((room as any)._extraFields).toContain('config'); + expect((room as any)._extraFields.length).toBe(1); + }); + + it('should not include config if filter fields are provided without config but should include it with extraFields', async () => { + const room = await createRoom(undefined, undefined, { xFields: 'roomName', xExtraFields: 'config' }); + + expect(Object.keys(room).length).toBe(3); // roomName, config, _extraFields + expect(room.roomName).toBeDefined(); + expect(room.config).toBeDefined(); + expect((room as any)._extraFields).toBeDefined(); + }); }); describe('Room Name Sanitization Tests', () => { @@ -388,7 +402,8 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, 'Room without encoding', 'room_without_encoding', 'expandable'); + expectValidRoom(room, 'Room without encoding', 'room_without_encoding', undefined); + expectExtraFieldsInResponse(room); const response = await getRoom(room.roomId, undefined, 'config'); expectValidRoom(response.body, 'Room without encoding', 'room_without_encoding', expectedConfig); @@ -418,7 +433,7 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, '1080p Preset Room', '1080p_preset_room', 'expandable'); + expectValidRoom(room, '1080p Preset Room', '1080p_preset_room', undefined); const response = await getRoom(room.roomId, undefined, 'config'); expectValidRoom(response.body, '1080p Preset Room', '1080p_preset_room', expectedConfig); }); @@ -447,7 +462,7 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, 'Portrait 720p Room', 'portrait_720p_room', 'expandable'); + expectValidRoom(room, 'Portrait 720p Room', 'portrait_720p_room', undefined); const response = await getRoom(room.roomId, undefined, 'config'); expectValidRoom(response.body, 'Portrait 720p Room', 'portrait_720p_room', expectedConfig); }); @@ -506,7 +521,7 @@ describe('Room API Tests', () => { e2ee: { enabled: false }, captions: { enabled: true } }; - expectValidRoom(room, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', 'expandable'); + expectValidRoom(room, 'Full Advanced Encoding Room', 'full_advanced_encoding_room', undefined); const response = await getRoom(room.roomId, undefined, 'config'); expectValidRoom( response.body, @@ -518,20 +533,20 @@ describe('Room API Tests', () => { }); describe('Room Creation Validation failures', () => { - it('should fail when x-Expand header has invalid value', async () => { + it('should fail when x-ExtraFields header has invalid value', async () => { const payload = { - roomName: 'Test Room with Invalid Expand Header' + roomName: 'Test Room with Invalid ExtraFields Header' }; const response = await request(app) .post(ROOMS_PATH) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) - .set('x-Expand', 'invalidField') + .set('x-ExtraFields', 'invalidField') .send(payload) .expect(422); expect(response.body.error).toContain('Unprocessable Entity'); - expect(JSON.stringify(response.body.details)).toContain('Invalid expand properties.'); + expect(JSON.stringify(response.body.details)).toContain('Invalid extraFields'); }); it('should fail when autoDeletionDate is negative', async () => { diff --git a/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts b/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts index b1dfd84b..1c6d38ef 100644 --- a/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/delete-room.test.ts @@ -321,7 +321,7 @@ describe('Room API Tests', () => { attempts++; if (attempts < maxAttempts) { - await new Promise(resolve => setTimeout(resolve, retryDelay)); + await new Promise((resolve) => setTimeout(resolve, retryDelay)); } } diff --git a/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts index 43f2678a..19d9f988 100644 --- a/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/e2ee-room-config.test.ts @@ -37,7 +37,7 @@ describe('E2EE Room Configuration Tests', () => { roomName: 'Test E2EE Default' }, undefined, - { xExpand: 'config' } + { xExtraFields: 'config' } ); // Validate room structure (skip config validation in expectValidRoom since we're checking it below) @@ -65,7 +65,7 @@ describe('E2EE Room Configuration Tests', () => { } }; - const room = await createRoom(payload, undefined, { xFields: 'roomName,config', xExpand: 'config' }); + const room = await createRoom(payload, undefined, { xFields: 'roomName,config', xExtraFields: 'config' }); expect(room.roomName).toBe('Test E2EE Enabled'); expect(room.config.e2ee.enabled).toBe(true); @@ -105,7 +105,7 @@ describe('E2EE Room Configuration Tests', () => { } }, undefined, - { xExpand: 'config' } + { xExtraFields: 'config' } ); expect(room.config.recording.enabled).toBe(true); @@ -183,7 +183,7 @@ describe('E2EE Room Configuration Tests', () => { roomName: 'Test E2EE Update Enabled' }, undefined, - { xExpand: 'config' } + { xExtraFields: 'config' } ); expect(room.config.e2ee.enabled).toBe(false); @@ -250,7 +250,7 @@ describe('E2EE Room Configuration Tests', () => { const response = await request(app) .get(ROOMS_PATH) .set(INTERNAL_CONFIG.API_KEY_HEADER, MEET_ENV.INITIAL_API_KEY) - .query({ expand: 'config' }) + .query({ extraFields: 'config' }) .expect(200); // Filter out any rooms from other test suites 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 c1a40b23..02d27680 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 @@ -7,6 +7,7 @@ import { } from '@openvidu-meet/typings'; import ms from 'ms'; import { + expectExtraFieldsInResponse, expectSuccessRoomResponse, expectValidationError, expectValidRoom, @@ -26,16 +27,15 @@ describe('Room API Tests', () => { }); describe('Get Room Tests', () => { - it('should successfully retrieve a room by its ID', async () => { + it('should successfully retrieve a room by its ID without config by default', async () => { const createdRoom = await createRoom({ roomName: 'test-room' }); - expectValidRoom(createdRoom, 'test-room', 'test_room', 'expandable'); - - // Get room without expand - should return expandable stub + // Get room without extraFields - config should not be present const response = await getRoom(createdRoom.roomId); - expectSuccessRoomResponse(response, 'test-room', 'test_room', undefined, 'expandable'); + expectValidRoom(response.body, 'test-room'); + expectExtraFieldsInResponse(response.body); }); it('should retrieve a room with custom config', async () => { @@ -88,6 +88,7 @@ describe('Room API Tests', () => { const response = await getRoom(dirtyRoomId); expectSuccessRoomResponse(response, 'test-room', 'test_room'); + expectExtraFieldsInResponse(response.body); }); it('should retrieve a room with autoDeletionDate', async () => { @@ -177,15 +178,15 @@ describe('Room API Tests', () => { expectValidationError(response, 'roomId', 'cannot be empty after sanitization'); }); - it('should fail when expand has invalid values', async () => { + it('should fail when extraFields has invalid values', async () => { const createdRoom = await createRoom({ - roomName: 'invalid-expand-test' + roomName: 'invalid-extrafields-test' }); - // Get room with invalid expand values + // Get room with invalid extraFields values const response = await getRoom(createdRoom.roomId, undefined, 'invalid,wrongparam'); - expectValidationError(response, 'expand', 'Invalid expand properties. Valid options: config'); + expectValidationError(response, 'extraFields', 'Invalid extraFields. 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 9adb5828..10f72122 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 @@ -2,6 +2,7 @@ import { afterEach, beforeAll, describe, expect, it } from '@jest/globals'; import { MeetRecordingEncodingPreset, MeetRecordingLayout, MeetRoom, MeetRoomStatus } from '@openvidu-meet/typings'; import ms from 'ms'; import { + expectExtraFieldsInResponse, expectSuccessRoomsResponse, expectValidationError, expectValidRoom, @@ -36,8 +37,8 @@ describe('Room API Tests', () => { const response = await getRooms(); expectSuccessRoomsResponse(response, 1, 10, false, false); - - expectValidRoom(response.body.rooms[0], 'test-room', 'test_room', 'expandable'); + expectValidRoom(response.body.rooms[0], 'test-room', 'test_room'); + expectExtraFieldsInResponse(response.body.rooms[0]); }); it('should return a list of rooms applying fields filter', async () => { @@ -52,6 +53,7 @@ describe('Room API Tests', () => { expectSuccessRoomsResponse(response, 1, 10, false, false); expectValidRoomWithFields(rooms[0], ['roomId', 'creationDate']); + expectExtraFieldsInResponse(rooms[0]); }); it('should return a list of rooms applying roomName filter', async () => { @@ -247,20 +249,20 @@ describe('Room API Tests', () => { }); }); - describe('List Rooms with Expand Parameter Tests', () => { - it('should return rooms with config as expandable stub when expand parameter is not provided', async () => { + describe('List Rooms with ExtraFields Parameter Tests', () => { + it('should return rooms without config when extraFields parameter is not provided', async () => { await createRoom({ - roomName: 'no-expand-list-test' + roomName: 'no-extrafields-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'); + expectValidRoom(room, 'no-extrafields-list-test', 'no_extrafields_list_test'); }); - it('should return rooms with full config when using expand=config', async () => { + it('should return rooms with full config when using extraFields=config', async () => { const customConfig = { recording: { enabled: false, @@ -274,27 +276,27 @@ describe('Room API Tests', () => { }; await createRoom({ - roomName: 'expand-list-test', + roomName: 'extrafields-list-test', config: customConfig }); - const response = await getRooms({ expand: 'config' }); + const response = await getRooms({ extraFields: 'config' }); expectSuccessRoomsResponse(response, 1, 10, false, false); const room = response.body.rooms[0]; - expectValidRoom(room, 'expand-list-test', 'expand_list_test', customConfig); + expectValidRoom(room, 'extrafields-list-test', 'extrafields_list_test', customConfig); }); - it('should fail when expand has invalid values', async () => { + it('should fail when extraFields has invalid values', async () => { await createRoom({ - roomName: 'invalid-expand-list' + roomName: 'invalid-extrafields-list' }); - const response = await getRooms({ expand: 'invalid,wrongparam' }); - expectValidationError(response, 'expand', 'Invalid expand properties. Valid options: config'); + const response = await getRooms({ extraFields: 'invalid,wrongparam' }); + expectValidationError(response, 'extraFields', 'Invalid extraFields'); }); - it('should return multiple rooms with full config when using expand=config', async () => { + it('should return multiple rooms with full config when using extraFields=config', async () => { const config1 = { recording: { enabled: true, @@ -320,22 +322,22 @@ describe('Room API Tests', () => { }; await Promise.all([ - createRoom({ roomName: 'multi-expand-1', config: config1 }), - createRoom({ roomName: 'multi-expand-2', config: config2 }) + createRoom({ roomName: 'multi-extrafields-1', config: config1 }), + createRoom({ roomName: 'multi-extrafields-2', config: config2 }) ]); - const response = await getRooms({ expand: 'config' }); + const response = await getRooms({ extraFields: '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'); + const room1 = rooms.find((r: MeetRoom) => r.roomName === 'multi-extrafields-1'); + const room2 = rooms.find((r: MeetRoom) => r.roomName === 'multi-extrafields-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); + expectValidRoom(room1, 'multi-extrafields-1', 'multi_extrafields_1', config1); + expectValidRoom(room2, 'multi-extrafields-2', 'multi_extrafields_2', config2); }); }); }); diff --git a/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts index 4721e93d..fc5f2eb3 100644 --- a/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/recording-layout-room-config.test.ts @@ -26,7 +26,7 @@ describe('Room API Tests', () => { } }; - const room = await createRoom(payload, undefined, { xExpand: 'config' }); + const room = await createRoom(payload, undefined, { xExtraFields: 'config' }); const expectedConfig = { recording: { @@ -53,7 +53,7 @@ describe('Room API Tests', () => { } }; - const room = await createRoom(payload, undefined, { xExpand: 'config' }); + const room = await createRoom(payload, undefined, { xExtraFields: 'config' }); const expectedConfig = { recording: { @@ -80,7 +80,7 @@ describe('Room API Tests', () => { } }; - const room = await createRoom(payload, undefined, { xExpand: 'config' }); + const room = await createRoom(payload, undefined, { xExtraFields: 'config' }); const expectedConfig = { recording: { diff --git a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts index a1e362bf..c441868c 100644 --- a/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts +++ b/meet-ce/backend/tests/integration/api/rooms/update-room-config.test.ts @@ -254,7 +254,7 @@ describe('Room API Tests', () => { } }, undefined, - { xExpand: 'config' } + { xExtraFields: 'config' } ); expect(createdRoom.config.recording.encoding).toMatchObject(recordingEncoding); @@ -288,7 +288,7 @@ describe('Room API Tests', () => { } }, undefined, - { xExpand: 'config' } + { xExtraFields: 'config' } ); expect(createdRoom.config.recording.layout).toBe(MeetRecordingLayout.SPEAKER); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-lobby.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-lobby.service.ts index 65392efd..95ca165f 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-lobby.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-lobby.service.ts @@ -157,7 +157,7 @@ export class MeetingLobbyService { const [room] = await Promise.all([ this.roomService.getRoom(roomId, { fields: ['roomId', 'roomName', 'status', 'config', 'accessUrl', 'anonymous'], - expand: ['config'] + extraFields: ['config'] }), this.setBackButtonText(), this.checkForRecordings(), diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/models/room-request.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/models/room-request.ts index 215e58ce..f323736e 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/models/room-request.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/models/room-request.ts @@ -1,4 +1,4 @@ -import { MeetRoomExpandableProperties, MeetRoomField } from '@openvidu-meet/typings'; +import { MeetRoomExtraField, MeetRoomField } from '@openvidu-meet/typings'; /** * Options for configuring the response MeetRoom REST API object @@ -10,8 +10,8 @@ export interface MeetRoomClientResponseOptions { */ fields?: MeetRoomField[]; /** - * Array of expandable properties to expand in the response. - * If not specified, expandable properties will not be expanded. + * Array of extra properties to include in the response. + * These are not included by default. */ - expand?: MeetRoomExpandableProperties[]; + extraFields?: MeetRoomExtraField[]; } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-wizard/room-wizard.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-wizard/room-wizard.component.ts index 8da2fc4b..7c12671f 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-wizard/room-wizard.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/pages/room-wizard/room-wizard.component.ts @@ -92,7 +92,7 @@ export class RoomWizardComponent implements OnInit { try { const { roomName, autoDeletionDate, config } = await this.roomService.getRoom(this.roomId, { fields: ['roomName', 'autoDeletionDate', 'config'], - expand: ['config'] + extraFields: ['config'] }); this.existingRoomData = { roomName, autoDeletionDate, config }; if (this.existingRoomData) { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/services/room.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/services/room.service.ts index 78befe59..6a69b158 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/services/room.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/rooms/services/room.service.ts @@ -40,7 +40,7 @@ export class RoomService { async createRoom(options?: MeetRoomOptions, responseOptions?: MeetRoomClientResponseOptions): Promise { const headers: Record = { 'X-Fields': responseOptions?.fields ? responseOptions.fields.join(',') : '', - 'X-Expand': responseOptions?.expand ? responseOptions.expand.join(',') : '' + 'X-ExtraFields': responseOptions?.extraFields ? responseOptions.extraFields.join(',') : '' }; return this.httpService.postRequest(this.ROOMS_API, options, headers); } @@ -91,8 +91,8 @@ export class RoomService { if (responseOptions?.fields) { queryParams.set('fields', responseOptions.fields.join(',')); } - if (responseOptions?.expand) { - queryParams.set('expand', responseOptions.expand.join(',')); + if (responseOptions?.extraFields) { + queryParams.set('extraFields', responseOptions.extraFields.join(',')); } const queryString = queryParams.toString(); const path = `${this.ROOMS_API}/${roomId}${queryString ? `?${queryString}` : ''}`; diff --git a/meet-ce/typings/src/room-response.ts b/meet-ce/typings/src/room-response.ts index acc9fa9c..1806ff03 100644 --- a/meet-ce/typings/src/room-response.ts +++ b/meet-ce/typings/src/room-response.ts @@ -24,19 +24,16 @@ export const MEET_ROOM_FIELDS = [ ] as const satisfies readonly (keyof MeetRoom)[]; /** - * Properties of a {@link MeetRoom} that can be expanded into full objects instead of stubs. + * Properties of a {@link MeetRoom} that can be included as extra fields in the API response. + * These fields are not included by default and must be explicitly requested via extraFields parameter. */ -export const MEET_ROOM_EXPANDABLE_FIELDS = ['config'] as const satisfies readonly ExpandableKey[]; +export const MEET_ROOM_EXTRA_FIELDS = ['config'] as const satisfies readonly ExtraFieldKey[]; /** - * Properties of a room that can be expanded in the API response. + * Properties of a room that can be requested as extra fields in the API response. */ -export type MeetRoomExpandableProperties = (typeof MEET_ROOM_EXPANDABLE_FIELDS)[number]; +export type MeetRoomExtraField = (typeof MEET_ROOM_EXTRA_FIELDS)[number]; -/** - * Properties of a room that can be collapsed in the API response. - */ -export type MeetRoomCollapsibleProperties = MeetRoomExpandableProperties; /** * Properties of a {@link MeetRoom} that can be included in the API response when fields filtering is applied. @@ -56,7 +53,7 @@ export const SENSITIVE_ROOM_FIELDS_ENTRIES = Object.entries(SENSITIVE_ROOM_FIELD >; /** - * Filters for querying rooms with pagination, sorting, field selection, and expand support. + * Filters for querying rooms with pagination, sorting, field selection, and extra fields support. */ export interface MeetRoomFilters extends SortAndPagination { /** @@ -72,24 +69,14 @@ export interface MeetRoomFilters extends SortAndPagination { */ fields?: MeetRoomField[]; /** - * Expand specified properties in the response + * Extra fields to include in the response (fields not included by default) */ - expand?: MeetRoomExpandableProperties[]; + extraFields?: MeetRoomExtraField[]; } /** - * Stub that indicates a property can be expanded. + * Utility type to extract keys of T that are objects, used to define which fields can be extraFields. */ -export interface ExpandableStub { - _expandable: true; - _href: string; -} - -/** - * It produces a union type of property names that can be considered - * "expandable", meaning they reference nested objects rather than - * primitive values. - */ -type ExpandableKey = { +type ExtraFieldKey = { [K in keyof T]: T[K] extends object ? K : never; }[keyof T];