mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-06-06 09:31:35 -04:00
refactor: replace TypeORM with knex.js
Co-authored-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Philip Molares <philip.molares@udo.edu> Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
6e151c8a1b
commit
c0ce00b3f9
242 changed files with 4601 additions and 6871 deletions
|
@ -11,8 +11,7 @@ import {
|
|||
import * as HedgeDocCommonsModule from '@hedgedoc/commons';
|
||||
import { Mock } from 'ts-mockery';
|
||||
|
||||
import { User } from '../../database/user.entity';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { FieldNameUser, User } from '../../database/types';
|
||||
import * as NameRandomizerModule from './random-word-lists/name-randomizer';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
|
@ -41,12 +40,15 @@ describe('websocket connection', () => {
|
|||
|
||||
const mockedUserName: string = 'mocked-user-name';
|
||||
const mockedDisplayName = 'mockedDisplayName';
|
||||
const mockedAuthorStyle = 42;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedRealtimeNote = new RealtimeNote(Mock.of<Note>({}), '');
|
||||
mockedRealtimeNote = new RealtimeNote(1, '');
|
||||
mockedUser = Mock.of<User>({
|
||||
username: mockedUserName,
|
||||
displayName: mockedDisplayName,
|
||||
[FieldNameUser.id]: 0,
|
||||
[FieldNameUser.username]: mockedUserName,
|
||||
[FieldNameUser.displayName]: mockedDisplayName,
|
||||
[FieldNameUser.authorStyle]: mockedAuthorStyle,
|
||||
});
|
||||
|
||||
mockedMessageTransporter = new MessageTransporter();
|
||||
|
@ -61,7 +63,10 @@ describe('websocket connection', () => {
|
|||
it('returns the correct transporter', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
@ -71,7 +76,10 @@ describe('websocket connection', () => {
|
|||
it('returns the correct realtime note', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
@ -107,12 +115,14 @@ describe('websocket connection', () => {
|
|||
(
|
||||
username,
|
||||
displayName,
|
||||
authorStyle,
|
||||
otherAdapterCollector: OtherAdapterCollector,
|
||||
messageTransporter,
|
||||
acceptCursorUpdateProvider,
|
||||
) => {
|
||||
expect(username).toBe(mockedUserName);
|
||||
expect(displayName).toBe(mockedDisplayName);
|
||||
expect(authorStyle).toBe(mockedAuthorStyle);
|
||||
expect(otherAdapterCollector()).toStrictEqual([
|
||||
realtimeUserStatus1,
|
||||
realtimeUserStatus2,
|
||||
|
@ -126,7 +136,10 @@ describe('websocket connection', () => {
|
|||
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
acceptEdits,
|
||||
);
|
||||
|
@ -157,7 +170,10 @@ describe('websocket connection', () => {
|
|||
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
acceptEdits,
|
||||
);
|
||||
|
@ -169,7 +185,10 @@ describe('websocket connection', () => {
|
|||
it('removes the client from the note on transporter disconnect', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
@ -181,46 +200,59 @@ describe('websocket connection', () => {
|
|||
expect(removeClientSpy).toHaveBeenCalledWith(sut);
|
||||
});
|
||||
|
||||
it('saves the correct user', () => {
|
||||
it('correctly return user id', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getUser()).toBe(mockedUser);
|
||||
expect(sut.getUserId()).toBe(mockedUser[FieldNameUser.id]);
|
||||
});
|
||||
|
||||
it('returns the correct username', () => {
|
||||
const mockedUserWithUsername = Mock.of<User>({ displayName: 'MockUser' });
|
||||
|
||||
it('correctly return username', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUserWithUsername,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getDisplayName()).toBe('MockUser');
|
||||
expect(sut.getUsername()).toBe(mockedUser[FieldNameUser.username]);
|
||||
});
|
||||
|
||||
it('returns a random fallback display name if the provided user has no display name', () => {
|
||||
const randomName = 'I am a random name';
|
||||
|
||||
jest
|
||||
.spyOn(NameRandomizerModule, 'generateRandomName')
|
||||
.mockReturnValue(randomName);
|
||||
|
||||
mockedUser = Mock.of<User>({});
|
||||
|
||||
it('correctly return displayName', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getDisplayName()).toBe(randomName);
|
||||
expect(sut.getDisplayName()).toBe(mockedUser[FieldNameUser.displayName]);
|
||||
});
|
||||
|
||||
it('correctly return authorStyle', () => {
|
||||
const sut = new RealtimeConnection(
|
||||
mockedMessageTransporter,
|
||||
mockedUser[FieldNameUser.id],
|
||||
mockedUser[FieldNameUser.username],
|
||||
mockedUser[FieldNameUser.displayName],
|
||||
mockedUser[FieldNameUser.authorStyle],
|
||||
mockedRealtimeNote,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sut.getAuthorStyle()).toBe(mockedUser[FieldNameUser.authorStyle]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,6 @@
|
|||
import { MessageTransporter, YDocSyncServerAdapter } from '@hedgedoc/commons';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { User } from '../../database/user.entity';
|
||||
import { generateRandomName } from './random-word-lists/name-randomizer';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
import { RealtimeUserStatusAdapter } from './realtime-user-status-adapter';
|
||||
|
||||
|
@ -19,24 +17,28 @@ export class RealtimeConnection {
|
|||
private readonly transporter: MessageTransporter;
|
||||
private readonly yDocSyncAdapter: YDocSyncServerAdapter;
|
||||
private readonly realtimeUserStateAdapter: RealtimeUserStatusAdapter;
|
||||
private readonly displayName: string;
|
||||
|
||||
/**
|
||||
* Instantiates the connection wrapper.
|
||||
*
|
||||
* @param messageTransporter The message transporter that handles the communication with the client.
|
||||
* @param user The user of the client
|
||||
* @param userId The id of the user of the client
|
||||
* @param username The username of the user of the client
|
||||
* @param displayName The displayName of the user of the client
|
||||
* @param authorStyle The authorStyle of the user of the client
|
||||
* @param realtimeNote The {@link RealtimeNote} that the client connected to.
|
||||
* @param acceptEdits If edits by this connection should be accepted.
|
||||
* @throws Error if the socket is not open
|
||||
*/
|
||||
constructor(
|
||||
messageTransporter: MessageTransporter,
|
||||
private user: User | null,
|
||||
private userId: number,
|
||||
private username: string | null,
|
||||
private displayName: string,
|
||||
private authorStyle: number,
|
||||
private realtimeNote: RealtimeNote,
|
||||
public acceptEdits: boolean,
|
||||
) {
|
||||
this.displayName = user?.displayName ?? generateRandomName();
|
||||
this.transporter = messageTransporter;
|
||||
|
||||
this.transporter.on('disconnected', () => {
|
||||
|
@ -48,8 +50,9 @@ export class RealtimeConnection {
|
|||
() => acceptEdits,
|
||||
);
|
||||
this.realtimeUserStateAdapter = new RealtimeUserStatusAdapter(
|
||||
this.user?.username ?? null,
|
||||
this.getDisplayName(),
|
||||
this.username ?? null,
|
||||
this.displayName,
|
||||
this.authorStyle,
|
||||
() =>
|
||||
this.realtimeNote
|
||||
.getConnections()
|
||||
|
@ -67,18 +70,26 @@ export class RealtimeConnection {
|
|||
return this.transporter;
|
||||
}
|
||||
|
||||
public getUser(): User | null {
|
||||
return this.user;
|
||||
}
|
||||
|
||||
public getSyncAdapter(): YDocSyncServerAdapter {
|
||||
return this.yDocSyncAdapter;
|
||||
}
|
||||
|
||||
public getUserId(): number {
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
public getDisplayName(): string {
|
||||
return this.displayName;
|
||||
}
|
||||
|
||||
public getUsername(): string | null {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
public getAuthorStyle(): number {
|
||||
return this.authorStyle;
|
||||
}
|
||||
|
||||
public getRealtimeNote(): RealtimeNote {
|
||||
return this.realtimeNote;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
|
||||
@Injectable()
|
||||
|
@ -15,29 +14,29 @@ export class RealtimeNoteStore {
|
|||
/**
|
||||
* Creates a new {@link RealtimeNote} for the given {@link Note} and memorizes it.
|
||||
*
|
||||
* @param note The note for which the realtime note should be created
|
||||
* @param noteId The note for which the realtime note should be created
|
||||
* @param initialTextContent the initial text content of realtime doc
|
||||
* @param initialYjsState the initial yjs state. If provided this will be used instead of the text content
|
||||
* @throws Error if there is already an realtime note for the given note.
|
||||
* @return The created realtime note
|
||||
*/
|
||||
public create(
|
||||
note: Note,
|
||||
noteId: number,
|
||||
initialTextContent: string,
|
||||
initialYjsState?: number[],
|
||||
initialYjsState?: ArrayBuffer,
|
||||
): RealtimeNote {
|
||||
if (this.noteIdToRealtimeNote.has(note.id)) {
|
||||
throw new Error(`Realtime note for note ${note.id} already exists.`);
|
||||
if (this.noteIdToRealtimeNote.has(noteId)) {
|
||||
throw new Error(`Realtime note for note ${noteId} already exists.`);
|
||||
}
|
||||
const realtimeNote = new RealtimeNote(
|
||||
note,
|
||||
noteId,
|
||||
initialTextContent,
|
||||
initialYjsState,
|
||||
);
|
||||
realtimeNote.on('destroy', () => {
|
||||
this.noteIdToRealtimeNote.delete(note.id);
|
||||
this.noteIdToRealtimeNote.delete(noteId);
|
||||
});
|
||||
this.noteIdToRealtimeNote.set(note.id, realtimeNote);
|
||||
this.noteIdToRealtimeNote.set(noteId, realtimeNote);
|
||||
return realtimeNote;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
import { LoggerModule } from '../../logger/logger.module';
|
||||
|
@ -18,7 +18,7 @@ import { RealtimeNoteService } from './realtime-note.service';
|
|||
imports: [
|
||||
LoggerModule,
|
||||
UsersModule,
|
||||
PermissionsModule,
|
||||
forwardRef(() => PermissionsModule),
|
||||
SessionModule,
|
||||
RevisionsModule,
|
||||
ScheduleModule.forRoot(),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -10,8 +10,8 @@ import { AppConfig } from '../../config/app.config';
|
|||
import { User } from '../../database/user.entity';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { NotePermission } from '../../permissions/note-permission.enum';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
|
||||
import { PermissionService } from '../../permissions/permission.service';
|
||||
import { Revision } from '../../revisions/revision.entity';
|
||||
import { RevisionsService } from '../../revisions/revisions.service';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
|
@ -29,7 +29,7 @@ describe('RealtimeNoteService', () => {
|
|||
let realtimeNoteService: RealtimeNoteService;
|
||||
let revisionsService: RevisionsService;
|
||||
let realtimeNoteStore: RealtimeNoteStore;
|
||||
let mockedPermissionService: PermissionsService;
|
||||
let mockedPermissionService: PermissionService;
|
||||
let consoleLoggerService: ConsoleLoggerService;
|
||||
let mockedAppConfig: AppConfig;
|
||||
let addIntervalSpy: jest.SpyInstance;
|
||||
|
@ -91,14 +91,14 @@ describe('RealtimeNoteService', () => {
|
|||
});
|
||||
|
||||
mockedAppConfig = Mock.of<AppConfig>({ persistInterval: 0 });
|
||||
mockedPermissionService = Mock.of<PermissionsService>({
|
||||
mockedPermissionService = Mock.of<PermissionService>({
|
||||
determinePermission: async (user: User | null) => {
|
||||
if (user?.username === readWriteUsername) {
|
||||
return NotePermission.WRITE;
|
||||
return NotePermissionLevel.WRITE;
|
||||
} else if (user?.username === onlyReadUsername) {
|
||||
return NotePermission.READ;
|
||||
return NotePermissionLevel.READ;
|
||||
} else {
|
||||
return NotePermission.DENY;
|
||||
return NotePermissionLevel.DENY;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2024 The HedgeDoc developers (see AUTHORS file)
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
@ -9,11 +9,11 @@ import { OnEvent } from '@nestjs/event-emitter';
|
|||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
|
||||
import appConfiguration, { AppConfig } from '../../config/app.config';
|
||||
import { FieldNameRevision } from '../../database/types';
|
||||
import { NoteEvent } from '../../events';
|
||||
import { ConsoleLoggerService } from '../../logger/console-logger.service';
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { NotePermission } from '../../permissions/note-permission.enum';
|
||||
import { PermissionsService } from '../../permissions/permissions.service';
|
||||
import { NotePermissionLevel } from '../../permissions/note-permission.enum';
|
||||
import { PermissionService } from '../../permissions/permission.service';
|
||||
import { RevisionsService } from '../../revisions/revisions.service';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
import { RealtimeNote } from './realtime-note';
|
||||
|
@ -28,7 +28,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
private schedulerRegistry: SchedulerRegistry,
|
||||
@Inject(appConfiguration.KEY)
|
||||
private appConfig: AppConfig,
|
||||
private permissionService: PermissionsService,
|
||||
private permissionService: PermissionService,
|
||||
) {}
|
||||
|
||||
beforeApplicationShutdown(): void {
|
||||
|
@ -44,9 +44,10 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
*/
|
||||
public saveRealtimeNote(realtimeNote: RealtimeNote): void {
|
||||
this.revisionsService
|
||||
.createAndSaveRevision(
|
||||
realtimeNote.getNote(),
|
||||
.createRevision(
|
||||
realtimeNote.getNoteId(),
|
||||
realtimeNote.getRealtimeDoc().getCurrentContent(),
|
||||
undefined,
|
||||
realtimeNote.getRealtimeDoc().encodeStateAsUpdate(),
|
||||
)
|
||||
.then(() => {
|
||||
|
@ -57,30 +58,30 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
|
||||
/**
|
||||
* Creates or reuses a {@link RealtimeNote} that is handling the real time editing of the {@link Note} which is identified by the given note id.
|
||||
* @param note The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
|
||||
* @param noteId The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
|
||||
* @throws NotInDBError if note doesn't exist or has no revisions.
|
||||
* @return A {@link RealtimeNote} that is linked to the given note.
|
||||
*/
|
||||
public async getOrCreateRealtimeNote(note: Note): Promise<RealtimeNote> {
|
||||
public async getOrCreateRealtimeNote(noteId: number): Promise<RealtimeNote> {
|
||||
return (
|
||||
this.realtimeNoteStore.find(note.id) ??
|
||||
(await this.createNewRealtimeNote(note))
|
||||
this.realtimeNoteStore.find(noteId) ??
|
||||
(await this.createNewRealtimeNote(noteId))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link RealtimeNote} for the given {@link Note}.
|
||||
*
|
||||
* @param note The note for which the realtime note should be created
|
||||
* @param noteId The note for which the realtime note should be created
|
||||
* @throws NotInDBError if note doesn't exist or has no revisions.
|
||||
* @return The created realtime note
|
||||
*/
|
||||
private async createNewRealtimeNote(note: Note): Promise<RealtimeNote> {
|
||||
const lastRevision = await this.revisionsService.getLatestRevision(note);
|
||||
private async createNewRealtimeNote(noteId: number): Promise<RealtimeNote> {
|
||||
const lastRevision = await this.revisionsService.getLatestRevision(noteId);
|
||||
const realtimeNote = this.realtimeNoteStore.create(
|
||||
note,
|
||||
noteId,
|
||||
lastRevision.content,
|
||||
lastRevision.yjsStateVector ?? undefined,
|
||||
lastRevision[FieldNameRevision.yjsStateVector] ?? undefined,
|
||||
);
|
||||
realtimeNote.on('beforeDestroy', () => {
|
||||
this.saveRealtimeNote(realtimeNote);
|
||||
|
@ -103,47 +104,47 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
|
|||
persistInterval * 60 * 1000,
|
||||
);
|
||||
this.schedulerRegistry.addInterval(
|
||||
`periodic-persist-${realtimeNote.getNote().id}`,
|
||||
`periodic-persist-${realtimeNote.getNoteId()}`,
|
||||
intervalId,
|
||||
);
|
||||
realtimeNote.on('destroy', () => {
|
||||
clearInterval(intervalId);
|
||||
this.schedulerRegistry.deleteInterval(
|
||||
`periodic-persist-${realtimeNote.getNote().id}`,
|
||||
`periodic-persist-${realtimeNote.getNoteId()}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent(NoteEvent.PERMISSION_CHANGE)
|
||||
public async handleNotePermissionChanged(note: Note): Promise<void> {
|
||||
const realtimeNote = this.realtimeNoteStore.find(note.id);
|
||||
public async handleNotePermissionChanged(noteId: number): Promise<void> {
|
||||
const realtimeNote = this.realtimeNoteStore.find(noteId);
|
||||
if (!realtimeNote) return;
|
||||
|
||||
realtimeNote.announceMetadataUpdate();
|
||||
const allConnections = realtimeNote.getConnections();
|
||||
await this.updateOrCloseConnection(allConnections, note);
|
||||
await this.updateOrCloseConnection(allConnections, noteId);
|
||||
}
|
||||
|
||||
private async updateOrCloseConnection(
|
||||
connections: RealtimeConnection[],
|
||||
note: Note,
|
||||
noteId: number,
|
||||
): Promise<void> {
|
||||
for (const connection of connections) {
|
||||
const permission = await this.permissionService.determinePermission(
|
||||
connection.getUser(),
|
||||
note,
|
||||
connection.getUserId(),
|
||||
noteId,
|
||||
);
|
||||
if (permission === NotePermission.DENY) {
|
||||
if (permission === NotePermissionLevel.DENY) {
|
||||
connection.getTransporter().disconnect();
|
||||
} else {
|
||||
connection.acceptEdits = permission > NotePermission.READ;
|
||||
connection.acceptEdits = permission > NotePermissionLevel.READ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent(NoteEvent.DELETION)
|
||||
public handleNoteDeleted(noteId: Note['id']): void {
|
||||
public handleNoteDeleted(noteId: number): void {
|
||||
const realtimeNote = this.realtimeNoteStore.find(noteId);
|
||||
if (realtimeNote) {
|
||||
realtimeNote.announceNoteDeletion();
|
||||
|
|
|
@ -29,7 +29,7 @@ describe('realtime note', () => {
|
|||
|
||||
it('can return the given note', () => {
|
||||
const sut = new RealtimeNote(mockedNote, 'nothing');
|
||||
expect(sut.getNote()).toBe(mockedNote);
|
||||
expect(sut.getNoteId()).toBe(mockedNote);
|
||||
});
|
||||
|
||||
it('can connect and disconnect clients', () => {
|
||||
|
|
|
@ -7,7 +7,6 @@ import { Message, MessageType, RealtimeDoc } from '@hedgedoc/commons';
|
|||
import { Logger } from '@nestjs/common';
|
||||
import { EventEmitter2, EventMap, Listener } from 'eventemitter2';
|
||||
|
||||
import { Note } from '../../notes/note.entity';
|
||||
import { RealtimeConnection } from './realtime-connection';
|
||||
|
||||
export interface RealtimeNoteEventMap extends EventMap {
|
||||
|
@ -33,16 +32,16 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
|
|||
private destroyEventTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly note: Note,
|
||||
private readonly noteId: number,
|
||||
initialTextContent: string,
|
||||
initialYjsState?: number[],
|
||||
initialYjsState?: ArrayBuffer,
|
||||
) {
|
||||
super();
|
||||
this.logger = new Logger(`${RealtimeNote.name} ${note.id}`);
|
||||
this.logger = new Logger(`${RealtimeNote.name} ${noteId}`);
|
||||
this.doc = new RealtimeDoc(initialTextContent, initialYjsState);
|
||||
const length = this.doc.getCurrentContent().length;
|
||||
this.logger.debug(
|
||||
`New realtime session for note ${note.id} created. Length of initial content: ${length} characters`,
|
||||
`New realtime session for note ${noteId} created. Length of initial content: ${length} characters`,
|
||||
);
|
||||
this.clientAddedListener = this.on(
|
||||
'clientAdded',
|
||||
|
@ -74,7 +73,7 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
|
|||
/**
|
||||
* Disconnects the given websocket client while cleaning-up if it was the last user in the realtime note.
|
||||
*
|
||||
* @param {WebSocket} client The websocket client that disconnects.
|
||||
* @param client The websocket client that disconnects.
|
||||
*/
|
||||
public removeClient(client: RealtimeConnection): void {
|
||||
this.clients.delete(client);
|
||||
|
@ -144,8 +143,8 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
|
|||
*
|
||||
* @return the {@link Note note}
|
||||
*/
|
||||
public getNote(): Note {
|
||||
return this.note;
|
||||
public getNoteId(): number {
|
||||
return this.noteId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@ export class RealtimeUserStatusAdapter {
|
|||
constructor(
|
||||
private readonly username: string | null,
|
||||
private readonly displayName: string,
|
||||
private readonly authorStyle: number,
|
||||
private collectOtherAdapters: OtherAdapterCollector,
|
||||
private messageTransporter: MessageTransporter,
|
||||
private acceptCursorUpdateProvider: () => boolean,
|
||||
|
@ -35,9 +36,7 @@ export class RealtimeUserStatusAdapter {
|
|||
username: this.username,
|
||||
displayName: this.displayName,
|
||||
active: true,
|
||||
styleIndex: this.findLeastUsedStyleIndex(
|
||||
this.createStyleIndexToCountMap(),
|
||||
),
|
||||
styleIndex: this.authorStyle,
|
||||
cursor: !this.acceptCursorUpdateProvider()
|
||||
? null
|
||||
: {
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
} from '@hedgedoc/commons';
|
||||
import { Mock } from 'ts-mockery';
|
||||
|
||||
import { User } from '../../../database/user.entity';
|
||||
import { FieldNameUser, User } from '../../../database/types';
|
||||
import { RealtimeConnection } from '../realtime-connection';
|
||||
import { RealtimeNote } from '../realtime-note';
|
||||
import { RealtimeUserStatusAdapter } from '../realtime-user-status-adapter';
|
||||
|
@ -21,14 +21,14 @@ enum RealtimeUserState {
|
|||
WITH_READONLY,
|
||||
}
|
||||
|
||||
const MOCK_FALLBACK_USERNAME: string = 'mock';
|
||||
|
||||
/**
|
||||
* Creates a mocked {@link RealtimeConnection realtime connection}.
|
||||
*/
|
||||
export class MockConnectionBuilder {
|
||||
private userId: number;
|
||||
private username: string | null;
|
||||
private displayName: string | undefined;
|
||||
private displayName: string;
|
||||
private authorStyle: number;
|
||||
private includeRealtimeUserStatus: RealtimeUserState =
|
||||
RealtimeUserState.WITHOUT;
|
||||
|
||||
|
@ -42,6 +42,8 @@ export class MockConnectionBuilder {
|
|||
public withGuestUser(displayName: string): this {
|
||||
this.username = null;
|
||||
this.displayName = displayName;
|
||||
this.authorStyle = 8;
|
||||
this.userId = 1000;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -50,10 +52,11 @@ export class MockConnectionBuilder {
|
|||
*
|
||||
* @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}.
|
||||
*/
|
||||
public withLoggedInUser(username?: string): this {
|
||||
const newUsername = username ?? MOCK_FALLBACK_USERNAME;
|
||||
this.username = newUsername;
|
||||
this.displayName = newUsername;
|
||||
public withLoggedInUser(username: string): this {
|
||||
this.username = username;
|
||||
this.displayName = username;
|
||||
this.userId = 1001;
|
||||
this.authorStyle = 1;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -80,16 +83,15 @@ export class MockConnectionBuilder {
|
|||
* @throws Error if neither withGuestUser nor withLoggedInUser has been called.
|
||||
*/
|
||||
public build(): RealtimeConnection {
|
||||
const displayName = this.deriveDisplayName();
|
||||
|
||||
const transporter = new MockMessageTransporter();
|
||||
transporter.setAdapter(new MockedBackendTransportAdapter(''));
|
||||
const realtimeUserStateAdapter: RealtimeUserStatusAdapter =
|
||||
this.includeRealtimeUserStatus === RealtimeUserState.WITHOUT
|
||||
? Mock.of<RealtimeUserStatusAdapter>({})
|
||||
: new RealtimeUserStatusAdapter(
|
||||
this.username ?? null,
|
||||
displayName,
|
||||
this.username,
|
||||
this.displayName,
|
||||
this.authorStyle,
|
||||
() =>
|
||||
this.realtimeNote
|
||||
.getConnections()
|
||||
|
@ -100,18 +102,19 @@ export class MockConnectionBuilder {
|
|||
RealtimeUserState.WITH_READWRITE,
|
||||
);
|
||||
|
||||
const mockUser =
|
||||
this.username === null
|
||||
? null
|
||||
: Mock.of<User>({
|
||||
username: this.username,
|
||||
displayName: this.displayName,
|
||||
});
|
||||
const mockUser = Mock.of<User>({
|
||||
[FieldNameUser.username]: this.username,
|
||||
[FieldNameUser.displayName]: this.displayName,
|
||||
[FieldNameUser.id]: this.userId,
|
||||
[FieldNameUser.authorStyle]: this.authorStyle,
|
||||
});
|
||||
const yDocSyncServerAdapter = Mock.of<YDocSyncServerAdapter>({});
|
||||
|
||||
const connection = Mock.of<RealtimeConnection>({
|
||||
getUser: jest.fn(() => mockUser),
|
||||
getDisplayName: jest.fn(() => displayName),
|
||||
getUserId: jest.fn(() => mockUser[FieldNameUser.id]),
|
||||
getUsername: jest.fn(() => mockUser[FieldNameUser.username]),
|
||||
getAuthorStyle: jest.fn(() => mockUser[FieldNameUser.authorStyle]),
|
||||
getDisplayName: jest.fn(() => mockUser[FieldNameUser.displayName]),
|
||||
getSyncAdapter: jest.fn(() => yDocSyncServerAdapter),
|
||||
getTransporter: jest.fn(() => transporter),
|
||||
getRealtimeUserStateAdapter: () => realtimeUserStateAdapter,
|
||||
|
@ -129,14 +132,4 @@ export class MockConnectionBuilder {
|
|||
|
||||
return connection;
|
||||
}
|
||||
|
||||
private deriveDisplayName(): string {
|
||||
if (this.displayName === undefined) {
|
||||
throw new Error(
|
||||
'Neither withGuestUser nor withLoggedInUser has been called.',
|
||||
);
|
||||
}
|
||||
|
||||
return this.displayName;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue