Files
stack/apps/api/src/knowledge/services/link-sync.service.ts
T
Jason Woltje 652ba50a19 fix: code review cleanup - schema sync, type safety, null handling
- 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
2026-01-29 23:36:41 -06:00

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[];
}
}