- Sync KnowledgeLink schema with migration (add displayText, positionStart, positionEnd, resolved) - Make targetId optional to support unresolved links - Fix null handling in graph.service.ts (skip unresolved links) - Add explicit types to frontend components (remove implicit any) - Remove unused WikiLink import - Add null-safe statusInfo check in EntryCard
202 lines
4.8 KiB
TypeScript
202 lines
4.8 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
|
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<void> {
|
|
// 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: Array<{
|
|
sourceId: string;
|
|
targetId: string | null;
|
|
linkText: string;
|
|
displayText: string;
|
|
positionStart: number;
|
|
positionEnd: number;
|
|
resolved: boolean;
|
|
}> = [];
|
|
|
|
for (const link of parsedLinks) {
|
|
const targetId = await this.linkResolver.resolveLink(
|
|
workspaceId,
|
|
link.target
|
|
);
|
|
|
|
linkCreations.push({
|
|
sourceId: entryId,
|
|
targetId: targetId,
|
|
linkText: link.target,
|
|
displayText: link.displayText,
|
|
positionStart: link.start,
|
|
positionEnd: link.end,
|
|
resolved: targetId !== null,
|
|
});
|
|
}
|
|
|
|
// 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<Backlink[]> {
|
|
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<UnresolvedLink[]> {
|
|
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[];
|
|
}
|
|
}
|