Skip to content

Commit

Permalink
Feature: Add POST /note/:notePublicId/relation route (#260)
Browse files Browse the repository at this point in the history
* Add createNoteRelation method

* Add POST /relation endpoint

* Add test for the new route

* Remove redundant inserts

* Return parent note

* Return parentNote

* Fix tests

---------

Co-authored-by: Ivan <ivan@RedmiBook-13.local>
  • Loading branch information
kloV148 and Ivan authored Jun 30, 2024
1 parent 8fcfee0 commit 07e5e4a
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/domain/service/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Note> {
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
Expand Down
165 changes: 165 additions & 0 deletions src/presentation/http/router/note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];

Expand Down
54 changes: 54 additions & 0 deletions src/presentation/http/router/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,60 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (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.
*/
Expand Down

0 comments on commit 07e5e4a

Please sign in to comment.