backend: update field selection to use arrays instead of comma-separated strings across repositories and services

This commit is contained in:
juancarmore 2026-02-19 14:03:55 +01:00
parent 66978509b1
commit 491c3392ce
7 changed files with 54 additions and 76 deletions

View File

@ -1,4 +1,4 @@
import { SortAndPagination } from '@openvidu-meet/typings';
import { SortAndPagination, SortOrder } from '@openvidu-meet/typings';
import { inject, injectable, unmanaged } from 'inversify';
import { Document, FilterQuery, Model, UpdateQuery } from 'mongoose';
import { PaginatedResult, PaginationCursor } from '../models/db-pagination.model.js';
@ -30,22 +30,17 @@ 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
* @param fields - Optional array of field names to select from database
* @returns The document or null if not found
*/
protected async findOne(filter: FilterQuery<TDocument>, fields?: string): Promise<TDocument | null> {
protected async findOne(filter: FilterQuery<TDocument>, fields?: string[]): Promise<TDocument | null> {
try {
let query = this.model.findOne(filter);
if (fields) {
//!FIXME: This transform should be optimized to avoid unnecessary string manipulation
const fieldSelection = fields
.split(',')
.map((field) => field.trim())
.filter((field) => field !== '')
.join(' ');
query = query.select(fieldSelection);
// Apply field selection if specified
if (fields && fields.length > 0) {
// Convert array of fields to space-separated string for Mongoose select()
query = query.select(fields.join(' '));
}
return await query.exec();
@ -62,20 +57,17 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
* WARNING: Use with caution on large collections. Consider using findMany() with pagination instead.
*
* @param filter - Base MongoDB query filter
* @param fields - Optional comma-separated list of fields to select from database
* @param fields - Optional array of field names to select from database
* @returns Array of domain objects matching the filter
*/
protected async findAll(filter: FilterQuery<TDocument> = {}, fields?: string): Promise<TDomain[]> {
protected async findAll(filter: FilterQuery<TDocument> = {}, fields?: string[]): Promise<TDomain[]> {
try {
let query = this.model.find(filter);
if (fields) {
const fieldSelection = fields
.split(',')
.map((field) => field.trim())
.filter((field) => field !== '')
.join(' ');
query = query.select(fieldSelection);
// Apply field selection if specified
if (fields && fields.length > 0) {
// Convert array of fields to space-separated string for Mongoose select()
query = query.select(fields.join(' '));
}
// Transform documents to domain objects
@ -96,15 +88,15 @@ 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
* @param fields - Optional array of field names to select from database
* @returns Paginated result with items, truncation flag, and optional next token
*/
protected async findMany(
filter: FilterQuery<TDocument> = {},
options: SortAndPagination = {},
fields?: string
fields?: string[]
): Promise<PaginatedResult<TDomain>> {
const { maxItems = 100, nextPageToken, sortField = '_id', sortOrder = 'desc' } = options;
const { maxItems = 100, nextPageToken, sortField = '_id', sortOrder = SortOrder.DESC } = options;
// Parse and apply pagination cursor if provided
if (nextPageToken) {
@ -113,7 +105,7 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
}
// Convert sort order to MongoDB format
const mongoSortOrder: 1 | -1 = sortOrder === 'asc' ? 1 : -1;
const mongoSortOrder: 1 | -1 = sortOrder === SortOrder.ASC ? 1 : -1;
// Build compound sort: primary field + _id
const sort: Record<string, 1 | -1> = {
@ -128,17 +120,9 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
let query = this.model.find(filter).sort(sort).limit(limit);
// Apply field selection if specified
if (fields) {
// !FIXME: This transform should be optimized to avoid unnecessary string manipulation.
// !The argument method should ideally accept an array of fields instead of a comma-separated string to avoid this overhead.
// 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);
if (fields && fields.length > 0) {
// Convert array of fields to space-separated string for Mongoose select()
query = query.select(fields.join(' '));
}
const documents = await query.exec();
@ -350,10 +334,10 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
filter: FilterQuery<TDocument>,
cursor: PaginationCursor,
sortField: string,
sortOrder: 'asc' | 'desc'
sortOrder: SortOrder
): void {
const comparison = sortOrder === 'asc' ? '$gt' : '$lt';
const equalComparison = sortOrder === 'asc' ? '$gt' : '$lt';
const comparison = sortOrder === SortOrder.ASC ? '$gt' : '$lt';
const equalComparison = sortOrder === SortOrder.ASC ? '$gt' : '$lt';
// Build compound filter for pagination
// This ensures correct ordering even when sortField values are not unique
@ -368,7 +352,7 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
} as FilterQuery<TDocument>);
// In ascending order, also include documents where the field exists (they come after missing fields)
if (sortOrder === 'asc') {
if (sortOrder === SortOrder.ASC) {
orConditions.push({
[sortField]: { $exists: true }
} as FilterQuery<TDocument>);
@ -386,7 +370,7 @@ export abstract class BaseRepository<TDomain, TDocument extends Document> {
);
// In descending order, also include documents where the field doesn't exist (they come after all values)
if (sortOrder === 'desc') {
if (sortOrder === SortOrder.DESC) {
orConditions.push({
[sortField]: { $exists: false }
} as FilterQuery<TDocument>);

View File

@ -2,7 +2,8 @@ import {
MeetRecordingField,
MeetRecordingFilters,
MeetRecordingInfo,
MeetRecordingStatus
MeetRecordingStatus,
SortOrder
} from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { uid as secureUid } from 'uid/secure';
@ -78,13 +79,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
* @param fields - Array of field names to include in the result
* @returns The recording (without access secrets), or null if not found
*/
async findByRecordingId(recordingId: string, fields?: MeetRecordingField[]): Promise<TRecording | null> {
//!FIXME: This transform should be removed because the findOne method should accept an array of fields instead of a comma-separated string, to avoid unnecessary string manipulation
const fieldsString = fields ? fields.join(',') : undefined;
const document = await this.findOne({ recordingId }, fieldsString);
const document = await this.findOne({ recordingId }, fields);
return document ? this.toDomain(document) : null;
}
@ -100,7 +99,7 @@ export class RecordingRepository<TRecording extends MeetRecordingInfo = MeetReco
* @param options.roomId - Optional room ID for exact match filtering
* @param options.roomName - Optional room name for regex match filtering (case-insensitive)
* @param options.status - Optional recording status to filter by
* @param options.fields - Comma-separated list of fields to include in the result
* @param options.fields - Array of field names to include in the result
* @param options.maxItems - Maximum number of results to return (default: 10)
* @param options.nextPageToken - Token for pagination (encoded cursor with last sortField value and _id)
* @param options.sortField - Field to sort by (default: 'startDate')
@ -121,7 +120,7 @@ export class RecordingRepository<TRecording extends MeetRecordingInfo = MeetReco
maxItems = 10,
nextPageToken,
sortField = 'startDate',
sortOrder = 'desc'
sortOrder = SortOrder.DESC
} = options;
// Build base filter
@ -156,8 +155,7 @@ export class RecordingRepository<TRecording extends MeetRecordingInfo = MeetReco
sortField,
sortOrder
},
//! FIXME: This transform should be removed because the findMany method should accept an array of fields instead of a comma-separated string, to avoid unnecessary string manipulation
fields?.join(',')
fields
);
return {

View File

@ -1,4 +1,4 @@
import { MeetRoomMember, MeetRoomMemberFilters, MeetRoomMemberPermissions } from '@openvidu-meet/typings';
import { MeetRoomMember, MeetRoomMemberFilters, MeetRoomMemberPermissions, SortOrder } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetRoomMemberDocument, MeetRoomMemberModel } from '../models/mongoose-schemas/room-member.schema.js';
import { LoggerService } from '../services/logger.service.js';
@ -67,10 +67,10 @@ export class RoomMemberRepository<TRoomMember extends MeetRoomMember = MeetRoomM
*
* @param roomId - The ID of the room
* @param memberIds - Array of member identifiers
* @param fields - Comma-separated list of fields to include in the result
* @param fields - Array of field names to include in the result
* @returns Array of found room members
*/
async findByRoomAndMemberIds(roomId: string, memberIds: string[], fields?: string): Promise<TRoomMember[]> {
async findByRoomAndMemberIds(roomId: string, memberIds: string[], fields?: string[]): Promise<TRoomMember[]> {
return await this.findAll({ roomId, memberId: { $in: memberIds } }, fields);
}
@ -81,7 +81,7 @@ export class RoomMemberRepository<TRoomMember extends MeetRoomMember = MeetRoomM
* @returns Array of room IDs where the user is a member
*/
async getRoomIdsByMemberId(memberId: string): Promise<string[]> {
const members = await this.findAll({ memberId }, 'roomId');
const members = await this.findAll({ memberId }, ['roomId']);
return members.map((m) => m.roomId);
}
@ -101,7 +101,7 @@ export class RoomMemberRepository<TRoomMember extends MeetRoomMember = MeetRoomM
memberId,
[`effectivePermissions.${permission}`]: true
},
'roomId'
['roomId']
);
return members.map((member) => member.roomId);
}
@ -112,7 +112,7 @@ export class RoomMemberRepository<TRoomMember extends MeetRoomMember = MeetRoomM
* @param roomId - The ID of the room
* @param options - Query options
* @param options.name - Optional member name to filter by (case-insensitive partial match)
* @param options.fields - Comma-separated list of fields to include in the result
* @param options.fields - Array of field names to include in the result
* @param options.maxItems - Maximum number of results to return (default: 100)
* @param options.nextPageToken - Token for pagination
* @param options.sortField - Field to sort by (default: 'membershipDate')
@ -133,7 +133,7 @@ export class RoomMemberRepository<TRoomMember extends MeetRoomMember = MeetRoomM
maxItems = 100,
nextPageToken,
sortField = 'membershipDate',
sortOrder = 'desc'
sortOrder = SortOrder.DESC
} = options;
// Build base filter

View File

@ -1,4 +1,4 @@
import { MeetRoom, MeetRoomField, MeetRoomFilters, MeetRoomStatus } from '@openvidu-meet/typings';
import { MeetRoom, MeetRoomField, MeetRoomFilters, MeetRoomStatus, SortOrder } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetRoomDocument, MeetRoomModel } from '../models/mongoose-schemas/room.schema.js';
import { LoggerService } from '../services/logger.service.js';
@ -73,13 +73,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
* @param fields - Array of field names to include in the result
* @returns The room or null if not found
*/
async findByRoomId(roomId: string, fields?: MeetRoomField[]): Promise<TRoom | null> {
//!FIXME: This transform should be removed once the controller is updated to pass the fields as an array of MeetRoomField instead of a comma-separated string.
const fieldsString = fields ? fields.join(',') : undefined;
const document = await this.findOne({ roomId }, fieldsString);
const document = await this.findOne({ roomId }, fields);
return document ? this.enrichRoomWithBaseUrls(document) : null;
}
@ -88,10 +86,10 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> extends BaseRepos
* Returns rooms with enriched URLs (including base URL).
*
* @param owner - The userId of the room owner
* @param fields - Comma-separated list of fields to include in the result
* @param fields - Array of field names to include in the result
* @returns Array of rooms owned by the user
*/
async findByOwner(owner: string, fields?: string): Promise<TRoom[]> {
async findByOwner(owner: string, fields?: MeetRoomField[]): Promise<TRoom[]> {
return await this.findAll({ owner }, fields);
}
@ -107,7 +105,7 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> extends BaseRepos
* @param options.status - Optional room status to filter by
* @param options.owner - Optional owner userId to filter by
* @param options.roomIds - Optional array of room IDs to filter by, representing rooms the user is a member of
* @param options.fields - Comma-separated list of fields to include in the result
* @param options.fields - Array of field names to include in the result
* @param options.maxItems - Maximum number of results to return (default: 100)
* @param options.nextPageToken - Token for pagination (encoded cursor with last sortField value and _id)
* @param options.sortField - Field to sort by (default: 'creationDate')
@ -128,7 +126,7 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> extends BaseRepos
maxItems = 100,
nextPageToken,
sortField = 'creationDate',
sortOrder = 'desc'
sortOrder = SortOrder.DESC
} = options;
// Build base filter
@ -160,8 +158,7 @@ export class RoomRepository<TRoom extends MeetRoom = MeetRoom> extends BaseRepos
sortField,
sortOrder
},
//! FIXME: This transform should be removed because the findMany method should accept an array of fields instead of a comma-separated string, to avoid unnecessary string manipulation
fields?.join(',')
fields
);
return {

View File

@ -1,4 +1,4 @@
import { MeetUser, MeetUserFilters } from '@openvidu-meet/typings';
import { MeetUser, MeetUserFilters, SortOrder } from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { MeetUserDocument, MeetUserModel } from '../models/mongoose-schemas/user.schema.js';
import { LoggerService } from '../services/logger.service.js';
@ -95,7 +95,7 @@ export class UserRepository<TUser extends MeetUser = MeetUser> extends BaseRepos
maxItems = 100,
nextPageToken,
sortField = 'registrationDate',
sortOrder = 'desc'
sortOrder = SortOrder.DESC
} = options;
// Build base filter

View File

@ -361,11 +361,10 @@ export class RoomMemberService {
deleted: string[];
failed: { memberId: string; error: string }[];
}> {
const membersToDelete = await this.roomMemberRepository.findByRoomAndMemberIds(
roomId,
memberIds,
'memberId,currentParticipantIdentity'
);
const membersToDelete = await this.roomMemberRepository.findByRoomAndMemberIds(roomId, memberIds, [
'memberId',
'currentParticipantIdentity'
]);
const foundMemberIds = membersToDelete.map((m) => m.memberId);
const failed = memberIds

View File

@ -396,7 +396,7 @@ export class RoomService {
// If USER role, also get owned room IDs
if (user.role === MeetUserRole.USER) {
const ownedRooms = await this.roomRepository.findByOwner(user.userId, 'roomId');
const ownedRooms = await this.roomRepository.findByOwner(user.userId, ['roomId']);
ownedRoomIds = ownedRooms.map((r) => r.roomId);
}