From 5aa0307d34dd696ca097802840130fa32f8941b7 Mon Sep 17 00:00:00 2001 From: e11sy Date: Wed, 3 Apr 2024 21:13:31 +0300 Subject: [PATCH 1/9] added method for deleting of note visits - added method for deletion of all note visits for certain note - added saving of note visits on post note and get note endpoints --- src/domain/service/noteVisits.ts | 9 +++++ src/presentation/http/http-api.ts | 1 + src/presentation/http/router/note.ts | 37 ++++++++++++++++++- src/repository/noteVisits.repository.ts | 9 +++++ .../postgres/orm/sequelize/noteVisits.ts | 20 +++++++++- 5 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/domain/service/noteVisits.ts b/src/domain/service/noteVisits.ts index e1a35c59c..3794a1868 100644 --- a/src/domain/service/noteVisits.ts +++ b/src/domain/service/noteVisits.ts @@ -31,4 +31,13 @@ export default class NoteVisitsService { public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise { return await this.noteVisitsRepository.saveVisit(noteId, userId); }; + + /** + * Deletes all visits of the note when a note is deleted + * + * @param noteId - note internal id + */ + public async deleteNoteVisits(noteId: NoteInternalId): Promise { + return await this.noteVisitsRepository.deleteNoteVisits(noteId); + } } \ No newline at end of file diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 030318967..95d21b327 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -195,6 +195,7 @@ export default class HttpApi implements Api { await this.server?.register(NoteRouter, { prefix: '/note', noteService: domainServices.noteService, + noteVisitsService: domainServices.noteVisitsService, noteSettingsService: domainServices.noteSettingsService, }); diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index ed51af199..08d69e409 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -8,6 +8,7 @@ import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettings import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js'; import { MemberRole } from '@domain/entities/team.js'; import { type NotePublic, definePublicNote } from '@domain/entities/notePublic.js'; +import type NoteVisitsService from '@domain/service/noteVisits.js'; /** * Interface for the note router. @@ -22,6 +23,11 @@ interface NoteRouterOptions { * Note Settings service instance */ noteSettingsService: NoteSettingsService, + + /** + * Note Visits service instanse + */ + noteVisitsService: NoteVisitsService } /** @@ -36,6 +42,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don * Get note service from options */ const noteService = opts.noteService; + const noteVisitsService = opts.noteVisitsService; const noteSettingsService = opts.noteSettingsService; /** @@ -111,7 +118,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don ], }, async (request, reply) => { const { note } = request; + const noteId = request.note?.id as number; const { memberRole } = request; + const { userId } = request; /** * Check if note exists @@ -119,6 +128,14 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don if (note === null) { return reply.notFound('Note not found'); } + + /** + * Check if user is authorized + */ + if (userId !== null) { + await noteVisitsService.saveVisit(noteId, userId); + } + const parentId = await noteService.getParentNoteIdByNoteId(note.id); const parentNote = parentId !== null ? definePublicNote(await noteService.getNoteById(parentId)) : undefined; @@ -171,6 +188,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const noteId = request.note?.id as number; const isDeleted = await noteService.deleteNoteById(noteId); + /** Delete all visits of the note */ + await noteVisitsService.deleteNoteVisits(noteId); + /** * Check if note does not exist */ @@ -205,6 +225,13 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const addedNote = await noteService.addNote(content as JSON, userId as number, parentId); // "authRequired" policy ensures that userId is not null + /** + * Check if user is authorized + */ + if (userId !== null) { + await noteVisitsService.saveVisit(addedNote.id, userId); + } + /** * @todo use event bus: emit 'note-added' event and subscribe to it in other modules like 'note-settings' */ @@ -417,8 +444,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }, }, async (request, reply) => { const params = request.params; - + const { userId } = request; const note = await noteService.getNoteByHostname(params.hostname); + const noteId = note?.id as number; /** * Check if note exists @@ -427,6 +455,13 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don return reply.notFound('Note not found'); } + /** + * Check if user is authorized + */ + if (userId !== null) { + await noteVisitsService.saveVisit(noteId, userId); + } + /** * By default, unauthorized user can not edit the note */ diff --git a/src/repository/noteVisits.repository.ts b/src/repository/noteVisits.repository.ts index 859ba04f9..74896f189 100644 --- a/src/repository/noteVisits.repository.ts +++ b/src/repository/noteVisits.repository.ts @@ -27,4 +27,13 @@ export default class NoteVisitsRepository { public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise { return await this.storage.saveVisit(noteId, userId); } + + /** + * Deletes all visits of the note when a note is deleted + * + * @param noteId - note internal id + */ + public async deleteNoteVisits(noteId: NoteInternalId): Promise { + return await this.storage.deleteNoteVisits(noteId); + } } \ No newline at end of file diff --git a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts index eba9c348a..0100ad656 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts @@ -117,7 +117,6 @@ export default class NoteVisitsSequelizeStorage { * If user has already visited note, then existing record will be updated * If user is visiting note for the first time, new record will be created */ - /* eslint-disable-next-line */ const [recentVisit, _] = await this.model.upsert({ noteId, userId, @@ -131,4 +130,23 @@ export default class NoteVisitsSequelizeStorage { return recentVisit; } + + /** + * Deletes all visits of the note when a note is deleted + * + * @param noteId - note internal id + */ + public async deleteNoteVisits(noteId: NoteInternalId): Promise { + const deletedNoteVisits = await this.model.destroy({ + where: { + noteId, + }, + }); + + if (deletedNoteVisits) { + return true; + } + + return false; + } } \ No newline at end of file From 83710c1d153af5cfade141789734ab1a40412779 Mon Sep 17 00:00:00 2001 From: e11sy Date: Thu, 4 Apr 2024 15:40:34 +0300 Subject: [PATCH 2/9] visitedAt could be literal for now - bad fix of the problem with camelcase to snake translation (in conflict where) --- src/domain/entities/noteVisit.ts | 3 ++- src/repository/storage/postgres/orm/sequelize/noteVisits.ts | 6 ++++-- src/tests/utils/database-helpers.ts | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/domain/entities/noteVisit.ts b/src/domain/entities/noteVisit.ts index 5c8f7996f..096471bca 100644 --- a/src/domain/entities/noteVisit.ts +++ b/src/domain/entities/noteVisit.ts @@ -1,5 +1,6 @@ import type { NoteInternalId } from '@domain/entities/note.ts'; import type User from '@domain/entities/user.ts'; +import type { Literal } from 'sequelize/lib/utils'; /** * NoteVisit is used to store data about the last interaction between the user and the note @@ -23,5 +24,5 @@ export default interface NoteVisit { /** * Time when note was visited for the last time (timestamp with timezone) */ - visitedAt: string, + visitedAt: string | Literal, } \ No newline at end of file diff --git a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts index 0100ad656..b0cbabc02 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts @@ -1,6 +1,7 @@ import type NoteVisit from '@domain/entities/noteVisit.js'; import type User from '@domain/entities/user.js'; import type { Sequelize, InferAttributes, InferCreationAttributes, CreationOptional, ModelStatic } from 'sequelize'; +import { literal } from 'sequelize'; import { Model, DataTypes } from 'sequelize'; import type Orm from '@repository/storage/postgres/orm/sequelize/index.js'; import { NoteModel } from './note.js'; @@ -120,10 +121,11 @@ export default class NoteVisitsSequelizeStorage { const [recentVisit, _] = await this.model.upsert({ noteId, userId, + visitedAt: literal('CURRENT_DATE'), }, { conflictWhere: { - noteId, - userId, + 'note_id': noteId, + 'user_id': userId, }, returning: true, }); diff --git a/src/tests/utils/database-helpers.ts b/src/tests/utils/database-helpers.ts index 330025e55..7f3e8f2c4 100644 --- a/src/tests/utils/database-helpers.ts +++ b/src/tests/utils/database-helpers.ts @@ -245,7 +245,7 @@ export default class DatabaseHelpers { * if no visitedAt passed, then visited_at would have CURRENT_DATE value */ public async insertNoteVisit(visit: NoteVisitCreationAttributes): Promise { - const visitedAt = visit.visitedAt ?? 'NOW()'; + const visitedAt = visit.visitedAt ?? 'CURRENT_DATE'; const [results, _] = await this.orm.connection.query(`INSERT INTO public.note_visits ("user_id", "note_id", "visited_at") From b153fae5e3a67e80e42f3ad7e0c1be9f4c6736a7 Mon Sep 17 00:00:00 2001 From: e11sy Date: Fri, 5 Apr 2024 18:52:02 +0300 Subject: [PATCH 3/9] separeted saving note visit to two sql queries --- src/domain/entities/noteVisit.ts | 3 +- .../postgres/orm/sequelize/noteVisits.ts | 46 +++++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/src/domain/entities/noteVisit.ts b/src/domain/entities/noteVisit.ts index 096471bca..5c8f7996f 100644 --- a/src/domain/entities/noteVisit.ts +++ b/src/domain/entities/noteVisit.ts @@ -1,6 +1,5 @@ import type { NoteInternalId } from '@domain/entities/note.ts'; import type User from '@domain/entities/user.ts'; -import type { Literal } from 'sequelize/lib/utils'; /** * NoteVisit is used to store data about the last interaction between the user and the note @@ -24,5 +23,5 @@ export default interface NoteVisit { /** * Time when note was visited for the last time (timestamp with timezone) */ - visitedAt: string | Literal, + visitedAt: string, } \ No newline at end of file diff --git a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts index b0cbabc02..766a5be7a 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts @@ -118,19 +118,45 @@ export default class NoteVisitsSequelizeStorage { * If user has already visited note, then existing record will be updated * If user is visiting note for the first time, new record will be created */ - const [recentVisit, _] = await this.model.upsert({ - noteId, - userId, - visitedAt: literal('CURRENT_DATE'), - }, { - conflictWhere: { - 'note_id': noteId, - 'user_id': userId, + const existingVisit = await this.model.findOne({ + where: { + noteId, + userId, }, - returning: true, }); - return recentVisit; + let updatedVisits: NoteVisit[]; + let _; + + if (existingVisit === null) { + return await this.model.create({ + noteId, + userId, + /** + * we should pass to model datatype respectfully to declared in NoteVisitsModel class + * if we will pass just 'CLOCK_TIMESTAMP()' it will be treated by orm just like a string, that is why we should use literal + * but model wants string, this is why we use this cast + */ + visitedAt: literal('CLOCK_TIMESTAMP()') as unknown as string, + }); + } else { + [_, updatedVisits] = await this.model.update({ + /** + * we should pass to model datatype respectfully to declared in NoteVisitsModel class + * if we will pass just 'CLOCK_TIMESTAMP()' it will be treated by orm just like a string, that is why we should use literal + * but model wants string, this is why we use this cast + */ + visitedAt: literal('CLOCK_TIMESTAMP()') as unknown as string, + }, { + where: { + noteId, + userId, + }, + returning: true, + }); + } + + return updatedVisits[0]; } /** From 968b9d2601a4261b500d07691e55d0da9d7af087 Mon Sep 17 00:00:00 2001 From: e11sy Date: Fri, 5 Apr 2024 18:54:38 +0300 Subject: [PATCH 4/9] changed CURRENT_TIME to CLOCK_TIMESTAMP() --- src/tests/utils/database-helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tests/utils/database-helpers.ts b/src/tests/utils/database-helpers.ts index 7f3e8f2c4..eff127e84 100644 --- a/src/tests/utils/database-helpers.ts +++ b/src/tests/utils/database-helpers.ts @@ -242,10 +242,10 @@ export default class DatabaseHelpers { * * @param visit object which contain all info about noteVisit (visitedAt is optional) * - * if no visitedAt passed, then visited_at would have CURRENT_DATE value + * if no visitedAt passed, then visited_at would have CLOCK_TIMESTAMP() value */ public async insertNoteVisit(visit: NoteVisitCreationAttributes): Promise { - const visitedAt = visit.visitedAt ?? 'CURRENT_DATE'; + const visitedAt = visit.visitedAt ?? 'CLOCK_TIMESTAMP()'; const [results, _] = await this.orm.connection.query(`INSERT INTO public.note_visits ("user_id", "note_id", "visited_at") From ca7a212237dc71392104b904496039ddd8f054d1 Mon Sep 17 00:00:00 2001 From: e11sy Date: Fri, 5 Apr 2024 19:59:10 +0300 Subject: [PATCH 5/9] descriptions updated --- src/presentation/http/router/note.ts | 6 ++++-- src/repository/storage/postgres/orm/sequelize/noteVisits.ts | 5 ----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 08d69e409..2c21a70d0 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -188,7 +188,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const noteId = request.note?.id as number; const isDeleted = await noteService.deleteNoteById(noteId); - /** Delete all visits of the note */ + /** + * Delete all visits of the note + */ await noteVisitsService.deleteNoteVisits(noteId); /** @@ -456,7 +458,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don } /** - * Check if user is authorized + * Save note visit if user is authorized */ if (userId !== null) { await noteVisitsService.saveVisit(noteId, userId); diff --git a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts index 766a5be7a..6bc9ad21f 100644 --- a/src/repository/storage/postgres/orm/sequelize/noteVisits.ts +++ b/src/repository/storage/postgres/orm/sequelize/noteVisits.ts @@ -141,11 +141,6 @@ export default class NoteVisitsSequelizeStorage { }); } else { [_, updatedVisits] = await this.model.update({ - /** - * we should pass to model datatype respectfully to declared in NoteVisitsModel class - * if we will pass just 'CLOCK_TIMESTAMP()' it will be treated by orm just like a string, that is why we should use literal - * but model wants string, this is why we use this cast - */ visitedAt: literal('CLOCK_TIMESTAMP()') as unknown as string, }, { where: { From 5753806e85a1b4004a31b73edc0d3ac0edf2b16f Mon Sep 17 00:00:00 2001 From: e11sy Date: Sat, 6 Apr 2024 17:20:57 +0300 Subject: [PATCH 6/9] get rid of noteVIsits service - get rid of noteVisits service to avoid circular dependency via shared service - moved noteVisitsService methods to noteService --- src/domain/index.ts | 12 +------- src/domain/service/note.ts | 30 ++++++++++++++++++- src/domain/service/noteVisits.ts | 43 ---------------------------- src/presentation/http/router/note.ts | 15 +++------- 4 files changed, 34 insertions(+), 66 deletions(-) delete mode 100644 src/domain/service/noteVisits.ts diff --git a/src/domain/index.ts b/src/domain/index.ts index 81d7766b9..4cea86409 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -7,7 +7,6 @@ import UserService from '@domain/service/user.js'; import AIService from './service/ai.js'; import EditorToolsService from '@domain/service/editorTools.js'; import FileUploaderService from './service/fileUploader.service.js'; -import NoteVisitsService from '@domain/service/noteVisits.js'; /** * Interface for initiated services @@ -47,11 +46,6 @@ export interface DomainServices { * File uploader service instance */ fileUploaderService: FileUploaderService, - - /** - * Note Visits service instance - */ - noteVisitsService: NoteVisitsService } /** @@ -61,16 +55,13 @@ export interface DomainServices { * @param appConfig - app config */ export function init(repositories: Repositories, appConfig: AppConfig): DomainServices { - const noteService = new NoteService(repositories.noteRepository, repositories.noteRelationsRepository); - + const noteService = new NoteService(repositories.noteRepository, repositories.noteRelationsRepository, repositories.noteVisitsRepository); const authService = new AuthService( appConfig.auth.accessSecret, appConfig.auth.accessExpiresIn, appConfig.auth.refreshExpiresIn, repositories.userSessionRepository ); - const noteVisitsService = new NoteVisitsService(repositories.noteVisitsRepository); - const editorToolsService = new EditorToolsService(repositories.editorToolsRepository); const sharedServices = { @@ -95,6 +86,5 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe authService, aiService, editorToolsService, - noteVisitsService, }; } diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 0a2fcda83..65e7abd53 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -1,10 +1,12 @@ 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'; import { DomainError } from '@domain/entities/DomainError.js'; import type NoteRelationsRepository from '@repository/noteRelations.repository.js'; import type User from '@domain/entities/user.js'; import type { NoteList } from '@domain/entities/noteList.js'; +import type NoteVisit from '@domain/entities/noteVisit.js'; /** * Note service @@ -20,6 +22,11 @@ export default class NoteService { */ public noteRelationsRepository: NoteRelationsRepository; + /** + * Note visits repository + */ + public noteVisitsRepository: NoteVisitsRepository; + /** * Number of the notes to be displayed on one page * it is used to calculate offset and limit for getting notes that the user has recently opened @@ -31,10 +38,12 @@ export default class NoteService { * * @param noteRepository - note repository * @param noteRelationsRepository - note relationship repository + * @param noteVisitsRepository - note visits repository */ - constructor(noteRepository: NoteRepository, noteRelationsRepository: NoteRelationsRepository) { + constructor(noteRepository: NoteRepository, noteRelationsRepository: NoteRelationsRepository, noteVisitsRepository: NoteVisitsRepository) { this.noteRepository = noteRepository; this.noteRelationsRepository = noteRelationsRepository; + this.noteVisitsRepository = noteVisitsRepository; } /** @@ -214,4 +223,23 @@ export default class NoteService { return await this.noteRelationsRepository.updateNoteRelationById(noteId, parentNote.id); }; + + /** + * Updates existing noteVisit's visitedAt or creates new record if user opens note for the first time + * + * @param noteId - note internal id + * @param userId - id of the user + */ + public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise { + return await this.noteVisitsRepository.saveVisit(noteId, userId); + }; + + /** + * Deletes all visits of the note when a note is deleted + * + * @param noteId - note internal id + */ + public async deleteNoteVisits(noteId: NoteInternalId): Promise { + return await this.noteVisitsRepository.deleteNoteVisits(noteId); + } } diff --git a/src/domain/service/noteVisits.ts b/src/domain/service/noteVisits.ts deleted file mode 100644 index 3794a1868..000000000 --- a/src/domain/service/noteVisits.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { NoteInternalId } from '@domain/entities/note.js'; -import type User from '@domain/entities/user.js'; -import type NoteVisit from '@domain/entities/noteVisit.js'; -import type NoteVisitsRepository from '@repository/noteVisits.repository.js'; - -/** - * Note Visits service, which will store latest note visit - * it is used to display recent notes for each user - */ -export default class NoteVisitsService { - /** - * Note Visits repository - */ - public noteVisitsRepository: NoteVisitsRepository; - - /** - * NoteVisits service constructor - * - * @param noteVisitRepository - note Visits repository - */ - constructor(noteVisitRepository: NoteVisitsRepository) { - this.noteVisitsRepository = noteVisitRepository; - } - - /** - * Updates existing noteVisit's visitedAt or creates new record if user opens note for the first time - * - * @param noteId - note internal id - * @param userId - id of the user - */ - public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise { - return await this.noteVisitsRepository.saveVisit(noteId, userId); - }; - - /** - * Deletes all visits of the note when a note is deleted - * - * @param noteId - note internal id - */ - public async deleteNoteVisits(noteId: NoteInternalId): Promise { - return await this.noteVisitsRepository.deleteNoteVisits(noteId); - } -} \ No newline at end of file diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 2c21a70d0..74ab21049 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -8,7 +8,6 @@ import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettings import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js'; import { MemberRole } from '@domain/entities/team.js'; import { type NotePublic, definePublicNote } from '@domain/entities/notePublic.js'; -import type NoteVisitsService from '@domain/service/noteVisits.js'; /** * Interface for the note router. @@ -23,11 +22,6 @@ interface NoteRouterOptions { * Note Settings service instance */ noteSettingsService: NoteSettingsService, - - /** - * Note Visits service instanse - */ - noteVisitsService: NoteVisitsService } /** @@ -42,7 +36,6 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don * Get note service from options */ const noteService = opts.noteService; - const noteVisitsService = opts.noteVisitsService; const noteSettingsService = opts.noteSettingsService; /** @@ -133,7 +126,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don * Check if user is authorized */ if (userId !== null) { - await noteVisitsService.saveVisit(noteId, userId); + await noteService.saveVisit(noteId, userId); } const parentId = await noteService.getParentNoteIdByNoteId(note.id); @@ -191,7 +184,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don /** * Delete all visits of the note */ - await noteVisitsService.deleteNoteVisits(noteId); + await noteService.deleteNoteVisits(noteId); /** * Check if note does not exist @@ -231,7 +224,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don * Check if user is authorized */ if (userId !== null) { - await noteVisitsService.saveVisit(addedNote.id, userId); + await noteService.saveVisit(addedNote.id, userId); } /** @@ -461,7 +454,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don * Save note visit if user is authorized */ if (userId !== null) { - await noteVisitsService.saveVisit(noteId, userId); + await noteService.saveVisit(noteId, userId); } /** From f6518065a621a1f7fd99d41fe2636ebc9295d113 Mon Sep 17 00:00:00 2001 From: e11sy Date: Sat, 6 Apr 2024 17:40:39 +0300 Subject: [PATCH 7/9] moved some logic from noteRouter to noteService --- src/domain/service/note.ts | 18 ++++++++++++++++-- src/presentation/http/http-api.ts | 1 - src/presentation/http/router/note.ts | 24 +++--------------------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 65e7abd53..49d245d55 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -61,6 +61,8 @@ export default class NoteService { creatorId, }); + await this.saveVisit(note.id, creatorId); + if (parentPublicId !== undefined) { const parentNote = await this.getNoteByPublicId(parentPublicId); @@ -96,6 +98,10 @@ export default class NoteService { throw new DomainError(`Relation with noteId ${id} was not deleted`); } } + /** + * Delete all visits of this note + */ + await this.deleteNoteVisits(id); const isNoteDeleted = await this.noteRepository.deleteNoteById(id); @@ -165,10 +171,18 @@ export default class NoteService { * Gets note by custom hostname * * @param hostname - hostname + * @param userId - id of the user * @returns { Promise } note */ - public async getNoteByHostname(hostname: string): Promise { - return await this.noteRepository.getNoteByHostname(hostname); + public async getNoteByHostname(hostname: string, userId: User['id'] | null): Promise { + const note = await this.noteRepository.getNoteByHostname(hostname); + const noteId = note?.id as number; + + if (userId !== null && noteId !== undefined) { + await this.saveVisit(noteId, userId); + } + + return note; } /** diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 95d21b327..030318967 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -195,7 +195,6 @@ export default class HttpApi implements Api { await this.server?.register(NoteRouter, { prefix: '/note', noteService: domainServices.noteService, - noteVisitsService: domainServices.noteVisitsService, noteSettingsService: domainServices.noteSettingsService, }); diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 74ab21049..a8d88bed0 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -124,6 +124,8 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don /** * Check if user is authorized + * We can not move this to noteService because this we get note from NoteResolver + * NoteResolver has no information about user to save noteVisit */ if (userId !== null) { await noteService.saveVisit(noteId, userId); @@ -181,11 +183,6 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const noteId = request.note?.id as number; const isDeleted = await noteService.deleteNoteById(noteId); - /** - * Delete all visits of the note - */ - await noteService.deleteNoteVisits(noteId); - /** * Check if note does not exist */ @@ -220,13 +217,6 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const addedNote = await noteService.addNote(content as JSON, userId as number, parentId); // "authRequired" policy ensures that userId is not null - /** - * Check if user is authorized - */ - if (userId !== null) { - await noteService.saveVisit(addedNote.id, userId); - } - /** * @todo use event bus: emit 'note-added' event and subscribe to it in other modules like 'note-settings' */ @@ -440,8 +430,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }, async (request, reply) => { const params = request.params; const { userId } = request; - const note = await noteService.getNoteByHostname(params.hostname); - const noteId = note?.id as number; + const note = await noteService.getNoteByHostname(params.hostname, userId); /** * Check if note exists @@ -450,13 +439,6 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don return reply.notFound('Note not found'); } - /** - * Save note visit if user is authorized - */ - if (userId !== null) { - await noteService.saveVisit(noteId, userId); - } - /** * By default, unauthorized user can not edit the note */ From 87c329bcc201c18f60ddaec496534b147cbcb917 Mon Sep 17 00:00:00 2001 From: e11sy Date: Sun, 7 Apr 2024 14:42:31 +0300 Subject: [PATCH 8/9] get back to noteVisitsService usage - left todo about event bus usage for saving and deleting noteVIsits --- src/domain/index.ts | 8 ++++++ src/domain/service/note.ts | 38 ++---------------------- src/domain/service/noteVisits.ts | 43 ++++++++++++++++++++++++++++ src/presentation/http/http-api.ts | 1 + src/presentation/http/router/note.ts | 31 +++++++++++++++++--- 5 files changed, 81 insertions(+), 40 deletions(-) create mode 100644 src/domain/service/noteVisits.ts diff --git a/src/domain/index.ts b/src/domain/index.ts index 4cea86409..c6bf75c1e 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -7,6 +7,7 @@ import UserService from '@domain/service/user.js'; import AIService from './service/ai.js'; import EditorToolsService from '@domain/service/editorTools.js'; import FileUploaderService from './service/fileUploader.service.js'; +import NoteVisitsService from './service/noteVisits.js'; /** * Interface for initiated services @@ -46,6 +47,11 @@ export interface DomainServices { * File uploader service instance */ fileUploaderService: FileUploaderService, + + /** + * Note visits service instance + */ + noteVisitsService: NoteVisitsService; } /** @@ -56,6 +62,7 @@ export interface DomainServices { */ export function init(repositories: Repositories, appConfig: AppConfig): DomainServices { const noteService = new NoteService(repositories.noteRepository, repositories.noteRelationsRepository, repositories.noteVisitsRepository); + const noteVisitsService = new NoteVisitsService(repositories.noteVisitsRepository); const authService = new AuthService( appConfig.auth.accessSecret, appConfig.auth.accessExpiresIn, @@ -86,5 +93,6 @@ export function init(repositories: Repositories, appConfig: AppConfig): DomainSe authService, aiService, editorToolsService, + noteVisitsService, }; } diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index 49d245d55..fda9ec92e 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -6,7 +6,6 @@ import { DomainError } from '@domain/entities/DomainError.js'; import type NoteRelationsRepository from '@repository/noteRelations.repository.js'; import type User from '@domain/entities/user.js'; import type { NoteList } from '@domain/entities/noteList.js'; -import type NoteVisit from '@domain/entities/noteVisit.js'; /** * Note service @@ -61,8 +60,6 @@ export default class NoteService { creatorId, }); - await this.saveVisit(note.id, creatorId); - if (parentPublicId !== undefined) { const parentNote = await this.getNoteByPublicId(parentPublicId); @@ -98,10 +95,6 @@ export default class NoteService { throw new DomainError(`Relation with noteId ${id} was not deleted`); } } - /** - * Delete all visits of this note - */ - await this.deleteNoteVisits(id); const isNoteDeleted = await this.noteRepository.deleteNoteById(id); @@ -171,18 +164,10 @@ export default class NoteService { * Gets note by custom hostname * * @param hostname - hostname - * @param userId - id of the user * @returns { Promise } note */ - public async getNoteByHostname(hostname: string, userId: User['id'] | null): Promise { - const note = await this.noteRepository.getNoteByHostname(hostname); - const noteId = note?.id as number; - - if (userId !== null && noteId !== undefined) { - await this.saveVisit(noteId, userId); - } - - return note; + public async getNoteByHostname(hostname: string): Promise { + return await this.noteRepository.getNoteByHostname(hostname); } /** @@ -237,23 +222,4 @@ export default class NoteService { return await this.noteRelationsRepository.updateNoteRelationById(noteId, parentNote.id); }; - - /** - * Updates existing noteVisit's visitedAt or creates new record if user opens note for the first time - * - * @param noteId - note internal id - * @param userId - id of the user - */ - public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise { - return await this.noteVisitsRepository.saveVisit(noteId, userId); - }; - - /** - * Deletes all visits of the note when a note is deleted - * - * @param noteId - note internal id - */ - public async deleteNoteVisits(noteId: NoteInternalId): Promise { - return await this.noteVisitsRepository.deleteNoteVisits(noteId); - } } diff --git a/src/domain/service/noteVisits.ts b/src/domain/service/noteVisits.ts new file mode 100644 index 000000000..3794a1868 --- /dev/null +++ b/src/domain/service/noteVisits.ts @@ -0,0 +1,43 @@ +import type { NoteInternalId } from '@domain/entities/note.js'; +import type User from '@domain/entities/user.js'; +import type NoteVisit from '@domain/entities/noteVisit.js'; +import type NoteVisitsRepository from '@repository/noteVisits.repository.js'; + +/** + * Note Visits service, which will store latest note visit + * it is used to display recent notes for each user + */ +export default class NoteVisitsService { + /** + * Note Visits repository + */ + public noteVisitsRepository: NoteVisitsRepository; + + /** + * NoteVisits service constructor + * + * @param noteVisitRepository - note Visits repository + */ + constructor(noteVisitRepository: NoteVisitsRepository) { + this.noteVisitsRepository = noteVisitRepository; + } + + /** + * Updates existing noteVisit's visitedAt or creates new record if user opens note for the first time + * + * @param noteId - note internal id + * @param userId - id of the user + */ + public async saveVisit(noteId: NoteInternalId, userId: User['id']): Promise { + return await this.noteVisitsRepository.saveVisit(noteId, userId); + }; + + /** + * Deletes all visits of the note when a note is deleted + * + * @param noteId - note internal id + */ + public async deleteNoteVisits(noteId: NoteInternalId): Promise { + return await this.noteVisitsRepository.deleteNoteVisits(noteId); + } +} \ No newline at end of file diff --git a/src/presentation/http/http-api.ts b/src/presentation/http/http-api.ts index 030318967..5bdc54934 100644 --- a/src/presentation/http/http-api.ts +++ b/src/presentation/http/http-api.ts @@ -196,6 +196,7 @@ export default class HttpApi implements Api { prefix: '/note', noteService: domainServices.noteService, noteSettingsService: domainServices.noteSettingsService, + noteVisitsService: domainServices.noteVisitsService, }); await this.server?.register(NoteListRouter, { diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index a8d88bed0..1efb9fcdb 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -8,6 +8,7 @@ import useNoteSettingsResolver from '../middlewares/noteSettings/useNoteSettings import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleResolver.js'; import { MemberRole } from '@domain/entities/team.js'; import { type NotePublic, definePublicNote } from '@domain/entities/notePublic.js'; +import type NoteVisitsService from '@domain/service/noteVisits.js'; /** * Interface for the note router. @@ -22,6 +23,11 @@ interface NoteRouterOptions { * Note Settings service instance */ noteSettingsService: NoteSettingsService, + + /** + * Note visits service instance + */ + noteVisitsService: NoteVisitsService; } /** @@ -37,6 +43,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don */ const noteService = opts.noteService; const noteSettingsService = opts.noteSettingsService; + const noteVisitsService = opts.noteVisitsService; /** * Prepare note id resolver middleware @@ -124,11 +131,11 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don /** * Check if user is authorized - * We can not move this to noteService because this we get note from NoteResolver - * NoteResolver has no information about user to save noteVisit + * + * @todo use event bus to save note visits */ if (userId !== null) { - await noteService.saveVisit(noteId, userId); + await noteVisitsService.saveVisit(noteId, userId); } const parentId = await noteService.getParentNoteIdByNoteId(note.id); @@ -379,6 +386,13 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don const isDeleted = await noteService.unlinkParent(noteId); + /** + * Delete all visits of the note + * + * @todo use event bus to delete note visits + */ + await noteVisitsService.deleteNoteVisits(noteId); + /** * Check if parent relation was successfully deleted */ @@ -430,7 +444,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don }, async (request, reply) => { const params = request.params; const { userId } = request; - const note = await noteService.getNoteByHostname(params.hostname, userId); + const note = await noteService.getNoteByHostname(params.hostname); /** * Check if note exists @@ -439,6 +453,15 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don return reply.notFound('Note not found'); } + /** + * Check if user is authorized + * + * @todo use event bus to save note visits + */ + if (userId !== null) { + await noteVisitsService.saveVisit(note.id, userId); + } + /** * By default, unauthorized user can not edit the note */ From d6e6e70dcc90a3e5c1385a6e70857c7279f54184 Mon Sep 17 00:00:00 2001 From: e11sy <130844513+e11sy@users.noreply.github.com> Date: Sun, 7 Apr 2024 15:39:37 +0300 Subject: [PATCH 9/9] Update src/presentation/http/router/note.ts Co-authored-by: Tatiana Fomina --- src/presentation/http/router/note.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/http/router/note.ts b/src/presentation/http/router/note.ts index 1efb9fcdb..96d65fb5e 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -454,7 +454,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don } /** - * Check if user is authorized + * Save note visit if user is authorized * * @todo use event bus to save note visits */