Skip to content

Commit 07b40e8

Browse files
authored
Merge pull request #290 from devnull-1337/feature/NoteTreePresentation
feature: Note tree presentation feature
2 parents a703367 + 86038c9 commit 07b40e8

11 files changed

Lines changed: 369 additions & 10 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { NoteContent, NotePublicId } from './note.js';
2+
3+
/**
4+
* Note Tree entity
5+
*/
6+
export interface NoteHierarchy {
7+
8+
/**
9+
* public note id
10+
*/
11+
id: NotePublicId;
12+
13+
/**
14+
* note content
15+
*/
16+
content: NoteContent;
17+
18+
/**
19+
* child notes
20+
*/
21+
childNotes: NoteHierarchy[] | null;
22+
23+
}

src/domain/entities/note.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@ export type ToolUsedInNoteContent = {
2626
id: EditorTool['id'];
2727
};
2828

29+
/**
30+
* NoteContent
31+
*/
32+
export type NoteContent = {
33+
blocks: Array<{
34+
id: string;
35+
type: string;
36+
data: unknown;
37+
tunes?: { [name: string]: unknown };
38+
}>;
39+
};
40+
2941
/**
3042
* Note entity
3143
*/
@@ -43,14 +55,7 @@ export interface Note {
4355
/**
4456
* Note content
4557
*/
46-
content: {
47-
blocks: Array<{
48-
id: string;
49-
type: string;
50-
data: unknown;
51-
tunes?: { [name: string]: unknown };
52-
}>;
53-
};
58+
content: NoteContent;
5459

5560
/**
5661
* Note creator id

src/domain/service/note.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type User from '@domain/entities/user.js';
99
import type { NoteList } from '@domain/entities/noteList.js';
1010
import type NoteHistoryRepository from '@repository/noteHistory.repository.js';
1111
import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryPublic } from '@domain/entities/noteHistory.js';
12+
import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js';
1213

1314
/**
1415
* Note service
@@ -453,4 +454,20 @@ export default class NoteService {
453454

454455
return noteParents;
455456
}
457+
458+
/**
459+
* Reutrn a tree structure of notes with childNotes for the given note id
460+
* @param noteId - id of the note to get structure
461+
* @returns - Object of notes.
462+
*/
463+
public async getNoteHierarchy(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
464+
const ultimateParent = await this.noteRelationsRepository.getUltimateParentNoteId(noteId);
465+
466+
// If there is no ultimate parent, the provided noteId is the ultimate parent
467+
const rootNoteId = ultimateParent ?? noteId;
468+
469+
const noteHierarchy = await this.noteRepository.getNoteHierarchyByNoteId(rootNoteId);
470+
471+
return noteHierarchy;
472+
}
456473
}

src/presentation/http/http-api.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { DomainError } from '@domain/entities/DomainError.js';
3333
import UploadRouter from './router/upload.js';
3434
import { ajvFilePlugin } from '@fastify/multipart';
3535
import { UploadSchema } from './schema/Upload.js';
36+
import { NoteHierarchySchema } from './schema/NoteHierarchy.js';
3637

3738
const appServerLogger = getLogger('appServer');
3839

@@ -300,6 +301,7 @@ export default class HttpApi implements Api {
300301
this.server?.addSchema(JoinSchemaResponse);
301302
this.server?.addSchema(OauthSchema);
302303
this.server?.addSchema(UploadSchema);
304+
this.server?.addSchema(NoteHierarchySchema);
303305
}
304306

305307
/**

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MemberRole } from '@domain/entities/team.js';
22
import { describe, test, expect, beforeEach } from 'vitest';
33
import type User from '@domain/entities/user.js';
4+
import type { Note } from '@domain/entities/note.js';
45

56
describe('Note API', () => {
67
/**
@@ -2272,4 +2273,97 @@ describe('Note API', () => {
22722273
expect(response?.json().message).toBe('Note not found');
22732274
});
22742275
});
2276+
2277+
describe('GET /note/note-hierarchy/:noteId', () => {
2278+
let accessToken = '';
2279+
let user: User;
2280+
2281+
beforeEach(async () => {
2282+
/** create test user */
2283+
user = await global.db.insertUser();
2284+
2285+
accessToken = global.auth(user.id);
2286+
});
2287+
2288+
test.each([
2289+
// Test case 1: No parent or child
2290+
{
2291+
description: 'Should get note hierarchy with no parent or child when noteId passed has no relations',
2292+
setup: async () => {
2293+
const note = await global.db.insertNote({ creatorId: user.id });
2294+
2295+
await global.db.insertNoteSetting({
2296+
noteId: note.id,
2297+
isPublic: true,
2298+
});
2299+
2300+
return {
2301+
note: note,
2302+
childNote: null,
2303+
};
2304+
},
2305+
2306+
expected: (note: Note, childNote: Note | null) => ({
2307+
id: note.publicId,
2308+
content: note.content,
2309+
childNotes: childNote,
2310+
}),
2311+
},
2312+
2313+
// Test case 2: With child
2314+
{
2315+
description: 'Should get note hierarchy with child when noteId passed has relations',
2316+
setup: async () => {
2317+
const childNote = await global.db.insertNote({ creatorId: user.id });
2318+
const parentNote = await global.db.insertNote({ creatorId: user.id });
2319+
2320+
await global.db.insertNoteSetting({
2321+
noteId: childNote.id,
2322+
isPublic: true,
2323+
});
2324+
await global.db.insertNoteSetting({
2325+
noteId: parentNote.id,
2326+
isPublic: true,
2327+
});
2328+
await global.db.insertNoteRelation({
2329+
noteId: childNote.id,
2330+
parentId: parentNote.id,
2331+
});
2332+
2333+
return {
2334+
note: parentNote,
2335+
childNote: childNote,
2336+
};
2337+
},
2338+
expected: (note: Note, childNote: Note | null) => ({
2339+
id: note.publicId,
2340+
content: note.content,
2341+
childNotes: [
2342+
{
2343+
id: childNote?.publicId,
2344+
content: childNote?.content,
2345+
childNotes: null,
2346+
},
2347+
],
2348+
}),
2349+
},
2350+
])('$description', async ({ setup, expected }) => {
2351+
// Setup the test data
2352+
const { note, childNote } = await setup();
2353+
2354+
// Make the API request
2355+
const response = await global.api?.fakeRequest({
2356+
method: 'GET',
2357+
headers: {
2358+
authorization: `Bearer ${accessToken}`,
2359+
},
2360+
url: `/note/note-hierarchy/${note.publicId}`,
2361+
});
2362+
2363+
// Verify the response
2364+
expect(response?.json().noteHierarchy).toStrictEqual(
2365+
expected(note, childNote)
2366+
);
2367+
});
2368+
});
22752369
});

