From b0b95f38a84b3749050ba3d2b918a6d4e918db40 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Tue, 23 Dec 2025 01:05:01 +0100 Subject: [PATCH] frontend: remove UtilsHelper and update repositories and services to handle field selection directly in database --- meet-ce/backend/src/helpers/utils.helper.ts | 33 --------------- .../backend/src/models/db-pagination.model.ts | 10 ----- .../src/repositories/base.repository.ts | 40 ++++++++++++++++--- .../src/repositories/recording.repository.ts | 21 ++++++---- .../src/repositories/room.repository.ts | 26 +++++++----- .../backend/src/services/recording.service.ts | 16 ++------ meet-ce/backend/src/services/room.service.ts | 20 +++------- 7 files changed, 72 insertions(+), 94 deletions(-) delete mode 100644 meet-ce/backend/src/helpers/utils.helper.ts diff --git a/meet-ce/backend/src/helpers/utils.helper.ts b/meet-ce/backend/src/helpers/utils.helper.ts deleted file mode 100644 index 7ed50ebf..00000000 --- a/meet-ce/backend/src/helpers/utils.helper.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { MeetRecordingInfo, MeetRoom } from '@openvidu-meet/typings'; - -export class UtilsHelper { - // 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(obj: T, fields?: string | string[]): Partial { - // 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); - } -} diff --git a/meet-ce/backend/src/models/db-pagination.model.ts b/meet-ce/backend/src/models/db-pagination.model.ts index 68f93c13..19f0299b 100644 --- a/meet-ce/backend/src/models/db-pagination.model.ts +++ b/meet-ce/backend/src/models/db-pagination.model.ts @@ -1,13 +1,3 @@ -/** - * Options for paginated find operations. - */ -export interface PaginatedFindOptions { - maxItems?: number; - nextPageToken?: string; - sortField?: string; - sortOrder?: 'asc' | 'desc'; -} - /** * Result of a paginated find operation. */ diff --git a/meet-ce/backend/src/repositories/base.repository.ts b/meet-ce/backend/src/repositories/base.repository.ts index fcf47aa6..1f0ef9c0 100644 --- a/meet-ce/backend/src/repositories/base.repository.ts +++ b/meet-ce/backend/src/repositories/base.repository.ts @@ -1,6 +1,7 @@ +import { SortAndPagination } from '@openvidu-meet/typings'; import { inject, injectable, unmanaged } from 'inversify'; import { Document, FilterQuery, Model, UpdateQuery } from 'mongoose'; -import { PaginatedFindOptions, PaginatedResult, PaginationCursor } from '../models/db-pagination.model.js'; +import { PaginatedResult, PaginationCursor } from '../models/db-pagination.model.js'; import { LoggerService } from '../services/logger.service.js'; /** @@ -29,11 +30,23 @@ export abstract class BaseRepository { /** * Finds a single document matching the given filter. * @param filter - MongoDB query filter + * @param fields - Optional comma-separated list of fields to select from database * @returns The document or null if not found */ - protected async findOne(filter: FilterQuery): Promise { + protected async findOne(filter: FilterQuery, fields?: string): Promise { try { - return await this.model.findOne(filter).exec(); + let query = this.model.findOne(filter); + + if (fields) { + const fieldSelection = fields + .split(',') + .map((field) => field.trim()) + .filter((field) => field !== '') + .join(' '); + query = query.select(fieldSelection); + } + + return await query.exec(); } catch (error) { this.logger.error('Error finding document with filter:', filter, error); throw error; @@ -70,11 +83,13 @@ export abstract class BaseRepository { * @param options.nextPageToken - Token for pagination (encoded cursor) * @param options.sortField - Field to sort by (default: 'createdAt') * @param options.sortOrder - Sort order: 'asc' or 'desc' (default: 'desc') + * @param fields - Optional comma-separated list of fields to select from database * @returns Paginated result with items, truncation flag, and optional next token */ protected async findMany( filter: FilterQuery = {}, - options: PaginatedFindOptions = {} + options: SortAndPagination = {}, + fields?: string ): Promise> { const { maxItems = 100, nextPageToken, sortField = '_id', sortOrder = 'desc' } = options; @@ -96,7 +111,22 @@ export abstract class BaseRepository { // Fetch one more than requested to check if there are more results const limit = maxItems + 1; - const documents = await this.model.find(filter).sort(sort).limit(limit).exec(); + // Build query + let query = this.model.find(filter).sort(sort).limit(limit); + + // Apply field selection if specified + if (fields) { + // Convert comma-separated string to space-separated format for MongoDB select() + const fieldSelection = fields + .split(',') + .map((field) => field.trim()) + .filter((field) => field !== '') + .join(' '); + + query = query.select(fieldSelection); + } + + const documents = await query.exec(); // Check if there are more results const hasMore = documents.length > maxItems; diff --git a/meet-ce/backend/src/repositories/recording.repository.ts b/meet-ce/backend/src/repositories/recording.repository.ts index 251fbbfd..7d5a4cea 100644 --- a/meet-ce/backend/src/repositories/recording.repository.ts +++ b/meet-ce/backend/src/repositories/recording.repository.ts @@ -73,10 +73,11 @@ export class RecordingRepository { - const document = await this.findOne({ recordingId }); + async findByRecordingId(recordingId: string, fields?: string): Promise { + const document = await this.findOne({ recordingId }, fields); return document ? this.toDomain(document) : null; } @@ -133,12 +134,16 @@ export class RecordingRepository extends BaseRepos * Returns the room with enriched URLs (including base URL). * * @param roomId - The unique room identifier + * @param fields - Comma-separated list of fields to include in the result * @returns The room or null if not found */ - async findByRoomId(roomId: string): Promise { - const document = await this.findOne({ roomId }); + async findByRoomId(roomId: string, fields?: string): Promise { + const document = await this.findOne({ roomId }, fields); return document ? this.enrichRoomWithBaseUrls(document) : null; } @@ -111,12 +112,16 @@ export class RoomRepository extends BaseRepos } // Use base repository's pagination method - const result = await this.findMany(filter, { - maxItems, - nextPageToken, - sortField, - sortOrder - }); + const result = await this.findMany( + filter, + { + maxItems, + nextPageToken, + sortField, + sortOrder + }, + fields + ); return { rooms: result.items, @@ -229,6 +234,7 @@ export class RoomRepository extends BaseRepos /** * Enriches room data by adding the base URL to URLs. * Converts MongoDB document to domain object. + * Only enriches URLs that are present in the document. * * @param document - The MongoDB document * @returns Room data with complete URLs @@ -239,8 +245,8 @@ export class RoomRepository extends BaseRepos return { ...room, - moderatorUrl: `${baseUrl}${room.moderatorUrl}`, - speakerUrl: `${baseUrl}${room.speakerUrl}` + ...(room.moderatorUrl !== undefined && { moderatorUrl: `${baseUrl}${room.moderatorUrl}` }), + ...(room.speakerUrl !== undefined && { speakerUrl: `${baseUrl}${room.speakerUrl}` }) }; } } diff --git a/meet-ce/backend/src/services/recording.service.ts b/meet-ce/backend/src/services/recording.service.ts index cf91a9b9..235b1459 100644 --- a/meet-ce/backend/src/services/recording.service.ts +++ b/meet-ce/backend/src/services/recording.service.ts @@ -8,7 +8,6 @@ import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_ENV } from '../environment.js'; import { RecordingHelper } from '../helpers/recording.helper.js'; import { MeetLock } from '../helpers/redis.helper.js'; -import { UtilsHelper } from '../helpers/utils.helper.js'; import { DistributedEventType } from '../models/distributed-event.model.js'; import { errorRecordingAlreadyStarted, @@ -213,16 +212,7 @@ export class RecordingService { nextPageToken?: string; }> { try { - const { fields, ...findOptions } = filters; - const response = await this.recordingRepository.find(findOptions); - - // Apply field filtering if specified - if (fields) { - response.recordings = response.recordings.map((rec: MeetRecordingInfo) => - UtilsHelper.filterObjectFields(rec, fields) - ) as MeetRecordingInfo[]; - } - + const response = await this.recordingRepository.find(filters); this.logger.info(`Retrieved ${response.recordings.length} recordings.`); return response; } catch (error) { @@ -435,13 +425,13 @@ export class RecordingService { * @returns A promise that resolves to a MeetRecordingInfo object. */ async getRecording(recordingId: string, fields?: string): Promise { - const recordingInfo = await this.recordingRepository.findByRecordingId(recordingId); + const recordingInfo = await this.recordingRepository.findByRecordingId(recordingId, fields); if (!recordingInfo) { throw errorRecordingNotFound(recordingId); } - return UtilsHelper.filterObjectFields(recordingInfo, fields) as MeetRecordingInfo; + return recordingInfo; } /** diff --git a/meet-ce/backend/src/services/room.service.ts b/meet-ce/backend/src/services/room.service.ts index 0bfc780f..c7c5ddc6 100644 --- a/meet-ce/backend/src/services/room.service.ts +++ b/meet-ce/backend/src/services/room.service.ts @@ -20,7 +20,6 @@ import { uid } from 'uid/single'; import { INTERNAL_CONFIG } from '../config/internal-config.js'; import { MEET_ENV } from '../environment.js'; import { MeetRoomHelper } from '../helpers/room.helper.js'; -import { UtilsHelper } from '../helpers/utils.helper.js'; import { errorDeletingRoom, errorRoomActiveMeeting, @@ -215,14 +214,7 @@ export class RoomService { isTruncated: boolean; nextPageToken?: string; }> { - const { fields, ...findOptions } = filters; - const response = await this.roomRepository.find(findOptions); - - if (fields) { - const filteredRooms = response.rooms.map((room: MeetRoom) => UtilsHelper.filterObjectFields(room, fields)); - response.rooms = filteredRooms as MeetRoom[]; - } - + const response = await this.roomRepository.find(filters); return response; } @@ -233,23 +225,21 @@ export class RoomService { * @returns A promise that resolves to an {@link MeetRoom} object. */ async getMeetRoom(roomId: string, fields?: string): Promise { - const meetRoom = await this.roomRepository.findByRoomId(roomId); + const room = await this.roomRepository.findByRoomId(roomId, fields); - if (!meetRoom) { + if (!room) { this.logger.error(`Meet room with ID ${roomId} not found.`); throw errorRoomNotFound(roomId); } - const filteredRoom = UtilsHelper.filterObjectFields(meetRoom, fields); - // Remove moderatorUrl if the room member is a speaker to prevent access to moderator links const role = this.requestSessionService.getRoomMemberRole(); if (role === MeetRoomMemberRole.SPEAKER) { - delete filteredRoom.moderatorUrl; + delete (room as Partial).moderatorUrl; } - return filteredRoom as MeetRoom; + return room; } /**