diff --git a/src/api/public/me/me.controller.ts b/src/api/public/me/me.controller.ts index 4bbf8ccdb..2cb412f2b 100644 --- a/src/api/public/me/me.controller.ts +++ b/src/api/public/me/me.controller.ts @@ -17,15 +17,16 @@ import { Request, } from '@nestjs/common'; import { HistoryEntryUpdateDto } from '../../../history/history-entry-update.dto'; -import { HistoryEntryDto } from '../../../history/history-entry.dto'; import { HistoryService } from '../../../history/history.service'; import { ConsoleLoggerService } from '../../../logger/console-logger.service'; import { NoteMetadataDto } from '../../../notes/note-metadata.dto'; import { NotesService } from '../../../notes/notes.service'; -import { UserInfoDto } from '../../../users/user-info.dto'; import { UsersService } from '../../../users/users.service'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; import { ApiSecurity } from '@nestjs/swagger'; +import { HistoryEntryDto } from '../../../history/history-entry.dto'; +import { UserInfoDto } from '../../../users/user-info.dto'; +import { NotInDBError } from '../../../errors/errors'; @ApiSecurity('token') @Controller('me') @@ -49,19 +50,37 @@ export class MeController { @UseGuards(TokenAuthGuard) @Get('history') - getUserHistory(@Request() req): HistoryEntryDto[] { - return this.historyService.getUserHistory(req.user.userName); + async getUserHistory(@Request() req): Promise { + const foundEntries = await this.historyService.getEntriesByUser(req.user); + return Promise.all( + foundEntries.map( + async (entry) => await this.historyService.toHistoryEntryDto(entry), + ), + ); } @UseGuards(TokenAuthGuard) @Put('history/:note') - updateHistoryEntry( + async updateHistoryEntry( @Request() req, @Param('note') note: string, @Body() entryUpdateDto: HistoryEntryUpdateDto, - ): HistoryEntryDto { + ): Promise { // ToDo: Check if user is allowed to pin this history entry - return this.historyService.updateHistoryEntry(note, entryUpdateDto); + try { + return this.historyService.toHistoryEntryDto( + await this.historyService.updateHistoryEntry( + note, + req.user, + entryUpdateDto, + ), + ); + } catch (e) { + if (e instanceof NotInDBError) { + throw new NotFoundException(e.message); + } + throw e; + } } @UseGuards(TokenAuthGuard) @@ -70,9 +89,12 @@ export class MeController { deleteHistoryEntry(@Request() req, @Param('note') note: string) { // ToDo: Check if user is allowed to delete note try { - return this.historyService.deleteHistoryEntry(note); + return this.historyService.deleteHistoryEntry(note, req.user); } catch (e) { - throw new NotFoundException(e.message); + if (e instanceof NotInDBError) { + throw new NotFoundException(e.message); + } + throw e; } } diff --git a/src/api/public/notes/notes.controller.ts b/src/api/public/notes/notes.controller.ts index 4478356d5..b4f53e01c 100644 --- a/src/api/public/notes/notes.controller.ts +++ b/src/api/public/notes/notes.controller.ts @@ -28,6 +28,7 @@ import { RevisionsService } from '../../../revisions/revisions.service'; import { MarkdownBody } from '../../utils/markdownbody-decorator'; import { TokenAuthGuard } from '../../../auth/token-auth.guard'; import { ApiSecurity } from '@nestjs/swagger'; +import { HistoryService } from '../../../history/history.service'; import { NoteDto } from '../../../notes/note.dto'; import { NoteMetadataDto } from '../../../notes/note-metadata.dto'; import { RevisionMetadataDto } from '../../../revisions/revision-metadata.dto'; @@ -40,6 +41,7 @@ export class NotesController { private readonly logger: ConsoleLoggerService, private noteService: NotesService, private revisionsService: RevisionsService, + private historyService: HistoryService, ) { this.logger.setContext(NotesController.name); } @@ -57,6 +59,22 @@ export class NotesController { ); } + @UseGuards(TokenAuthGuard) + @Get(':noteIdOrAlias') + async getNote(@Request() req, @Param('noteIdOrAlias') noteIdOrAlias: string) { + // ToDo: check if user is allowed to view this note + try { + const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias); + await this.historyService.createOrUpdateHistoryEntry(note, req.user); + return this.noteService.toNoteDto(note); + } catch (e) { + if (e instanceof NotInDBError) { + throw new NotFoundException(e.message); + } + throw e; + } + } + @UseGuards(TokenAuthGuard) @Post(':noteAlias') async createNamedNote( @@ -71,25 +89,6 @@ export class NotesController { ); } - @UseGuards(TokenAuthGuard) - @Get(':noteIdOrAlias') - async getNote( - @Request() req, - @Param('noteIdOrAlias') noteIdOrAlias: string, - ): Promise { - // ToDo: check if user is allowed to view this note - try { - return this.noteService.toNoteDto( - await this.noteService.getNoteByIdOrAlias(noteIdOrAlias), - ); - } catch (e) { - if (e instanceof NotInDBError) { - throw new NotFoundException(e.message); - } - throw e; - } - } - @UseGuards(TokenAuthGuard) @Delete(':noteIdOrAlias') async deleteNote( diff --git a/src/history/history-entry.dto.ts b/src/history/history-entry.dto.ts index b990c92e4..2312c5a21 100644 --- a/src/history/history-entry.dto.ts +++ b/src/history/history-entry.dto.ts @@ -4,15 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsBoolean, ValidateNested } from 'class-validator'; -import { NoteMetadataDto } from '../notes/note-metadata.dto'; +import { IsArray, IsBoolean, IsDate, IsString } from 'class-validator'; export class HistoryEntryDto { /** - * Metadata of this note + * ID or Alias of the note */ - @ValidateNested() - metadata: NoteMetadataDto; + @IsString() + identifier: string; + + /** + * Title of the note + * Does not contain any markup but might be empty + * @example "Shopping List" + */ + @IsString() + title: string; + + /** + * Datestring of the last time this note was updated + * @example "2020-12-01 12:23:34" + */ + @IsDate() + lastVisited: Date; + + @IsArray() + @IsString({ each: true }) + tags: string[]; /** * True if this note is pinned diff --git a/src/history/history-entry.entity.ts b/src/history/history-entry.entity.ts index 597e137b4..11e59888c 100644 --- a/src/history/history-entry.entity.ts +++ b/src/history/history-entry.entity.ts @@ -4,12 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { - Column, - Entity, - ManyToOne, - UpdateDateColumn, -} from 'typeorm'; +import { Column, Entity, ManyToOne, UpdateDateColumn } from 'typeorm'; import { User } from '../users/user.entity'; import { Note } from '../notes/note.entity'; diff --git a/src/history/history.module.ts b/src/history/history.module.ts index 507845309..410d50863 100644 --- a/src/history/history.module.ts +++ b/src/history/history.module.ts @@ -7,10 +7,19 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '../logger/logger.module'; import { HistoryService } from './history.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { HistoryEntry } from './history-entry.entity'; +import { UsersModule } from '../users/users.module'; +import { NotesModule } from '../notes/notes.module'; @Module({ providers: [HistoryService], exports: [HistoryService], - imports: [LoggerModule], + imports: [ + LoggerModule, + TypeOrmModule.forFeature([HistoryEntry]), + UsersModule, + NotesModule, + ], }) export class HistoryModule {} diff --git a/src/history/history.service.ts b/src/history/history.service.ts index 2a0c9d736..16343b16d 100644 --- a/src/history/history.service.ts +++ b/src/history/history.service.ts @@ -8,90 +8,98 @@ import { Injectable } from '@nestjs/common'; import { ConsoleLoggerService } from '../logger/console-logger.service'; import { HistoryEntryUpdateDto } from './history-entry-update.dto'; import { HistoryEntryDto } from './history-entry.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { HistoryEntry } from './history-entry.enity'; +import { UsersService } from '../users/users.service'; +import { NotesService } from '../notes/notes.service'; +import { User } from '../users/user.entity'; +import { Note } from '../notes/note.entity'; +import { NotInDBError } from '../errors/errors'; @Injectable() export class HistoryService { - constructor(private readonly logger: ConsoleLoggerService) { + constructor( + private readonly logger: ConsoleLoggerService, + @InjectRepository(HistoryEntry) + private historyEntryRepository: Repository, + private usersService: UsersService, + private notesService: NotesService, + ) { this.logger.setContext(HistoryService.name); } - getUserHistory(username: string): HistoryEntryDto[] { - //TODO: Use the database - this.logger.warn('Using hardcoded data!'); - return [ - { - metadata: { - alias: null, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], - id: 'foobar-barfoo', - permissions: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, - tags: [], - title: 'Title!', - updateTime: new Date(), - updateUser: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, - }, - pinStatus: false, - }, - ]; + async getEntriesByUser(user: User): Promise { + return await this.historyEntryRepository.find({ + where: { user: user }, + relations: ['note'], + }); } - updateHistoryEntry( - noteId: string, - updateDto: HistoryEntryUpdateDto, - ): HistoryEntryDto { - //TODO: Use the database - this.logger.warn('Using hardcoded data!'); - return { - metadata: { - alias: null, - createTime: new Date(), - description: 'Very descriptive text.', - editedBy: [], - id: 'foobar-barfoo', - permissions: { - owner: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - sharedToUsers: [], - sharedToGroups: [], - }, - tags: [], - title: 'Title!', - updateTime: new Date(), - updateUser: { - displayName: 'foo', - userName: 'fooUser', - email: 'foo@example.com', - photo: '', - }, - viewCount: 42, + private async getEntryByNoteIdOrAlias( + noteIdOrAlias: string, + user: User, + ): Promise { + const note = await this.notesService.getNoteByIdOrAlias(noteIdOrAlias); + return await this.getEntryByNote(note, user); + } + + private async getEntryByNote(note: Note, user: User): Promise { + return await this.historyEntryRepository.findOne({ + where: { + note: note, + user: user, }, - pinStatus: updateDto.pinStatus, + relations: ['note', 'user'], + }); + } + + async createOrUpdateHistoryEntry( + note: Note, + user: User, + ): Promise { + let entry = await this.getEntryByNote(note, user); + if (!entry) { + entry = HistoryEntry.create(user, note); + } else { + entry.updatedAt = new Date(); + } + return await this.historyEntryRepository.save(entry); + } + + async updateHistoryEntry( + noteIdOrAlias: string, + user: User, + updateDto: HistoryEntryUpdateDto, + ): Promise { + const entry = await this.getEntryByNoteIdOrAlias(noteIdOrAlias, user); + if (!entry) { + throw new NotInDBError( + `User '${user.userName}' has no HistoryEntry for Note with id '${noteIdOrAlias}'`, + ); + } + entry.pinStatus = updateDto.pinStatus; + return this.historyEntryRepository.save(entry); + } + + async deleteHistoryEntry(noteIdOrAlias: string, user: User): Promise { + const entry = await this.getEntryByNoteIdOrAlias(noteIdOrAlias, user); + if (!entry) { + throw new NotInDBError( + `User '${user.userName}' has no HistoryEntry for Note with id '${noteIdOrAlias}'`, + ); + } + await this.historyEntryRepository.remove(entry); + return; + } + + async toHistoryEntryDto(entry: HistoryEntry): Promise { + return { + identifier: entry.note.alias ? entry.note.alias : entry.note.id, + lastVisited: entry.updatedAt, + tags: this.notesService.toTagList(entry.note), + title: entry.note.title, + pinStatus: entry.pinStatus, }; } - - deleteHistoryEntry(note: string) { - //TODO: Use the database and actually do stuff - throw new Error('Not implemented'); - } } diff --git a/src/notes/notes.service.ts b/src/notes/notes.service.ts index 8a377f482..aa03546ce 100644 --- a/src/notes/notes.service.ts +++ b/src/notes/notes.service.ts @@ -177,6 +177,10 @@ export class NotesService { return this.getCurrentContent(note); } + toTagList(note: Note): string[] { + return note.tags.map((tag) => tag.name); + } + async toNotePermissionsDto(note: Note): Promise { return { owner: this.usersService.toUserDto(note.owner), @@ -204,7 +208,7 @@ export class NotesService { ), // TODO: Extract into method permissions: await this.toNotePermissionsDto(note), - tags: note.tags.map((tag) => tag.name), + tags: this.toTagList(note), updateTime: (await this.getLatestRevision(note)).createdAt, // TODO: Get actual updateUser updateUser: { diff --git a/test/public-api/users.e2e-spec.ts b/test/public-api/users.e2e-spec.ts index fd4689c3d..4bcea119c 100644 --- a/test/public-api/users.e2e-spec.ts +++ b/test/public-api/users.e2e-spec.ts @@ -85,7 +85,7 @@ describe('Notes', () => { .delete('/me/history/test3') .expect(204); expect(response.body.content).toBeNull(); - const history = historyService.getUserHistory('testuser'); + const history = historyService.getEntriesByUser('testuser'); let historyEntry: HistoryEntryDto = null; for (const e of history) { if (e.metadata.alias === noteName) { @@ -106,7 +106,7 @@ describe('Notes', () => { .send(historyEntryUpdateDto) .expect(200); // TODO parameter is not used for now - const history = historyService.getUserHistory('testuser'); + const history = historyService.getEntriesByUser('testuser'); let historyEntry: HistoryEntryDto; for (const e of response.body.content) { if ((e).metadata.alias === noteName) {