Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 NoteRow = {
Comment thread
neSpecc marked this conversation as resolved.
Outdated
/**
* Note id
*/
noteId: NoteInternalId;

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

/**
* Note content
*/
content: NoteContent;
Comment thread
neSpecc marked this conversation as resolved.

/**
* 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.getNoteRowByNoteId(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);
};
}
31 changes: 24 additions & 7 deletions src/presentation/http/router/note.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +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';
import type { Note, NoteContent } from '@domain/entities/note.js';

describe('Note API', () => {
/**
Expand Down Expand Up @@ -2278,6 +2278,23 @@ describe('Note API', () => {
let accessToken = '';
let user: User;

const getTitleFromContent = (content: NoteContent | undefined): string => {
Comment thread
e11sy marked this conversation as resolved.
Outdated
Comment thread
neSpecc marked this conversation as resolved.
Outdated
const limitCharsForNoteTitle = 50;

if (!content) {
return 'Untitled';
}

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

beforeEach(async () => {
/** create test user */
user = await global.db.insertUser();
Expand All @@ -2304,8 +2321,8 @@ describe('Note API', () => {
},

expected: (note: Note, childNote: Note | null) => ({
id: note.publicId,
content: note.content,
noteId: note.publicId,
noteTitle: getTitleFromContent(note.content),
childNotes: childNote,
}),
},
Expand Down Expand Up @@ -2336,12 +2353,12 @@ describe('Note API', () => {
};
},
expected: (note: Note, childNote: Note | null) => ({
id: note.publicId,
content: note.content,
noteId: note.publicId,
noteTitle: getTitleFromContent(note.content),
childNotes: [
{
id: childNote?.publicId,
content: childNote?.content,
noteId: childNote?.publicId,
noteTitle: getTitleFromContent(childNote?.content),
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, NoteRow } 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
* Fetches the raw recursive note tree data from DB
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Improve the comment please

* @param noteId - note id
* @returns NoteHierarchy structure
* @returns Array of raw note rows (note with parent_id)
*/
public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
return await this.storage.getNoteHierarchybyNoteId(noteId);
public async getNoteRowByNoteId(noteId: NoteInternalId): Promise<NoteRow[] | null> {
return await this.storage.getNoteRowbyNoteId(noteId);
}
}
65 changes: 13 additions & 52 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, NoteRow } 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
* Fetches the raw recursive note tree data from DB
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Improve the comment please

* @param noteId - note id
* @returns Array of raw note rows (note with parent_id)
*/
public async getNoteHierarchybyNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
public async getNoteRowbyNoteId(noteId: NoteInternalId): Promise<NoteRow[] | 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
}

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