From 085241999b493fefb3cd693411f2f4ad8de328f7 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sat, 10 Apr 2021 23:26:58 +0200 Subject: [PATCH 1/3] PrivateAPI: Rename HistoryEntryCreationDto to HistoryEntryImportDto As the DTO is used to import a whole list of history entries rather than creating a single history entry (there is no way of doing that at the moment) Signed-off-by: Philip Molares --- src/api/private/me/history/history.controller.ts | 4 ++-- ...tory-entry-creation.dto.ts => history-entry-import.dto.ts} | 2 +- test/private-api/history.e2e-spec.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/history/{history-entry-creation.dto.ts => history-entry-import.dto.ts} (86%) diff --git a/src/api/private/me/history/history.controller.ts b/src/api/private/me/history/history.controller.ts index 85203b78b..3274720c5 100644 --- a/src/api/private/me/history/history.controller.ts +++ b/src/api/private/me/history/history.controller.ts @@ -19,7 +19,7 @@ import { UsersService } from '../../../../users/users.service'; import { NotesService } from '../../../../notes/notes.service'; import { HistoryEntryDto } from '../../../../history/history-entry.dto'; import { NotInDBError } from '../../../../errors/errors'; -import { HistoryEntryCreationDto } from '../../../../history/history-entry-creation.dto'; +import { HistoryEntryImportDto } from '../../../../history/history-entry-import.dto'; import { HistoryEntryUpdateDto } from '../../../../history/history-entry-update.dto'; import { ConsoleLoggerService } from '../../../../logger/console-logger.service'; import { HistoryService } from '../../../../history/history.service'; @@ -55,7 +55,7 @@ export class HistoryController { @Post() async setHistory( - @Body('history') history: HistoryEntryCreationDto[], + @Body('history') history: HistoryEntryImportDto[], ): Promise { try { // ToDo: use actual user here diff --git a/src/history/history-entry-creation.dto.ts b/src/history/history-entry-import.dto.ts similarity index 86% rename from src/history/history-entry-creation.dto.ts rename to src/history/history-entry-import.dto.ts index aa565ca1d..122024f3f 100644 --- a/src/history/history-entry-creation.dto.ts +++ b/src/history/history-entry-import.dto.ts @@ -6,7 +6,7 @@ import { IsString } from 'class-validator'; -export class HistoryEntryCreationDto { +export class HistoryEntryImportDto { /** * ID or Alias of the note */ diff --git a/test/private-api/history.e2e-spec.ts b/test/private-api/history.e2e-spec.ts index c7c81f2dd..07ad0bf72 100644 --- a/test/private-api/history.e2e-spec.ts +++ b/test/private-api/history.e2e-spec.ts @@ -31,7 +31,7 @@ import { UsersModule } from '../../src/users/users.module'; import { PrivateApiModule } from '../../src/api/private/private-api.module'; import { HistoryService } from '../../src/history/history.service'; import { Note } from '../../src/notes/note.entity'; -import { HistoryEntryCreationDto } from '../../src/history/history-entry-creation.dto'; +import { HistoryEntryImportDto } from '../../src/history/history-entry-import.dto'; describe('History', () => { let app: INestApplication; @@ -105,7 +105,7 @@ describe('History', () => { }); it('POST /me/history', async () => { - const postEntryDto = new HistoryEntryCreationDto(); + const postEntryDto = new HistoryEntryImportDto(); postEntryDto.note = note2.alias; await request(app.getHttpServer()) .post('/me/history') From 4f858c51d2306fa5ca699f92d76f61f26c7ab60f Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sat, 10 Apr 2021 23:31:18 +0200 Subject: [PATCH 2/3] PrivateAPI: Add pinStatus to HistoryEntryImportDto As the DTO is only for importing an existing history the pinStatus of those entries should also be posted. Signed-off-by: Philip Molares --- .../private/me/history/history.controller.ts | 6 +- src/history/history-entry-import.dto.ts | 8 ++- src/history/history.service.spec.ts | 55 +++++++++++++++++-- src/history/history.service.ts | 5 ++ test/private-api/history.e2e-spec.ts | 4 ++ 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/api/private/me/history/history.controller.ts b/src/api/private/me/history/history.controller.ts index 3274720c5..d74795847 100644 --- a/src/api/private/me/history/history.controller.ts +++ b/src/api/private/me/history/history.controller.ts @@ -65,7 +65,11 @@ export class HistoryController { const note = await this.noteService.getNoteByIdOrAlias( historyEntry.note, ); - await this.historyService.createOrUpdateHistoryEntry(note, user); + await this.historyService.createOrUpdateHistoryEntry( + note, + user, + historyEntry.pinStatus, + ); } } catch (e) { if (e instanceof NotInDBError) { diff --git a/src/history/history-entry-import.dto.ts b/src/history/history-entry-import.dto.ts index 122024f3f..edc8b7c1e 100644 --- a/src/history/history-entry-import.dto.ts +++ b/src/history/history-entry-import.dto.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsString } from 'class-validator'; +import { IsBoolean, IsString } from 'class-validator'; export class HistoryEntryImportDto { /** @@ -12,4 +12,10 @@ export class HistoryEntryImportDto { */ @IsString() note: string; + /** + * True if the note should be pinned + * @example true + */ + @IsBoolean() + pinStatus: boolean; } diff --git a/src/history/history.service.spec.ts b/src/history/history.service.spec.ts index 985215f99..f823b8400 100644 --- a/src/history/history.service.spec.ts +++ b/src/history/history.service.spec.ts @@ -149,9 +149,10 @@ describe('HistoryService', () => { describe('createOrUpdateHistoryEntry', () => { describe('works', () => { - it('without an preexisting entry', async () => { - const user = {} as User; - const alias = 'alias'; + const user = {} as User; + const alias = 'alias'; + const pinStatus = true; + it('without an preexisting entry and without pinStatus', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest .spyOn(historyRepo, 'save') @@ -168,9 +169,25 @@ describe('HistoryService', () => { expect(createHistoryEntry.pinStatus).toEqual(false); }); - it('with an preexisting entry', async () => { - const user = {} as User; - const alias = 'alias'; + it('without an preexisting entry and with pinStatus', async () => { + jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); + jest + .spyOn(historyRepo, 'save') + .mockImplementation( + async (entry: HistoryEntry): Promise => entry, + ); + const createHistoryEntry = await service.createOrUpdateHistoryEntry( + Note.create(user, alias), + user, + pinStatus, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(pinStatus); + }); + + it('with an preexisting entry and without pinStatus', async () => { const historyEntry = HistoryEntry.create( user, Note.create(user, alias), @@ -193,6 +210,32 @@ describe('HistoryService', () => { historyEntry.updatedAt.getTime(), ); }); + + it('with an preexisting entry and with pinStatus', async () => { + const historyEntry = HistoryEntry.create( + user, + Note.create(user, alias), + pinStatus, + ); + jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); + jest + .spyOn(historyRepo, 'save') + .mockImplementation( + async (entry: HistoryEntry): Promise => entry, + ); + const createHistoryEntry = await service.createOrUpdateHistoryEntry( + Note.create(user, alias), + user, + pinStatus, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(pinStatus); + expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual( + historyEntry.updatedAt.getTime(), + ); + }); }); }); diff --git a/src/history/history.service.ts b/src/history/history.service.ts index d2793ac04..de4ad0264 100644 --- a/src/history/history.service.ts +++ b/src/history/history.service.ts @@ -80,15 +80,20 @@ export class HistoryService { * Create or update a history entry by the user and note. If the entry is merely updated the updatedAt date is set to the current date. * @param {Note} note - the note that the history entry belongs to * @param {User} user - the user that the history entry belongs to + * @param {boolean} pinStatus - if the pinStatus of the history entry should be set * @return {HistoryEntry} the requested history entry */ async createOrUpdateHistoryEntry( note: Note, user: User, + pinStatus?: boolean, ): Promise { let entry = await this.getEntryByNote(note, user); if (!entry) { entry = HistoryEntry.create(user, note); + if (pinStatus !== undefined) { + entry.pinStatus = pinStatus; + } } else { entry.updatedAt = new Date(); } diff --git a/test/private-api/history.e2e-spec.ts b/test/private-api/history.e2e-spec.ts index 07ad0bf72..3dbf1db90 100644 --- a/test/private-api/history.e2e-spec.ts +++ b/test/private-api/history.e2e-spec.ts @@ -105,8 +105,10 @@ describe('History', () => { }); it('POST /me/history', async () => { + const pinStatus = true; const postEntryDto = new HistoryEntryImportDto(); postEntryDto.note = note2.alias; + postEntryDto.pinStatus = pinStatus; await request(app.getHttpServer()) .post('/me/history') .set('Content-Type', 'application/json') @@ -115,6 +117,8 @@ describe('History', () => { const userEntries = await historyService.getEntriesByUser(user); expect(userEntries.length).toEqual(1); expect(userEntries[0].note.alias).toEqual(note2.alias); + expect(userEntries[0].user.userName).toEqual(user.userName); + expect(userEntries[0].pinStatus).toEqual(pinStatus); }); it('DELETE /me/history', async () => { From adffd68e683d276736cfc32c662e2b3f82bea8de Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Sun, 11 Apr 2021 12:37:47 +0200 Subject: [PATCH 3/3] PrivateAPI: Add lastVisited to HistoryEntryImportDto As the DTO is only for importing an existing history the lastVisited of those entries should also be posted. Signed-off-by: Philip Molares --- .../private/me/history/history.controller.ts | 1 + src/history/history-entry-import.dto.ts | 8 +- src/history/history.service.spec.ts | 97 ++++++++++++++++--- src/history/history.service.ts | 5 + test/private-api/history.e2e-spec.ts | 4 + 5 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/api/private/me/history/history.controller.ts b/src/api/private/me/history/history.controller.ts index d74795847..da00d7223 100644 --- a/src/api/private/me/history/history.controller.ts +++ b/src/api/private/me/history/history.controller.ts @@ -69,6 +69,7 @@ export class HistoryController { note, user, historyEntry.pinStatus, + historyEntry.lastVisited, ); } } catch (e) { diff --git a/src/history/history-entry-import.dto.ts b/src/history/history-entry-import.dto.ts index edc8b7c1e..7e258c7f6 100644 --- a/src/history/history-entry-import.dto.ts +++ b/src/history/history-entry-import.dto.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { IsBoolean, IsString } from 'class-validator'; +import { IsBoolean, IsDate, IsString } from 'class-validator'; export class HistoryEntryImportDto { /** @@ -18,4 +18,10 @@ export class HistoryEntryImportDto { */ @IsBoolean() pinStatus: boolean; + /** + * Datestring of the last time this note was updated + * @example "2020-12-01 12:23:34" + */ + @IsDate() + lastVisited: Date; } diff --git a/src/history/history.service.spec.ts b/src/history/history.service.spec.ts index f823b8400..206b40d14 100644 --- a/src/history/history.service.spec.ts +++ b/src/history/history.service.spec.ts @@ -152,7 +152,9 @@ describe('HistoryService', () => { const user = {} as User; const alias = 'alias'; const pinStatus = true; - it('without an preexisting entry and without pinStatus', async () => { + const lastVisited = new Date('2020-12-01 12:23:34'); + const historyEntry = HistoryEntry.create(user, Note.create(user, alias)); + it('without an preexisting entry, without pinStatus and without lastVisited', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest .spyOn(historyRepo, 'save') @@ -169,7 +171,7 @@ describe('HistoryService', () => { expect(createHistoryEntry.pinStatus).toEqual(false); }); - it('without an preexisting entry and with pinStatus', async () => { + it('without an preexisting entry, with pinStatus and without lastVisited', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); jest .spyOn(historyRepo, 'save') @@ -187,11 +189,47 @@ describe('HistoryService', () => { expect(createHistoryEntry.pinStatus).toEqual(pinStatus); }); - it('with an preexisting entry and without pinStatus', async () => { - const historyEntry = HistoryEntry.create( - user, + it('without an preexisting entry, without pinStatus and with lastVisited', async () => { + jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); + jest + .spyOn(historyRepo, 'save') + .mockImplementation( + async (entry: HistoryEntry): Promise => entry, + ); + const createHistoryEntry = await service.createOrUpdateHistoryEntry( Note.create(user, alias), + user, + undefined, + lastVisited, ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(false); + expect(createHistoryEntry.updatedAt).toEqual(lastVisited); + }); + + it('without an preexisting entry, with pinStatus and with lastVisited', async () => { + jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(undefined); + jest + .spyOn(historyRepo, 'save') + .mockImplementation( + async (entry: HistoryEntry): Promise => entry, + ); + const createHistoryEntry = await service.createOrUpdateHistoryEntry( + Note.create(user, alias), + user, + pinStatus, + lastVisited, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(pinStatus); + expect(createHistoryEntry.updatedAt).toEqual(lastVisited); + }); + + it('with an preexisting entry, without pinStatus and without lastVisited', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest .spyOn(historyRepo, 'save') @@ -211,12 +249,7 @@ describe('HistoryService', () => { ); }); - it('with an preexisting entry and with pinStatus', async () => { - const historyEntry = HistoryEntry.create( - user, - Note.create(user, alias), - pinStatus, - ); + it('with an preexisting entry, with pinStatus and without lastVisited', async () => { jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); jest .spyOn(historyRepo, 'save') @@ -231,11 +264,51 @@ describe('HistoryService', () => { expect(createHistoryEntry.note.alias).toEqual(alias); expect(createHistoryEntry.note.owner).toEqual(user); expect(createHistoryEntry.user).toEqual(user); - expect(createHistoryEntry.pinStatus).toEqual(pinStatus); + expect(createHistoryEntry.pinStatus).not.toEqual(pinStatus); expect(createHistoryEntry.updatedAt.getTime()).toBeGreaterThanOrEqual( historyEntry.updatedAt.getTime(), ); }); + + it('with an preexisting entry, without pinStatus and with lastVisited', async () => { + jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); + jest + .spyOn(historyRepo, 'save') + .mockImplementation( + async (entry: HistoryEntry): Promise => entry, + ); + const createHistoryEntry = await service.createOrUpdateHistoryEntry( + Note.create(user, alias), + user, + undefined, + lastVisited, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).toEqual(false); + expect(createHistoryEntry.updatedAt).not.toEqual(lastVisited); + }); + + it('with an preexisting entry, with pinStatus and with lastVisited', async () => { + jest.spyOn(historyRepo, 'findOne').mockResolvedValueOnce(historyEntry); + jest + .spyOn(historyRepo, 'save') + .mockImplementation( + async (entry: HistoryEntry): Promise => entry, + ); + const createHistoryEntry = await service.createOrUpdateHistoryEntry( + Note.create(user, alias), + user, + pinStatus, + lastVisited, + ); + expect(createHistoryEntry.note.alias).toEqual(alias); + expect(createHistoryEntry.note.owner).toEqual(user); + expect(createHistoryEntry.user).toEqual(user); + expect(createHistoryEntry.pinStatus).not.toEqual(pinStatus); + expect(createHistoryEntry.updatedAt).not.toEqual(lastVisited); + }); }); }); diff --git a/src/history/history.service.ts b/src/history/history.service.ts index de4ad0264..276af8190 100644 --- a/src/history/history.service.ts +++ b/src/history/history.service.ts @@ -81,12 +81,14 @@ export class HistoryService { * @param {Note} note - the note that the history entry belongs to * @param {User} user - the user that the history entry belongs to * @param {boolean} pinStatus - if the pinStatus of the history entry should be set + * @param {Date} lastVisited - the last time the associated note was accessed * @return {HistoryEntry} the requested history entry */ async createOrUpdateHistoryEntry( note: Note, user: User, pinStatus?: boolean, + lastVisited?: Date, ): Promise { let entry = await this.getEntryByNote(note, user); if (!entry) { @@ -94,6 +96,9 @@ export class HistoryService { if (pinStatus !== undefined) { entry.pinStatus = pinStatus; } + if (lastVisited !== undefined) { + entry.updatedAt = lastVisited; + } } else { entry.updatedAt = new Date(); } diff --git a/test/private-api/history.e2e-spec.ts b/test/private-api/history.e2e-spec.ts index 3dbf1db90..8f9262101 100644 --- a/test/private-api/history.e2e-spec.ts +++ b/test/private-api/history.e2e-spec.ts @@ -105,10 +105,13 @@ describe('History', () => { }); it('POST /me/history', async () => { + expect((await historyService.getEntriesByUser(user)).length).toEqual(1); const pinStatus = true; + const lastVisited = new Date('2020-12-01 12:23:34'); const postEntryDto = new HistoryEntryImportDto(); postEntryDto.note = note2.alias; postEntryDto.pinStatus = pinStatus; + postEntryDto.lastVisited = lastVisited; await request(app.getHttpServer()) .post('/me/history') .set('Content-Type', 'application/json') @@ -119,6 +122,7 @@ describe('History', () => { expect(userEntries[0].note.alias).toEqual(note2.alias); expect(userEntries[0].user.userName).toEqual(user.userName); expect(userEntries[0].pinStatus).toEqual(pinStatus); + expect(userEntries[0].updatedAt).toEqual(lastVisited); }); it('DELETE /me/history', async () => {