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