Files
stack/apps/api/src/knowledge/services/link-sync.service.ts
Jason Woltje c221b63d14
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
fix: Resolve CI typecheck failures and improve type safety
Fixes CI pipeline failures caused by missing Prisma Client generation and TypeScript type safety issues. Added Prisma generation step to CI pipeline, installed missing type dependencies, and resolved 40+ exactOptionalPropertyTypes violations across service layer.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-30 20:39:03 -06:00

192 lines
4.7 KiB
TypeScript

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