diff --git a/src/domain/event-bus/events/noteAddedEvent.ts b/src/domain/event-bus/events/noteAddedEvent.ts new file mode 100644 index 00000000..2c02f4d8 --- /dev/null +++ b/src/domain/event-bus/events/noteAddedEvent.ts @@ -0,0 +1,22 @@ +/** + * Event that is emitted when a note is added. + */ +export const NOTE_ADDED_EVENT_NAME = 'noteAdded'; + + +/** + * Note added event + */ +export class NoteAddedEvent extends CustomEvent<{ noteId: number; userId: number }> { + /** + * Note added event constructor + * @param noteId - note internal id + * @param userId - user id + */ + constructor(noteId: number, userId: number) { + super(NOTE_ADDED_EVENT_NAME, { + detail: { noteId, + userId }, + }); + } +} diff --git a/src/domain/event-bus/events/noteVisitedEvent.ts b/src/domain/event-bus/events/noteVisitedEvent.ts new file mode 100644 index 00000000..9fdd4d47 --- /dev/null +++ b/src/domain/event-bus/events/noteVisitedEvent.ts @@ -0,0 +1,18 @@ +// domain/event-bus/events/noteVisitedEvent.ts + +export const NOTE_VISITED_EVENT_NAME = 'noteVisited'; + +/** + * Note visited event + */ +export class NoteVisitedEvent extends CustomEvent<{ noteId: number; userId: number }> { + /** + * Note visited event constructor + * @param noteId - note internal id + * @param userId - user id + */ + constructor(noteId: number, userId: number) { + super(NOTE_VISITED_EVENT_NAME, { detail: { noteId, + userId } }); + } +} diff --git a/src/domain/event-bus/index.ts b/src/domain/event-bus/index.ts new file mode 100644 index 00000000..0f9c5a10 --- /dev/null +++ b/src/domain/event-bus/index.ts @@ -0,0 +1,43 @@ +import type { NOTE_ADDED_EVENT_NAME, NoteAddedEvent } from './events/noteAddedEvent.js'; + +/** + * Event Bus provides a loosely coupled communication way between Domain and some other layers + * + * Extends native event emitter called EventTarget + */ +export default class EventBus extends EventTarget { + private static instance: EventBus; + + /** + * EventBus constructor + */ + constructor() { + super(); + } + + /** + * Gets the singleton instance of the EventBus + */ + public static getInstance(): EventBus { + if (EventBus.instance === undefined) { + EventBus.instance = new EventBus(); + } + + return EventBus.instance; + } + + /** + * Dispatches an event + * + * @param event - The event to dispatch + */ + public dispatch(event: Event): boolean { + return this.dispatchEvent(event); + } +} + +export type CrossDomainEventMap = { + [NOTE_ADDED_EVENT_NAME]: NoteAddedEvent, +}; + + diff --git a/src/domain/service/note.ts b/src/domain/service/note.ts index fda9ec92..9b67b0db 100644 --- a/src/domain/service/note.ts +++ b/src/domain/service/note.ts @@ -6,6 +6,8 @@ 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 EventBus from '@domain/event-bus/index.js'; +import { NoteAddedEvent } from '@domain/event-bus/events/noteAddedEvent.js'; /** * Note service @@ -69,6 +71,10 @@ export default class NoteService { await this.noteRelationsRepository.addNoteRelation(note.id, parentNote.id); } + /** + * Dispatches an event when a note is added + */ + EventBus.getInstance().dispatch(new NoteAddedEvent(note.id, creatorId)); return note; } diff --git a/src/domain/service/noteSettings.ts b/src/domain/service/noteSettings.ts index 5832bf22..461a94e9 100644 --- a/src/domain/service/noteSettings.ts +++ b/src/domain/service/noteSettings.ts @@ -9,6 +9,8 @@ import type User from '@domain/entities/user.js'; import { createInvitationHash } from '@infrastructure/utils/invitationHash.js'; import { DomainError } from '@domain/entities/DomainError.js'; import type { SharedDomainMethods } from './shared/index.js'; +import EventBus from '@domain/event-bus/index.js'; +import { NOTE_ADDED_EVENT_NAME } from '@domain/event-bus/events/noteAddedEvent.js'; /** * Service responsible for Note Settings @@ -31,6 +33,24 @@ export default class NoteSettingsService { constructor(noteSettingsRepository: NoteSettingsRepository, teamRepository: TeamRepository, private readonly shared: SharedDomainMethods) { this.noteSettingsRepository = noteSettingsRepository; this.teamRepository = teamRepository; + + /** + * Listen to the note related events + */ + EventBus.getInstance().addEventListener(NOTE_ADDED_EVENT_NAME, async (event) => { + const { noteId, userId } = (event as CustomEvent<{ noteId: number; userId: number }>).detail; + + try { + await this.addNoteSettings(noteId); + await this.createTeamMember({ + noteId: noteId, + userId: userId, + role: MemberRole.Write, + }); + } catch (error) { + throw error; + } + }); } /** diff --git a/src/domain/service/noteVisits.ts b/src/domain/service/noteVisits.ts index 3794a186..571699db 100644 --- a/src/domain/service/noteVisits.ts +++ b/src/domain/service/noteVisits.ts @@ -2,6 +2,9 @@ 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'; +import EventBus from '@domain/event-bus/index.js'; +import { NOTE_ADDED_EVENT_NAME } from '@domain/event-bus/events/noteAddedEvent.js'; +import { NOTE_VISITED_EVENT_NAME } from '@domain/event-bus/events/noteVisitedEvent.js'; /** * Note Visits service, which will store latest note visit @@ -20,6 +23,29 @@ export default class NoteVisitsService { */ constructor(noteVisitRepository: NoteVisitsRepository) { this.noteVisitsRepository = noteVisitRepository; + + /** + * Listen to the note related events + */ + EventBus.getInstance().addEventListener(NOTE_ADDED_EVENT_NAME, async (event) => { + const { noteId, userId } = (event as CustomEvent<{ noteId: number; userId: number }>).detail; + + try { + return await this.noteVisitsRepository.saveVisit(noteId, userId); + } catch (error) { + throw error; + } + }); + + EventBus.getInstance().addEventListener(NOTE_VISITED_EVENT_NAME, async (event) => { + const { noteId, userId } = (event as CustomEvent<{ noteId: number; userId: number }>).detail; + + try { + await this.noteVisitsRepository.saveVisit(noteId, userId); + } catch (error) { + console.error('Error saving note visit', error); + } + }); } /** @@ -40,4 +66,4 @@ export default class NoteVisitsService { 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 f152e09a..f6c606ed 100644 --- a/src/presentation/http/router/note.ts +++ b/src/presentation/http/router/note.ts @@ -9,6 +9,8 @@ import useMemberRoleResolver from '../middlewares/noteSettings/useMemberRoleReso import { MemberRole } from '@domain/entities/team.js'; import { type NotePublic, definePublicNote } from '@domain/entities/notePublic.js'; import type NoteVisitsService from '@domain/service/noteVisits.js'; +import EventBus from '@domain/event-bus/index.js'; +import { NoteVisitedEvent } from '@domain/event-bus/events/noteVisitedEvent.js'; /** * Interface for the note router. @@ -43,7 +45,6 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don */ const noteService = opts.noteService; const noteSettingsService = opts.noteSettingsService; - const noteVisitsService = opts.noteVisitsService; /** * Prepare note id resolver middleware @@ -135,7 +136,7 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don * @todo use event bus to save note visits */ if (userId !== null) { - await noteVisitsService.saveVisit(noteId, userId); + EventBus.getInstance().dispatch(new NoteVisitedEvent(noteId, userId)); } const parentId = await noteService.getParentNoteIdByNoteId(note.id); @@ -224,28 +225,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 - /** - * Save note visit when note created - * - * @todo use even bus to save noteVisit - */ - - await noteVisitsService.saveVisit(addedNote.id, userId!); - - /** - * @todo use event bus: emit 'note-added' event and subscribe to it in other modules like 'note-settings' - */ - await noteSettingsService.addNoteSettings(addedNote.id); - - /** - * Creates TeamMember with write priveleges - */ - await noteSettingsService.createTeamMember({ - noteId: addedNote.id, - userId: userId as number, - role: MemberRole.Write, - }); - return reply.send({ id: addedNote.publicId, }); @@ -391,15 +370,11 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don ], }, async (request, reply) => { const noteId = request.note?.id as number; + const userId = request.note?.creatorId as number; const isDeleted = await noteService.unlinkParent(noteId); - /** - * Delete all visits of the note - * - * @todo use event bus to delete note visits - */ - await noteVisitsService.deleteNoteVisits(noteId); + EventBus.getInstance().dispatch(new NoteVisitedEvent(noteId, userId)); /** * Check if parent relation was successfully deleted @@ -464,10 +439,9 @@ const NoteRouter: FastifyPluginCallback = (fastify, opts, don /** * Save note visit if user is authorized * - * @todo use event bus to save note visits */ if (userId !== null) { - await noteVisitsService.saveVisit(note.id, userId); + EventBus.getInstance().dispatch(new NoteVisitedEvent(note.id, userId)); } /**