mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-23 02:06:29 -05:00
Merge pull request #1595 from Abhilasha06/deleteRevisions
Add new API to purge note history
This commit is contained in:
commit
67297f71a4
4 changed files with 147 additions and 0 deletions
|
@ -182,6 +182,39 @@ export class NotesController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Delete(':noteIdOrAlias/revisions')
|
||||||
|
@HttpCode(204)
|
||||||
|
async purgeNoteRevisions(
|
||||||
|
@Param('noteIdOrAlias') noteIdOrAlias: string,
|
||||||
|
): Promise<void> {
|
||||||
|
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')
|
@Get(':noteIdOrAlias/revisions/:revisionId')
|
||||||
async getNoteRevision(
|
async getNoteRevision(
|
||||||
@Param('noteIdOrAlias', GetNotePipe) note: Note,
|
@Param('noteIdOrAlias', GetNotePipe) note: Note,
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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<Revision[]> {
|
||||||
|
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<Revision> {
|
async getRevision(note: Note, revisionId: number): Promise<Revision> {
|
||||||
const revision = await this.revisionRepository.findOne({
|
const revision = await this.revisionRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -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}', () => {
|
describe('GET /notes/{note}/revisions/{revision-id}', () => {
|
||||||
it('works with an existing alias', async () => {
|
it('works with an existing alias', async () => {
|
||||||
const note = await notesService.createNote(content, 'test5', user);
|
const note = await notesService.createNote(content, 'test5', user);
|
||||||
|
|
Loading…
Reference in a new issue