src/presentation/http/router/note.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type NoteVisitsService from '@domain/service/noteVisits.js';
1212
import type EditorToolsService from '@domain/service/editorTools.js';
1313
import type EditorTool from '@domain/entities/editorTools.js';
1414
import type { NoteHistoryMeta, NoteHistoryPublic, NoteHistoryRecord } from '@domain/entities/noteHistory.js';
15+
import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js';
1516

1617
/**
1718
* Interface for the note router.
@@ -141,7 +142,9 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
141142
],
142143
}, async (request, reply) => {
143144
const { note } = request;
145+
144146
const noteId = request.note?.id as number;
147+
145148
const { memberRole } = request;
146149
const { userId } = request;
147150

@@ -774,6 +777,58 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
774777
});
775778
});
776779

780+
fastify.get<{
781+
Params: {
782+
notePublicId: NotePublicId;
783+
};
784+
Reply: {
785+
noteHierarchy: NoteHierarchy | null;
786+
} | ErrorResponse;
787+
}>('/note-hierarchy/:notePublicId', {
788+
config: {
789+
policy: [
790+
'notePublicOrUserInTeam',
791+
],
792+
},
793+
schema: {
794+
params: {
795+
notePublicId: {
796+
$ref: 'NoteSchema#/properties/id',
797+
},
798+
},
799+
response: {
800+
'2xx': {
801+
type: 'object',
802+
properties: {
803+
noteHierarchy: {
804+
$ref: 'NoteHierarchySchema#',
805+
},
806+
},
807+
},
808+
},
809+
},
810+
preHandler: [
811+
noteResolver,
812+
noteSettingsResolver,
813+
memberRoleResolver,
814+
],
815+
}, async (request, reply) => {
816+
const noteId = request.note?.id as number;
817+
818+
/**
819+
* Check if note exists
820+
*/
821+
if (noteId === null) {
822+
return reply.notAcceptable('Note not found');
823+
}
824+
825+
const noteHierarchy = await noteService.getNoteHierarchy(noteId);
826+
827+
return reply.send({
828+
noteHierarchy: noteHierarchy,
829+
});
830+
});
831+
777832
done();
778833
};
779834

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const NoteHierarchySchema = {
2+
$id: 'NoteHierarchySchema',
3+
properties: {
4+
id: {
5+
type: 'string',
6+
pattern: '[a-zA-Z0-9-_]+',
7+
maxLength: 10,
8+
minLength: 10,
9+
},
10+
content: {
11+
type: 'object',
12+
properties: {
13+
time: {
14+
type: 'number',
15+
},
16+
blocks: {
17+
type: 'array',
18+
},
19+
version: {
20+
type: 'string',
21+
},
22+
},
23+
},
24+
childNotes: {
25+
type: 'array',
26+
items: { $ref: 'NoteHierarchySchema#' },
27+
nullable: true,
28+
},
29+
},
30+
};

src/repository/note.repository.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js';
2+
import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js';
23
import type NoteStorage from '@repository/storage/note.storage.js';
34

45
/**
@@ -90,4 +91,13 @@ export default class NoteRepository {
9091
public async getNotesByIds(noteIds: NoteInternalId[]): Promise<Note[]> {
9192
return await this.storage.getNotesByIds(noteIds);
9293
}
94+
95+
/**
96+
* Gets the Note tree by note id
97+
* @param noteId - note id
98+
* @returns NoteHierarchy structure
99+
*/
100+
public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
101+
return await this.storage.getNoteHierarchybyNoteId(noteId);
102+
}
93103
}

src/repository/noteRelations.repository.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,13 @@ export default class NoteRelationsRepository {
7676
public async getNoteParentsIds(noteId: NoteInternalId): Promise<NoteInternalId[]> {
7777
return await this.storage.getNoteParentsIds(noteId);
7878
}
79+
80+
/**
81+
* Get the ultimate parent of a note with note id
82+
* @param noteId - note id to get ultimate parent
83+
* @returns - note id of the ultimate parent
84+
*/
85+
public async getUltimateParentNoteId(noteId: NoteInternalId): Promise<NoteInternalId | null> {
86+
return await this.storage.getUltimateParentByNoteId(noteId);
87+
}
7988
}

0 commit comments

Comments
 (0)