backend: enhance room member repository and service to support partial updates and selective field retrieval

This commit is contained in:
juancarmore 2026-02-27 09:16:02 +01:00
parent db279faee4
commit e0d811237b
4 changed files with 76 additions and 45 deletions

View File

@ -205,7 +205,7 @@ export const roomMemberTokenValidator: AuthValidator = {
// If the token has a memberId, validate that permissions haven't been updated after token issuance
if (memberId) {
const roomMemberRepository = container.get(RoomMemberRepository);
const roomMember = await roomMemberRepository.findByRoomAndMemberId(roomId, memberId);
const roomMember = await roomMemberRepository.findByRoomAndMemberId(roomId, memberId, ['permissionsUpdatedAt']);
// If member not found or permissions were updated after token issuance, invalidate token
if (!roomMember || iat < roomMember.permissionsUpdatedAt) {

View File

@ -1,4 +1,10 @@
import { MeetRoomMember, MeetRoomMemberFilters, MeetRoomMemberPermissions, SortOrder } from '@openvidu-meet/typings';
import {
MeetRoomMember,
MeetRoomMemberField,
MeetRoomMemberFilters,
MeetRoomMemberPermissions,
SortOrder
} from '@openvidu-meet/typings';
import { inject, injectable } from 'inversify';
import { QueryFilter, Require_id } from 'mongoose';
import { INTERNAL_CONFIG } from '../config/internal-config.js';
@ -46,13 +52,30 @@ export class RoomMemberRepository extends BaseRepository<MeetRoomMember, MeetRoo
}
/**
* Updates an existing room member.
* Updates specific fields of a room member without replacing the entire document.
*
* @param roomId - The ID of the room
* @param memberId - The ID of the member
* @param fieldsToUpdate - Partial member data with fields to update
* @returns The updated room member
* @throws Error if room member not found
*/
async updatePartial(
roomId: string,
memberId: string,
fieldsToUpdate: Partial<MeetRoomMember>
): Promise<MeetRoomMember> {
return this.updatePartialOne({ roomId, memberId }, fieldsToUpdate);
}
/**
* Replaces an existing room member with new data.
*
* @param member - The complete updated room member data
* @returns The updated room member
* @throws Error if room member not found
*/
async update(member: MeetRoomMember): Promise<MeetRoomMember> {
async replace(member: MeetRoomMember): Promise<MeetRoomMember> {
return this.replaceOne({ roomId: member.roomId, memberId: member.memberId }, member);
}
@ -61,10 +84,15 @@ export class RoomMemberRepository extends BaseRepository<MeetRoomMember, MeetRoo
*
* @param roomId - The ID of the room
* @param memberId - The ID of the member
* @param fields - Array of field names to include in the result
* @returns The room member or null if not found
*/
async findByRoomAndMemberId(roomId: string, memberId: string): Promise<MeetRoomMember | null> {
return this.findOne({ roomId, memberId });
async findByRoomAndMemberId(
roomId: string,
memberId: string,
fields?: MeetRoomMemberField[]
): Promise<MeetRoomMember | null> {
return this.findOne({ roomId, memberId }, fields);
}
/**
@ -75,7 +103,11 @@ export class RoomMemberRepository extends BaseRepository<MeetRoomMember, MeetRoo
* @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<MeetRoomMember[]> {
async findByRoomAndMemberIds(
roomId: string,
memberIds: string[],
fields?: MeetRoomMemberField[]
): Promise<MeetRoomMember[]> {
return this.findAll({ roomId, memberId: { $in: memberIds } }, fields);
}

View File

@ -1,6 +1,7 @@
import {
LiveKitPermissions,
MeetRoomMember,
MeetRoomMemberField,
MeetRoomMemberFilters,
MeetRoomMemberOptions,
MeetRoomMemberPermissions,
@ -82,9 +83,9 @@ export class RoomMemberService {
}
// Check if user is already a member of the room
const existingMember = await this.getRoomMember(roomId, userId);
const memberExists = await this.isRoomMember(roomId, userId);
if (existingMember) {
if (memberExists) {
throw errorRoomMemberAlreadyExists(roomId, userId);
}
@ -155,7 +156,7 @@ export class RoomMemberService {
async isRoomMember(roomId: string, memberId: string): Promise<boolean> {
// Verify room exists first
await this.roomService.getMeetRoom(roomId, ['roomId']);
const member = await this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId);
const member = await this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId, ['memberId']);
return !!member;
}
@ -164,10 +165,15 @@ export class RoomMemberService {
*
* @param roomId - The ID of the room
* @param memberId - The ID of the member
* @param fields - Array of field names to include in the result
* @returns A promise that resolves to the MeetRoomMember object or null if not found
*/
async getRoomMember(roomId: string, memberId: string): Promise<MeetRoomMember | null> {
return this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId);
async getRoomMember(
roomId: string,
memberId: string,
fields?: MeetRoomMemberField[]
): Promise<MeetRoomMember | null> {
return this.roomMemberRepository.findByRoomAndMemberId(roomId, memberId, fields);
}
/**
@ -202,35 +208,28 @@ export class RoomMemberService {
memberId: string,
updates: { baseRole?: MeetRoomMemberRole; customPermissions?: Partial<MeetRoomMemberPermissions> }
): Promise<MeetRoomMember> {
const member = await this.getRoomMember(roomId, memberId);
const member = await this.getRoomMember(roomId, memberId, ['baseRole', 'customPermissions']);
if (!member) {
throw errorRoomMemberNotFound(roomId, memberId);
}
// Update baseRole if provided
if (updates.baseRole) {
member.baseRole = updates.baseRole;
}
// Update customPermissions if provided
if (updates.customPermissions) {
member.customPermissions = updates.customPermissions;
}
const nextBaseRole = updates.baseRole ?? member.baseRole;
const nextCustomPermissions = updates.customPermissions ?? member.customPermissions;
// Recompute effective permissions
const { roles } = await this.roomService.getMeetRoom(roomId, ['roles']);
member.effectivePermissions = this.computeEffectivePermissions(
roles,
member.baseRole,
member.customPermissions
);
member.permissionsUpdatedAt = Date.now();
const effectivePermissions = this.computeEffectivePermissions(roles, nextBaseRole, nextCustomPermissions);
const updatedMember = await this.roomMemberRepository.update(member);
const updatedMember = await this.roomMemberRepository.updatePartial(roomId, memberId, {
baseRole: nextBaseRole,
customPermissions: nextCustomPermissions,
effectivePermissions,
permissionsUpdatedAt: Date.now()
});
// If member lost permission to join meeting, kick them out
if (!updatedMember.effectivePermissions.canJoinMeeting) {
if (!effectivePermissions.canJoinMeeting) {
await this.kickMembersFromMeetingInBatches(roomId, [memberId]);
} else {
// TODO: Notify participant of role/permission changes if currently in a meeting
@ -255,7 +254,7 @@ export class RoomMemberService {
let batchNumber = 0;
let nextPageToken: string | undefined;
let totalUpdated = 0;
const totalMembers: MeetRoomMember[] = [];
const membersToKick: string[] = [];
do {
batchNumber++;
@ -267,9 +266,9 @@ export class RoomMemberService {
nextPageToken: token
} = await this.getAllRoomMembers(roomId, {
maxItems: BATCH_SIZE,
nextPageToken
nextPageToken,
fields: ['memberId', 'baseRole', 'customPermissions']
});
totalMembers.push(...members);
if (members.length === 0) {
break;
@ -288,9 +287,14 @@ export class RoomMemberService {
);
// Update the member with new effective permissions
member.effectivePermissions = effectivePermissions;
member.permissionsUpdatedAt = Date.now();
await this.roomMemberRepository.update(member);
if (!effectivePermissions.canJoinMeeting) {
membersToKick.push(member.memberId);
}
await this.roomMemberRepository.updatePartial(roomId, member.memberId, {
effectivePermissions,
permissionsUpdatedAt: Date.now()
});
this.logger.verbose(
`Updated effective permissions for member '${member.memberId}' in room '${roomId}'`
@ -319,8 +323,6 @@ export class RoomMemberService {
}
// Kick members who lost canJoinMeeting permission
const membersToKick = totalMembers.filter((m) => !m.effectivePermissions.canJoinMeeting).map((m) => m.memberId);
if (membersToKick.length > 0) {
await this.kickMembersFromMeetingInBatches(roomId, membersToKick);
}
@ -335,9 +337,9 @@ export class RoomMemberService {
* @param memberId - The ID of the member to delete
*/
async deleteRoomMember(roomId: string, memberId: string): Promise<void> {
const member = await this.getRoomMember(roomId, memberId);
const memberExists = await this.isRoomMember(roomId, memberId);
if (!member) {
if (!memberExists) {
throw errorRoomMemberNotFound(roomId, memberId);
}
@ -361,10 +363,7 @@ 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']);
const foundMemberIds = membersToDelete.map((m) => m.memberId);
const failed = memberIds

View File

@ -911,7 +911,7 @@ export class RoomService {
return roomMemberService.getAllPermissions();
}
const member = await roomMemberService.getRoomMember(roomId, user.userId);
const member = await roomMemberService.getRoomMember(roomId, user.userId, ['effectivePermissions']);
if (!member) {
return roomMemberService.getNoPermissions();