Skip to content

Commit 07e5e4a

Browse files
kloV148Ivan
andauthored
Feature: Add POST /note/:notePublicId/relation route (#260)
* 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 <[email protected]>
1 parent 8fcfee0 commit 07e5e4a

File tree

3 files changed

+262
-0
lines changed

3 files changed

+262
-0
lines changed

src/domain/service/note.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,49 @@ export default class NoteService {
195195
};
196196
}
197197

198+
/**
199+
* Create note relation
200+
* @param noteId - id of the current note
201+
* @param parentPublicId - id of the parent note
202+
*/
203+
public async createNoteRelation(noteId: NoteInternalId, parentPublicId: NotePublicId): Promise<Note> {
204+
const currenParentNote = await this.noteRelationsRepository.getParentNoteIdByNoteId(noteId);
205+
206+
/**
207+
* Check if the note already has a parent
208+
*/
209+
if (currenParentNote !== null) {
210+
throw new DomainError(`Note already has parent note`);
211+
}
212+
213+
const parentNote = await this.noteRepository.getNoteByPublicId(parentPublicId);
214+
215+
if (parentNote === null) {
216+
throw new DomainError(`Incorrect parent note Id`);
217+
}
218+
219+
let parentNoteId: number | null = parentNote.id;
220+
221+
/**
222+
* This loop checks for cyclic reference when updating a note's parent.
223+
*/
224+
while (parentNoteId !== null) {
225+
if (parentNoteId === noteId) {
226+
throw new DomainError(`Forbidden relation. Note can't be a child of own child`);
227+
}
228+
229+
parentNoteId = await this.noteRelationsRepository.getParentNoteIdByNoteId(parentNoteId);
230+
}
231+
232+
const isCreated = await this.noteRelationsRepository.addNoteRelation(noteId, parentNote.id);
233+
234+
if (!isCreated) {
235+
throw new DomainError(`Relation was not created`);
236+
}
237+
238+
return parentNote;
239+
}
240+
198241
/**
199242
* Update note relation
200243
* @param noteId - id of the current note

src/presentation/http/router/note.test.ts

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,171 @@ describe('Note API', () => {
14361436
});
14371437
});
14381438

1439+
describe('POST /note/:notePublicId/relation', () => {
1440+
let accessToken = '';
1441+
let user: User;
1442+
1443+
beforeEach(async () => {
1444+
/** create test user */
1445+
user = await global.db.insertUser();
1446+
1447+
accessToken = global.auth(user.id);
1448+
});
1449+
test('Returns 200 and isCreated=true when relation was successfully created', async () => {
1450+
/* create test child note */
1451+
const childNote = await global.db.insertNote({
1452+
creatorId: user.id,
1453+
});
1454+
1455+
/* create test parent note */
1456+
const parentNote = await global.db.insertNote({
1457+
creatorId: user.id,
1458+
});
1459+
1460+
/* create note settings for child note */
1461+
await global.db.insertNoteSetting({
1462+
noteId: childNote.id,
1463+
isPublic: true,
1464+
});
1465+
1466+
let response = await global.api?.fakeRequest({
1467+
method: 'POST',
1468+
headers: {
1469+
authorization: `Bearer ${accessToken}`,
1470+
},
1471+
body: {
1472+
parentNoteId: parentNote.publicId,
1473+
},
1474+
url: `/note/${childNote.publicId}/relation`,
1475+
});
1476+
1477+
expect(response?.statusCode).toBe(200);
1478+
1479+
response = await global.api?.fakeRequest({
1480+
method: 'GET',
1481+
headers: {
1482+
authorization: `Bearer ${accessToken}`,
1483+
},
1484+
url: `/note/${childNote.publicId}`,
1485+
});
1486+
1487+
expect(response?.json().parentNote.id).toBe(parentNote.publicId);
1488+
});
1489+
1490+
test('Returns 400 when note already has parent note', async () => {
1491+
/* create test child note */
1492+
const childNote = await global.db.insertNote({
1493+
creatorId: user.id,
1494+
});
1495+
1496+
/* create test parent note */
1497+
const parentNote = await global.db.insertNote({
1498+
creatorId: user.id,
1499+
});
1500+
1501+
/* create test note, that will be new parent for the child note */
1502+
const newParentNote = await global.db.insertNote({
1503+
creatorId: user.id,
1504+
});
1505+
1506+
/* create test relation */
1507+
await global.db.insertNoteRelation({
1508+
noteId: childNote.id,
1509+
parentId: parentNote.id,
1510+
});
1511+
1512+
let response = await global.api?.fakeRequest({
1513+
method: 'POST',
1514+
headers: {
1515+
authorization: `Bearer ${accessToken}`,
1516+
},
1517+
body: {
1518+
parentNoteId: newParentNote.publicId,
1519+
},
1520+
url: `/note/${childNote.publicId}/relation`,
1521+
});
1522+
1523+
expect(response?.statusCode).toBe(400);
1524+
1525+
expect(response?.json().message).toStrictEqual('Note already has parent note');
1526+
});
1527+
1528+
test('Returns 400 when parent is the same as child', async () => {
1529+
/* create test child note */
1530+
const childNote = await global.db.insertNote({
1531+
creatorId: user.id,
1532+
});
1533+
1534+
const response = await global.api?.fakeRequest({
1535+
method: 'POST',
1536+
headers: {
1537+
authorization: `Bearer ${accessToken}`,
1538+
},
1539+
body: {
1540+
parentNoteId: childNote.publicId,
1541+
},
1542+
url: `/note/${childNote.publicId}/relation`,
1543+
});
1544+
1545+
expect(response?.statusCode).toBe(400);
1546+
1547+
expect(response?.json().message).toStrictEqual(`Forbidden relation. Note can't be a child of own child`);
1548+
});
1549+
1550+
test('Return 400 when parent note does not exist', async () => {
1551+
const nonExistentParentId = '47L43yY7dp';
1552+
1553+
const childNote = await global.db.insertNote({
1554+
creatorId: user.id,
1555+
});
1556+
1557+
const response = await global.api?.fakeRequest({
1558+
method: 'POST',
1559+
headers: {
1560+
authorization: `Bearer ${accessToken}`,
1561+
},
1562+
body: {
1563+
parentNoteId: nonExistentParentId,
1564+
},
1565+
url: `/note/${childNote.publicId}/relation`,
1566+
});
1567+
1568+
expect(response?.statusCode).toBe(400);
1569+
1570+
expect(response?.json().message).toStrictEqual('Incorrect parent note Id');
1571+
});
1572+
1573+
test('Return 400 when circular reference occurs', async () => {
1574+
const parentNote = await global.db.insertNote({
1575+
creatorId: user.id,
1576+
});
1577+
1578+
const childNote = await global.db.insertNote({
1579+
creatorId: user.id,
1580+
});
1581+
1582+
await global.db.insertNoteRelation({
1583+
noteId: childNote.id,
1584+
parentId: parentNote.id,
1585+
});
1586+
1587+
const response = await global.api?.fakeRequest({
1588+
method: 'POST',
1589+
headers: {
1590+
authorization: `Bearer ${accessToken}`,
1591+
},
1592+
body: {
1593+
parentNoteId: childNote.publicId,
1594+
},
1595+
url: `/note/${parentNote.publicId}/relation`,
1596+
});
1597+
1598+
expect(response?.statusCode).toBe(400);
1599+
1600+
expect(response?.json().message).toStrictEqual(`Forbidden relation. Note can't be a child of own child`);
1601+
});
1602+
});
1603+
14391604
describe('PATCH /note/:notePublicId', () => {
14401605
const tools = [headerTool, listTool];
14411606

src/presentation/http/router/note.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,60 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
372372
});
373373
});
374374

