track link nodes (backlinks)

This commit is contained in:
Philipinho
2026-04-05 01:35:52 +01:00
parent b38658077f
commit 2277f5d129
4 changed files with 54 additions and 8 deletions
@@ -11,6 +11,7 @@ import {
import { import {
extractMentions, extractMentions,
extractPageMentions, extractPageMentions,
extractInternalLinkSlugIds,
} from '../../common/helpers/prosemirror/utils'; } from '../../common/helpers/prosemirror/utils';
import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo'; import { PageHistoryRepo } from '@docmost/db/repos/page/page-history.repo';
import { PageRepo } from '@docmost/db/repos/page/page.repo'; import { PageRepo } from '@docmost/db/repos/page/page.repo';
@@ -77,12 +78,14 @@ export class HistoryProcessor extends WorkerHost implements OnModuleDestroy {
const mentions = extractMentions(page.content); const mentions = extractMentions(page.content);
const pageMentions = extractPageMentions(mentions); const pageMentions = extractPageMentions(mentions);
const internalLinkSlugIds = extractInternalLinkSlugIds(page.content);
await this.generalQueue await this.generalQueue
.add(QueueJob.PAGE_BACKLINKS, { .add(QueueJob.PAGE_BACKLINKS, {
pageId, pageId,
workspaceId: page.workspaceId, workspaceId: page.workspaceId,
mentions: pageMentions, mentions: pageMentions,
internalLinkSlugIds,
} as IPageBacklinkJob) } as IPageBacklinkJob)
.catch((err) => { .catch((err) => {
this.logger.error( this.logger.error(
@@ -7,6 +7,10 @@ import { validate as isValidUUID } from 'uuid';
import { Transform } from '@tiptap/pm/transform'; import { Transform } from '@tiptap/pm/transform';
import { TiptapTransformer } from '@hocuspocus/transformer'; import { TiptapTransformer } from '@hocuspocus/transformer';
import * as Y from 'yjs'; import * as Y from 'yjs';
import {
INTERNAL_LINK_REGEX,
extractPageSlugId,
} from '../../../integrations/export/utils';
export interface MentionNode { export interface MentionNode {
id: string; id: string;
@@ -64,6 +68,27 @@ export function extractPageMentions(mentionList: MentionNode[]): MentionNode[] {
return pageMentionList as MentionNode[]; return pageMentionList as MentionNode[];
} }
export function extractInternalLinkSlugIds(prosemirrorJson: any): string[] {
const slugIds: string[] = [];
const doc = jsonToNode(prosemirrorJson);
doc.descendants((node: Node) => {
for (const mark of node.marks) {
if (mark.type.name === 'link' && mark.attrs.internal && mark.attrs.href) {
const match = mark.attrs.href.match(INTERNAL_LINK_REGEX);
if (match) {
const slugId = extractPageSlugId(match[5]);
if (slugId && !slugIds.includes(slugId)) {
slugIds.push(slugId);
}
}
}
}
});
return slugIds;
}
export function extractUserMentionIdsFromJson(json: any): string[] { export function extractUserMentionIdsFromJson(json: any): string[] {
const userIds: string[] = []; const userIds: string[] = [];
@@ -4,6 +4,7 @@ export interface IPageBacklinkJob {
pageId: string; pageId: string;
workspaceId: string; workspaceId: string;
mentions: MentionNode[]; mentions: MentionNode[];
internalLinkSlugIds?: string[];
} }
export interface IAddPageWatchersJob { export interface IAddPageWatchersJob {
@@ -11,7 +11,7 @@ export async function processBacklinks(
backlinkRepo: BacklinkRepo, backlinkRepo: BacklinkRepo,
data: IPageBacklinkJob, data: IPageBacklinkJob,
): Promise<void> { ): Promise<void> {
const { pageId, mentions, workspaceId } = data; const { pageId, mentions, workspaceId, internalLinkSlugIds = [] } = data;
await executeTx(db, async (trx) => { await executeTx(db, async (trx) => {
const existingBacklinks = await trx const existingBacklinks = await trx
@@ -20,7 +20,28 @@ export async function processBacklinks(
.where('sourcePageId', '=', pageId) .where('sourcePageId', '=', pageId)
.execute(); .execute();
if (existingBacklinks.length === 0 && mentions.length === 0) { const mentionTargetPageIds = mentions
.filter((mention) => mention.entityId !== pageId)
.map((mention) => mention.entityId);
let resolvedLinkPageIds: string[] = [];
if (internalLinkSlugIds.length > 0) {
const resolvedPages = await trx
.selectFrom('pages')
.select('id')
.where('slugId', 'in', internalLinkSlugIds)
.where('workspaceId', '=', workspaceId)
.execute();
resolvedLinkPageIds = resolvedPages
.map((p) => p.id)
.filter((id) => id !== pageId);
}
const allTargetPageIds = [
...new Set([...mentionTargetPageIds, ...resolvedLinkPageIds]),
];
if (existingBacklinks.length === 0 && allTargetPageIds.length === 0) {
return; return;
} }
@@ -28,16 +49,12 @@ export async function processBacklinks(
(backlink) => backlink.targetPageId, (backlink) => backlink.targetPageId,
); );
const targetPageIds = mentions
.filter((mention) => mention.entityId !== pageId)
.map((mention) => mention.entityId);
let validTargetPages = []; let validTargetPages = [];
if (targetPageIds.length > 0) { if (allTargetPageIds.length > 0) {
validTargetPages = await trx validTargetPages = await trx
.selectFrom('pages') .selectFrom('pages')
.select('id') .select('id')
.where('id', 'in', targetPageIds) .where('id', 'in', allTargetPageIds)
.where('workspaceId', '=', workspaceId) .where('workspaceId', '=', workspaceId)
.execute(); .execute();
} }