Skip to content
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

feat(parent structure): add method that return note parent structure #276

Open
wants to merge 15 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
3 changes: 2 additions & 1 deletion docker-compose.yml
Copy link
Member

Choose a reason for hiding this comment

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

unexpected change

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
version: "3.2"
version: '3.2'

services:
api:
build:
Expand Down
12 changes: 12 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 { NotePublic } from '@domain/entities/notePublic.js';

/**
* Note service
Expand Down Expand Up @@ -441,4 +442,15 @@ export default class NoteService {

return noteHistoryPublic;
}

/**
* Get note parent structure recursively by note id and user id
* and check if user has access to the parent note.
* @param noteId - id of the note to get parent structure
* @param userId - id of the user that is requesting the parent structure
* @returns - array of notes that are parent structure of the note
*/
public async getNoteParentStructure(noteId: NoteInternalId, userId: number): Promise<NotePublic[]> {
return await this.noteRepository.getNoteParents(noteId, userId);
}
}
229 changes: 229 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 @@ -180,6 +181,49 @@ describe('Note API', () => {
});

describe('GET note/:notePublicId ', () => {
let context: {
user: User;
user2: User;
parentNote: Note;
childNote: Note;
differentChildNote: Note;
grandChildNote: Note;
} = {
user: {} as User,
user2: {} as User,
parentNote: {} as Note,
childNote: {} as Note,
differentChildNote: {} as Note,
grandChildNote: {} as Note,
};

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

context.user2 = await global.db.insertUser();

/** Create test note - a parent note */
context.parentNote = await global.db.insertNote({
creatorId: context.user.id,
});

/** Create test note - a child note */
context.childNote = await global.db.insertNote({
creatorId: context.user.id,
});

/** Create test note - create note with different user */
context.differentChildNote = await global.db.insertNote({
creatorId: context.user2.id,
});

/** Create test note - a grandchild note */
context.grandChildNote = await global.db.insertNote({
creatorId: context.user.id,
});
});

