Skip to content

Commit 485ebc8

Browse files
author
Abhishek Jasud
committed
Note tree presentation feature
1 parent a703367 commit 485ebc8

File tree

10 files changed

+274
-10
lines changed

10 files changed

+274
-10
lines changed

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/entities/noteTree.ts

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

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 { NoteTree } from '@domain/entities/noteTree.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<NoteTree | null> {
464+
const ultimateParent = await this.noteRelationsRepository.getUltimateParent(noteId);
465+
466+
// If there is no ultimate parent, the provided noteId is the ultimate parent
467+
const rootNoteId = ultimateParent ?? noteId;
468+
469+
const noteTree = await this.noteRepository.getNoteTreeByNoteId(rootNoteId);
470+
471+
return noteTree;
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 { NoteTreeSchema } from './schema/NoteTree.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(NoteTreeSchema);
303305
}
304306

305307
/**

src/presentation/http/router/note.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ 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 { NoteTree } from '@domain/entities/noteTree.js';
16+
import logger from '@infrastructure/logging/index.js';
1517

1618
/**
1719
* Interface for the note router.
@@ -140,8 +142,14 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
140142
memberRoleResolver,
141143
],
142144
}, async (request, reply) => {
145+
logger.warn(request);
143146
const { note } = request;
147+
148+
logger.warn(note);
149+
144150
const noteId = request.note?.id as number;
151+
152+
logger.warn(noteId);
145153
const { memberRole } = request;
146154
const { userId } = request;
147155

@@ -774,6 +782,56 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
774782
});
775783
});
776784

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export const NoteTreeSchema = {
2+
$id: 'NoteTreeSchema',
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: 'NoteTreeSchema#' },
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 { NoteTree } from '@domain/entities/noteTree.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 NoteTree structure
99+
*/
100+
public async getNoteTreeByNoteId(noteId: NoteInternalId): Promise<NoteTree | null> {
101+
return await this.storage.getNoteTreebyNoteId(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 getUltimateParent(noteId: NoteInternalId): Promise<NoteInternalId | null> {
86+
return await this.storage.getUltimateParentByNoteId(noteId);
87+
}
7988
}

src/repository/storage/postgres/orm/sequelize/note.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { CreationOptional, InferAttributes, InferCreationAttributes, ModelStatic, NonAttribute, Sequelize } from 'sequelize';
2-
import { DataTypes, Model, Op } from 'sequelize';
2+
import { DataTypes, Model, Op, QueryTypes } from 'sequelize';
33
import type Orm from '@repository/storage/postgres/orm/sequelize/index.js';
4-
import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js';
4+
import type { Note, NoteContent, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js';
55
import { UserModel } from '@repository/storage/postgres/orm/sequelize/user.js';
66
import type { NoteSettingsModel } from './noteSettings.js';
77
import type { NoteVisitsModel } from './noteVisits.js';
88
import type { NoteHistoryModel } from './noteHistory.js';
9+
import type { NoteTree } from '@domain/entities/noteTree.js';
910

1011
/* eslint-disable @typescript-eslint/naming-convention */
1112

@@ -346,4 +347,86 @@ export default class NoteSequelizeStorage {
346347

347348
return notes;
348349
}
350+
351+
/**
352+
* Creates a tree of notes
353+
* @param noteId - public note id
354+
* @returns NoteTree
355+
*/
356+
public async getNoteTreebyNoteId(noteId: NoteInternalId): Promise<NoteTree | null> {
357+
// Fetch all notes and relations in a recursive query
358+
const query = `
359+
WITH RECURSIVE note_tree AS (
360+
SELECT
361+
n.id AS noteId,
362+
n.content,
363+
n.public_id,
364+
nr.parent_id
365+
FROM ${String(this.database.literal(this.tableName).val)} n
366+
LEFT JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id
367+
WHERE n.id = :startNoteId
368+
369+
UNION ALL
370+
371+
SELECT
372+
n.id AS noteId,
373+
n.content,
374+
n.public_id,
375+
nr.parent_id
376+
FROM ${String(this.database.literal(this.tableName).val)} n
377+
INNER JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id
378+
INNER JOIN note_tree nt ON nr.parent_id = nt.noteId
379+
)
380+
SELECT * FROM note_tree;
381+
`;
382+
383+
const result = await this.model.sequelize?.query(query, {
384+
replacements: { startNoteId: noteId },
385+
type: QueryTypes.SELECT,
386+
});
387+
388+
if (!result || result.length === 0) {
389+
return null; // No data found
390+
}
391+
392+
type NoteRow = {
393+
noteid: NoteInternalId;
394+
public_id: NotePublicId;
395+
content: NoteContent;
396+
parent_id: NoteInternalId | null;
397+
};
398+
399+
const notes = result as NoteRow[];
400+
401+
const notesMap = new Map<NoteInternalId, NoteTree>();
402+
const publicIdMap = new Map<NoteInternalId, NotePublicId>(); // Internal to Public ID lookup
403+
404+
let root: NoteTree | null = null;
405+
406+
// Step 1: Parse and initialize all notes
407+
notes.forEach((note) => {
408+
notesMap.set(note.noteid, {
409+
id: note.public_id,
410+
content: note.content,
411+
childNotes: [],
412+
});
413+
414+
publicIdMap.set(note.noteid, note.public_id);
415+
});
416+
417+
// Step 2: Build hierarchy
418+
notes.forEach((note) => {
419+
if (note.parent_id === null) {
420+
root = notesMap.get(note.noteid) ?? null;
421+
} else {
422+
const parent = notesMap.get(note.parent_id);
423+
424+
if (parent) {
425+
parent.childNotes?.push(notesMap.get(note.noteid)!);
426+
}
427+
}
428+
});
429+
430+
return root;
431+
}
349432
}

src/repository/storage/postgres/orm/sequelize/noteRelations.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,34 @@ export default class NoteRelationsSequelizeStorage {
245245

246246
return noteParents;
247247
}
248+
249+
/**
250+
* Get ultimate parent noteId by noteId
251+
* @param noteId - the ID of note
252+
*/
253+
public async getUltimateParentByNoteId(noteId: NoteInternalId): Promise<NoteInternalId | null> {
254+
const query = `
255+
WITH RECURSIVE note_parents AS (
256+
SELECT np.note_id, np.parent_id
257+
FROM ${String(this.database.literal(this.tableName).val)} np
258+
WHERE np.note_id = :startNoteId
259+
UNION ALL
260+
SELECT nr.note_id, nr.parent_id
261+
FROM ${String(this.database.literal(this.tableName).val)} nr
262+
INNER JOIN note_parents np ON np.parent_id = nr.note_id
263+
)
264+
SELECT np.parent_id AS "parentId"
265+
FROM note_parents np
266+
WHERE np.parent_id IS NOT NULL
267+
ORDER BY np.parent_id ASC
268+
LIMIT 1;`;
269+
270+
const result = await this.model.sequelize?.query(query, {
271+
replacements: { startNoteId: noteId },
272+
type: QueryTypes.SELECT,
273+
});
274+
let ultimateParent = (result as { parentId: number }[])[0]?.parentId ?? null;
275+
276+
return ultimateParent;
277+
}
248278
}

0 commit comments

Comments
 (0)