backend: Enhance recording service to support field filtering in getRecording and getAllRecordings methods

This commit is contained in:
Carlos Santos 2025-04-10 13:12:26 +02:00
parent 18e0fe6a64
commit a0b7d42002
6 changed files with 49 additions and 42 deletions

View File

@ -70,10 +70,12 @@ export const getRecording = async (req: Request, res: Response) => {
const logger = container.get(LoggerService);
const recordingService = container.get(RecordingService);
const recordingId = req.params.recordingId;
const fields = req.query.fields as string | undefined;
logger.info(`Getting recording ${recordingId}`);
try {
const recordingInfo = await recordingService.getRecording(recordingId);
const recordingInfo = await recordingService.getRecording(recordingId, fields);
return res.status(200).json(recordingInfo);
} catch (error) {
if (error instanceof OpenViduMeetError) {

View File

@ -1,18 +1,33 @@
import { MeetRecordingInfo, MeetRoom } from '@typings-ce';
export class UtilsHelper {
private constructor() {
// Prevent instantiation of this utility class
// Prevent instantiation of this utility class.
private constructor() {}
/**
* Filters the fields of an object based on a list of keys.
*
* @param obj - The object to filter (it can be a MeetRoom or MeetRecordingInfo).
* @param fields - A comma-separated string or an array of field names to keep.
* @returns A new object containing only the specified keys.
*/
static filterObjectFields<T extends MeetRecordingInfo | MeetRoom>(obj: T, fields?: string | string[]): Partial<T> {
// If no fields are provided, return the full object.
if (!fields || (typeof fields === 'string' && fields.trim().length === 0)) {
return obj;
}
// Convert the string to an array if necessary.
const fieldsArray = Array.isArray(fields) ? fields : fields.split(',').map((f) => f.trim());
// Reduce the object by only including the specified keys.
return fieldsArray.reduce((acc, field) => {
if (Object.prototype.hasOwnProperty.call(obj, field)) {
// Use keyof T to properly type the field access
acc[field as keyof T] = obj[field as keyof T];
}
return acc;
}, {} as Partial<T>);
}
static filterObjectFields = (obj: Record<string, unknown>, fields: string[]): Record<string, any> => {
return fields.reduce(
(acc, field) => {
if (Object.prototype.hasOwnProperty.call(obj, field)) {
acc[field] = obj[field];
}
return acc;
},
{} as Record<string, unknown>
);
};
}

View File

@ -52,7 +52,8 @@ const GetRecordingsFiltersSchema: z.ZodType<MeetRecordingFilters> = z.object({
.default(10),
// status: z.string().optional(),
roomId: z.string().optional(),
nextPageToken: z.string().optional()
nextPageToken: z.string().optional(),
fields: z.string().optional(),
});
export const withValidStartRecordingRequest = (req: Request, res: Response, next: NextFunction) => {
@ -77,7 +78,7 @@ export const withValidRecordingId = (req: Request, res: Response, next: NextFunc
next();
};
export const withValidGetRecordingsRequest = (req: Request, res: Response, next: NextFunction) => {
export const withValidRecordingFiltersRequest = (req: Request, res: Response, next: NextFunction) => {
const { success, error, data } = GetRecordingsFiltersSchema.safeParse(req.query);
if (!success) {

View File

@ -8,7 +8,7 @@ import {
tokenAndRoleValidator,
withRecordingEnabled,
withCorrectPermissions,
withValidGetRecordingsRequest,
withValidRecordingFiltersRequest,
withValidRecordingBulkDeleteRequest,
withValidRecordingId,
withValidStartRecordingRequest,
@ -35,7 +35,7 @@ recordingRouter.get(
recordingRouter.get(
'/',
withAuth(apiKeyValidator, tokenAndRoleValidator(UserRole.ADMIN)),
withValidGetRecordingsRequest,
withValidRecordingFiltersRequest,
recordingCtrl.getRecordings
);
recordingRouter.delete(

View File

@ -37,6 +37,7 @@ import { MeetLock } from '../helpers/redis.helper.js';
import { IScheduledTask, TaskSchedulerService } from './task-scheduler.service.js';
import { SystemEventService } from './system-event.service.js';
import { SystemEventType } from '../models/system-event.model.js';
import { UtilsHelper } from '../helpers/utils.helper.js';
@injectable()
export class RecordingService {
@ -228,10 +229,10 @@ export class RecordingService {
* @param recordingId - The unique identifier of the recording.
* @returns A promise that resolves to a MeetRecordingInfo object.
*/
async getRecording(recordingId: string): Promise<MeetRecordingInfo> {
async getRecording(recordingId: string, fields?: string): Promise<MeetRecordingInfo> {
const { recordingInfo } = await this.getMeetRecordingInfoFromMetadata(recordingId);
return recordingInfo;
return UtilsHelper.filterObjectFields(recordingInfo, fields) as MeetRecordingInfo;
}
/**
@ -245,7 +246,7 @@ export class RecordingService {
* - `nextPageToken`: (Optional) A token to retrieve the next page of results, if available.
* @throws Will throw an error if there is an issue retrieving the recordings.
*/
async getAllRecordings({ maxItems, nextPageToken, roomId }: MeetRecordingFilters): Promise<{
async getAllRecordings({ maxItems, nextPageToken, roomId, fields }: MeetRecordingFilters): Promise<{
recordings: MeetRecordingInfo[];
isTruncated: boolean;
nextPageToken?: string;
@ -274,7 +275,9 @@ export class RecordingService {
}
});
const recordings: MeetRecordingInfo[] = await Promise.all(promises);
let recordings: MeetRecordingInfo[] = await Promise.all(promises);
recordings = recordings.map((rec) => UtilsHelper.filterObjectFields(rec, fields)) as MeetRecordingInfo[];
this.logger.info(`Retrieved ${recordings.length} recordings.`);
// Return the paginated list of recordings

View File

@ -89,7 +89,7 @@ export class RoomService {
metadata: JSON.stringify({
createdBy: MEET_NAME_ID,
roomOptions: MeetRoomHelper.toOpenViduOptions(meetRoom)
}),
})
//TODO: Uncomment this when bug in LiveKit is fixed
// When it is defined, the room will be closed although there are participants
// emptyTimeout: ms('20s') / 1000,
@ -129,13 +129,8 @@ export class RoomService {
}> {
const response = await this.storageService.getMeetRooms(maxItems, nextPageToken);
if (fields && fields.length > 0) {
const fieldsArray = Array.isArray(fields) ? fields : fields.split(',').map((f) => f.trim());
const filteredRooms = response.rooms.map((room) =>
UtilsHelper.filterObjectFields(room as unknown as Record<string, unknown>, fieldsArray)
);
response.rooms = filteredRooms as MeetRoom[];
}
const filteredRooms = response.rooms.map((room) => UtilsHelper.filterObjectFields(room, fields));
response.rooms = filteredRooms as MeetRoom[];
return response;
}
@ -149,16 +144,7 @@ export class RoomService {
async getMeetRoom(roomId: string, fields?: string): Promise<MeetRoom> {
const meetRoom = await this.storageService.getMeetRoom(roomId);
if (fields && fields.length > 0) {
const fieldsArray = Array.isArray(fields) ? fields : fields.split(',').map((f) => f.trim());
const filteredRoom = UtilsHelper.filterObjectFields(
meetRoom as unknown as Record<string, unknown>,
fieldsArray
);
return filteredRoom as MeetRoom;
}
return meetRoom;
return UtilsHelper.filterObjectFields(meetRoom, fields) as MeetRoom;
}
/**