chore(esdoc): update and unify ESDoc and parameter names (2)

Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Erik Michelson 2025-05-29 21:59:40 +00:00
parent f47915dbb3
commit 3278e25dd1
No known key found for this signature in database
GPG key ID: DB99ADDDC5C0AF82
22 changed files with 273 additions and 129 deletions

View file

@ -10,7 +10,8 @@ export const PERMISSION_METADATA_KEY = 'requiredPermission';
/**
* This decorator gathers the {@link PermissionLevel} a user must hold for the {@link PermissionsGuard}
* @param permissionLevel the required permission for the decorated action.
* @param permissionLevel the required permission for the decorated action
* @returns The custom decorator action
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export function RequirePermission(

View file

@ -1,3 +0,0 @@
SPDX-FileCopyrightText: The author of https://www.randomlists.com/
SPDX-License-Identifier: CC0-1.0

View file

@ -12,7 +12,7 @@ import * as HedgeDocCommonsModule from '@hedgedoc/commons';
import { FieldNameUser, User } from '@hedgedoc/database';
import { Mock } from 'ts-mockery';
import * as NameRandomizerModule from './random-word-lists/name-randomizer';
import * as NameRandomizerModule from '../../users/random-word-lists/name-randomizer';
import { RealtimeConnection } from './realtime-connection';
import { RealtimeNote } from './realtime-note';
import {
@ -21,7 +21,7 @@ import {
} from './realtime-user-status-adapter';
import * as RealtimeUserStatusModule from './realtime-user-status-adapter';
jest.mock('./random-word-lists/name-randomizer');
jest.mock('../../users/random-word-lists/name-randomizer');
jest.mock('./realtime-user-status-adapter');
jest.mock(
'@hedgedoc/commons',

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@ -26,8 +26,8 @@ export class RealtimeConnection {
* @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.
* @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(
@ -62,34 +62,74 @@ export class RealtimeConnection {
);
}
/**
* Returns the realtime user state adapter of this connection.
*
* @returns the realtime user state adapter
*/
public getRealtimeUserStateAdapter(): RealtimeUserStatusAdapter {
return this.realtimeUserStateAdapter;
}
/**
* Returns the message transporter of this connection.
*
* @returns the message transporter
*/
public getTransporter(): MessageTransporter {
return this.transporter;
}
/**
* Returns the YDoc sync adapter of this connection.
*
* @returns the YDoc sync adapter
*/
public getSyncAdapter(): YDocSyncServerAdapter {
return this.yDocSyncAdapter;
}
/**
* Returns the user id of the user of this connection.
*
* @returns the user id
*/
public getUserId(): number {
return this.userId;
}
/**
* Returns the display name of the user of this connection.
*
* @returns the display name
*/
public getDisplayName(): string {
return this.displayName;
}
/**
* Returns the username of the user of this connection.
*
* @returns the username or null for guest users
*/
public getUsername(): string | null {
return this.username;
}
/**
* Returns the author style of the user of this connection.
*
* @returns the author style
*/
public getAuthorStyle(): number {
return this.authorStyle;
}
/**
* Returns the realtime note that this connection is connected to.
*
* @returns the realtime note
*/
public getRealtimeNote(): RealtimeNote {
return this.realtimeNote;
}

View file

