From 9d6aa109236746687b232421c3aec64b5a5aed4d Mon Sep 17 00:00:00 2001 From: Abhilasha Sinha Date: Mon, 30 Aug 2021 05:37:35 +0530 Subject: [PATCH] Add new API to purge note history #1064 Signed-off-by: Abhilasha Sinha Combine the describe block Signed-off-by: Abhilasha Sinha Fix naming Signed-off-by: Abhilasha Sinha Rename purgeRevision to purgeRevisions Signed-off-by: Abhilasha Sinha Fix notes e2e test description Signed-off-by: Abhilasha Sinha Add yarn.lock Fix lint and format Signed-off-by: Abhilasha Sinha --- src/api/private/notes/notes.controller.ts | 33 +++++++++++++ src/revisions/revisions.service.spec.ts | 59 +++++++++++++++++++++++ src/revisions/revisions.service.ts | 21 ++++++++ test/private-api/notes.e2e-spec.ts | 34 +++++++++++++ 4 files changed, 147 insertions(+) diff --git a/src/api/private/notes/notes.controller.ts b/src/api/private/notes/notes.controller.ts index 634712794..6ab130701 100644 --- a/src/api/private/notes/notes.controller.ts +++ b/src/api/private/notes/notes.controller.ts @@ -182,6 +182,39 @@ export class NotesController { } } + @Delete(':noteIdOrAlias/revisions') + @HttpCode(204) + async purgeNoteRevisions( + @Param('noteIdOrAlias') noteIdOrAlias: string, + ): Promise { + try { + // ToDo: use actual user here + const user = await this.userService.getUserByUsername('hardcoded'); + const note = await this.noteService.getNoteByIdOrAlias(noteIdOrAlias); + if (!this.permissionsService.mayRead(user, note)) { + throw new UnauthorizedException('Reading note denied!'); + } + this.logger.debug( + 'Purging history of note: ' + noteIdOrAlias, + 'purgeNoteRevisions', + ); + await this.revisionsService.purgeRevisions(note); + this.logger.debug( + 'Successfully purged history of note ' + noteIdOrAlias, + 'purgeNoteRevisions', + ); + return; + } catch (e) { + if (e instanceof NotInDBError) { + throw new NotFoundException(e.message); + } + if (e instanceof ForbiddenIdError) { + throw new BadRequestException(e.message); + } + throw e; + } + } + @Get(':noteIdOrAlias/revisions/:revisionId') async getNoteRevision( @Param('noteIdOrAlias', GetNotePipe) note: Note, diff --git a/src/revisions/revisions.service.spec.ts b/src/revisions/revisions.service.spec.ts index 95c7da207..c7824de63 100644 --- a/src/revisions/revisions.service.spec.ts +++ b/src/revisions/revisions.service.spec.ts @@ -97,4 +97,63 @@ describe('RevisionsService', () => { ); }); }); + + describe('purgeRevisions', () => { + it('purges the revision history', async () => { + const note = {} as Note; + note.id = 'test'; + let revisions: Revision[] = []; + const revision1 = Revision.create('a', 'a'); + revision1.id = 1; + const revision2 = Revision.create('b', 'b'); + revision2.id = 2; + const revision3 = Revision.create('c', 'c'); + revision3.id = 3; + revisions.push(revision1, revision2, revision3); + note.revisions = Promise.resolve(revisions); + jest.spyOn(revisionRepo, 'find').mockResolvedValueOnce(revisions); + jest.spyOn(service, 'getLatestRevision').mockResolvedValueOnce(revision3); + revisionRepo.remove = jest + .fn() + .mockImplementation((deleteList: Revision[]) => { + revisions = revisions.filter( + (item: Revision) => !deleteList.includes(item), + ); + return Promise.resolve(deleteList); + }); + + // expected to return all the purged revisions + expect(await service.purgeRevisions(note)).toHaveLength(2); + + // expected to have only the latest revision + const updatedRevisions: Revision[] = [revision3]; + expect(revisions).toEqual(updatedRevisions); + }); + it('has no effect on revision history when a single revision is present', async () => { + const note = {} as Note; + note.id = 'test'; + let revisions: Revision[] = []; + const revision1 = Revision.create('a', 'a'); + revision1.id = 1; + revisions.push(revision1); + note.revisions = Promise.resolve(revisions); + jest.spyOn(revisionRepo, 'find').mockResolvedValueOnce(revisions); + jest.spyOn(service, 'getLatestRevision').mockResolvedValueOnce(revision1); + revisionRepo.remove = jest + .fn() + .mockImplementation((deleteList: Revision[]) => { + revisions = revisions.filter( + (item: Revision) => !deleteList.includes(item), + ); + return Promise.resolve(deleteList); + }); + + // expected to return all the purged revisions + expect(await service.purgeRevisions(note)).toHaveLength(0); + + // expected to have only the latest revision + const updatedRevisions: Revision[] = [revision1]; + expect(revisions).toEqual(updatedRevisions); + }); + }); }); diff --git a/src/revisions/revisions.service.ts b/src/revisions/revisions.service.ts index c745da897..130edadb2 100644 --- a/src/revisions/revisions.service.ts +++ b/src/revisions/revisions.service.ts @@ -34,6 +34,27 @@ export class RevisionsService { }); } + /** + * @async + * Purge revision history of a note. + * @param {Note} note - the note to purge the history + * @return {Revision[]} an array of purged revisions + */ + async purgeRevisions(note: Note): Promise { + const revisions = await this.revisionRepository.find({ + where: { + note: note, + }, + }); + const latestRevison = await this.getLatestRevision(note); + // get all revisions except the latest + const oldRevisions = revisions.filter( + (item) => item.id !== latestRevison.id, + ); + // delete the old revisions + return await this.revisionRepository.remove(oldRevisions); + } + async getRevision(note: Note, revisionId: number): Promise { const revision = await this.revisionRepository.findOne({ where: { diff --git a/test/private-api/notes.e2e-spec.ts b/test/private-api/notes.e2e-spec.ts index d04347a56..ce77abb74 100644 --- a/test/private-api/notes.e2e-spec.ts +++ b/test/private-api/notes.e2e-spec.ts @@ -231,6 +231,40 @@ describe('Notes', () => { }); }); + describe('DELETE /notes/{note}/revisions', () => { + it('works with an existing alias', async () => { + const noteId = 'test8'; + const note = await notesService.createNote(content, noteId, user); + await notesService.updateNote(note, 'update'); + const responseBeforeDeleting = await request(app.getHttpServer()) + .get('/notes/test8/revisions') + .expect('Content-Type', /json/) + .expect(200); + expect(responseBeforeDeleting.body).toHaveLength(2); + await request(app.getHttpServer()) + .delete(`/notes/${noteId}/revisions`) + .set('Content-Type', 'application/json') + .expect(204); + const responseAfterDeleting = await request(app.getHttpServer()) + .get('/notes/test8/revisions') + .expect('Content-Type', /json/) + .expect(200); + expect(responseAfterDeleting.body).toHaveLength(1); + }); + it('fails with a forbidden alias', async () => { + await request(app.getHttpServer()) + .delete(`/notes/${forbiddenNoteId}/revisions`) + .expect(400); + }); + it('fails with non-existing alias', async () => { + // check if a missing note correctly returns 404 + await request(app.getHttpServer()) + .delete('/notes/i_dont_exist/revisions') + .expect('Content-Type', /json/) + .expect(404); + }); + }); + describe('GET /notes/{note}/revisions/{revision-id}', () => { it('works with an existing alias', async () => { const note = await notesService.createNote(content, 'test5', user);