375+
/**
376+
* Create note relation by id.
377+
*/
378+
fastify.post<{
379+
Params: {
380+
notePublicId: NotePublicId;
381+
};
382+
Body: {
383+
parentNoteId: NotePublicId;
384+
};
385+
Reply: {
386+
parentNote: Note;
387+
};
388+
}>('/:notePublicId/relation', {
389+
schema: {
390+
params: {
391+
notePublicId: {
392+
$ref: 'NoteSchema#/properties/id',
393+
},
394+
},
395+
body: {
396+
parentNoteId: {
397+
$ref: 'NoteSchema#/properties/id',
398+
},
399+
},
400+
response: {
401+
'2xx': {
402+
type: 'object',
403+
properties: {
404+
parentNote: {
405+
$ref: 'NoteSchema#',
406+
},
407+
},
408+
},
409+
},
410+
},
411+
config: {
412+
policy: [
413+
'authRequired',
414+
'userCanEdit',
415+
],
416+
},
417+
preHandler: [
418+
noteResolver,
419+
],
420+
}, async (request, reply) => {
421+
const noteId = request.note?.id as number;
422+
const parentNoteId = request.body.parentNoteId;
423+
424+
const parentNote = await noteService.createNoteRelation(noteId, parentNoteId);
425+
426+
return reply.send({ parentNote });
427+
});
428+
375429
/**
376430
* Update note relation by id.
377431
*/

0 commit comments

Comments
 (0)