diff --git a/docker-compose.yml b/docker-compose.yml index 6c9aacf3..d4ae844d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,6 @@ services: - 127.0.0.1:1337:1337 volumes: - ./app-config.yaml:/usr/app/app-config.yaml - - ./app-config.local.yaml:/usr/app/app-config.local.yaml restart: unless-stopped postgres: diff --git a/src/domain/entities/note.ts b/src/domain/entities/note.ts index 0537c851..b393a680 100644 --- a/src/domain/entities/note.ts +++ b/src/domain/entities/note.ts @@ -82,25 +82,3 @@ export interface Note { * Part of note entity used to create new note */ export type NoteCreationAttributes = Pick; - -/** - * This type represents only note's content - * Used for work with just note's content in web part - * This type would be used as a part of the structure, that represents all parent notes of the current note - */ -export type NoteIdAndContent = { - /** - * Note public id - */ - noteId: Note['publicId']; - - /** - * Note content - */ - content: Note['content']; -}; - -/** - * This type represents the structure, that represents all parent notes of the current note - */ -export type NoteParentsStructure = NoteIdAndContent[]; diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index fd073182..6ce2162f 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -1,4 +1,4 @@ -import type { Note, NoteInternalId, NoteParentsStructure, NotePublicId } from '@domain/entities/note.js'; +import type { Note, 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'; @@ -457,7 +457,7 @@ export default class NoteService { * @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 { - return await this.teamRepository.getAllNotesParents(noteId, userId); + public async getNoteParentStructure(noteId: NoteInternalId, userId: number): Promise { + return await this.noteRepository.getAllNotesParents(noteId, userId); } } diff --git a/src/presentation/http/router/note.test.ts b/src/presentation/http/router/note.test.ts index c2cd9e90..de1f4563 100644 --- a/src/presentation/http/router/note.test.ts +++ b/src/presentation/http/router/note.test.ts @@ -519,7 +519,7 @@ describe('Note API', () => { expect(response?.json().message).toStrictEqual(expectedMessage); }); - test('Returns two objects of note parent structure by note public id with 200 status', async () => { + test('Returns two parents in case of relation between child and parent notes with 200 status', async () => { /** Create test user */ const user = await global.db.insertUser(); @@ -559,20 +559,22 @@ describe('Note API', () => { expect(response?.statusCode).toBe(200); expect(response?.json()).toMatchObject({ - parentStructure: [ - { - noteId: parentNote.publicId, - content: parentNote.content, - }, - { - noteId: childNote.publicId, - content: childNote.content, - }, - ], + parents: { + items: [ + { + id: parentNote.publicId, + content: parentNote.content, + }, + { + id: childNote.publicId, + content: childNote.content, + }, + ], + }, }); }); - test('Returns multiple objects of note parent structure by note public id with 200 status', async () => { + test('Returns multiple parents in case of multiple notes relations with user presence in team in each note with 200 status', async () => { /** Create test user */ const user = await global.db.insertUser(); @@ -622,24 +624,26 @@ describe('Note API', () => { expect(response?.statusCode).toBe(200); expect(response?.json()).toMatchObject({ - parentStructure: [ - { - noteId: parentNote.publicId, - content: parentNote.content, - }, - { - noteId: childNote.publicId, - content: childNote.content, - }, - { - noteId: grandchildNote.publicId, - content: grandchildNote.content, - }, - ], + parents: { + items: [ + { + id: parentNote.publicId, + content: parentNote.content + }, + { + id: childNote.publicId, + content: childNote.content + }, + { + id: grandchildNote.publicId, + content: grandchildNote.content + }, + ], + } }); }); - test('Returns one object in array of note parent structure by note public id with 200 status', async () => { + test('Returns one parent in case where there is no note relation with 200 status', async () => { /** Create test user */ const user = await global.db.insertUser(); @@ -667,12 +671,14 @@ describe('Note API', () => { expect(response?.statusCode).toBe(200); expect(response?.json()).toMatchObject({ - parentStructure: [ - { - noteId: note.publicId, - content: note.content, - }, - ], + parents: { + items: [ + { + id: note.publicId, + content: note.content, + }, + ], + } }); }); }); diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 5207344e..5c9a307f 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -2,7 +2,7 @@ import type { FastifyPluginCallback } from 'fastify'; import type NoteService from '@domain/service/note.js'; import type NoteSettingsService from '@domain/service/noteSettings.js'; import type { ErrorResponse } from '@presentation/http/types/HttpResponse.js'; -import type { Note, NoteParentsStructure, NotePublicId } from '@domain/entities/note.js'; +import type { Note, NotePublicId } from '@domain/entities/note.js'; import useNoteResolver from '../middlewares/note/useNoteResolver.js'; import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettingsResolver.js'; import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js'; @@ -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 { NoteList } from '@domain/entities/noteList.js'; /** * Interface for the note router. @@ -85,7 +86,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don canEdit: boolean; }; tools: EditorTool[]; - parentStructure: NoteParentsStructure; + parents: NoteList; } | ErrorResponse; }>('/:notePublicId', { config: { @@ -124,16 +125,13 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don $ref: 'EditorToolSchema', }, }, - parentStructure: { - type: 'array', - items: { - type: 'object', - properties: { - noteId: { - $ref: 'NoteSchema#/properties/id', - }, - content: { - $ref: 'NoteSchema#/properties/content', + parents: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + $ref: 'NoteSchema', }, }, }, @@ -194,7 +192,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don parentNote: parentNote, accessRights: { canEdit: canEdit }, tools: noteTools, - parentStructure: noteParentStructure, + parents: noteParentStructure, }); }); diff --git a/src/repository/index.ts b/src/repository/index.ts index d07f5aa1..d21da8ec 100644 --- a/src/repository/index.ts +++ b/src/repository/index.ts @@ -133,13 +133,14 @@ export async function init(orm: Orm, s3Config: S3StorageConfig): Promise { 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 getAllNotesParents(noteId: NoteInternalId, userId: number): Promise { + return await this.storage.getAllNoteParents(noteId, userId); + } } diff --git a/src/repository/storage/postgres/orm/sequelize/note.ts b/src/repository/storage/postgres/orm/sequelize/note.ts index d6a4d1ac..1a8baa10 100644 --- a/src/repository/storage/postgres/orm/sequelize/note.ts +++ b/src/repository/storage/postgres/orm/sequelize/note.ts @@ -6,6 +6,10 @@ 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 { NoteRelationsModel } from './noteRelations.js'; +import { notEmpty } from '@infrastructure/utils/empty.js'; +import { NoteList } from '@domain/entities/noteList.js'; +import { TeamsModel } from './teams.js'; /* eslint-disable @typescript-eslint/naming-convention */ @@ -75,6 +79,10 @@ export default class NoteSequelizeStorage { public historyModel: typeof NoteHistoryModel | null = null; + public noteRelationModel: typeof NoteRelationsModel | null = null; + + public teamModel: typeof TeamsModel | null = null; + /** * Database instance */ @@ -155,6 +163,22 @@ export default class NoteSequelizeStorage { }); }; + public createAssociationWithNoteRelationModel(model: ModelStatic): void { + + /** + * Create association with note relations + */ + this.noteRelationModel = model; + } + + public createAssociationWithTeamsModel(model: ModelStatic): void { + + /** + * Create association with teams + */ + this.teamModel = model; + } + /** * Insert note to database * @param options - note creation options @@ -323,4 +347,68 @@ export default class NoteSequelizeStorage { }, }); }; + + + /** + * Get all parent notes of a note that a user has access to, + * by checking the team access. + * @param noteId - the ID of the note. + * @param userId - the ID of the user. + */ + public async getAllNoteParents(noteId: NoteInternalId, userId: number): Promise { + if (!this.teamModel || !this.noteRelationModel) { + throw new Error(`${this.model !== null ? 'TeamStorage: Note relation model is not defined' : 'TeamStorage: Note model is not defined'}`); + } + + const parentNotes: NoteList = { items: [] }; + let currentNoteId: NoteInternalId | null = noteId; + /** + * Store notes that user can not access, to check the inherited team if has access + */ + let storeUnaccessibleNote: Note[] = []; + + while (currentNoteId != null) { + const teamMember = await this.teamModel.findOne({ + where: { + noteId: currentNoteId, + userId, + }, + include: { + model: this.model, + as: this.model.tableName, + required: true, + }, + }); + + if (teamMember && notEmpty(teamMember.notes)) { + if (storeUnaccessibleNote.length > 0) { + parentNotes.items.push(...storeUnaccessibleNote); + storeUnaccessibleNote = []; + } + parentNotes.items.push(teamMember.notes); + } else if (teamMember === null) { + const note = await this.model.findOne({ + where: { id: currentNoteId }, + }); + + if (note !== null) { + storeUnaccessibleNote.push(note); + } + } + + // Retrieve the parent note + const noteRelation: NoteRelationsModel | null = await this.noteRelationModel.findOne({ + where: { noteId: currentNoteId }, + }); + + if (noteRelation != null) { + currentNoteId = noteRelation.parentId; + } else { + currentNoteId = null; + } + } + + parentNotes.items.reverse(); + return parentNotes; + } } diff --git a/src/repository/storage/postgres/orm/sequelize/teams.ts b/src/repository/storage/postgres/orm/sequelize/teams.ts index bce9855f..824280f0 100644 --- a/src/repository/storage/postgres/orm/sequelize/teams.ts +++ b/src/repository/storage/postgres/orm/sequelize/teams.ts @@ -6,9 +6,7 @@ import type { Team, TeamMemberCreationAttributes, TeamMember } from '@domain/ent import { UserModel } from './user.js'; import { MemberRole } from '@domain/entities/team.js'; import type User from '@domain/entities/user.js'; -import type { NoteInternalId, NoteParentsStructure } from '@domain/entities/note.js'; -import type { NoteRelationsModel } from './noteRelations.js'; -import { isEmpty } from '@infrastructure/utils/empty.js'; +import type { NoteInternalId } from '@domain/entities/note.js'; /** * Class representing a teams model in database @@ -64,11 +62,6 @@ export default class TeamsSequelizeStorage { */ private userModel: typeof UserModel | null = null; - /** - * Note relation model instance - */ - private noteRelationModel: typeof NoteRelationsModel | null = null; - /** * Teams table name */ @@ -152,14 +145,6 @@ export default class TeamsSequelizeStorage { }); } - /** - * create association with note relations model - * @param model - initialized note relations model - */ - public createAssociationWithNoteRelationsModel(model: ModelStatic): void { - this.noteRelationModel = model; - } - /** * Create new team member membership * @param data - team membership data @@ -222,75 +207,6 @@ export default class TeamsSequelizeStorage { }); } - /** - * Get note parent structure - * @param noteId - the ID of the note. - * @param userId - the ID of the user. - */ - public async getAllNoteParents(noteId: NoteInternalId, userId: number): Promise { - if (!this.noteModel || !this.noteRelationModel) { - throw new Error(`${this.noteModel !== null ? 'TeamStorage: Note relation model is not defined' : 'TeamStorage: Note model is not defined'}`); - } - - const parentNotes: NoteParentsStructure = []; - let currentNoteId: NoteInternalId | null = noteId; - /** - * Store notes that user can not access, to check the inherited team if has access - */ - let storeUnaccessibleNote: NoteParentsStructure = []; - - while (currentNoteId != null) { - const teamMember = await this.model.findOne({ - where: { - noteId: currentNoteId, - userId, - }, - include: { - model: this.noteModel, - as: this.noteModel.tableName, - required: true, - attributes: ['publicId', 'content'], - }, - }); - - if (teamMember && !isEmpty(teamMember.notes)) { - if (storeUnaccessibleNote.length > 0) { - parentNotes.push(...storeUnaccessibleNote); - storeUnaccessibleNote = []; - } - parentNotes.push({ - noteId: teamMember.notes.publicId, - content: teamMember.notes.content, - }); - } else if (teamMember === null) { - const note = await this.noteModel.findOne({ - where: { id: currentNoteId }, - attributes: ['publicId', 'content'], - }); - - if (note !== null) { - storeUnaccessibleNote.push({ - noteId: note.publicId, - content: note.content, - }); - } - } - - // Retrieve the parent note - const noteRelation: NoteRelationsModel | null = await this.noteRelationModel.findOne({ - where: { noteId: currentNoteId }, - }); - - if (noteRelation != null) { - currentNoteId = noteRelation.parentId; - } else { - currentNoteId = null; - } - } - - return parentNotes.reverse(); - } - /** * Remove team member by id * @param userId - id of team member diff --git a/src/repository/team.repository.ts b/src/repository/team.repository.ts index c426c35f..fb01f0d3 100644 --- a/src/repository/team.repository.ts +++ b/src/repository/team.repository.ts @@ -1,6 +1,6 @@ import type TeamStorage from '@repository/storage/team.storage.js'; import type { Team, TeamMember, TeamMemberCreationAttributes, MemberRole } from '@domain/entities/team.js'; -import type { NoteInternalId, NoteParentsStructure } from '@domain/entities/note.js'; +import type { NoteInternalId } from '@domain/entities/note.js'; import type User from '@domain/entities/user.js'; /** @@ -57,16 +57,6 @@ export default class TeamRepository { return await this.storage.getTeamMembersWithUserInfoByNoteId(noteId); }; - /** - * 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 getAllNotesParents(noteId: NoteInternalId, userId: number): Promise { - return await this.storage.getAllNoteParents(noteId, userId); - } - /** * Remove team member by id * @param userId - id of the team member