diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index ee42141e..323ce519 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -195,6 +195,49 @@ 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 Id`); + } + + 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); + } + + const isCreated = await this.noteRelationsRepository.addNoteRelation(noteId, parentNote.id); + + if (!isCreated) { + throw new DomainError(`Relation was not created`); + } + + return parentNote; + } + /** * Update note relation * @param noteId - id of the current note diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index 38550f12..684ecd64 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -1436,6 +1436,171 @@ 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); + + 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 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 Id'); + }); + + 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]; diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 64cd933a..ed7500d5 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -372,6 +372,60 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }); }); + /** + * Create note relation by id. + */ + fastify.post<{ + Params: { + notePublicId: NotePublicId; + }; + Body: { + parentNoteId: NotePublicId; + }; + Reply: { + parentNote: Note; + }; + }>('/:notePublicId/relation', { + schema: { + params: { + notePublicId: { + $ref: 'NoteSchema#/properties/id', + }, + }, + body: { + parentNoteId: { + $ref: 'NoteSchema#/properties/id', + }, + }, + response: { + '2xx': { + type: 'object', + properties: { + parentNote: { + $ref: 'NoteSchema#', + }, + }, + }, + }, + }, + config: { + policy: [ + 'authRequired', + 'userCanEdit', + ], + }, + preHandler: [ + noteResolver, + ], + }, async (request, reply) => { + const noteId = request.note?.id as number; + const parentNoteId = request.body.parentNoteId; + + const parentNote = await noteService.createNoteRelation(noteId, parentNoteId); + + return reply.send({ parentNote }); + }); + /** * Update note relation by id. */