@ -7,17 +7,21 @@ import { Injectable } from '@nestjs/common';
import { RealtimeNote } from './realtime-note';
/**
* A store for {@link RealtimeNote} instances that are linked to a specific note.
* It allows creating, finding, and retrieving all realtime notes.
*/
@Injectable()
export class RealtimeNoteStore {
private noteIdToRealtimeNote = new Map<number, RealtimeNote>();
/**
* Creates a new {@link RealtimeNote} for the given {@link Note} and memorizes it.
* Creates a new {@link RealtimeNote} for the given note and memorizes it
*
* @param noteId The note for which the realtime note should be created
* @param noteId The id of 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.
* @throws Error if there is already a realtime note for the given note.
* @returns The created realtime note
*/
public create(
@ -41,16 +45,19 @@ export class RealtimeNoteStore {
}
/**
* Retrieves a {@link RealtimeNote} that is linked to the given {@link Note} id.
* @param noteId The id of the {@link Note}
* @returns A {@link RealtimeNote} or {@code undefined} if no instance is existing.
* Retrieves a {@link RealtimeNote} that is linked to the given note id
*
* @param noteId The id of the note
* @returns A {@link RealtimeNote} or undefined if no instance is existing
*/
public find(noteId: number): RealtimeNote | undefined {
return this.noteIdToRealtimeNote.get(noteId);
}
/**
* Returns all registered {@link RealtimeNote realtime notes}.
* Returns all registered {@link RealtimeNote realtime notes}
*
* @returns An array of all realtime notes
*/
public getAllRealtimeNotes(): RealtimeNote[] {
return [...this.noteIdToRealtimeNote.values()];

View file

@ -31,6 +31,10 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
private permissionService: PermissionService,
) {}
/**
* Cleans up all {@link RealtimeNote} instances before the application is shut down
* This method is called by NestJS when the application is shutting down
*/
beforeApplicationShutdown(): void {
this.realtimeNoteStore
.getAllRealtimeNotes()
@ -38,7 +42,7 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
}
/**
* Reads the current content from the given {@link RealtimeNote} and creates a new {@link Revision} for the linked {@link Note}.
* Reads the current content from the given {@link RealtimeNote} and creates a new revision for the linked note.
*
* @param realtimeNote The realtime note for which a revision should be created
*/
@ -58,10 +62,11 @@ 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 noteId The {@link Note} for which a {@link RealtimeNote realtime note} should be retrieved.
* Creates or reuses a {@link RealtimeNote} that is handling the real-time-editing of the note which is identified by the given note id
*
* @param noteId The id of the note for which a {@link RealtimeNote} should be retrieved
* @returns A RealtimeNote that is linked to the given note.
* @throws NotInDBError if note doesn't exist or has no revisions.
* @returns A {@link RealtimeNote} that is linked to the given note.
*/
public async getOrCreateRealtimeNote(noteId: number): Promise<RealtimeNote> {
return (
@ -71,11 +76,12 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
}
/**
* Creates a new {@link RealtimeNote} for the given {@link Note}.
* Creates a new {@link RealtimeNote} for the given note and registers event listeners
* to persist the note periodically and before it is destroyed
*
* @param noteId The note for which the realtime note should be created
* @throws NotInDBError if note doesn't exist or has no revisions.
* @param noteId The id of the note for which the realtime note should be created
* @returns The created realtime note
* @throws NotInDBError if the note doesn't exist or has no revisions
*/
private async createNewRealtimeNote(noteId: number): Promise<RealtimeNote> {
const lastRevision = await this.revisionsService.getLatestRevision(noteId);
@ -117,16 +123,31 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
});
}
/**
* Reflects the changes of the note's permissions to all connections of the note
*
* @param noteId The id of the note for that permissions changed
*/
@OnEvent(NoteEvent.PERMISSION_CHANGE)
public async handleNotePermissionChanged(noteId: number): Promise<void> {
const realtimeNote = this.realtimeNoteStore.find(noteId);
if (!realtimeNote) return;
if (realtimeNote === undefined) {
return;
}
realtimeNote.announceMetadataUpdate();
const allConnections = realtimeNote.getConnections();
await this.updateOrCloseConnection(allConnections, noteId);
}
/**
* Updates the connections of the given note based on the current permissions of the user.
* If the user has no permission to edit the note, the connection is closed.
* Otherwise, it updates the acceptEdits property of the connection.
*
* @param connections The connections to update
* @param noteId The id of the note for which the connections should be updated
*/
private async updateOrCloseConnection(
connections: RealtimeConnection[],
noteId: number,
@ -145,6 +166,11 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
}
}
/**
* Reflects the deletion of a note to all connections of the note
*
* @param noteId The id of the just deleted note
*/
@OnEvent(NoteEvent.DELETION)
public handleNoteDeleted(noteId: number): void {
const realtimeNote = this.realtimeNoteStore.find(noteId);
@ -153,11 +179,16 @@ export class RealtimeNoteService implements BeforeApplicationShutdown {
}
}
/**
* Closes the realtime note for the given note id and saves its content
* This is called when the note is updated externally, e.g. by the API
*
* @param noteId The id of the note for which the realtime note should be closed
*/
@OnEvent(NoteEvent.CLOSE_REALTIME)
public closeRealtimeNote(noteId: number): void {
const realtimeNote = this.realtimeNoteStore.find(noteId);
if (realtimeNote) {
this.saveRealtimeNote(realtimeNote);
realtimeNote.destroy();
}
}

