From 4478b075e360b5e8be910d7fd941ba0c9a04febb Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 01:45:54 +0300 Subject: [PATCH 1/7] Add createNoteRelation method --- src/domain/service/note.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index ee42141e..6a4cd52a 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -195,6 +195,43 @@ export default class NoteService { }; } + /** + * Create note relation + * @param noteId - id of the current note + * @param parentPublicId - id of the parent note + */ + public async createNoteRelation(noteId: NoteInternalId, parentPublicId: NotePublicId): Promise { + const currenParentNote = await this.noteRelationsRepository.getParentNoteIdByNoteId(noteId); + + /** + * Check if the note already has a parent + */ + if (currenParentNote !== null) { + throw new DomainError(`Note already has parent note`); + } + + const parentNote = await this.noteRepository.getNoteByPublicId(parentPublicId); + + if (parentNote === null) { + throw new DomainError(`Incorrect parent note`); + } + + let parentNoteId: number | null = parentNote.id; + + /** + * This loop checks for cyclic reference when updating a note's parent. + */ + while (parentNoteId !== null) { + if (parentNoteId === noteId) { + throw new DomainError(`Forbidden relation. Note can't be a child of own child`); + } + + parentNoteId = await this.noteRelationsRepository.getParentNoteIdByNoteId(parentNoteId); + } + + return await this.noteRelationsRepository.addNoteRelation(noteId, parentNote.id); + } + /** * Update note relation * @param noteId - id of the current note From bb031cf33959505cb3254accb3ca586052c3353f Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 01:51:04 +0300 Subject: [PATCH 2/7] Add POST /relation endpoint --- src/presentation/http/router/note.ts | 55 ++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 64cd933a..eb0bc7d7 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -372,6 +372,61 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }); }); + /** + * Create note relation by id. + */ + fastify.post<{ + Params: { + notePublicId: NotePublicId; + }; + Body: { + parentNoteId: NotePublicId; + }; + Reply: { + isCreated: boolean; + }; + }>('/:notePublicId/relation', { + schema: { + params: { + notePublicId: { + $ref: 'NoteSchema#/properties/id', + }, + }, + body: { + parentNoteId: { + $ref: 'NoteSchema#/properties/id', + }, + }, + response: { + '2xx': { + type: 'object', + description: 'Was the relation created', + properties: { + isCreated: { + type: 'boolean', + }, + }, + }, + }, + }, + config: { + policy: [ + 'authRequired', + 'userCanEdit', + ], + }, + preHandler: [ + noteResolver, + ], + }, async (request, reply) => { + const noteId = request.note?.id as number; + const parentNoteId = request.body.parentNoteId; + + const isCreated = await noteService.createNoteRelation(noteId, parentNoteId); + + return reply.send({ isCreated }); + }); + /** * Update note relation by id. */ From 05ec4b11392a8d4669635acc3b656c47445d7805 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 12:51:25 +0300 Subject: [PATCH 3/7] Add test for the new route --- src/presentation/http/router/note.test.ts | 173 ++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index 38550f12..add95ade 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -1436,6 +1436,179 @@ describe('Note API', () => { }); }); + describe('POST /note/:notePublicId/relation', () => { + let accessToken = ''; + let user: User; + + beforeEach(async () => { + /** create test user */ + user = await global.db.insertUser(); + + accessToken = global.auth(user.id); + }); + test('Returns 200 and isCreated=true when relation was successfully created', async () => { + /* create test child note */ + const childNote = await global.db.insertNote({ + creatorId: user.id, + }); + + /* create test parent note */ + const parentNote = await global.db.insertNote({ + creatorId: user.id, + }); + + /* create note settings for child note */ + await global.db.insertNoteSetting({ + noteId: childNote.id, + isPublic: true, + }); + + let response = await global.api?.fakeRequest({ + method: 'POST', + headers: { + authorization: `Bearer ${accessToken}`, + }, + body: { + parentNoteId: parentNote.publicId, + }, + url: `/note/${childNote.publicId}/relation`, + }); + + expect(response?.statusCode).toBe(200); + + expect(response?.json().isCreated).toBe(true); + + response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/note/${childNote.publicId}`, + }); + + expect(response?.json().parentNote.id).toBe(parentNote.publicId); + }); + + test('Returns 400 when note already has parent note', async () => { + /* create test child note */ + const childNote = await global.db.insertNote({ + creatorId: user.id, + }); + + /* create test parent note */ + const parentNote = await global.db.insertNote({ + creatorId: user.id, + }); + + /* create test note, that will be new parent for the child note */ + const newParentNote = await global.db.insertNote({ + creatorId: user.id, + }); + + /* create note settings for child note */ + await global.db.insertNoteSetting({ + noteId: childNote.id, + isPublic: true, + }); + + /* create test relation */ + await global.db.insertNoteRelation({ + noteId: childNote.id, + parentId: parentNote.id, + }); + + let response = await global.api?.fakeRequest({ + method: 'POST', + headers: { + authorization: `Bearer ${accessToken}`, + }, + body: { + parentNoteId: newParentNote.publicId, + }, + url: `/note/${childNote.publicId}/relation`, + }); + + expect(response?.statusCode).toBe(400); + + expect(response?.json().message).toStrictEqual('Note already has parent note'); + }); + + test('Returns 400 when parent is the same as child', async () => { + /* create test child note */ + const childNote = await global.db.insertNote({ + creatorId: user.id, + }); + + const response = await global.api?.fakeRequest({ + method: 'POST', + headers: { + authorization: `Bearer ${accessToken}`, + }, + body: { + parentNoteId: childNote.publicId, + }, + url: `/note/${childNote.publicId}/relation`, + }); + + expect(response?.statusCode).toBe(400); + + expect(response?.json().message).toStrictEqual(`Forbidden relation. Note can't be a child of own child`); + }); + + test('Return 400 when parent note does not exist', async () => { + const nonExistentParentId = '47L43yY7dp'; + + const childNote = await global.db.insertNote({ + creatorId: user.id, + }); + + const response = await global.api?.fakeRequest({ + method: 'POST', + headers: { + authorization: `Bearer ${accessToken}`, + }, + body: { + parentNoteId: nonExistentParentId, + }, + url: `/note/${childNote.publicId}/relation`, + }); + + expect(response?.statusCode).toBe(400); + + expect(response?.json().message).toStrictEqual('Incorrect parent note'); + }); + + test('Return 400 when circular reference occurs', async () => { + const parentNote = await global.db.insertNote({ + creatorId: user.id, + }); + + const childNote = await global.db.insertNote({ + creatorId: user.id, + }); + + await global.db.insertNoteRelation({ + noteId: childNote.id, + parentId: parentNote.id, + }); + + const response = await global.api?.fakeRequest({ + method: 'POST', + headers: { + authorization: `Bearer ${accessToken}`, + }, + body: { + parentNoteId: childNote.publicId, + }, + url: `/note/${parentNote.publicId}/relation`, + }); + + expect(response?.statusCode).toBe(400); + + expect(response?.json().message).toStrictEqual(`Forbidden relation. Note can't be a child of own child`); + }); + }); + describe('PATCH /note/:notePublicId', () => { const tools = [headerTool, listTool]; From a4b3e6ccd2e6cb518c24b3a5ad63da8cde9a2cc5 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 13:55:35 +0300 Subject: [PATCH 4/7] Remove redundant inserts --- src/presentation/http/router/note.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index add95ade..1864c51a 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -1505,12 +1505,6 @@ describe('Note API', () => { creatorId: user.id, }); - /* create note settings for child note */ - await global.db.insertNoteSetting({ - noteId: childNote.id, - isPublic: true, - }); - /* create test relation */ await global.db.insertNoteRelation({ noteId: childNote.id, From 73841990c205b1197f8916ddfa9b09f1adf4ff49 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 17:39:31 +0300 Subject: [PATCH 5/7] Return parent note --- src/domain/service/note.ts | 12 +++++++++--- src/presentation/http/router/note.ts | 12 ++++++------ 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 6a4cd52a..323ce519 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -200,7 +200,7 @@ export default class NoteService { * @param noteId - id of the current note * @param parentPublicId - id of the parent note */ - public async createNoteRelation(noteId: NoteInternalId, parentPublicId: NotePublicId): Promise { + public async createNoteRelation(noteId: NoteInternalId, parentPublicId: NotePublicId): Promise { const currenParentNote = await this.noteRelationsRepository.getParentNoteIdByNoteId(noteId); /** @@ -213,7 +213,7 @@ export default class NoteService { const parentNote = await this.noteRepository.getNoteByPublicId(parentPublicId); if (parentNote === null) { - throw new DomainError(`Incorrect parent note`); + throw new DomainError(`Incorrect parent note Id`); } let parentNoteId: number | null = parentNote.id; @@ -229,7 +229,13 @@ export default class NoteService { parentNoteId = await this.noteRelationsRepository.getParentNoteIdByNoteId(parentNoteId); } - return await this.noteRelationsRepository.addNoteRelation(noteId, parentNote.id); + const isCreated = await this.noteRelationsRepository.addNoteRelation(noteId, parentNote.id); + + if (!isCreated) { + throw new DomainError(`Relation was not created`); + } + + return parentNote; } /** diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index eb0bc7d7..294cba87 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -383,7 +383,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don parentNoteId: NotePublicId; }; Reply: { - isCreated: boolean; + parentNote: Note; }; }>('/:notePublicId/relation', { schema: { @@ -400,10 +400,10 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don response: { '2xx': { type: 'object', - description: 'Was the relation created', + description: 'Parent note', properties: { - isCreated: { - type: 'boolean', + parentNote: { + type: 'Note', }, }, }, @@ -422,9 +422,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const noteId = request.note?.id as number; const parentNoteId = request.body.parentNoteId; - const isCreated = await noteService.createNoteRelation(noteId, parentNoteId); + const parentNote = await noteService.createNoteRelation(noteId, parentNoteId); - return reply.send({ isCreated }); + return reply.send({ parentNote }); }); /** From dcc1ec7c990cd30918b89f6d3c2f1a13f4a499d5 Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 17:58:37 +0300 Subject: [PATCH 6/7] Return parentNote --- src/presentation/http/router/note.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 294cba87..ed7500d5 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -400,10 +400,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don response: { '2xx': { type: 'object', - description: 'Parent note', properties: { parentNote: { - type: 'Note', + $ref: 'NoteSchema#', }, }, }, From 3feb8b9ffbb5b4ce2888cd660a4fec788b41d38d Mon Sep 17 00:00:00 2001 From: Ivan Date: Sun, 30 Jun 2024 18:01:20 +0300 Subject: [PATCH 7/7] Fix tests --- src/presentation/http/router/note.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index 1864c51a..684ecd64 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -1476,8 +1476,6 @@ describe('Note API', () => { expect(response?.statusCode).toBe(200); - expect(response?.json().isCreated).toBe(true); - response = await global.api?.fakeRequest({ method: 'GET', headers: { @@ -1569,7 +1567,7 @@ describe('Note API', () => { expect(response?.statusCode).toBe(400); - expect(response?.json().message).toStrictEqual('Incorrect parent note'); + expect(response?.json().message).toStrictEqual('Incorrect parent note Id'); }); test('Return 400 when circular reference occurs', async () => {