Skip to content

Fix/note title composer #291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
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'>;

/**
* Part of note Hierarchy
*/
export type NoteDAO = {
/**
* Note id
*/
noteId: NoteInternalId;

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

/**
* Note content
*/
content: NoteContent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why there is a 'content' instead of 'title' like in NoteHierarchy entity

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the data we get from the database with the query and then change the content to title build the heirarchy structure and then return to frontend

Copy link
Member

@neSpecc neSpecc May 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we really need this type at all? Why not to use just Note entity like in other methods which returns a note from the database?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still actual question

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note entity dose not have parentId in it.


/**
* 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, NoteDAO } 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 note DAO
*/
public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
return await this.storage.getNoteHierarchybyNoteId(noteId);
public async getNoteTreeByNoteId(noteId: NoteInternalId): Promise<NoteDAO[] | 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, 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 */

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 note DAO
*/
public async getNoteHierarchybyNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
public async getNoteTreebyNoteId(noteId: NoteInternalId): Promise<NoteDAO[] | 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 NoteDAO[];

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