View file

@ -31,6 +31,13 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
private isClosing = false;
private destroyEventTimer: NodeJS.Timeout | null = null;
/**
* Creates a new realtime note for the given note id and initial text content
*
* @param noteId the id of the note that is being edited
* @param initialTextContent the initial text content of the note
* @param initialYjsState the initial yjs state of the note, if available
*/
constructor(
private readonly noteId: number,
initialTextContent: string,
@ -58,9 +65,8 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
}
/**
* Connects a new client to the note.
*
* For this purpose a {@link RealtimeConnection} is created and added to the client map.
* Connects a new client to the note
* For this purpose a {@link RealtimeConnection} is created and added to the client map
*
* @param client the websocket connection to the client
*/
@ -73,7 +79,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 client The websocket client that disconnects.
* @param client The websocket client that disconnects
*/
public removeClient(client: RealtimeConnection): void {
this.clients.delete(client);
@ -94,7 +100,7 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
}
/**
* Destroys the current realtime note by deleting the y-js doc and disconnecting all clients.
* Destroys the current realtime note by deleting the yjs doc and disconnecting all clients
*
* @throws Error if note has already been destroyed
*/
@ -112,60 +118,60 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
}
/**
* Checks if there's still clients connected to this note.
* Checks if there are still clients connected to this note
*
* @returns {@code true} if there a still clinets connected, otherwise {@code false}
* @returns true if there are still clients connected, otherwise false
*/
public hasConnections(): boolean {
return this.clients.size !== 0;
}
/**
* Returns all {@link RealtimeConnection WebsocketConnections} currently hold by this note.
* Returns all {@link RealtimeConnection} currently hold by this note
*
* @returns an array of {@link RealtimeConnection WebsocketConnections}
* @returns an array of {@link RealtimeConnection}s
*/
public getConnections(): RealtimeConnection[] {
return [...this.clients];
}
/**
* Get the {@link RealtimeDoc realtime note} of the note.
* Gets the {@link RealtimeDoc} for the note.
*
* @returns the {@link RealtimeDoc realtime note} of the note
* @returns the {@link RealtimeDoc} for the note
*/
public getRealtimeDoc(): RealtimeDoc {
return this.doc;
}
/**
* Get the {@link Note note} that is edited.
* Gets the id of the note that is edited.
*
* @returns the {@link Note note}
* @returns the note id
*/
public getNoteId(): number {
return this.noteId;
}
/**
* Announce to all clients that the metadata of the note have been changed.
* This could for example be a permission change or a revision being saved.
* Announces to all clients that the metadata of the note has been changed,
* for example on a permission change or a revision being saved
*/
public announceMetadataUpdate(): void {
this.sendToAllClients({ type: MessageType.METADATA_UPDATED });
}
/**
* Announce to all clients that the note has been deleted.
* Announces to all clients that the note has been deleted
*/
public announceNoteDeletion(): void {
this.sendToAllClients({ type: MessageType.DOCUMENT_DELETED });
}
/**
* Broadcasts the given content to all connected clients.
* Broadcasts the given content to all connected clients
*
* @param {Uint8Array} content The binary message to broadcast
* @param content The binary message to broadcast
*/
private sendToAllClients(content: Message<MessageType>): void {
this.getConnections().forEach((connection) => {
@ -174,7 +180,9 @@ export class RealtimeNote extends EventEmitter2<RealtimeNoteEventMap> {
}
/**
* Indicates if a realtime note is ready to get destroyed.
* Indicates if a realtime note is ready to get destroyed
*
* @returns true if the note can be destroyed, otherwise false
*/
private canBeDestroyed(): boolean {
return !this.hasConnections() && !this.isClosing;

View file

@ -19,6 +19,16 @@ export type OtherAdapterCollector = () => RealtimeUserStatusAdapter[];
export class RealtimeUserStatusAdapter {
private readonly realtimeUser: RealtimeUser;
/**
* Creates a new realtime user status adapter.
*
* @param username the username of the user, or null if the user is a guest
* @param displayName the display name of the user
* @param authorStyle the style index of the author
* @param collectOtherAdapters a function that returns all other adapters to send updates to
* @param messageTransporter the message transporter to use for sending messages
* @param acceptCursorUpdateProvider a function that returns whether cursor updates should be accepted
*/
constructor(
private readonly username: string | null,
private readonly displayName: string,
@ -31,6 +41,11 @@ export class RealtimeUserStatusAdapter {
this.bindRealtimeUserStateEvents();
}
/**
* Returns the current realtime user state
*
* @returns the current realtime user state
*/
private createInitialRealtimeUserState(): RealtimeUser {
return {
username: this.username,
@ -46,6 +61,9 @@ export class RealtimeUserStatusAdapter {
};
}
/**
* Registers the listeners for the realtime user state events
*/
private bindRealtimeUserStateEvents(): void {
const transporterMessagesListener = this.messageTransporter.on(
MessageType.REALTIME_USER_SINGLE_UPDATE,
@ -102,10 +120,19 @@ export class RealtimeUserStatusAdapter {
});
}
/**
* Gets the current real-time user state if the message transporter is ready
*
* @returns the current real-time user state or undefined if the transporter is not ready
*/
private getSendableState(): RealtimeUser | undefined {
return this.messageTransporter.isReady() ? this.realtimeUser : undefined;
}
/**
* Sends the current real-time user state to all other clients
* This includes the own user state and the states of all other users
*/
public sendCompleteStateToClient(): void {
if (!this.messageTransporter.isReady()) {
return;
@ -126,29 +153,4 @@ export class RealtimeUserStatusAdapter {
},
});
}
private findLeastUsedStyleIndex(map: Map<number, number>): number {
let leastUsedStyleIndex = 0;
let leastUsedStyleIndexCount = map.get(0) ?? 0;
for (let styleIndex = 0; styleIndex < 8; styleIndex++) {
const count = map.get(styleIndex) ?? 0;
if (count < leastUsedStyleIndexCount) {
leastUsedStyleIndexCount = count;
leastUsedStyleIndex = styleIndex;
}
}
return leastUsedStyleIndex;
}
private createStyleIndexToCountMap(): Map<number, number> {
return this.collectOtherAdapters()
.map((adapter) => adapter.realtimeUser.styleIndex)
.reduce((map, styleIndex) => {
if (styleIndex !== undefined) {
const count = (map.get(styleIndex) ?? 0) + 1;
map.set(styleIndex, count);
}
return map;
}, new Map<number, number>());
}
}

View file

@ -42,7 +42,7 @@ export class MockConnectionBuilder {
public withGuestUser(displayName: string): this {
this.username = null;
this.displayName = displayName;
this.authorStyle = 8;
this.authorStyle = 2;
this.userId = 1000;
return this;
}
@ -50,7 +50,7 @@ export class MockConnectionBuilder {
/**
* Defines that the user who belongs to this connection is a logged-in user.
*
* @param username the username of the mocked user. If this value is omitted then the builder will user a {@link MOCK_FALLBACK_USERNAME fallback}.
* @param username the username of the mocked user
*/
public withLoggedInUser(username: string): this {
this.username = username;
@ -79,7 +79,7 @@ export class MockConnectionBuilder {
/**
* Creates a new connection based on the given configuration.
*
* @returns {RealtimeConnection} The constructed mocked connection
* @returns The constructed mocked connection
* @throws Error if neither withGuestUser nor withLoggedInUser has been called.
*/
public build(): RealtimeConnection {

View file

@ -1,12 +1,12 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MessageTransporter, MessageType } from '@hedgedoc/commons';
/**
* A message transporter that is only used in testing where certain conditions like resending of requests isn't needed.
* A message transporter that is only used in testing where certain conditions like resending of requests aren't needed.
*/
export class MockMessageTransporter extends MessageTransporter {
protected startSendingOfReadyRequests(): void {

View file

@ -194,6 +194,13 @@ export class RevisionsService {
});
}
/**
* Get a revision by its UUID
*
* @param revisionUuid The UUID of the revision to get
* @returns The revision DTO
* @throws NotInDBError if the revision with the given UUID does not exist
*/
async getRevisionDto(revisionUuid: string): Promise<RevisionDto> {
const revision = await this.knex(TableRevision)
.select(
@ -225,9 +232,10 @@ export class RevisionsService {
}
/**
* Get the latest
* @param noteId
* @param transaction
* Gets the latest revision of a note
*
* @param noteId The id of the note for which the latest revision should be retrieved
* @param transaction The optional pre-existing database transaction to use
*/
async getLatestRevision(
noteId: number,
@ -249,6 +257,13 @@ export class RevisionsService {
return revision;
}
/**
* Gets the user information of the authors of a revision
*
* @param revisionUuid The UUID of the revision for which the user information should be retrieved
* @param transaction The optional pre-existing database transaction to use
* @returns An object containing the usernames and guest UUIDs of the authors and the count of guest users
*/
async getRevisionUserInfo(
revisionUuid: string,
transaction?: Knex,
@ -291,17 +306,15 @@ export class RevisionsService {
}
/**
* Creates (but does not persist(!)) a new {@link Revision} for the given {@link Note}.
* Useful if the revision is saved together with the note in one action.
*
* Creates a new revision for the given note
* This method wraps the actual action in a database transaction
*
* @param noteId The note for which the revision should be created
* @param newContent The new note content
* @param firstRevision Whether this is called for the first revision of a note
* @param transaction The optional pre-existing database transaction to use
* @param yjsStateVector The yjs state vector that describes the new content
* @returns {Revision} the created revision
* @returns {undefined} if the revision couldn't be created because e.g. the content hasn't changed
* @returns the created revision or undefined if the revision couldn't be created
*/
async createRevision(
noteId: number,
@ -331,6 +344,16 @@ export class RevisionsService {
);
}
/**
* Internal method to create a revision for the given note
* This method is used by the public createRevision method and should not be called directly
*
* @param noteId The note for which the revision should be created
* @param newContent The new note content
* @param firstRevision Whether this is called for the first revision of a note
* @param transaction The database transaction to use
* @param yjsStateVector The yjs state vector that describes the new content
*/
private async innerCreateRevision(
noteId: number,
newContent: string,
@ -388,6 +411,13 @@ export class RevisionsService {
}
}
/**
* Get all tags of a revision
*
* @param revisionUuid The UUID of the revision for which the tags should be retrieved
* @param transaction The optional pre-existing database transaction to use
* @returns An array of tags associated with the revision
*/
async getTagsByRevisionUuid(
revisionUuid: string,
transaction?: Knex,
@ -412,7 +442,7 @@ export class RevisionsService {
}
/**
* Delete old {@link Revision}s except the latest one.
* Deletes old revisions except the latest one if the clean-up is enabled
*/
async removeOldRevisions(): Promise<void> {
const currentTime = new Date().getTime();

View file

@ -29,9 +29,10 @@ interface FrontmatterParserResult {
}
/**
* Parses the frontmatter of the given content and extracts the metadata that are necessary to create a new revision..
* Parses the frontmatter of the given content and extracts the metadata that are necessary to create a new revision
*
* @param content the revision content that contains the frontmatter.
* @param content the revision content that contains the frontmatter
* @returns the extracted metadata, including the title, description, tags, and note type
*/
export function extractRevisionMetadataFromContent(
content: string,
@ -52,6 +53,13 @@ export function extractRevisionMetadataFromContent(
return { title, description, tags, noteType };
}
/**
* Generates the content of a revision without the frontmatter.
*
* @param firstLineOfContentIndex the index of the first line of content after the frontmatter
* @param content the full content including frontmatter
* @returns the content without frontmatter
*/
function generateContentWithoutFrontmatter(
firstLineOfContentIndex: number | undefined,
content: string,
@ -61,6 +69,12 @@ function generateContentWithoutFrontmatter(
: content.split('\n').slice(firstLineOfContentIndex).join('\n');
}
/**
* Parses the frontmatter from the given content and returns the parsed frontmatter and the index of the first line of content.
*
* @param content the content to parse
* @returns an object containing the parsed frontmatter and the index of the first line of content, or undefined if no frontmatter was found
*/
function parseFrontmatter(
content: string,
): FrontmatterParserResult | undefined {
@ -82,6 +96,12 @@ function parseFrontmatter(
};
}
/**
* Extracts the first heading from the given markdown content
*
* @param content the content to extract the first heading from
* @returns the first heading or undefined if no heading was found
*/
function extractFirstHeadingFromContent(content: string): string | undefined {
const markdownIt = new MarkdownIt('default');
const html = markdownIt.render(content);

View file

@ -60,7 +60,7 @@ export class SessionService {
}
/**
* Extracts the hedgedoc session cookie from the given {@link IncomingMessage request} and checks if the signature is correct.
* Extracts the HedgeDoc session cookie from the given {@link IncomingMessage request} and checks if the signature is correct.
*
* @param request The http request that contains a session cookie
* @returns An {@link Optional optional} that either contains the extracted session id or is empty if no session cookie has been found

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -6,7 +6,7 @@
import { adjectives, items } from './random-words';
/**
* Generates a random names based on an adjective and a noun.
* Generates a random name based on an adjective and a noun
*
* @returns the generated name
*/
@ -16,6 +16,12 @@ export function generateRandomName(): string {
return `${adjective} ${things}`;
}
/**
* Generates a random word from a given list and capitalizes the first letter
*
* @param list - The list of words to choose from
* @returns a randomly selected word with the first letter capitalized
*/
function generateRandomWord(list: string[]): string {
const index = Math.floor(Math.random() * list.length);
const word = list[index];

View file

@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/

View file

@ -23,7 +23,7 @@ import { v4 as uuidv4 } from 'uuid';
import { GenericDBError, NotInDBError } from '../errors/errors';
import { ConsoleLoggerService } from '../logger/console-logger.service';
import { generateRandomName } from '../realtime/realtime-note/random-word-lists/name-randomizer';
import { generateRandomName } from './random-word-lists/name-randomizer';
@Injectable()
export class UsersService {
@ -41,13 +41,13 @@ export class UsersService {
*
* @param username New user's username
* @param displayName New user's displayName
* @param [email] New user's email address if exists
* @param [photoUrl] URL of the user's profile picture if exists
* @param email New user's email address if exists
* @param photoUrl URL of the user's profile picture if exists
* @param transaction The optional transaction to access the db
* @returns The id of newly created user
* @throws {BadRequestException} if the username contains invalid characters or is too short
* @throws {AlreadyInDBError} the username is already taken.
* @thorws {GenericDBError} the database returned a non-expected value
* @throws BadRequestException if the username contains invalid characters or is too short
* @throws AlreadyInDBError if the username is already taken
* @thorws GenericDBError if the database returned a non-expected value
*/
async createUser(
username: string,
@ -97,7 +97,7 @@ export class UsersService {
* Creates a new guest user with a random displayName
*
* @returns The guest uuid and the id of the newly created user
* @throws {GenericDBError} the database returned a non-expected value
* @throws GenericDBError if the database returned a non-expected value
*/
async createGuestUser(): Promise<[string, number]> {
const randomName = generateRandomName();
@ -128,7 +128,7 @@ export class UsersService {
* Deletes a user by its id
*
* @param userId id of the user to be deleted
* @throws {NotInDBError} the username has no user associated with it
* @throws NotInDBError if the username has no user associated with it
*/
async deleteUser(userId: number): Promise<void> {
const usersDeleted = await this.knex(TableUser)
@ -229,7 +229,7 @@ export class UsersService {
*
* @param username The username to fetch
* @returns The found user object
* @throws {NotInDBError} if the user could not be found
* @throws NotInDBError if the user could not be found
*/
async getUserIdByUsername(username: string): Promise<number> {
const userId = await this.knex(TableUser)
@ -247,11 +247,11 @@ export class UsersService {
}
/**
* Fetches the userId for a given username from the database
* Fetches the userId for a given guest uuid from the database
*
* @param uuid The uuid to fetch
* @param uuid The guest uuid to fetch
* @returns The found user object
* @throws {NotInDBError} if the user could not be found
* @throws NotInDBError if the guest user could not be found
*/
async getUserIdByGuestUuid(uuid: string): Promise<User[FieldNameUser.id]> {
const userId = await this.knex(TableUser)
@ -273,7 +273,7 @@ export class UsersService {
*
* @param username The username to fetch
* @returns The found user object
* @throws {NotInDBError} if the user could not be found
* @throws NotInDBError if the user could not be found
*/
async getUserDtoByUsername(username: string): Promise<UserInfoDto> {
const user = await this.knex(TableUser)

View file

@ -1,9 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export function hasArrayDuplicates<T>(array: Array<T>): boolean {
return new Set(array).size !== array.length;
}

View file

@ -1,10 +0,0 @@
/*
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* The base class for all DTOs.
*/
export abstract class BaseDto {}

View file

@ -37,11 +37,11 @@ export async function checkPassword(
}
/**
* Transform a {@link Buffer} into a base64Url encoded string
* Transforms a {@link Buffer} into a base64Url encoded string
*
* This is necessary as the is no base64url encoding in the toString method
* This is necessary as there is no base64url encoding in the toString method
* but as can be seen on https://tools.ietf.org/html/rfc4648#page-7
* base64url is quite easy buildable from base64
* base64url is quite easily buildable from base64
*
* @param text The buffer we want to decode
* @returns The base64Url encoded string
@ -73,7 +73,7 @@ export function hashApiToken(token: string): string {
*
* @param userSecret The secret of the token the user gave us
* @param databaseSecretHash The secret hash we have saved in the database.
* @returns Wether or not the tokens are the equal
* @returns Whether or not the tokens are equal
*/
export function checkTokenEquality(
userSecret: string,

View file

@ -11,10 +11,10 @@ import { join as joinPath } from 'path';
let versionCache: ServerVersionDto | undefined = undefined;
/**
* Reads the HedgeDoc version from the root package.json. This is done only once per run.
* Reads the HedgeDoc version from the root package.json. This is done only once per run and then cached for further calls
*
* @returns {Promise<ServerVersionDto>} A Promise that contains the parsed server version.
* @throws {Error} if the package.json couldn't be found or doesn't contain a correct version.
* @returns A Promise that contains the parsed server version.
* @throws Error if the package.json couldn't be found or doesn't contain a correct version.
*/
export async function getServerVersionFromPackageJson(): Promise<ServerVersionDto> {
if (!versionCache) {
@ -23,6 +23,12 @@ export async function getServerVersionFromPackageJson(): Promise<ServerVersionDt
return versionCache;
}
/**
* Parses the version from the root package.json file.
*
* @returns A Promise that contains the parsed server version.
* @throws Error if the package.json couldn't be found or doesn't contain a correct version.
*/
async function parseVersionFromPackageJson(): Promise<ServerVersionDto> {
const rawFileContent: string = await fs.readFile(
joinPath(__dirname, '../../../package.json'),
@ -47,6 +53,11 @@ async function parseVersionFromPackageJson(): Promise<ServerVersionDto> {
};
}
/**
* Clears the cached version information
*
* This function is useful for testing purposes or when the version information needs to be reloaded
*/
export function clearCachedVersion(): void {
versionCache = undefined;
}

View file

@ -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
*/
@ -10,6 +10,11 @@ import { PrivateApiModule } from '../api/private/private-api.module';
import { PublicApiModule } from '../api/public/public-api.module';
import { getServerVersionFromPackageJson } from './server-version';
/**
* Sets up the public API documentation for HedgeDoc.
*
* @param app The NestJS application instance to set up the Swagger module on.
*/
export async function setupPublicApiDocs(app: INestApplication): Promise<void> {
const version = await getServerVersionFromPackageJson();
const publicApiOptions = new DocumentBuilder()
@ -26,6 +31,11 @@ export async function setupPublicApiDocs(app: INestApplication): Promise<void> {
SwaggerModule.setup('api/doc/v2', app, publicApi);
}
/**
* Sets up the private API documentation for HedgeDoc.
*
* @param app The NestJS application instance to set up the Swagger module on.
*/
export async function setupPrivateApiDocs(
app: INestApplication,
): Promise<void> {