mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-23 03:27:05 -04:00
wip: refactoring to knex and general chores, starting with User
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> 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
7adce05412
198 changed files with 3865 additions and 5899 deletions
369
backend/src/notes/note.service.ts
Normal file
369
backend/src/notes/note.service.ts
Normal file
|
@ -0,0 +1,369 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import {
|
||||
NoteDto,
|
||||
NoteMetadataDto,
|
||||
NotePermissionsDto,
|
||||
SpecialGroup,
|
||||
} from '@hedgedoc/commons';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Knex } from 'knex';
|
||||
import { InjectConnection } from 'nest-knexjs';
|
||||
|
||||
import { AliasService } from '../alias/alias.service';
|
||||
import { DefaultAccessLevel } from '../config/default-access-level.enum';
|
||||
import noteConfiguration, { NoteConfig } from '../config/note.config';
|
||||
import {
|
||||
FieldNameAlias,
|
||||
FieldNameNote,
|
||||
Note,
|
||||
TableAlias,
|
||||
TableNote,
|
||||
User,
|
||||
} from '../database/types';
|
||||
import {
|
||||
ForbiddenIdError,
|
||||
GenericDBError,
|
||||
MaximumDocumentLengthExceededError,
|
||||
NotInDBError,
|
||||
} from '../errors/errors';
|
||||
import { NoteEventMap } from '../events';
|
||||
import { GroupsService } from '../groups/groups.service';
|
||||
import { ConsoleLoggerService } from '../logger/console-logger.service';
|
||||
import { PermissionService } from '../permissions/permission.service';
|
||||
import { RealtimeNoteStore } from '../realtime/realtime-note/realtime-note-store';
|
||||
import { RealtimeNoteService } from '../realtime/realtime-note/realtime-note.service';
|
||||
import { RevisionsService } from '../revisions/revisions.service';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { getPrimaryAlias } from './utils';
|
||||
|
||||
@Injectable()
|
||||
export class NoteService {
|
||||
constructor(
|
||||
@InjectConnection()
|
||||
private readonly knex: Knex,
|
||||
|
||||
private readonly logger: ConsoleLoggerService,
|
||||
@Inject(UsersService) private usersService: UsersService,
|
||||
@Inject(GroupsService) private groupsService: GroupsService,
|
||||
private revisionsService: RevisionsService,
|
||||
@Inject(noteConfiguration.KEY)
|
||||
private noteConfig: NoteConfig,
|
||||
@Inject(AliasService)
|
||||
private aliasService: AliasService,
|
||||
@Inject(PermissionService) private permissionService: PermissionService,
|
||||
private realtimeNoteService: RealtimeNoteService,
|
||||
private realtimeNoteStore: RealtimeNoteStore,
|
||||
private eventEmitter: EventEmitter2<NoteEventMap>,
|
||||
) {
|
||||
this.logger.setContext(NoteService.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notes owned by a user
|
||||
*
|
||||
* @param userId The id of the user who owns the notes
|
||||
* @return Array of notes owned by the user
|
||||
*/
|
||||
async getUserNotes(userId: number): Promise<Note[]> {
|
||||
// noinspection ES6RedundantAwait
|
||||
return await this.knex(TableNote)
|
||||
.select()
|
||||
.where(FieldNameNote.ownerId, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new note
|
||||
*
|
||||
* @param noteContent The content of the new note, in most cases an empty string
|
||||
* @param givenAlias An optional alias the note should have
|
||||
* @param ownerUserId The owner of the note
|
||||
* @return The newly created note
|
||||
* @throws {AlreadyInDBError} a note with the requested id or aliases already exists
|
||||
* @throws {ForbiddenIdError} the requested id or aliases is forbidden
|
||||
* @throws {MaximumDocumentLengthExceededError} the noteContent is longer than the maxDocumentLength
|
||||
* @thorws {GenericDBError} the database returned a non-expected value
|
||||
*/
|
||||
async createNote(
|
||||
noteContent: string,
|
||||
ownerUserId: number,
|
||||
givenAlias?: string,
|
||||
): Promise<number> {
|
||||
// Check if new note doesn't violate application constraints
|
||||
if (noteContent.length > this.noteConfig.maxDocumentLength) {
|
||||
throw new MaximumDocumentLengthExceededError();
|
||||
}
|
||||
return await this.knex.transaction(async (transaction) => {
|
||||
// Create note itself in the database
|
||||
const createdNotes = await transaction(TableNote).insert(
|
||||
{
|
||||
[FieldNameNote.ownerId]: ownerUserId,
|
||||
[FieldNameNote.version]: 2,
|
||||
},
|
||||
[FieldNameNote.id],
|
||||
);
|
||||
|
||||
if (createdNotes.length !== 1) {
|
||||
throw new GenericDBError(
|
||||
'The note could not be created in the database',
|
||||
this.logger.getContext(),
|
||||
'createNote',
|
||||
);
|
||||
}
|
||||
|
||||
const noteId = createdNotes[0][FieldNameNote.id];
|
||||
|
||||
if (givenAlias !== undefined) {
|
||||
await this.aliasService.ensureAliasIsAvailable(givenAlias, transaction);
|
||||
}
|
||||
const newAlias =
|
||||
givenAlias === undefined
|
||||
? this.aliasService.generateRandomAlias()
|
||||
: givenAlias;
|
||||
await this.aliasService.addAlias(noteId, newAlias, transaction);
|
||||
|
||||
await this.revisionsService.createRevision(
|
||||
noteId,
|
||||
noteContent,
|
||||
transaction,
|
||||
);
|
||||
|
||||
const isUserRegistered = await this.usersService.isRegisteredUser(
|
||||
ownerUserId,
|
||||
transaction,
|
||||
);
|
||||
|
||||
const everyoneAccessLevel = isUserRegistered
|
||||
? // Use the default access level from the config for registered users
|
||||
this.noteConfig.permissions.default.everyone
|
||||
: // If the owner is a guest, this is an anonymous note
|
||||
// Anonymous notes are always writeable by everyone
|
||||
DefaultAccessLevel.WRITE;
|
||||
|
||||
const loggedInUsersAccessLevel =
|
||||
this.noteConfig.permissions.default.loggedIn;
|
||||
|
||||
await this.permissionService.setGroupPermission(
|
||||
noteId,
|
||||
SpecialGroup.EVERYONE,
|
||||
everyoneAccessLevel,
|
||||
transaction,
|
||||
);
|
||||
|
||||
await this.permissionService.setGroupPermission(
|
||||
noteId,
|
||||
SpecialGroup.LOGGED_IN,
|
||||
loggedInUsersAccessLevel,
|
||||
transaction,
|
||||
);
|
||||
|
||||
return noteId;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current content of the note.
|
||||
* @param noteId the note to use
|
||||
* @throws {NotInDBError} the note is not in the DB
|
||||
* @return {string} the content of the note
|
||||
*/
|
||||
async getNoteContent(noteId: Note[FieldNameNote.id]): Promise<string> {
|
||||
const realtimeContent = this.realtimeNoteStore
|
||||
.find(noteId)
|
||||
?.getRealtimeDoc()
|
||||
.getCurrentContent();
|
||||
if (realtimeContent) {
|
||||
return realtimeContent;
|
||||
}
|
||||
|
||||
const latestRevision =
|
||||
await this.revisionsService.getLatestRevision(noteId);
|
||||
return latestRevision.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a note by either their id or aliases.
|
||||
* @param alias the notes id or aliases
|
||||
* @throws {NotInDBError} there is no note with this id or aliases
|
||||
* @throws {ForbiddenIdError} the requested id or aliases is forbidden
|
||||
* @return the note id
|
||||
*/
|
||||
async getNoteIdByAlias(alias: string, transaction?: Knex): Promise<number> {
|
||||
const dbActor = transaction ?? this.knex;
|
||||
const isForbidden = this.aliasService.isAliasForbidden(alias);
|
||||
if (isForbidden) {
|
||||
throw new ForbiddenIdError(
|
||||
`The note id or alias '${alias}' is forbidden by the administrator.`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug(`Trying to find note '${alias}'`, 'getNoteIdByAlias');
|
||||
|
||||
/**
|
||||
* This query gets the note's aliases, owner, groupPermissions (and the groups), userPermissions (and the users) and tags and
|
||||
* then only gets the note, that either has a publicId :noteIdOrAlias or has any aliases with this name.
|
||||
**/
|
||||
const note = await dbActor(TableAlias)
|
||||
.select<Pick<Note, FieldNameNote.id>>(`${TableNote}.${FieldNameNote.id}`)
|
||||
.where(FieldNameAlias.alias, alias)
|
||||
.join(
|
||||
TableNote,
|
||||
`${TableAlias}.${FieldNameAlias.noteId}`,
|
||||
`${TableNote}.${FieldNameNote.id}`,
|
||||
)
|
||||
.first();
|
||||
|
||||
if (note === undefined) {
|
||||
const message = `Could not find note '${alias}'`;
|
||||
this.logger.debug(message, 'getNoteIdByAlias');
|
||||
throw new NotInDBError(message);
|
||||
}
|
||||
this.logger.debug(`Found note '${alias}'`, 'getNoteIdByAlias');
|
||||
return note[FieldNameNote.id];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users that ever appeared as an author for the given note
|
||||
* @param note The note to search authors for
|
||||
*/
|
||||
async getAuthorUsers(note: Note): Promise<User[]> {
|
||||
// return await this.userRepository
|
||||
// .createQueryBuilder('user')
|
||||
// .innerJoin('user.authors', 'author')
|
||||
// .innerJoin('author.edits', 'edit')
|
||||
// .innerJoin('edit.revisions', 'revision')
|
||||
// .innerJoin('revision.note', 'note')
|
||||
// .where('note.id = :id', { id: note.id })
|
||||
// .getMany();
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a note
|
||||
*
|
||||
* @param noteId If of the note to delete
|
||||
* @throws {NotInDBError} if there is no note with this id
|
||||
*/
|
||||
async deleteNote(noteId: Note[FieldNameNote.id]): Promise<void> {
|
||||
const numberOfDeletedNotes = await this.knex(TableNote)
|
||||
.where(FieldNameNote.id, noteId)
|
||||
.delete();
|
||||
if (numberOfDeletedNotes === 0) {
|
||||
throw new NotInDBError(
|
||||
`There is no note with the id '${noteId}' to delete.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Update the content of a note
|
||||
*
|
||||
* @param noteId - the note
|
||||
* @param noteContent - the new content
|
||||
* @return the note with a new revision and new content
|
||||
* @throws {NotInDBError} there is no note with this id or aliases
|
||||
*/
|
||||
async updateNote(noteId: number, noteContent: string): Promise<void> {
|
||||
// TODO Disconnect realtime clients first
|
||||
await this.revisionsService.createRevision(noteId, noteContent);
|
||||
// TODO Reload realtime note
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Calculate the updateUser (for the NoteDto) for a Note.
|
||||
* @param {Note} noteId - the note to use
|
||||
* @return {User} user to be used as updateUser in the NoteDto
|
||||
*/
|
||||
async getLastUpdatedNoteUser(noteId: number): Promise<number> {
|
||||
const lastRevision = await this.revisionsService.getLatestRevision(noteId);
|
||||
// const edits = await lastRevision.edits;
|
||||
// if (edits.length > 0) {
|
||||
// // Sort the last Revisions Edits by their updatedAt Date to get the latest one
|
||||
// // the user of that Edit is the updateUser
|
||||
// return await (
|
||||
// await edits.sort(
|
||||
// (a, b) => b.updatedAt.getTime() - a.updatedAt.getTime(),
|
||||
// )[0].author
|
||||
// ).user;
|
||||
// }
|
||||
// // If there are no Edits, the owner is the updateUser
|
||||
// return await noteId.owner;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build NotePermissionsDto from a note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {NotePermissionsDto} the built NotePermissionDto
|
||||
*/
|
||||
async toNotePermissionsDto(noteId: number): Promise<NotePermissionsDto> {
|
||||
const owner = await note.owner;
|
||||
const userPermissions = await note.userPermissions;
|
||||
const groupPermissions = await note.groupPermissions;
|
||||
return {
|
||||
owner: owner ? owner.username : null,
|
||||
sharedToUsers: await Promise.all(
|
||||
userPermissions.map(async (noteUserPermission) => ({
|
||||
username: (await noteUserPermission.user).username,
|
||||
canEdit: noteUserPermission.canEdit,
|
||||
})),
|
||||
),
|
||||
sharedToGroups: await Promise.all(
|
||||
groupPermissions.map(async (noteGroupPermission) => ({
|
||||
groupName: (await noteGroupPermission.group).name,
|
||||
canEdit: noteGroupPermission.canEdit,
|
||||
})),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Build NoteMetadataDto from a note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {NoteMetadataDto} the built NoteMetadataDto
|
||||
*/
|
||||
async toNoteMetadataDto(note: Note): Promise<NoteMetadataDto> {
|
||||
const updateUser = await this.getLastUpdatedNoteUser(note);
|
||||
const latestRevision = await this.revisionsService.getLatestRevision(note);
|
||||
return {
|
||||
id: note.publicId,
|
||||
aliases: await Promise.all(
|
||||
(await note.aliases).map((alias) =>
|
||||
this.aliasService.toAliasDto(alias, note),
|
||||
),
|
||||
),
|
||||
primaryAddress: (await getPrimaryAlias(note)) ?? note.publicId,
|
||||
title: latestRevision.title,
|
||||
description: latestRevision.description,
|
||||
tags: (await latestRevision.tags).map((tag) => tag.name),
|
||||
createdAt: note.createdAt,
|
||||
editedBy: (await this.getAuthorUsers(note)).map((user) => user.username),
|
||||
permissions: await this.toNotePermissionsDto(note),
|
||||
version: note.version,
|
||||
updatedAt: latestRevision.createdAt,
|
||||
updateUsername: updateUser ? updateUser.username : null,
|
||||
viewCount: note.viewCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @async
|
||||
* Build NoteDto from a note.
|
||||
* @param {Note} note - the note to use
|
||||
* @return {NoteDto} the built NoteDto
|
||||
*/
|
||||
async toNoteDto(note: Note): Promise<NoteDto> {
|
||||
return {
|
||||
content: await this.getNoteContent(note),
|
||||
metadata: await this.toNoteMetadataDto(note),
|
||||
editedByAtPosition: [],
|
||||
};
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue