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:
parent
c0b77314b5
commit
b8e7baf705
@ -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
|
||||
@ -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)'
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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.
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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}'`);
|
||||
|
||||
185
meet-ce/backend/src/helpers/field-filter.helper.ts
Normal file
185
meet-ce/backend/src/helpers/field-filter.helper.ts
Normal 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]
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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({
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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}`))
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}` : ''}`;
|
||||
|
||||
@ -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];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user