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
23 changes: 23 additions & 0 deletions src/domain/entities/NoteHierarchy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { NoteContent, NotePublicId } from './note.js';

/**
* Note Tree entity
*/
export interface NoteHierarchy {

/**
* public note id
*/
id: NotePublicId;

/**
* note content
*/
content: NoteContent;

/**
* child notes
*/
childNotes: NoteHierarchy[] | null;

}
21 changes: 13 additions & 8 deletions src/domain/entities/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ export type ToolUsedInNoteContent = {
id: EditorTool['id'];
};

/**
* NoteContent
*/
export type NoteContent = {
blocks: Array<{
id: string;
type: string;
data: unknown;
tunes?: { [name: string]: unknown };
}>;
};

/**
* Note entity
*/
Expand All @@ -43,14 +55,7 @@ export interface Note {
/**
* Note content
*/
content: {
blocks: Array<{
id: string;
type: string;
data: unknown;
tunes?: { [name: string]: unknown };
}>;
};
content: NoteContent;

/**
* Note creator id
Expand Down
17 changes: 17 additions & 0 deletions src/domain/service/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type User from '@domain/entities/user.js';
import type { NoteList } from '@domain/entities/noteList.js';
import type NoteHistoryRepository from '@repository/noteHistory.repository.js';
import type { NoteHistoryMeta, NoteHistoryRecord, NoteHistoryPublic } from '@domain/entities/noteHistory.js';
import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js';

/**
* Note service
Expand Down Expand Up @@ -453,4 +454,20 @@ export default class NoteService {

return noteParents;
}

/**
* Reutrn a tree structure of notes with childNotes for the given note id
* @param noteId - id of the note to get structure
* @returns - Object of notes.
*/
public async getNoteHierarchy(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
const ultimateParent = await this.noteRelationsRepository.getUltimateParentNoteId(noteId);

// If there is no ultimate parent, the provided noteId is the ultimate parent
const rootNoteId = ultimateParent ?? noteId;

const noteHierarchy = await this.noteRepository.getNoteHierarchyByNoteId(rootNoteId);

return noteHierarchy;
}
}
2 changes: 2 additions & 0 deletions src/presentation/http/http-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { DomainError } from '@domain/entities/DomainError.js';
import UploadRouter from './router/upload.js';
import { ajvFilePlugin } from '@fastify/multipart';
import { UploadSchema } from './schema/Upload.js';
import { NoteHierarchySchema } from './schema/NoteHierarchy.js';

const appServerLogger = getLogger('appServer');

Expand Down Expand Up @@ -300,6 +301,7 @@ export default class HttpApi implements Api {
this.server?.addSchema(JoinSchemaResponse);
this.server?.addSchema(OauthSchema);
this.server?.addSchema(UploadSchema);
this.server?.addSchema(NoteHierarchySchema);
}

/**
Expand Down
94 changes: 94 additions & 0 deletions src/presentation/http/router/note.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +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';

describe('Note API', () => {
/**
Expand Down Expand Up @@ -2272,4 +2273,97 @@ describe('Note API', () => {
expect(response?.json().message).toBe('Note not found');
});
});

describe('GET /note/note-hierarchy/:noteId', () => {
let accessToken = '';
let user: User;

beforeEach(async () => {
/** create test user */
user = await global.db.insertUser();

accessToken = global.auth(user.id);
});

test.each([
// Test case 1: No parent or child
{
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 });

await global.db.insertNoteSetting({
noteId: note.id,
isPublic: true,
});

return {
note: note,
childNote: null,
};
},

expected: (note: Note, childNote: Note | null) => ({
id: note.publicId,
content: note.content,
childNotes: childNote,
}),
},

// Test case 2: With child
{
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 });

await global.db.insertNoteSetting({
noteId: childNote.id,
isPublic: true,
});
await global.db.insertNoteSetting({
noteId: parentNote.id,
isPublic: true,
});
await global.db.insertNoteRelation({
noteId: childNote.id,
parentId: parentNote.id,
});

