Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/domain/entities/NoteHierarchy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NoteContent, NotePublicId } from './note.js';
import type { NotePublicId } from './note.js';

/**
* Note Tree entity
Expand All @@ -8,12 +8,12 @@ export interface NoteHierarchy {
/**
* public note id
*/
id: NotePublicId;
noteId: NotePublicId;

/**
* note content
* note title
*/
content: NoteContent;
noteTitle: string;

/**
* child notes
Expand Down
25 changes: 25 additions & 0 deletions src/domain/entities/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,28 @@ export interface Note {
* Part of note entity used to create new note
*/
export type NoteCreationAttributes = Pick<Note, 'publicId' | 'content' | 'creatorId' | 'tools'>;

/**
* Note preview entity used to display notes in a sidebar hierarchy
*/
export type NotePreview = {
/**
* Note id
*/
noteId: NoteInternalId;

/**
* Note public id
*/
publicId: NotePublicId;

/**
* Note content
*/
content: NoteContent;

/**
* Parent note id
*/
parentId: NoteInternalId | null;
};
56 changes: 53 additions & 3 deletions src/domain/service/note.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<NoteInternalId, NoteHierarchy>();

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(/&nbsp;/g, ' ').slice(0, limitCharsForNoteTitle);
};
}
27 changes: 18 additions & 9 deletions src/presentation/http/router/note.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
}),
},
Expand All @@ -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,
Expand All @@ -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,
},
],
Expand Down
18 changes: 4 additions & 14 deletions src/presentation/http/schema/NoteHierarchy.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
11 changes: 5 additions & 6 deletions src/repository/note.repository.ts
Original file line number Diff line number Diff line change
@@ -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, NotePreview } from '@domain/entities/note.js';
import type NoteStorage from '@repository/storage/note.storage.js';

/**
Expand Down Expand Up @@ -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 NotePreview
*/
public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
return await this.storage.getNoteHierarchybyNoteId(noteId);
public async getNoteTreeByNoteId(noteId: NoteInternalId): Promise<NotePreview[] | null> {
return await this.storage.getNoteTreebyNoteId(noteId);
}
}
67 changes: 14 additions & 53 deletions src/repository/storage/postgres/orm/sequelize/note.ts
Original file line number Diff line number Diff line change
@@ -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, NotePreview } 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 */

Expand Down Expand Up @@ -349,33 +348,33 @@ 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 NotePreview
*/
public async getNoteHierarchybyNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
public async getNoteTreebyNoteId(noteId: NoteInternalId): Promise<NotePreview[] | null> {
// 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

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;
`;
Expand All @@ -388,46 +387,8 @@ export default class NoteSequelizeStorage {
if (!result || result.length === 0) {
return null; // No data found
}
const notes = result as NotePreview[];

type NoteRow = {
noteid: NoteInternalId;
public_id: NotePublicId;
content: NoteContent;
parent_id: NoteInternalId | null;
};

const notes = result as NoteRow[];

const notesMap = new Map<NoteInternalId, NoteHierarchy>();

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;
}
}
Loading