From 6af74eb3d465820ffe7822ead96a579aa8f97390 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Fri, 15 May 2026 01:59:02 +0100 Subject: [PATCH] feat(webhooks): dispatch domain events to webhook subscribers --- .../extensions/persistence.extension.ts | 16 ++++++ .../attachment/services/attachment.service.ts | 22 +++++++- .../src/core/comment/comment.controller.ts | 14 +++++ .../src/core/comment/comment.service.ts | 34 ++++++++++++ apps/server/src/core/page/page.controller.ts | 27 ++++++++++ .../src/core/page/services/page.service.ts | 54 ++++++++++++++++++- .../src/core/space/services/space.service.ts | 35 ++++++++++++ .../services/workspace-invitation.service.ts | 15 ++++++ .../workspace/services/workspace.service.ts | 14 +++++ apps/server/src/ee | 2 +- 10 files changed, 230 insertions(+), 3 deletions(-) diff --git a/apps/server/src/collaboration/extensions/persistence.extension.ts b/apps/server/src/collaboration/extensions/persistence.extension.ts index 53bc8b334..c9d3a7da3 100644 --- a/apps/server/src/collaboration/extensions/persistence.extension.ts +++ b/apps/server/src/collaboration/extensions/persistence.extension.ts @@ -33,6 +33,8 @@ import { HISTORY_INTERVAL, } from '../constants'; import { TransclusionService } from '../../core/page/transclusion/transclusion.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class PersistenceExtension implements Extension { @@ -47,6 +49,7 @@ export class PersistenceExtension implements Extension { @InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue, private readonly collabHistory: CollabHistoryService, private readonly transclusionService: TransclusionService, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async onLoadDocument(data: onLoadDocumentPayload) { @@ -199,6 +202,19 @@ export class PersistenceExtension implements Extension { }); await this.enqueuePageHistory(page); + + this.webhookDispatcher.dispatch( + page.workspaceId, + WebhookEvent.PageUpdated, + { + id: page.id, + slugId: page.slugId, + title: page.title, + spaceId: page.spaceId, + workspaceId: page.workspaceId, + updatedAt: page.updatedAt, + }, + ); } } diff --git a/apps/server/src/core/attachment/services/attachment.service.ts b/apps/server/src/core/attachment/services/attachment.service.ts index 766c9f65f..316a0681d 100644 --- a/apps/server/src/core/attachment/services/attachment.service.ts +++ b/apps/server/src/core/attachment/services/attachment.service.ts @@ -27,6 +27,8 @@ import { InjectQueue } from '@nestjs/bullmq'; import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { Queue } from 'bullmq'; import { createByteCountingStream } from '../../../common/helpers/utils'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class AttachmentService { @@ -39,6 +41,7 @@ export class AttachmentService { private readonly spaceRepo: SpaceRepo, @InjectKysely() private readonly db: KyselyDB, @InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async uploadFile(opts: { @@ -271,7 +274,7 @@ export class AttachmentService { spaceId, trx, } = opts; - return this.attachmentRepo.insertAttachment( + const attachment = await this.attachmentRepo.insertAttachment( { id: attachmentId, type: type, @@ -287,6 +290,23 @@ export class AttachmentService { }, trx, ); + + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.AttachmentUploaded, + { + id: attachment.id, + fileName: attachment.fileName, + mimeType: attachment.mimeType, + fileSize: attachment.fileSize, + pageId: attachment.pageId, + spaceId: attachment.spaceId, + workspaceId: attachment.workspaceId, + creatorId: attachment.creatorId, + }, + ); + + return attachment; } async handleDeleteAiChatAttachments(aiChatId: string) { diff --git a/apps/server/src/core/comment/comment.controller.ts b/apps/server/src/core/comment/comment.controller.ts index 22458848b..9d5600aa9 100644 --- a/apps/server/src/core/comment/comment.controller.ts +++ b/apps/server/src/core/comment/comment.controller.ts @@ -32,6 +32,8 @@ import { IAuditService, } from '../../integrations/audit/audit.service'; import { WsService } from '../../ws/ws.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @UseGuards(JwtAuthGuard) @Controller('comments') @@ -44,6 +46,7 @@ export class CommentController { private readonly pageAccessService: PageAccessService, private readonly wsService: WsService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, + private readonly webhookDispatcher: WebhookDispatcher, ) {} @HttpCode(HttpStatus.OK) @@ -192,5 +195,16 @@ export class CommentController { }, }, }); + + this.webhookDispatcher.dispatch( + comment.workspaceId, + WebhookEvent.CommentDeleted, + { + id: comment.id, + pageId: comment.pageId, + spaceId: comment.spaceId, + workspaceId: comment.workspaceId, + }, + ); } } diff --git a/apps/server/src/core/comment/comment.service.ts b/apps/server/src/core/comment/comment.service.ts index e888ef501..b822102c7 100644 --- a/apps/server/src/core/comment/comment.service.ts +++ b/apps/server/src/core/comment/comment.service.ts @@ -19,6 +19,8 @@ import { QueueJob, QueueName } from '../../integrations/queue/constants'; import { extractUserMentionIdsFromJson } from '../../common/helpers/prosemirror/utils'; import { ICommentNotificationJob } from '../../integrations/queue/constants/queue.interface'; import { WsService } from '../../ws/ws.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class CommentService { @@ -33,6 +35,7 @@ export class CommentService { private generalQueue: Queue, @InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async findById(commentId: string) { @@ -142,6 +145,21 @@ export class CommentService { comment, }); + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.CommentCreated, + { + id: comment.id, + pageId: comment.pageId, + spaceId: comment.spaceId, + workspaceId: comment.workspaceId, + type: comment.type, + content: comment.content, + creatorId: comment.creatorId, + createdAt: comment.createdAt, + }, + ); + return comment; } @@ -203,6 +221,22 @@ export class CommentService { comment, }); + this.webhookDispatcher.dispatch( + comment.workspaceId, + WebhookEvent.CommentUpdated, + { + id: comment.id, + pageId: comment.pageId, + spaceId: comment.spaceId, + workspaceId: comment.workspaceId, + type: comment.type, + content: comment.content, + creatorId: comment.creatorId, + createdAt: comment.createdAt, + updatedAt: comment.updatedAt, + }, + ); + return comment; } diff --git a/apps/server/src/core/page/page.controller.ts b/apps/server/src/core/page/page.controller.ts index 773774ea7..2151cdd18 100644 --- a/apps/server/src/core/page/page.controller.ts +++ b/apps/server/src/core/page/page.controller.ts @@ -52,6 +52,8 @@ import { IAuditService, } from '../../integrations/audit/audit.service'; import { getPageTitle } from '../../common/helpers'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @UseGuards(JwtAuthGuard) @Controller('pages') @@ -65,6 +67,7 @@ export class PageController { private readonly backlinkService: BacklinkService, private readonly labelService: LabelService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, + private readonly webhookDispatcher: WebhookDispatcher, ) {} @HttpCode(HttpStatus.OK) @@ -366,6 +369,18 @@ export class PageController { }, }, }); + + this.webhookDispatcher.dispatch( + workspace.id, + WebhookEvent.PageDeleted, + { + id: page.id, + slugId: page.slugId, + title: page.title, + spaceId: page.spaceId, + workspaceId: workspace.id, + }, + ); } } @@ -406,6 +421,18 @@ export class PageController { }, }); + this.webhookDispatcher.dispatch( + workspace.id, + WebhookEvent.PageRestored, + { + id: page.id, + slugId: page.slugId, + title: page.title, + spaceId: page.spaceId, + workspaceId: workspace.id, + }, + ); + return this.pageRepo.findById(pageIdDto.pageId, { includeHasChildren: true, }); diff --git a/apps/server/src/core/page/services/page.service.ts b/apps/server/src/core/page/services/page.service.ts index 4c149cead..7598eb41a 100644 --- a/apps/server/src/core/page/services/page.service.ts +++ b/apps/server/src/core/page/services/page.service.ts @@ -55,6 +55,8 @@ import { markdownToHtml } from '@docmost/editor-ext'; import { WatcherService } from '../../watcher/watcher.service'; import { sql } from 'kysely'; import { TransclusionService } from '../transclusion/transclusion.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class PageService { @@ -73,6 +75,7 @@ export class PageService { private collaborationGateway: CollaborationGateway, private readonly watcherService: WatcherService, private readonly transclusionService: TransclusionService, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async findById( @@ -156,9 +159,30 @@ export class PageService { this.logger.warn(`Failed to queue add-page-watchers: ${err.message}`), ); + this.webhookDispatcher.dispatch( + page.workspaceId, + WebhookEvent.PageCreated, + this.toWebhookPagePayload(page), + ); + return page; } + private toWebhookPagePayload(page: Page) { + return { + id: page.id, + slugId: page.slugId, + title: page.title, + icon: page.icon, + parentPageId: page.parentPageId, + spaceId: page.spaceId, + workspaceId: page.workspaceId, + creatorId: page.creatorId, + createdAt: page.createdAt, + updatedAt: page.updatedAt, + }; + } + async nextPagePosition(spaceId: string, parentPageId?: string) { let pagePosition: string; @@ -245,13 +269,21 @@ export class PageService { ); } - return await this.pageRepo.findById(page.id, { + const updatedPage = await this.pageRepo.findById(page.id, { includeSpace: true, includeContent: true, includeCreator: true, includeLastUpdatedBy: true, includeContributors: true, }); + + this.webhookDispatcher.dispatch( + updatedPage.workspaceId, + WebhookEvent.PageUpdated, + this.toWebhookPagePayload(updatedPage), + ); + + return updatedPage; } async updatePageContent( @@ -487,6 +519,18 @@ export class PageService { } }); + this.webhookDispatcher.dispatch( + rootPage.workspaceId, + WebhookEvent.PageMoved, + { + id: rootPage.id, + slugId: rootPage.slugId, + fromSpaceId: rootPage.spaceId, + toSpaceId: spaceId, + workspaceId: rootPage.workspaceId, + }, + ); + return { childPageIds }; } @@ -1015,6 +1059,14 @@ export class PageService { pageIds: pageIds, workspaceId, }); + + for (const id of pageIds) { + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.PageDeleted, + { id, workspaceId }, + ); + } } } diff --git a/apps/server/src/core/space/services/space.service.ts b/apps/server/src/core/space/services/space.service.ts index 2675a9e6a..76aed06b6 100644 --- a/apps/server/src/core/space/services/space.service.ts +++ b/apps/server/src/core/space/services/space.service.ts @@ -29,6 +29,8 @@ import { AUDIT_SERVICE, IAuditService, } from '../../../integrations/audit/audit.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class SpaceService { @@ -41,6 +43,7 @@ export class SpaceService { @InjectKysely() private readonly db: KyselyDB, @InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async createSpace( @@ -85,6 +88,17 @@ export class SpaceService { }, }); + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.SpaceCreated, + { + id: space.id, + name: space.name, + slug: space.slug, + workspaceId, + }, + ); + return { ...space, memberCount: 1 }; } @@ -244,6 +258,17 @@ export class SpaceService { spaceId: updateSpaceDto.spaceId, changes: { before, after }, }); + + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.SpaceUpdated, + { + id: updatedSpace.id, + name: updatedSpace.name, + slug: updatedSpace.slug, + workspaceId, + }, + ); } return updatedSpace; @@ -289,5 +314,15 @@ export class SpaceService { }, }, }); + + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.SpaceDeleted, + { + id: spaceId, + name: space.name, + workspaceId, + }, + ); } } diff --git a/apps/server/src/core/workspace/services/workspace-invitation.service.ts b/apps/server/src/core/workspace/services/workspace-invitation.service.ts index 50ed49f05..a88ddf9f6 100644 --- a/apps/server/src/core/workspace/services/workspace-invitation.service.ts +++ b/apps/server/src/core/workspace/services/workspace-invitation.service.ts @@ -40,6 +40,8 @@ import { AUDIT_SERVICE, IAuditService, } from '../../../integrations/audit/audit.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class WorkspaceInvitationService { @@ -55,6 +57,7 @@ export class WorkspaceInvitationService { @InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue, private readonly environmentService: EnvironmentService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async getInvitations(workspaceId: string, pagination: PaginationOptions) { @@ -304,6 +307,18 @@ export class WorkspaceInvitationService { return; } + this.webhookDispatcher.dispatch( + workspace.id, + WebhookEvent.UserCreated, + { + id: newUser.id, + name: newUser.name, + email: newUser.email, + role: newUser.role, + workspaceId: workspace.id, + }, + ); + // notify the inviter const invitedByUser = await this.userRepo.findById( invitation.invitedById, diff --git a/apps/server/src/core/workspace/services/workspace.service.ts b/apps/server/src/core/workspace/services/workspace.service.ts index 267eb13b7..4c456a558 100644 --- a/apps/server/src/core/workspace/services/workspace.service.ts +++ b/apps/server/src/core/workspace/services/workspace.service.ts @@ -48,6 +48,8 @@ import { AUDIT_SERVICE, IAuditService, } from '../../../integrations/audit/audit.service'; +import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service'; +import { WebhookEvent } from '@docmost/ee/webhook/constants'; @Injectable() export class WorkspaceService { @@ -72,6 +74,7 @@ export class WorkspaceService { @InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, private userSessionRepo: UserSessionRepo, + private readonly webhookDispatcher: WebhookDispatcher, ) {} async findById(workspaceId: string) { @@ -736,6 +739,17 @@ export class WorkspaceService { }, }, }); + + this.webhookDispatcher.dispatch( + workspaceId, + WebhookEvent.UserDeactivated, + { + id: user.id, + name: user.name, + email: user.email, + workspaceId, + }, + ); } async activateUser( diff --git a/apps/server/src/ee b/apps/server/src/ee index 5c4c9e9bf..0defcae38 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 5c4c9e9bf33ce46658352a15f8a5c3d84816e8f9 +Subproject commit 0defcae3855eaa15e70de6f709b933de27075244