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];