Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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;
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.getNoteDAOByNoteId(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 row by noteId
* @param noteId - note id
* @returns NoteHierarchy structure
* @returns an array of note rows
*/
public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
return await this.storage.getNoteHierarchybyNoteId(noteId);
public async getNoteDAOByNoteId(noteId: NoteInternalId): Promise<NoteDAO[] | null> {
return await this.storage.getNoteDAObyNoteId(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 row by noteId
* @param noteId - note id
* @returns an array of note rows
*/
public async getNoteHierarchybyNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
public async getNoteDAObyNoteId(noteId: NoteInternalId): Promise<NoteDAO[] | null> {
Comment thread
neSpecc marked this conversation as resolved.
Outdated
// 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