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 logger = container.get(LoggerService);
const recordingService = container.get(RecordingService); const recordingService = container.get(RecordingService);
const recordingId = req.params.recordingId; const recordingId = req.params.recordingId;
const fields = req.query.fields as string | undefined;
logger.info(`Getting recording ${recordingId}`); logger.info(`Getting recording ${recordingId}`);
try { try {
const recordingInfo = await recordingService.getRecording(recordingId); const recordingInfo = await recordingService.getRecording(recordingId, fields);
return res.status(200).json(recordingInfo); return res.status(200).json(recordingInfo);
} catch (error) { } catch (error) {
if (error instanceof OpenViduMeetError) { if (error instanceof OpenViduMeetError) {

View File

@ -1,18 +1,33 @@
import { MeetRecordingInfo, MeetRoom } from '@typings-ce';
export class UtilsHelper { 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), .default(10),
// status: z.string().optional(), // status: z.string().optional(),
roomId: 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) => { export const withValidStartRecordingRequest = (req: Request, res: Response, next: NextFunction) => {
@ -77,7 +78,7 @@ export const withValidRecordingId = (req: Request, res: Response, next: NextFunc
next(); 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); const { success, error, data } = GetRecordingsFiltersSchema.safeParse(req.query);
if (!success) { if (!success) {

View File

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

View File

@ -37,6 +37,7 @@ import { MeetLock } from '../helpers/redis.helper.js';
import { IScheduledTask, TaskSchedulerService } from './task-scheduler.service.js'; import { IScheduledTask, TaskSchedulerService } from './task-scheduler.service.js';
import { SystemEventService } from './system-event.service.js'; import { SystemEventService } from './system-event.service.js';
import { SystemEventType } from '../models/system-event.model.js'; import { SystemEventType } from '../models/system-event.model.js';
import { UtilsHelper } from '../helpers/utils.helper.js';
@injectable() @injectable()
export class RecordingService { export class RecordingService {
@ -228,10 +229,10 @@ export class RecordingService {
* @param recordingId - The unique identifier of the recording. * @param recordingId - The unique identifier of the recording.
* @returns A promise that resolves to a MeetRecordingInfo object. * @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); 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. * - `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. * @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[]; recordings: MeetRecordingInfo[];
isTruncated: boolean; isTruncated: boolean;
nextPageToken?: string; 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.`); this.logger.info(`Retrieved ${recordings.length} recordings.`);
// Return the paginated list of recordings // Return the paginated list of recordings

View File

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