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
This commit is contained in:
CSantosM 2026-02-11 17:20:16 +01:00
parent c0b77314b5
commit b8e7baf705
35 changed files with 644 additions and 447 deletions

View File

@ -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.
<br/>
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

View File

@ -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)'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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.
<br/>
**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.
<br/>
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'

View File

@ -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}'`);

View File

@ -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<TField extends string, TExtraField extends TField>(
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<TField>([...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<TField>([...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<TEntity, TExtraField extends string>(
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<string, unknown>;
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<string, unknown>;
// 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<string>([...fields, ...(extraFields || [])]);
const filteredEntity = {} as Record<string, unknown>;
const entityAsRecord = entity as Record<string, unknown>;
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<T, TExtraField extends string>(
obj: T,
extraFieldsList: readonly TExtraField[]
): T & { _extraFields: TExtraField[] } {
return {
...obj,
_extraFields: [...extraFieldsList]
};
}

View File

@ -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<string, unknown>;
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<T>(obj: T): T & { _extraFields: MeetRoomExtraField[] } {
return addHttpResponseMetadata(obj, MEET_ROOM_EXTRA_FIELDS);
}
}

View File

@ -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;
}

View File

@ -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<MeetRoomOptions> = 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({

View File

@ -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<boolean> {
// 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<string> {
// 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<MeetRoomMemberRole> {
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<void> {
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

View File

@ -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<MeetRoom>} A promise that resolves to the created OpenVidu room.
*
* @throws {Error} If the room creation fails.
*
*/
async createMeetRoom(roomOptions: MeetRoomOptions, responseOpts?: MeetRoomServerResponseOptions): Promise<MeetRoom> {
async createMeetRoom(roomOptions: MeetRoomOptions): Promise<MeetRoom> {
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<MeetRoom> {
const { collapse, applyPermissionFiltering, fields } = responseOpts || {};
let room = await this.roomRepository.findByRoomId(roomId, fields);
async getMeetRoom(roomId: string, fields?: MeetRoomField[]): Promise<MeetRoom> {
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<boolean> {
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<boolean> {
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<boolean> {
// 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

View File

@ -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);
}

View File

@ -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<MeetRoom> => {
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<string, unknown> = {}) => {
*
* @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<string, string> = {};
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}`))

View File

@ -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 () => {

View File

@ -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));
}
}

View File

@ -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

View File

@ -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');
});
});
});

View File

@ -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);
});
});
});

View File

@ -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: {

View File

@ -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);

View File

@ -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(),

View File

@ -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[];
}

View File

@ -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) {

View File

@ -40,7 +40,7 @@ export class RoomService {
async createRoom(options?: MeetRoomOptions, responseOptions?: MeetRoomClientResponseOptions): Promise<MeetRoom> {
const headers: Record<string, string> = {
'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}` : ''}`;

View File

@ -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<MeetRoom>[];
export const MEET_ROOM_EXTRA_FIELDS = ['config'] as const satisfies readonly ExtraFieldKey<MeetRoom>[];
/**
* 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<T> = {
type ExtraFieldKey<T> = {
[K in keyof T]: T[K] extends object ? K : never;
}[keyof T];