frontend: remove UtilsHelper and update repositories and services to handle field selection directly in database
This commit is contained in:
parent
6ad700f538
commit
b0b95f38a8
@ -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>);
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}` })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user