import { Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { PrismaService } from "../../prisma/prisma.service"; import { LinkResolutionService } from "./link-resolution.service"; import { parseWikiLinks } from "../utils/wiki-link-parser"; /** * Represents a backlink to a knowledge entry */ export interface Backlink { id: string; sourceId: string; targetId: string; linkText: string; displayText: string; positionStart: number; positionEnd: number; resolved: boolean; context: string | null; createdAt: Date; source: { id: string; title: string; slug: string; }; } /** * Represents an unresolved wiki link */ export interface UnresolvedLink { id: string; sourceId: string; targetId: string | null; linkText: string; displayText: string; positionStart: number; positionEnd: number; resolved: boolean; context: string | null; createdAt: Date; source: { id: string; title: string; slug: string; }; } /** * Service for synchronizing wiki-style links in knowledge entries * * Responsibilities: * - Parse content for wiki links * - Resolve links to knowledge entries * - Store/update link records * - Handle orphaned links */ @Injectable() export class LinkSyncService { constructor( private readonly prisma: PrismaService, private readonly linkResolver: LinkResolutionService ) {} /** * Sync links for a knowledge entry * Parses content, resolves links, and updates the database * * @param workspaceId - The workspace scope * @param entryId - The entry being updated * @param content - The markdown content to parse */ async syncLinks(workspaceId: string, entryId: string, content: string): Promise { // Parse wiki links from content const parsedLinks = parseWikiLinks(content); // Get existing links for this entry const existingLinks = await this.prisma.knowledgeLink.findMany({ where: { sourceId: entryId, }, }); // Resolve all parsed links const linkCreations: Prisma.KnowledgeLinkUncheckedCreateInput[] = []; for (const link of parsedLinks) { const targetId = await this.linkResolver.resolveLink(workspaceId, link.target); // Only create link record if targetId was resolved // (Schema requires targetId to be non-null) if (targetId) { linkCreations.push({ sourceId: entryId, targetId, linkText: link.target, displayText: link.displayText, positionStart: link.start, positionEnd: link.end, resolved: true, }); } } // Determine which existing links to keep/delete // We'll use a simple strategy: delete all existing and recreate // (In production, you might want to diff and only update changed links) const existingLinkIds = existingLinks.map((link) => link.id); // Delete all existing links and create new ones in a transaction await this.prisma.$transaction(async (tx) => { // Delete all existing links if (existingLinkIds.length > 0) { await tx.knowledgeLink.deleteMany({ where: { sourceId: entryId, id: { in: existingLinkIds, }, }, }); } // Create new links for (const linkData of linkCreations) { await tx.knowledgeLink.create({ data: linkData, }); } }); } /** * Get all backlinks for an entry * Returns entries that link TO this entry * * @param entryId - The target entry * @returns Array of backlinks with source entry information */ async getBacklinks(entryId: string): Promise { const backlinks = await this.prisma.knowledgeLink.findMany({ where: { targetId: entryId, resolved: true, }, include: { source: { select: { id: true, title: true, slug: true, }, }, }, orderBy: { createdAt: "desc", }, }); return backlinks as Backlink[]; } /** * Get all unresolved links for a workspace * Useful for finding broken links or pages that need to be created * * @param workspaceId - The workspace scope * @returns Array of unresolved links */ async getUnresolvedLinks(workspaceId: string): Promise { const unresolvedLinks = await this.prisma.knowledgeLink.findMany({ where: { source: { workspaceId, }, resolved: false, }, include: { source: { select: { id: true, title: true, slug: true, }, }, }, }); return unresolvedLinks as UnresolvedLink[]; } }