return {
note: parentNote,
childNote: childNote,
};
},
expected: (note: Note, childNote: Note | null) => ({
id: note.publicId,
content: note.content,
childNotes: [
{
id: childNote?.publicId,
content: childNote?.content,
childNotes: null,
},
],
}),
},
])('$description', async ({ setup, expected }) => {
// Setup the test data
const { note, childNote } = await setup();

// Make the API request
const response = await global.api?.fakeRequest({
method: 'GET',
headers: {
authorization: `Bearer ${accessToken}`,
},
url: `/note/note-hierarchy/${note.publicId}`,
});

// Verify the response
expect(response?.json().noteHierarchy).toStrictEqual(
expected(note, childNote)
);
});
});
});
55 changes: 55 additions & 0 deletions src/presentation/http/router/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type NoteVisitsService from '@domain/service/noteVisits.js';
import type EditorToolsService from '@domain/service/editorTools.js';
import type EditorTool from '@domain/entities/editorTools.js';
import type { NoteHistoryMeta, NoteHistoryPublic, NoteHistoryRecord } from '@domain/entities/noteHistory.js';
import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js';

/**
* Interface for the note router.
Expand Down Expand Up @@ -141,7 +142,9 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
],
}, async (request, reply) => {
const { note } = request;

const noteId = request.note?.id as number;

const { memberRole } = request;
const { userId } = request;

Expand Down Expand Up @@ -774,6 +777,58 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
});
});

fastify.get<{
Params: {
notePublicId: NotePublicId;
};
Reply: {
noteHierarchy: NoteHierarchy | null;
} | ErrorResponse;
}>('/note-hierarchy/:notePublicId', {
config: {
policy: [
'notePublicOrUserInTeam',
],
},
schema: {
params: {
notePublicId: {
$ref: 'NoteSchema#/properties/id',
},
},
response: {
'2xx': {
type: 'object',
properties: {
noteHierarchy: {
$ref: 'NoteHierarchySchema#',
},
},
},
},
},
preHandler: [
noteResolver,
noteSettingsResolver,
memberRoleResolver,
],
}, async (request, reply) => {
const noteId = request.note?.id as number;

/**
* Check if note exists
*/
if (noteId === null) {
return reply.notAcceptable('Note not found');
}

const noteHierarchy = await noteService.getNoteHierarchy(noteId);

return reply.send({
noteHierarchy: noteHierarchy,
});
});

done();
};

Expand Down
30 changes: 30 additions & 0 deletions src/presentation/http/schema/NoteHierarchy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const NoteHierarchySchema = {
$id: 'NoteHierarchySchema',
properties: {
id: {
type: 'string',
pattern: '[a-zA-Z0-9-_]+',
maxLength: 10,
minLength: 10,
},
content: {
type: 'object',
properties: {
time: {
type: 'number',
},
blocks: {
type: 'array',
},
version: {
type: 'string',
},
},
},
childNotes: {
type: 'array',
items: { $ref: 'NoteHierarchySchema#' },
nullable: true,
},
},
};
10 changes: 10 additions & 0 deletions src/repository/note.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Note, NoteCreationAttributes, NoteInternalId, NotePublicId } from '@domain/entities/note.js';
import type { NoteHierarchy } from '@domain/entities/NoteHierarchy.js';
import type NoteStorage from '@repository/storage/note.storage.js';

/**
Expand Down Expand Up @@ -90,4 +91,13 @@ export default class NoteRepository {
public async getNotesByIds(noteIds: NoteInternalId[]): Promise<Note[]> {
return await this.storage.getNotesByIds(noteIds);
}

/**
* Gets the Note tree by note id
* @param noteId - note id
* @returns NoteHierarchy structure
*/
public async getNoteHierarchyByNoteId(noteId: NoteInternalId): Promise<NoteHierarchy | null> {
return await this.storage.getNoteHierarchybyNoteId(noteId);
}
}
9 changes: 9 additions & 0 deletions src/repository/noteRelations.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,13 @@ export default class NoteRelationsRepository {
public async getNoteParentsIds(noteId: NoteInternalId): Promise<NoteInternalId[]> {
return await this.storage.getNoteParentsIds(noteId);
}

/**
* Get the ultimate parent of a note with note id
* @param noteId - note id to get ultimate parent
* @returns - note id of the ultimate parent
*/
public async getUltimateParentNoteId(noteId: NoteInternalId): Promise<NoteInternalId | null> {
return await this.storage.getUltimateParentByNoteId(noteId);
}
}
Loading
Loading