test.each([
/** Returns 200 if user is team member with a Read role */
{
Expand Down Expand Up @@ -518,6 +562,191 @@ describe('Note API', () => {

expect(response?.json().message).toStrictEqual(expectedMessage);
});

e11sy marked this conversation as resolved.
Show resolved Hide resolved
test.each([
/** Returns two parents in case of relation between child and parent notes with 200 status */
{
testCase: 1,
numberOfNotes: 2,
childNoteCreatedByDifferentUser: false,
isPublic: true,
expectedStatusCode: 200,
},
/** Returns multiple parents in case of multiple notes relations with user presence in team in each note with 200 status */
{
testCase: 2,
numberOfNotes: 3,
childNoteCreatedByDifferentUser: false,
isPublic: true,
expectedStatusCode: 200,
},
/** Returns one parent in case where there is no note relation with 200 status */
{
testCase: 3,
numberOfNotes: 1,
childNoteCreatedByDifferentUser: false,
isPublic: true,
expectedStatusCode: 200,
},
/** Returns mutiple parents in case where user is not in the team of a note with 200 status */
{
testCase: 4,
numberOfNotes: 3,
childNoteCreatedByDifferentUser: true,
isPublic: true,
expectedStatusCode: 200,
},
/** Returns multiple parents in case when note is not public with 200 status */
{
testCase: 5,
numberOfNotes: 3,
childNoteCreatedByDifferentUser: true,
isPublic: false,
expectedStatusCode: 200,
},
/** Returns no note in case when the user is not authorized to note with 403 status */
{
testCase: 6,
numberOfNotes: 2,
childNoteCreatedByDifferentUser: true,
isPublic: false,
expectedStatusCode: 403,
},
/** Returns one note in case when the user has no access to note with 200 status */
{
testCase: 7,
numberOfNotes: 2,
childNoteCreatedByDifferentUser: true,
isPublic: true,
expectedStatusCode: 200,
},
])('Get note parents in different scenarios', async ({ testCase, numberOfNotes, childNoteCreatedByDifferentUser, isPublic, expectedStatusCode }) => {
if (context !== undefined) {
/** Create acces token for the user */
let accessToken = global.auth(context.user.id);

if (testCase === 6 || testCase == 7) {
accessToken = global.auth(context.user2.id);
}
let noteId = context.parentNote.id;
let notePublicId = context.parentNote.publicId;

if (numberOfNotes == 2 && !childNoteCreatedByDifferentUser) {
noteId = context.childNote.id;
notePublicId = context.childNote.publicId;
} else if (numberOfNotes == 2 && childNoteCreatedByDifferentUser) {
noteId = context.differentChildNote.id;
notePublicId = context.differentChildNote.publicId;
} else if (numberOfNotes == 3) {
noteId = context.grandChildNote.id;
notePublicId = context.grandChildNote.publicId;
}

/** Create test note settings */
await global.db.insertNoteSetting({
noteId: noteId,
isPublic: isPublic,
});

for (let i = 0; i < numberOfNotes - 1; i++) {
/** Create test note relation */
await global.db.insertNoteRelation({
parentId: i == 0 ? context.parentNote.id : (childNoteCreatedByDifferentUser ? context.differentChildNote.id : context.childNote.id),
noteId: i == 0 ? (childNoteCreatedByDifferentUser ? context.differentChildNote.id : context.childNote.id) : context.grandChildNote.id,
});
}

const response = await global.api?.fakeRequest({
method: 'GET',
headers: {
authorization: `Bearer ${accessToken}`,
},
url: `/note/${notePublicId}`,
});

switch (testCase) {
case (1):
expect(response?.statusCode).toBe(expectedStatusCode);
expect(response?.json()).toMatchObject({
parents: [
{
id: context.parentNote.publicId,
content: context.parentNote.content,
},
{
id: context.childNote.publicId,
content: context.childNote.content,
},
],
});
break;
case (2):
expect(response?.statusCode).toBe(expectedStatusCode);
expect(response?.json()).toMatchObject({
parents: [
{
id: context.parentNote.publicId,
content: context.parentNote.content,
},
{
id: context.childNote.publicId,
content: context.childNote.content,
},
{
id: context.grandChildNote.publicId,
content: context.grandChildNote.content,
},
],
});
break;
case (3):
expect(response?.statusCode).toBe(expectedStatusCode);
expect(response?.json()).toMatchObject({
parents: [
{
id: context.parentNote.publicId,
content: context.parentNote.content,
},
],
});
break;
case (4):
case (5):
expect(response?.statusCode).toBe(expectedStatusCode);
expect(response?.json()).toMatchObject({
parents: [
{
id: context.parentNote.publicId,
content: context.parentNote.content,
},
{
id: context.differentChildNote.publicId,
content: context.differentChildNote.content,
},
{
id: context.grandChildNote.publicId,
content: context.grandChildNote.content,
},
],
});
break;
case (6):
expect(response?.statusCode).toBe(expectedStatusCode);
break;
case (7):
expect(response?.statusCode).toBe(expectedStatusCode);
expect(response?.json()).toMatchObject({
parents: [
{
id: context.differentChildNote.publicId,
content: context.differentChildNote.content,
},
],
});
break;
}
}
});
});

describe('PATCH note/:notePublicId ', () => {
Expand Down
10 changes: 10 additions & 0 deletions src/presentation/http/router/note.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
canEdit: boolean;
};
tools: EditorTool[];
parents: NotePublic[];
} | ErrorResponse;
}>('/:notePublicId', {
config: {
Expand Down Expand Up @@ -123,6 +124,12 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
$ref: 'EditorToolSchema',
},
},
parents: {
type: 'array',
items: {
$ref: 'NoteSchema',
},
},
},
},
},
Expand Down Expand Up @@ -172,11 +179,14 @@ const NoteRouter: FastifyPluginCallback<NoteRouterOptions> = (fastify, opts, don
*/
const canEdit = memberRole === MemberRole.Write;

const noteParentStructure = await noteService.getNoteParentStructure(noteId, userId!);

return reply.send({
note: notePublic,
parentNote: parentNote,
accessRights: { canEdit: canEdit },
tools: noteTools,
parents: noteParentStructure,
});
});

Expand Down
2 changes: 2 additions & 0 deletions src/repository/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,14 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise<Reposit
/**
* Create associations between note and team, user and team
*/
noteStorage.createAssociationWithTeamsModel(teamStorage.model);
teamStorage.createAssociationWithNoteModel(noteStorage.model);
teamStorage.createAssociationWithUserModel(userStorage.model);

/**
* Create associations between note and relations table
*/
noteStorage.createAssociationWithNoteRelationModel(noteRelationshipStorage.model);
noteRelationshipStorage.createAssociationWithNoteModel(noteStorage.model);

/**
Expand Down
11 changes: 11 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 { NotePublic } from '@domain/entities/notePublic.js';
import type NoteStorage from '@repository/storage/note.storage.js';

/**
Expand Down Expand Up @@ -81,4 +82,14 @@ export default class NoteRepository {
public async getNoteListByUserId(id: number, offset: number, limit: number): Promise<Note[]> {
return await this.storage.getNoteListByUserId(id, offset, limit);
}

/**
* Get all notes parents based on note id and user id, by checking team access
* @param noteId : note id to get all its parents
* @param userId : user id to check access
* @returns an array of note parents objects containing public id and content
*/
public async getNoteParents(noteId: NoteInternalId, userId: number): Promise<NotePublic[]> {
return await this.storage.getAllNoteParents(noteId, userId);
}
}
Loading
Loading