feat(webhooks): dispatch domain events to webhook subscribers

This commit is contained in:
Philipinho
2026-05-15 01:59:02 +01:00
parent e7fff3c9b5
commit 6af74eb3d4
10 changed files with 230 additions and 3 deletions
@@ -33,6 +33,8 @@ import {
HISTORY_INTERVAL, HISTORY_INTERVAL,
} from '../constants'; } from '../constants';
import { TransclusionService } from '../../core/page/transclusion/transclusion.service'; 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() @Injectable()
export class PersistenceExtension implements Extension { export class PersistenceExtension implements Extension {
@@ -47,6 +49,7 @@ export class PersistenceExtension implements Extension {
@InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue, @InjectQueue(QueueName.NOTIFICATION_QUEUE) private notificationQueue: Queue,
private readonly collabHistory: CollabHistoryService, private readonly collabHistory: CollabHistoryService,
private readonly transclusionService: TransclusionService, private readonly transclusionService: TransclusionService,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async onLoadDocument(data: onLoadDocumentPayload) { async onLoadDocument(data: onLoadDocumentPayload) {
@@ -199,6 +202,19 @@ export class PersistenceExtension implements Extension {
}); });
await this.enqueuePageHistory(page); 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,
},
);
} }
} }
@@ -27,6 +27,8 @@ import { InjectQueue } from '@nestjs/bullmq';
import { QueueJob, QueueName } from '../../../integrations/queue/constants'; import { QueueJob, QueueName } from '../../../integrations/queue/constants';
import { Queue } from 'bullmq'; import { Queue } from 'bullmq';
import { createByteCountingStream } from '../../../common/helpers/utils'; import { createByteCountingStream } from '../../../common/helpers/utils';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@Injectable() @Injectable()
export class AttachmentService { export class AttachmentService {
@@ -39,6 +41,7 @@ export class AttachmentService {
private readonly spaceRepo: SpaceRepo, private readonly spaceRepo: SpaceRepo,
@InjectKysely() private readonly db: KyselyDB, @InjectKysely() private readonly db: KyselyDB,
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue, @InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async uploadFile(opts: { async uploadFile(opts: {
@@ -271,7 +274,7 @@ export class AttachmentService {
spaceId, spaceId,
trx, trx,
} = opts; } = opts;
return this.attachmentRepo.insertAttachment( const attachment = await this.attachmentRepo.insertAttachment(
{ {
id: attachmentId, id: attachmentId,
type: type, type: type,
@@ -287,6 +290,23 @@ export class AttachmentService {
}, },
trx, 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) { async handleDeleteAiChatAttachments(aiChatId: string) {
@@ -32,6 +32,8 @@ import {
IAuditService, IAuditService,
} from '../../integrations/audit/audit.service'; } from '../../integrations/audit/audit.service';
import { WsService } from '../../ws/ws.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) @UseGuards(JwtAuthGuard)
@Controller('comments') @Controller('comments')
@@ -44,6 +46,7 @@ export class CommentController {
private readonly pageAccessService: PageAccessService, private readonly pageAccessService: PageAccessService,
private readonly wsService: WsService, private readonly wsService: WsService,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
@HttpCode(HttpStatus.OK) @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,
},
);
} }
} }
@@ -19,6 +19,8 @@ import { QueueJob, QueueName } from '../../integrations/queue/constants';
import { extractUserMentionIdsFromJson } from '../../common/helpers/prosemirror/utils'; import { extractUserMentionIdsFromJson } from '../../common/helpers/prosemirror/utils';
import { ICommentNotificationJob } from '../../integrations/queue/constants/queue.interface'; import { ICommentNotificationJob } from '../../integrations/queue/constants/queue.interface';
import { WsService } from '../../ws/ws.service'; import { WsService } from '../../ws/ws.service';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@Injectable() @Injectable()
export class CommentService { export class CommentService {
@@ -33,6 +35,7 @@ export class CommentService {
private generalQueue: Queue, private generalQueue: Queue,
@InjectQueue(QueueName.NOTIFICATION_QUEUE) @InjectQueue(QueueName.NOTIFICATION_QUEUE)
private notificationQueue: Queue, private notificationQueue: Queue,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async findById(commentId: string) { async findById(commentId: string) {
@@ -142,6 +145,21 @@ export class CommentService {
comment, 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; return comment;
} }
@@ -203,6 +221,22 @@ export class CommentService {
comment, 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; return comment;
} }
@@ -52,6 +52,8 @@ import {
IAuditService, IAuditService,
} from '../../integrations/audit/audit.service'; } from '../../integrations/audit/audit.service';
import { getPageTitle } from '../../common/helpers'; import { getPageTitle } from '../../common/helpers';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@Controller('pages') @Controller('pages')
@@ -65,6 +67,7 @@ export class PageController {
private readonly backlinkService: BacklinkService, private readonly backlinkService: BacklinkService,
private readonly labelService: LabelService, private readonly labelService: LabelService,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
@HttpCode(HttpStatus.OK) @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, { return this.pageRepo.findById(pageIdDto.pageId, {
includeHasChildren: true, includeHasChildren: true,
}); });
@@ -55,6 +55,8 @@ import { markdownToHtml } from '@docmost/editor-ext';
import { WatcherService } from '../../watcher/watcher.service'; import { WatcherService } from '../../watcher/watcher.service';
import { sql } from 'kysely'; import { sql } from 'kysely';
import { TransclusionService } from '../transclusion/transclusion.service'; import { TransclusionService } from '../transclusion/transclusion.service';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@Injectable() @Injectable()
export class PageService { export class PageService {
@@ -73,6 +75,7 @@ export class PageService {
private collaborationGateway: CollaborationGateway, private collaborationGateway: CollaborationGateway,
private readonly watcherService: WatcherService, private readonly watcherService: WatcherService,
private readonly transclusionService: TransclusionService, private readonly transclusionService: TransclusionService,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async findById( async findById(
@@ -156,9 +159,30 @@ export class PageService {
this.logger.warn(`Failed to queue add-page-watchers: ${err.message}`), this.logger.warn(`Failed to queue add-page-watchers: ${err.message}`),
); );
this.webhookDispatcher.dispatch(
page.workspaceId,
WebhookEvent.PageCreated,
this.toWebhookPagePayload(page),
);
return 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) { async nextPagePosition(spaceId: string, parentPageId?: string) {
let pagePosition: 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, includeSpace: true,
includeContent: true, includeContent: true,
includeCreator: true, includeCreator: true,
includeLastUpdatedBy: true, includeLastUpdatedBy: true,
includeContributors: true, includeContributors: true,
}); });
this.webhookDispatcher.dispatch(
updatedPage.workspaceId,
WebhookEvent.PageUpdated,
this.toWebhookPagePayload(updatedPage),
);
return updatedPage;
} }
async updatePageContent( 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 }; return { childPageIds };
} }
@@ -1015,6 +1059,14 @@ export class PageService {
pageIds: pageIds, pageIds: pageIds,
workspaceId, workspaceId,
}); });
for (const id of pageIds) {
this.webhookDispatcher.dispatch(
workspaceId,
WebhookEvent.PageDeleted,
{ id, workspaceId },
);
}
} }
} }
@@ -29,6 +29,8 @@ import {
AUDIT_SERVICE, AUDIT_SERVICE,
IAuditService, IAuditService,
} from '../../../integrations/audit/audit.service'; } from '../../../integrations/audit/audit.service';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@Injectable() @Injectable()
export class SpaceService { export class SpaceService {
@@ -41,6 +43,7 @@ export class SpaceService {
@InjectKysely() private readonly db: KyselyDB, @InjectKysely() private readonly db: KyselyDB,
@InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue, @InjectQueue(QueueName.ATTACHMENT_QUEUE) private attachmentQueue: Queue,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async createSpace( 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 }; return { ...space, memberCount: 1 };
} }
@@ -244,6 +258,17 @@ export class SpaceService {
spaceId: updateSpaceDto.spaceId, spaceId: updateSpaceDto.spaceId,
changes: { before, after }, changes: { before, after },
}); });
this.webhookDispatcher.dispatch(
workspaceId,
WebhookEvent.SpaceUpdated,
{
id: updatedSpace.id,
name: updatedSpace.name,
slug: updatedSpace.slug,
workspaceId,
},
);
} }
return updatedSpace; return updatedSpace;
@@ -289,5 +314,15 @@ export class SpaceService {
}, },
}, },
}); });
this.webhookDispatcher.dispatch(
workspaceId,
WebhookEvent.SpaceDeleted,
{
id: spaceId,
name: space.name,
workspaceId,
},
);
} }
} }
@@ -40,6 +40,8 @@ import {
AUDIT_SERVICE, AUDIT_SERVICE,
IAuditService, IAuditService,
} from '../../../integrations/audit/audit.service'; } from '../../../integrations/audit/audit.service';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@Injectable() @Injectable()
export class WorkspaceInvitationService { export class WorkspaceInvitationService {
@@ -55,6 +57,7 @@ export class WorkspaceInvitationService {
@InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue, @InjectQueue(QueueName.BILLING_QUEUE) private billingQueue: Queue,
private readonly environmentService: EnvironmentService, private readonly environmentService: EnvironmentService,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async getInvitations(workspaceId: string, pagination: PaginationOptions) { async getInvitations(workspaceId: string, pagination: PaginationOptions) {
@@ -304,6 +307,18 @@ export class WorkspaceInvitationService {
return; 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 // notify the inviter
const invitedByUser = await this.userRepo.findById( const invitedByUser = await this.userRepo.findById(
invitation.invitedById, invitation.invitedById,
@@ -48,6 +48,8 @@ import {
AUDIT_SERVICE, AUDIT_SERVICE,
IAuditService, IAuditService,
} from '../../../integrations/audit/audit.service'; } from '../../../integrations/audit/audit.service';
import { WebhookDispatcher } from '@docmost/ee/webhook/services/webhook-dispatcher.service';
import { WebhookEvent } from '@docmost/ee/webhook/constants';
@Injectable() @Injectable()
export class WorkspaceService { export class WorkspaceService {
@@ -72,6 +74,7 @@ export class WorkspaceService {
@InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue, @InjectQueue(QueueName.AI_QUEUE) private aiQueue: Queue,
@Inject(AUDIT_SERVICE) private readonly auditService: IAuditService, @Inject(AUDIT_SERVICE) private readonly auditService: IAuditService,
private userSessionRepo: UserSessionRepo, private userSessionRepo: UserSessionRepo,
private readonly webhookDispatcher: WebhookDispatcher,
) {} ) {}
async findById(workspaceId: string) { 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( async activateUser(