diff --git a/src/domain/entities/NoteHierarchy.ts b/src/domain/entities/NoteHierarchy.ts index 8ada950f..c6ff0765 100644 --- a/src/domain/entities/NoteHierarchy.ts +++ b/src/domain/entities/NoteHierarchy.ts @@ -1,4 +1,4 @@ -import type { NoteContent, NotePublicId } from './note.js'; +import type { NotePublicId } from './note.js'; /** * Note Tree entity @@ -8,12 +8,12 @@ export interface NoteHierarchy { /** * public note id */ - id: NotePublicId; + noteId: NotePublicId; /** - * note content + * note title */ - content: NoteContent; + noteTitle: string; /** * child notes diff --git a/src/domain/entities/note.ts b/src/domain/entities/note.ts index 2af128e9..b85e7208 100644 --- a/src/domain/entities/note.ts +++ b/src/domain/entities/note.ts @@ -87,3 +87,28 @@ export interface Note { * Part of note entity used to create new note */ export type NoteCreationAttributes = Pick; + +/** + * Part of note Hierarchy + */ +export type NoteDAO = { + /** + * Note id + */ + noteId: NoteInternalId; + + /** + * Note public id + */ + publicId: NotePublicId; + + /** + * Note content + */ + content: NoteContent; + + /** + * Parent note id + */ + parentId: NoteInternalId | null; +}; diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 111b98fd..c4ae5b52 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -1,4 +1,4 @@ -import type { Note, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; +import type { Note, NoteContent, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; import type NoteRepository from '@repository/note.repository.js'; import type NoteVisitsRepository from '@repository/noteVisits.repository.js'; import { createPublicId } from '@infrastructure/utils/id.js'; @@ -466,8 +466,58 @@ export default class NoteService { // If there is no ultimate parent, the provided noteId is the ultimate parent const rootNoteId = ultimateParent ?? noteId; - const noteHierarchy = await this.noteRepository.getNoteHierarchyByNoteId(rootNoteId); + const notesRows = await this.noteRepository.getNoteTreeByNoteId(rootNoteId); - return noteHierarchy; + const notesMap = new Map(); + + let root: NoteHierarchy | null = null; + + if (!notesRows || notesRows.length === 0) { + return null; + } + // Step 1: Parse and initialize all notes + notesRows.forEach((note) => { + notesMap.set(note.noteId, { + noteId: note.publicId, + noteTitle: this.getTitleFromContent(note.content), + childNotes: null, + }); + }); + + // Step 2: Build hierarchy + notesRows.forEach((note) => { + if (note.parentId === null) { + root = notesMap.get(note.noteId) ?? null; + } else { + const parent = notesMap.get(note.parentId); + + if (parent) { + // Initialize childNotes as an array if it's null + if (parent.childNotes === null) { + parent.childNotes = []; + } + parent.childNotes?.push(notesMap.get(note.noteId)!); + } + } + }); + + return root; } + + /** + * Get the title of the note + * @param content - content of the note + * @returns the title of the note + */ + public getTitleFromContent(content: NoteContent): string { + const limitCharsForNoteTitle = 50; + const firstNoteBlock = content.blocks[0]; + const text = (firstNoteBlock?.data as { text?: string })?.text; + + if (text === undefined || text.trim() === '') { + return 'Untitled'; + } + + return text.replace(/ /g, ' ').slice(0, limitCharsForNoteTitle); + }; } diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index 2d3ad260..575932ac 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -2290,7 +2290,10 @@ describe('Note API', () => { { 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 }); + const note = await global.db.insertNote({ + creatorId: user.id, + content: DEFAULT_NOTE_CONTENT, + }); await global.db.insertNoteSetting({ noteId: note.id, @@ -2304,8 +2307,8 @@ describe('Note API', () => { }, expected: (note: Note, childNote: Note | null) => ({ - id: note.publicId, - content: note.content, + noteId: note.publicId, + noteTitle: 'text', childNotes: childNote, }), }, @@ -2314,8 +2317,14 @@ describe('Note API', () => { { 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 }); + const childNote = await global.db.insertNote({ + creatorId: user.id, + content: DEFAULT_NOTE_CONTENT, + }); + const parentNote = await global.db.insertNote({ + creatorId: user.id, + content: DEFAULT_NOTE_CONTENT, + }); await global.db.insertNoteSetting({ noteId: childNote.id, @@ -2336,12 +2345,12 @@ describe('Note API', () => { }; }, expected: (note: Note, childNote: Note | null) => ({ - id: note.publicId, - content: note.content, + noteId: note.publicId, + noteTitle: 'text', childNotes: [ { - id: childNote?.publicId, - content: childNote?.content, + noteId: childNote?.publicId, + noteTitle: 'text', childNotes: null, }, ], diff --git a/src/presentation/http/schema/NoteHierarchy.ts b/src/presentation/http/schema/NoteHierarchy.ts index 84b27fd5..0e169f60 100644 --- a/src/presentation/http/schema/NoteHierarchy.ts +++ b/src/presentation/http/schema/NoteHierarchy.ts @@ -1,25 +1,15 @@ export const NoteHierarchySchema = { $id: 'NoteHierarchySchema', properties: { - id: { + noteId: { type: 'string', pattern: '[a-zA-Z0-9-_]+', maxLength: 10, minLength: 10, }, - content: { - type: 'object', - properties: { - time: { - type: 'number', - }, - blocks: { - type: 'array', - }, - version: { - type: 'string', - }, - }, + noteTitle: { + type: 'string', + maxLength: 50, }, childNotes: { type: 'array', diff --git a/src/repository/note.repository.ts b/src/repository/note.repository.ts index a56df5d0..f0b1e5c0 100644 --- a/src/repository/note.repository.ts +++ b/src/repository/note.repository.ts @@ -1,5 +1,4 @@ -import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; -import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js'; +import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId, NoteDAO } from '@domain/entities/note.js'; import type NoteStorage from '@repository/storage/note.storage.js'; /** @@ -93,11 +92,11 @@ export default class NoteRepository { } /** - * Gets the Note tree by note id + * Get note and all of its children recursively * @param noteId - note id - * @returns NoteHierarchy structure + * @returns an array of note DAO */ - public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise { - return await this.storage.getNoteHierarchybyNoteId(noteId); + public async getNoteTreeByNoteId(noteId: NoteInternalId): Promise { + return await this.storage.getNoteTreebyNoteId(noteId); } } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index b621ffc9..ef65799e 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -1,12 +1,11 @@ import type { CreationOptional, InferAttributes, InferCreationAttributes, ModelStatic, NonAttribute, Sequelize } from 'sequelize'; import { DataTypes, Model, Op, QueryTypes } from 'sequelize'; import type Orm from '@repository/storage/postgres/orm/sequelize/index.js'; -import type { Note, NoteContent, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js'; +import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId, NoteDAO } 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 { NoteHierarchy } from '@domain/entities/NoteHierarchy.js'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -349,19 +348,19 @@ export default class NoteSequelizeStorage { } /** - * Creates a tree of notes - * @param noteId - public note id - * @returns NoteHierarchy + * Get note and all of its children recursively + * @param noteId - note id + * @returns an array of note DAO */ - public async getNoteHierarchybyNoteId(noteId: NoteInternalId): Promise { + 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.id AS "noteId", n.content, - n.public_id, - nr.parent_id + n.public_id AS "publicId", + nr.parent_id AS "parentId" 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 @@ -369,13 +368,13 @@ export default class NoteSequelizeStorage { UNION ALL SELECT - n.id AS noteId, + n.id AS "noteId", n.content, - n.public_id, - nr.parent_id + n.public_id AS "publicId", + nr.parent_id AS "parentId" 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 + INNER JOIN note_tree nt ON nr.parent_id = nt."noteId" ) SELECT * FROM note_tree; `; @@ -388,46 +387,8 @@ export default class NoteSequelizeStorage { if (!result || result.length === 0) { return null; // No data found } + const notes = result as NoteDAO[]; - type NoteRow = { - noteid: NoteInternalId; - public_id: NotePublicId; - content: NoteContent; - parent_id: NoteInternalId | null; - }; - - const notes = result as NoteRow[]; - - const notesMap = new Map(); - - let root: NoteHierarchy | null = null; - - // Step 1: Parse and initialize all notes - notes.forEach((note) => { - notesMap.set(note.noteid, { - id: note.public_id, - content: note.content, - childNotes: null, - }); - }); - - // 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) { - // Initialize childNotes as an array if it's null - if (parent.childNotes === null) { - parent.childNotes = []; - } - parent.childNotes?.push(notesMap.get(note.noteid)!); - } - } - }); - - return root; + return notes; } }