backend: implement atomic update paths for recording and room repositories

This commit is contained in:
juancarmore 2026-02-26 19:51:42 +01:00
parent 2572fd3960
commit 54bb06adfd
4 changed files with 26 additions and 3 deletions

View File

@ -35,6 +35,16 @@ export abstract class BaseRepository<TDomain, TDocument extends TDomain = TDomai
return [];
}
/**
* Returns document paths that must be updated atomically.
*
* Paths listed here are treated as leaf values during partial updates,
* so nested properties are not flattened into dot notation.
*/
protected getAtomicUpdatePaths(): readonly string[] {
return [];
}
/**
* Creates a new document.
*
@ -338,6 +348,7 @@ export abstract class BaseRepository<TDomain, TDocument extends TDomain = TDomai
protected buildUpdateQuery(partial: Partial<TDocument>): UpdateQuery<TDocument> {
const $set: Record<string, unknown> = {};
const $unset: Record<string, ''> = {};
const atomicUpdatePaths = new Set(this.getAtomicUpdatePaths());
const buildUpdateQueryDeep = (input: Record<string, unknown>, prefix = ''): void => {
for (const key in input) {
@ -347,8 +358,8 @@ export abstract class BaseRepository<TDomain, TDocument extends TDomain = TDomai
if (value === undefined) {
// Mark field for unsetting if value is undefined
$unset[path] = '';
} else if (this.isPlainObject(value)) {
// Recursively build update query for nested objects
} else if (this.isPlainObject(value) && !atomicUpdatePaths.has(path)) {
// Recursively build update query for nested objects that are not atomic paths
buildUpdateQueryDeep(value, path);
} else {
// Set field value for $set operator

View File

@ -38,6 +38,12 @@ export class RecordingRepository extends BaseRepository<MeetRecordingInfo, MeetR
return MEET_RECORDING_DOCUMENT_ONLY_FIELDS;
}
protected override getAtomicUpdatePaths(): readonly string[] {
// Recording encoding must be treated as an atomic update path because
// it can be either a string or an object, and we want to ensure it is fully replaced rather than partially updated.
return ['encoding'];
}
/**
* Creates a new recording with generated access secrets.
*

View File

@ -40,6 +40,12 @@ export class RoomRepository extends BaseRepository<MeetRoom, MeetRoomDocument> {
return MEET_ROOM_DOCUMENT_ONLY_FIELDS;
}
protected override getAtomicUpdatePaths(): readonly string[] {
// Recording encoding must be treated as an atomic update path because
// it can be either a string or an object, and we want to ensure it is fully replaced rather than partially updated.
return ['config.recording.encoding'];
}
/**
* Creates a new room.
* URLs are stored in the database without the base URL.

View File

@ -98,7 +98,7 @@ describe('Expired Rooms GC Tests', () => {
expect(response.body).toHaveProperty('meetingEndAction', 'delete');
// End the meeting
const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room);
const { moderatorSecret } = MeetRoomHelper.extractSecretsFromRoom(room.anonymous);
const moderatorToken = await generateRoomMemberToken(room.roomId, { secret: moderatorSecret });
await endMeeting(room.roomId, moderatorToken);