Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add POST /note/:notePublicId/relation route #260

Merged
merged 7 commits into from
Jun 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
slaveeks marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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
Loading