frontend: remove UtilsHelper and update repositories and services to handle field selection directly in database

This commit is contained in:
juancarmore 2025-12-23 01:05:01 +01:00
parent 6ad700f538
commit b0b95f38a8
7 changed files with 72 additions and 94 deletions

View File

@ -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<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>);
}
}

View File

@ -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.
*/

View File

@ -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<TDomain, TDocument extends Document> {
/**
* 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<TDocument>): Promise<TDocument | null> {
protected async findOne(filter: FilterQuery<TDocument>, fields?: string): Promise<TDocument | null> {
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<TDomain, TDocument extends Document> {
* @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<TDocument> = {},
options: PaginatedFindOptions = {}
options: SortAndPagination = {},
fields?: string
): Promise<PaginatedResult<TDomain>> {
const { maxItems = 100, nextPageToken, sortField = '_id', sortOrder = 'desc' } = options;
@ -96,7 +111,22 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
// 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;

View File

@ -73,10 +73,11 @@ export class RecordingRepository<TRecording extends MeetRecordingInfo = MeetReco
* Finds a recording by its recordingId.
*
* @param recordingId - The ID of the recording to find
* @param fields - Comma-separated list of fields to include in the result
* @returns The recording (without access secrets), or null if not found
*/
async findByRecordingId(recordingId: string): Promise<TRecording | null> {
const document = await this.findOne({ recordingId });
async findByRecordingId(recordingId: string, fields?: string): Promise<TRecording | null> {
const document = await this.findOne({ recordingId }, fields);
return document ? this.toDomain(document) : null;
}
@ -133,12 +134,16 @@ export class RecordingRepository<TRecording extends MeetRecordingInfo = MeetReco
}
// 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 {
recordings: result.items,

View File

@ -60,10 +60,11 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> 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<TRoom | null> {
const document = await this.findOne({ roomId });
async findByRoomId(roomId: string, fields?: string): Promise<TRoom | null> {
const document = await this.findOne({ roomId }, fields);
return document ? this.enrichRoomWithBaseUrls(document) : null;
}
@ -111,12 +112,16 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> 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<TRoom extends MeetRoom = MeetRoom> 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<TRoom extends MeetRoom = MeetRoom> 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}` })
};
}
}

View File

@ -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<MeetRecordingInfo> {
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;
}
/**

View File

@ -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<MeetRoom> {
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<MeetRoom>).moderatorUrl;
}
return filteredRoom as MeetRoom;
return room;
}
/**