From 485ebc893e63e80e22b55610d957b98f0f5c4ace Mon Sep 17 00:00:00 2001 From: Abhishek Jasud Date: Sun, 9 Feb 2025 18:12:04 +0530 Subject: [PATCH 1/6] Note tree presentation feature --- src/domain/entities/note.ts | 21 +++-- src/domain/entities/noteTree.ts | 20 +++++ src/domain/service/note.ts | 17 ++++ src/presentation/http/http-api.ts | 2 + src/presentation/http/router/note.ts | 58 +++++++++++++ src/presentation/http/schema/NoteTree.ts | 30 +++++++ src/repository/note.repository.ts | 10 +++ src/repository/noteRelations.repository.ts | 9 ++ .../storage/postgres/orm/sequelize/note.ts | 87 ++++++++++++++++++- .../postgres/orm/sequelize/noteRelations.ts | 30 +++++++ 10 files changed, 274 insertions(+), 10 deletions(-) create mode 100644 src/domain/entities/noteTree.ts create mode 100644 src/presentation/http/schema/NoteTree.ts diff --git a/src/domain/entities/note.ts b/src/domain/entities/note.ts index b393a680..2af128e9 100644 --- a/src/domain/entities/note.ts +++ b/src/domain/entities/note.ts @@ -26,6 +26,18 @@ export type ToolUsedInNoteContent = { id: EditorTool['id']; }; +/** + * NoteContent + */ +export type NoteContent = { + blocks: Array<{ + id: string; + type: string; + data: unknown; + tunes?: { [name: string]: unknown }; + }>; +}; + /** * Note entity */ @@ -43,14 +55,7 @@ export interface Note { /** * Note content */ - content: { - blocks: Array<{ - id: string; - type: string; - data: unknown; - tunes?: { [name: string]: unknown }; - }>; - }; + content: NoteContent; /** * Note creator id diff --git a/src/domain/entities/noteTree.ts b/src/domain/entities/noteTree.ts new file mode 100644 index 00000000..75c22355 --- /dev/null +++ b/src/domain/entities/noteTree.ts @@ -0,0 +1,20 @@ +import type { NoteContent, NotePublicId } from './note.js'; + +export interface NoteTree { + + /** + * public note id + */ + id: NotePublicId; + + /** + * note content + */ + content: NoteContent; + + /** + * child notes + */ + childNotes: NoteTree[] | null; + +} diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 95976baf..e40a0e98 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -9,6 +9,7 @@ import type User from '@domain/entities/user.js'; import type { NoteList } from '@domain/entities/noteList.js'; import type NoteHistoryRepository from '@repository/noteHistory.repository.js'; import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryPublic } from '@domain/entities/noteHistory.js'; +import type { NoteTree } from '@domain/entities/noteTree.js'; /** * Note service @@ -453,4 +454,20 @@ export default class NoteService { return noteParents; } + + /** + * Reutrn a tree structure of notes with childNotes for the given note id + * @param noteId - id of the note to get structure + * @returns - Object of notes. + */ + public async getNoteHierarchy(noteId: NoteInternalId): Promise { + const ultimateParent = await this.noteRelationsRepository.getUltimateParent(noteId); + + // If there is no ultimate parent, the provided noteId is the ultimate parent + const rootNoteId = ultimateParent ?? noteId; + + const noteTree = await this.noteRepository.getNoteTreeByNoteId(rootNoteId); + + return noteTree; + } } diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 31c1046a..000ed943 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -33,6 +33,7 @@ import { DomainError } from '@domain/entities/DomainError.js'; import UploadRouter from './router/upload.js'; import { ajvFilePlugin } from '@fastify/multipart'; import { UploadSchema } from './schema/Upload.js'; +import { NoteTreeSchema } from './schema/NoteTree.js'; const appServerLogger = getLogger('appServer'); @@ -300,6 +301,7 @@ export default class HttpApi implements Api { this.server?.addSchema(JoinSchemaResponse); this.server?.addSchema(OauthSchema); this.server?.addSchema(UploadSchema); + this.server?.addSchema(NoteTreeSchema); } /** diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 3427890b..c91e02ec 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -12,6 +12,8 @@ import type NoteVisitsService from '@domain/service/noteVisits.js'; import type EditorToolsService from '@domain/service/editorTools.js'; import type EditorTool from '@domain/entities/editorTools.js'; import type { NoteHistoryMeta, NoteHistoryPublic, NoteHistoryRecord } from '@domain/entities/noteHistory.js'; +import type { NoteTree } from '@domain/entities/noteTree.js'; +import logger from '@infrastructure/logging/index.js'; /** * Interface for the note router. @@ -140,8 +142,14 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don memberRoleResolver, ], }, async (request, reply) => { + logger.warn(request); const { note } = request; + + logger.warn(note); + const noteId = request.note?.id as number; + + logger.warn(noteId); const { memberRole } = request; const { userId } = request; @@ -774,6 +782,56 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }); }); + fastify.get<{ + Params: { + notePublicId: NotePublicId; + }; + Reply: { + notehierarchy: NoteTree | null; + } | ErrorResponse; + }>('/notehierarchy/:notePublicId', { + config: { + policy: [ + 'authRequired', + ], + }, + schema: { + params: { + notePublicId: { + $ref: 'NoteSchema#/properties/id', + }, + }, + response: { + '2xx': { + type: 'object', + properties: { + notehierarchy: { + $ref: 'NoteTreeSchema#', + }, + }, + }, + }, + }, + preHandler: [ + noteResolver, + ], + }, async (request, reply) => { + const noteId = request?.note?.id as number; + + /** + * Check if note exists + */ + if (noteId === null) { + return reply.notFound('Note not found'); + } + + const noteHierarchy = await noteService.getNoteHierarchy(noteId); + + return reply.send({ + notehierarchy: noteHierarchy, + }); + }); + done(); }; diff --git a/src/presentation/http/schema/NoteTree.ts b/src/presentation/http/schema/NoteTree.ts new file mode 100644 index 00000000..ec3144d9 --- /dev/null +++ b/src/presentation/http/schema/NoteTree.ts @@ -0,0 +1,30 @@ +export const NoteTreeSchema = { + $id: 'NoteTreeSchema', + properties: { + id: { + type: 'string', + pattern: '[a-zA-Z0-9-_]+', + maxLength: 10, + minLength: 10, + }, + content: { + type: 'object', + properties: { + time: { + type: 'number', + }, + blocks: { + type: 'array', + }, + version: { + type: 'string', + }, + }, + }, + childNotes: { + type: 'array', + items: { $ref: 'NoteTreeSchema#' }, + nullable: true, + }, + }, +}; diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index 4bfa3aa5..9c409bc3 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -1,4 +1,5 @@ import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; +import type { NoteTree } from '@domain/entities/noteTree.js'; import type NoteStorage from '@repository/storage/note.storage.js'; /** @@ -90,4 +91,13 @@ export default class NoteRepository { public async getNotesByIds(noteIds: NoteInternalId[]): Promise { return await this.storage.getNotesByIds(noteIds); } + + /** + * Gets the Note tree by note id + * @param noteId - note id + * @returns NoteTree structure + */ + public async getNoteTreeByNoteId(noteId: NoteInternalId): Promise { + return await this.storage.getNoteTreebyNoteId(noteId); + } } diff --git a/src/repository/noteRelations.repository.ts b/src/repository/noteRelations.repository.ts index cb740640..52bd8121 100644 --- a/src/repository/noteRelations.repository.ts +++ b/src/repository/noteRelations.repository.ts @@ -76,4 +76,13 @@ export default class NoteRelationsRepository { public async getNoteParentsIds(noteId: NoteInternalId): Promise { return await this.storage.getNoteParentsIds(noteId); } + + /** + * Get the ultimate parent of a note with note id + * @param noteId - note id to get ultimate parent + * @returns - note id of the ultimate parent + */ + public async getUltimateParent(noteId: NoteInternalId): Promise { + return await this.storage.getUltimateParentByNoteId(noteId); + } } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index c45b3be5..7eca1d1a 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -1,11 +1,12 @@ import type { CreationOptional, InferAttributes, InferCreationAttributes, ModelStatic, NonAttribute, Sequelize } from 'sequelize'; -import { DataTypes, Model, Op } from 'sequelize'; +import { DataTypes, Model, Op, QueryTypes } from 'sequelize'; import type Orm from '@repository/storage/postgres/orm/sequelize/index.js'; -import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; +import type { Note, NoteContent, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; import { UserModel } from '@repository/storage/postgres/orm/sequelize/user.js'; import type { NoteSettingsModel } from './noteSettings.js'; import type { NoteVisitsModel } from './noteVisits.js'; import type { NoteHistoryModel } from './noteHistory.js'; +import type { NoteTree } from '@domain/entities/noteTree.js'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -346,4 +347,86 @@ export default class NoteSequelizeStorage { return notes; } + + /** + * Creates a tree of notes + * @param noteId - public note id + * @returns NoteTree + */ + public async getNoteTreebyNoteId(noteId: NoteInternalId): Promise { + // Fetch all notes and relations in a recursive query + const query = ` + WITH RECURSIVE note_tree AS ( + SELECT + n.id AS noteId, + n.content, + n.public_id, + nr.parent_id + FROM ${String(this.database.literal(this.tableName).val)} n + LEFT JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id + WHERE n.id = :startNoteId + + UNION ALL + + SELECT + n.id AS noteId, + n.content, + n.public_id, + nr.parent_id + FROM ${String(this.database.literal(this.tableName).val)} n + INNER JOIN ${String(this.database.literal('note_relations').val)} nr ON n.id = nr.note_id + INNER JOIN note_tree nt ON nr.parent_id = nt.noteId + ) + SELECT * FROM note_tree; + `; + + const result = await this.model.sequelize?.query(query, { + replacements: { startNoteId: noteId }, + type: QueryTypes.SELECT, + }); + + if (!result || result.length === 0) { + return null; // No data found + } + + type NoteRow = { + noteid: NoteInternalId; + public_id: NotePublicId; + content: NoteContent; + parent_id: NoteInternalId | null; + }; + + const notes = result as NoteRow[]; + + const notesMap = new Map(); + const publicIdMap = new Map(); // Internal to Public ID lookup + + let root: NoteTree | null = null; + + // Step 1: Parse and initialize all notes + notes.forEach((note) => { + notesMap.set(note.noteid, { + id: note.public_id, + content: note.content, + childNotes: [], + }); + + publicIdMap.set(note.noteid, note.public_id); + }); + + // Step 2: Build hierarchy + notes.forEach((note) => { + if (note.parent_id === null) { + root = notesMap.get(note.noteid) ?? null; + } else { + const parent = notesMap.get(note.parent_id); + + if (parent) { + parent.childNotes?.push(notesMap.get(note.noteid)!); + } + } + }); + + return root; + } } diff --git a/src/repository/storage/postgres/orm/sequelize/noteRelations.ts b/src/repository/storage/postgres/orm/sequelize/noteRelations.ts index e7eb9aa8..427702bb 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteRelations.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteRelations.ts @@ -245,4 +245,34 @@ export default class NoteRelationsSequelizeStorage { return noteParents; } + + /** + * Get ultimate parent noteId by noteId + * @param noteId - the ID of note + */ + public async getUltimateParentByNoteId(noteId: NoteInternalId): Promise { + const query = ` + WITH RECURSIVE note_parents AS ( + SELECT np.note_id, np.parent_id + FROM ${String(this.database.literal(this.tableName).val)} np + WHERE np.note_id = :startNoteId + UNION ALL + SELECT nr.note_id, nr.parent_id + FROM ${String(this.database.literal(this.tableName).val)} nr + INNER JOIN note_parents np ON np.parent_id = nr.note_id + ) + SELECT np.parent_id AS "parentId" + FROM note_parents np + WHERE np.parent_id IS NOT NULL + ORDER BY np.parent_id ASC + LIMIT 1;`; + + const result = await this.model.sequelize?.query(query, { + replacements: { startNoteId: noteId }, + type: QueryTypes.SELECT, + }); + let ultimateParent = (result as { parentId: number }[])[0]?.parentId ?? null; + + return ultimateParent; + } } From dfcdab7d17fde5d5ff36dbe7bb748faf4c8e5016 Mon Sep 17 00:00:00 2001 From: Abhishek Jasud Date: Sun, 9 Feb 2025 19:39:17 +0530 Subject: [PATCH 2/6] unused code removed --- src/repository/storage/postgres/orm/sequelize/note.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index 7eca1d1a..07f83a95 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -399,7 +399,6 @@ export default class NoteSequelizeStorage { const notes = result as NoteRow[]; const notesMap = new Map(); - const publicIdMap = new Map(); // Internal to Public ID lookup let root: NoteTree | null = null; @@ -410,8 +409,6 @@ export default class NoteSequelizeStorage { content: note.content, childNotes: [], }); - - publicIdMap.set(note.noteid, note.public_id); }); // Step 2: Build hierarchy From 1acb937bb78fd0e2894e6bff1934b004a8f6011b Mon Sep 17 00:00:00 2001 From: Abhishek Jasud Date: Fri, 14 Feb 2025 22:25:44 +0530 Subject: [PATCH 3/6] Tests added, removed logger leftovers and added a entity doc --- src/domain/entities/noteTree.ts | 3 ++ src/presentation/http/router/note.test.ts | 65 +++++++++++++++++++++++ src/presentation/http/router/note.ts | 5 -- 3 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/domain/entities/noteTree.ts b/src/domain/entities/noteTree.ts index 75c22355..923e3991 100644 --- a/src/domain/entities/noteTree.ts +++ b/src/domain/entities/noteTree.ts @@ -1,5 +1,8 @@ import type { NoteContent, NotePublicId } from './note.js'; +/** + * Note Tree entity + */ export interface NoteTree { /** diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index 4b5ad2b9..eb3125f6 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -2272,4 +2272,69 @@ describe('Note API', () => { expect(response?.json().message).toBe('Note not found'); }); }); + + describe('GET /note/notehierarchy/:noteId', () => { + let accessToken = ''; + let user: User; + + beforeEach(async () => { + /** create test user */ + user = await global.db.insertUser(); + + accessToken = global.auth(user.id); + }); + + test('Get note hierarchy with no parent or child', async () => { + /** + * Insert test note, note history record will be inserted automatically + */ + const note = await global.db.insertNote({ + creatorId: user.id, + }); + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/note/notehierarchy/${note.publicId}`, + }); + + expect(response?.json().notehierarchy.id).toBe(note.publicId); + }); + + test('Get note hierarchy with parent or child', 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, + }); + + /* create test relation */ + await global.db.insertNoteRelation({ + noteId: childNote.id, + parentId: parentNote.id, + }); + + const response = await global.api?.fakeRequest({ + method: 'GET', + headers: { + authorization: `Bearer ${accessToken}`, + }, + url: `/note/notehierarchy/${parentNote.publicId}`, + }); + + expect(response?.json().notehierarchy.childNotes[0].id).toBe(childNote.publicId); + }); + }); }); diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index c91e02ec..8ac4eb47 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -13,7 +13,6 @@ import type EditorToolsService from '@domain/service/editorTools.js'; import type EditorTool from '@domain/entities/editorTools.js'; import type { NoteHistoryMeta, NoteHistoryPublic, NoteHistoryRecord } from '@domain/entities/noteHistory.js'; import type { NoteTree } from '@domain/entities/noteTree.js'; -import logger from '@infrastructure/logging/index.js'; /** * Interface for the note router. @@ -142,14 +141,10 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don memberRoleResolver, ], }, async (request, reply) => { - logger.warn(request); const { note } = request; - logger.warn(note); - const noteId = request.note?.id as number; - logger.warn(noteId); const { memberRole } = request; const { userId } = request; From 47a30562b0516b8caa165870f9224e8c65f4e160 Mon Sep 17 00:00:00 2001 From: Abhishek Jasud Date: Sun, 16 Feb 2025 13:06:32 +0530 Subject: [PATCH 4/6] Naming convention changes, added tests --- .../entities/{noteTree.ts => NoteHierarchy.ts} | 4 ++-- src/domain/service/note.ts | 10 +++++----- src/presentation/http/http-api.ts | 4 ++-- src/presentation/http/router/note.test.ts | 17 ++++++++++++----- src/presentation/http/router/note.ts | 8 ++++---- .../schema/{NoteTree.ts => NoteHierarchy.ts} | 6 +++--- src/repository/note.repository.ts | 8 ++++---- src/repository/noteRelations.repository.ts | 2 +- .../storage/postgres/orm/sequelize/note.ts | 10 +++++----- 9 files changed, 38 insertions(+), 31 deletions(-) rename src/domain/entities/{noteTree.ts => NoteHierarchy.ts} (76%) rename src/presentation/http/schema/{NoteTree.ts => NoteHierarchy.ts} (79%) diff --git a/src/domain/entities/noteTree.ts b/src/domain/entities/NoteHierarchy.ts similarity index 76% rename from src/domain/entities/noteTree.ts rename to src/domain/entities/NoteHierarchy.ts index 923e3991..8ada950f 100644 --- a/src/domain/entities/noteTree.ts +++ b/src/domain/entities/NoteHierarchy.ts @@ -3,7 +3,7 @@ import type { NoteContent, NotePublicId } from './note.js'; /** * Note Tree entity */ -export interface NoteTree { +export interface NoteHierarchy { /** * public note id @@ -18,6 +18,6 @@ export interface NoteTree { /** * child notes */ - childNotes: NoteTree[] | null; + childNotes: NoteHierarchy[] | null; } diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index e40a0e98..111b98fd 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -9,7 +9,7 @@ import type User from '@domain/entities/user.js'; import type { NoteList } from '@domain/entities/noteList.js'; import type NoteHistoryRepository from '@repository/noteHistory.repository.js'; import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryPublic } from '@domain/entities/noteHistory.js'; -import type { NoteTree } from '@domain/entities/noteTree.js'; +import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js'; /** * Note service @@ -460,14 +460,14 @@ export default class NoteService { * @param noteId - id of the note to get structure * @returns - Object of notes. */ - public async getNoteHierarchy(noteId: NoteInternalId): Promise { - const ultimateParent = await this.noteRelationsRepository.getUltimateParent(noteId); + public async getNoteHierarchy(noteId: NoteInternalId): Promise { + const ultimateParent = await this.noteRelationsRepository.getUltimateParentNoteId(noteId); // If there is no ultimate parent, the provided noteId is the ultimate parent const rootNoteId = ultimateParent ?? noteId; - const noteTree = await this.noteRepository.getNoteTreeByNoteId(rootNoteId); + const noteHierarchy = await this.noteRepository.getNoteHierarchyByNoteId(rootNoteId); - return noteTree; + return noteHierarchy; } } diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 000ed943..6e5cb37a 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -33,7 +33,7 @@ import { DomainError } from '@domain/entities/DomainError.js'; import UploadRouter from './router/upload.js'; import { ajvFilePlugin } from '@fastify/multipart'; import { UploadSchema } from './schema/Upload.js'; -import { NoteTreeSchema } from './schema/NoteTree.js'; +import { NoteHierarchySchema } from './schema/NoteHierarchy.js'; const appServerLogger = getLogger('appServer'); @@ -301,7 +301,7 @@ export default class HttpApi implements Api { this.server?.addSchema(JoinSchemaResponse); this.server?.addSchema(OauthSchema); this.server?.addSchema(UploadSchema); - this.server?.addSchema(NoteTreeSchema); + this.server?.addSchema(NoteHierarchySchema); } /** diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index eb3125f6..f45272f5 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -2273,7 +2273,7 @@ describe('Note API', () => { }); }); - describe('GET /note/notehierarchy/:noteId', () => { + describe('GET /note/note-hierarchy/:noteId', () => { let accessToken = ''; let user: User; @@ -2297,13 +2297,14 @@ describe('Note API', () => { headers: { authorization: `Bearer ${accessToken}`, }, - url: `/note/notehierarchy/${note.publicId}`, + url: `/note/note-hierarchy/${note.publicId}`, }); expect(response?.json().notehierarchy.id).toBe(note.publicId); + expect(response?.json().notehierarchy.childNotes).toHaveLength(0); }); - test('Get note hierarchy with parent or child', async () => { + test('Get note hierarchy with child', async () => { /* create test child note */ const childNote = await global.db.insertNote({ creatorId: user.id, @@ -2331,10 +2332,16 @@ describe('Note API', () => { headers: { authorization: `Bearer ${accessToken}`, }, - url: `/note/notehierarchy/${parentNote.publicId}`, + url: `/note/note-hierarchy/${parentNote.publicId}`, }); - expect(response?.json().notehierarchy.childNotes[0].id).toBe(childNote.publicId); + const childNoteObj = { + id: childNote.publicId, + content: childNote.content, + childNotes: [], + }; + + expect(response?.json().notehierarchy.childNotes[0]).toStrictEqual(childNoteObj); }); }); }); diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 8ac4eb47..76a1823e 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -12,7 +12,7 @@ import type NoteVisitsService from '@domain/service/noteVisits.js'; import type EditorToolsService from '@domain/service/editorTools.js'; import type EditorTool from '@domain/entities/editorTools.js'; import type { NoteHistoryMeta, NoteHistoryPublic, NoteHistoryRecord } from '@domain/entities/noteHistory.js'; -import type { NoteTree } from '@domain/entities/noteTree.js'; +import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js'; /** * Interface for the note router. @@ -782,9 +782,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don notePublicId: NotePublicId; }; Reply: { - notehierarchy: NoteTree | null; + notehierarchy: NoteHierarchy | null; } | ErrorResponse; - }>('/notehierarchy/:notePublicId', { + }>('/note-hierarchy/:notePublicId', { config: { policy: [ 'authRequired', @@ -801,7 +801,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don type: 'object', properties: { notehierarchy: { - $ref: 'NoteTreeSchema#', + $ref: 'NoteHierarchySchema#', }, }, }, diff --git a/src/presentation/http/schema/NoteTree.ts b/src/presentation/http/schema/NoteHierarchy.ts similarity index 79% rename from src/presentation/http/schema/NoteTree.ts rename to src/presentation/http/schema/NoteHierarchy.ts index ec3144d9..84b27fd5 100644 --- a/src/presentation/http/schema/NoteTree.ts +++ b/src/presentation/http/schema/NoteHierarchy.ts @@ -1,5 +1,5 @@ -export const NoteTreeSchema = { - $id: 'NoteTreeSchema', +export const NoteHierarchySchema = { + $id: 'NoteHierarchySchema', properties: { id: { type: 'string', @@ -23,7 +23,7 @@ export const NoteTreeSchema = { }, childNotes: { type: 'array', - items: { $ref: 'NoteTreeSchema#' }, + items: { $ref: 'NoteHierarchySchema#' }, nullable: true, }, }, diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index 9c409bc3..a56df5d0 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -1,5 +1,5 @@ import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; -import type { NoteTree } from '@domain/entities/noteTree.js'; +import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js'; import type NoteStorage from '@repository/storage/note.storage.js'; /** @@ -95,9 +95,9 @@ export default class NoteRepository { /** * Gets the Note tree by note id * @param noteId - note id - * @returns NoteTree structure + * @returns NoteHierarchy structure */ - public async getNoteTreeByNoteId(noteId: NoteInternalId): Promise { - return await this.storage.getNoteTreebyNoteId(noteId); + public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise { + return await this.storage.getNoteHierarchybyNoteId(noteId); } } diff --git a/src/repository/noteRelations.repository.ts b/src/repository/noteRelations.repository.ts index 52bd8121..69f30779 100644 --- a/src/repository/noteRelations.repository.ts +++ b/src/repository/noteRelations.repository.ts @@ -82,7 +82,7 @@ export default class NoteRelationsRepository { * @param noteId - note id to get ultimate parent * @returns - note id of the ultimate parent */ - public async getUltimateParent(noteId: NoteInternalId): Promise { + public async getUltimateParentNoteId(noteId: NoteInternalId): Promise { return await this.storage.getUltimateParentByNoteId(noteId); } } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index 07f83a95..912957a4 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -6,7 +6,7 @@ import { UserModel } from '@repository/storage/postgres/orm/sequelize/user.js'; import type { NoteSettingsModel } from './noteSettings.js'; import type { NoteVisitsModel } from './noteVisits.js'; import type { NoteHistoryModel } from './noteHistory.js'; -import type { NoteTree } from '@domain/entities/noteTree.js'; +import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -351,9 +351,9 @@ export default class NoteSequelizeStorage { /** * Creates a tree of notes * @param noteId - public note id - * @returns NoteTree + * @returns NoteHierarchy */ - public async getNoteTreebyNoteId(noteId: NoteInternalId): Promise { + public async getNoteHierarchybyNoteId(noteId: NoteInternalId): Promise { // Fetch all notes and relations in a recursive query const query = ` WITH RECURSIVE note_tree AS ( @@ -398,9 +398,9 @@ export default class NoteSequelizeStorage { const notes = result as NoteRow[]; - const notesMap = new Map(); + const notesMap = new Map(); - let root: NoteTree | null = null; + let root: NoteHierarchy | null = null; // Step 1: Parse and initialize all notes notes.forEach((note) => { From 6d3919a0d4caebac0f9a888f832ca43bfbc6d40e Mon Sep 17 00:00:00 2001 From: Abhishek Jasud Date: Sun, 16 Feb 2025 21:09:46 +0530 Subject: [PATCH 5/6] Changes to note test and default childnotes null --- src/presentation/http/router/note.test.ts | 114 +++++++++++------- src/presentation/http/router/note.ts | 14 ++- .../storage/postgres/orm/sequelize/note.ts | 6 +- 3 files changed, 81 insertions(+), 53 deletions(-) diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index f45272f5..f7a911b9 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -1,6 +1,7 @@ import { MemberRole } from '@domain/entities/team.js'; import { describe, test, expect, beforeEach } from 'vitest'; import type User from '@domain/entities/user.js'; +import type { Note } from '@domain/entities/note.js'; describe('Note API', () => { /** @@ -2284,64 +2285,85 @@ describe('Note API', () => { accessToken = global.auth(user.id); }); - test('Get note hierarchy with no parent or child', async () => { - /** - * Insert test note, note history record will be inserted automatically - */ - const note = await global.db.insertNote({ - creatorId: user.id, - }); - - const response = await global.api?.fakeRequest({ - method: 'GET', - headers: { - authorization: `Bearer ${accessToken}`, - }, - url: `/note/note-hierarchy/${note.publicId}`, - }); - - expect(response?.json().notehierarchy.id).toBe(note.publicId); - expect(response?.json().notehierarchy.childNotes).toHaveLength(0); - }); + test.each([ + // Test case 1: No parent or child + { + description: 'Get note hierarchy with no parent or child', + setup: async () => { + const note = await global.db.insertNote({ creatorId: user.id }); - test('Get note hierarchy with child', async () => { - /* create test child note */ - const childNote = await global.db.insertNote({ - creatorId: user.id, - }); + await global.db.insertNoteSetting({ + noteId: note.id, + isPublic: true, + }); - /* create test parent note */ - const parentNote = await global.db.insertNote({ - creatorId: user.id, - }); + return { + note: note, + childNote: null, + }; + }, - /* create note settings for child note */ - await global.db.insertNoteSetting({ - noteId: childNote.id, - isPublic: true, - }); + expected: (note: Note, childNote: Note | null) => ({ + id: note.publicId, + content: note.content, + childNotes: childNote, + }), + }, - /* create test relation */ - await global.db.insertNoteRelation({ - noteId: childNote.id, - parentId: parentNote.id, - }); + // Test case 2: With child + { + description: 'Get note hierarchy with child', + setup: async () => { + const childNote = await global.db.insertNote({ creatorId: user.id }); + const parentNote = await global.db.insertNote({ creatorId: user.id }); + + await global.db.insertNoteSetting({ + noteId: childNote.id, + isPublic: true, + }); + await global.db.insertNoteSetting({ + noteId: parentNote.id, + isPublic: true, + }); + await global.db.insertNoteRelation({ + noteId: childNote.id, + parentId: parentNote.id, + }); + + return { + note: parentNote, + childNote: childNote, + }; + }, + expected: (note: Note, childNote: Note | null) => ({ + id: note.publicId, + content: note.content, + childNotes: [ + { + id: childNote?.publicId, + content: childNote?.content, + childNotes: null, + }, + ], + }), + }, + ])('$description', async ({ setup, expected }) => { + // Setup the test data + const { note, childNote } = await setup(); + // Make the API request const response = await global.api?.fakeRequest({ method: 'GET', headers: { authorization: `Bearer ${accessToken}`, }, - url: `/note/note-hierarchy/${parentNote.publicId}`, + url: `/note/note-hierarchy/${note.publicId}`, }); - const childNoteObj = { - id: childNote.publicId, - content: childNote.content, - childNotes: [], - }; - - expect(response?.json().notehierarchy.childNotes[0]).toStrictEqual(childNoteObj); + // Verify the response + expect(response?.json().noteHierarchy).toStrictEqual( + expected(note, childNote) + ); }); }); }); diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 76a1823e..f6cb0492 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -782,12 +782,12 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don notePublicId: NotePublicId; }; Reply: { - notehierarchy: NoteHierarchy | null; + noteHierarchy: NoteHierarchy | null; } | ErrorResponse; }>('/note-hierarchy/:notePublicId', { config: { policy: [ - 'authRequired', + 'notePublicOrUserInTeam', ], }, schema: { @@ -800,7 +800,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don '2xx': { type: 'object', properties: { - notehierarchy: { + noteHierarchy: { $ref: 'NoteHierarchySchema#', }, }, @@ -809,21 +809,23 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }, preHandler: [ noteResolver, + noteSettingsResolver, + memberRoleResolver, ], }, async (request, reply) => { - const noteId = request?.note?.id as number; + const noteId = request.note?.id as number; /** * Check if note exists */ if (noteId === null) { - return reply.notFound('Note not found'); + return reply.notAcceptable('Note not found'); } const noteHierarchy = await noteService.getNoteHierarchy(noteId); return reply.send({ - notehierarchy: noteHierarchy, + noteHierarchy: noteHierarchy, }); }); diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index 912957a4..b621ffc9 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -407,7 +407,7 @@ export default class NoteSequelizeStorage { notesMap.set(note.noteid, { id: note.public_id, content: note.content, - childNotes: [], + childNotes: null, }); }); @@ -419,6 +419,10 @@ export default class NoteSequelizeStorage { const parent = notesMap.get(note.parent_id); if (parent) { + // Initialize childNotes as an array if it's null + if (parent.childNotes === null) { + parent.childNotes = []; + } parent.childNotes?.push(notesMap.get(note.noteid)!); } } From 86038c99e84bb05c18b5c778046466c7fe6c0d33 Mon Sep 17 00:00:00 2001 From: Abhishek Jasud Date: Sun, 16 Feb 2025 23:26:56 +0530 Subject: [PATCH 6/6] Comments changes --- src/presentation/http/router/note.test.ts | 4 ++-- .../storage/postgres/orm/sequelize/noteRelations.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index f7a911b9..2d3ad260 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -2288,7 +2288,7 @@ describe('Note API', () => { test.each([ // Test case 1: No parent or child { - description: 'Get note hierarchy with no parent or child', + description: 'Should get note hierarchy with no parent or child when noteId passed has no relations', setup: async () => { const note = await global.db.insertNote({ creatorId: user.id }); @@ -2312,7 +2312,7 @@ describe('Note API', () => { // Test case 2: With child { - description: 'Get note hierarchy with child', + description: 'Should get note hierarchy with child when noteId passed has relations', setup: async () => { const childNote = await global.db.insertNote({ creatorId: user.id }); const parentNote = await global.db.insertNote({ creatorId: user.id }); diff --git a/src/repository/storage/postgres/orm/sequelize/noteRelations.ts b/src/repository/storage/postgres/orm/sequelize/noteRelations.ts index 427702bb..82380ea4 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteRelations.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteRelations.ts @@ -247,7 +247,7 @@ export default class NoteRelationsSequelizeStorage { } /** - * Get ultimate parent noteId by noteId + * Gets the ultimate parent noteId or `null` if none exists * @param noteId - the ID of note */ public async getUltimateParentByNoteId(noteId: NoteInternalId): Promise {