feat(knowledge): add link resolution service
- Add resolveLinksFromContent() to parse wiki links from content and resolve them - Add getBacklinks() to find all entries that link to a target entry - Import parseWikiLinks from utils for content parsing - Export new types: ResolvedLink, Backlink - Add comprehensive tests for new functionality (27 tests total)
This commit is contained in:
@@ -63,6 +63,9 @@ describe("LinkResolutionService", () => {
|
||||
findFirst: vi.fn(),
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
knowledgeLink: {
|
||||
findMany: vi.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -403,4 +406,186 @@ describe("LinkResolutionService", () => {
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveLinksFromContent", () => {
|
||||
it("should parse and resolve wiki links from content", async () => {
|
||||
const content =
|
||||
"Check out [[TypeScript Guide]] and [[React Hooks]] for more info.";
|
||||
|
||||
// Mock resolveLink for each target
|
||||
mockPrismaService.knowledgeEntry.findFirst
|
||||
.mockResolvedValueOnce({ id: "entry-1" }) // TypeScript Guide
|
||||
.mockResolvedValueOnce({ id: "entry-2" }); // React Hooks
|
||||
|
||||
const result = await service.resolveLinksFromContent(content, workspaceId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].link.target).toBe("TypeScript Guide");
|
||||
expect(result[0].entryId).toBe("entry-1");
|
||||
expect(result[1].link.target).toBe("React Hooks");
|
||||
expect(result[1].entryId).toBe("entry-2");
|
||||
});
|
||||
|
||||
it("should return null entryId for unresolved links", async () => {
|
||||
const content = "See [[Non-existent Page]] for details.";
|
||||
|
||||
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
|
||||
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce(null);
|
||||
mockPrismaService.knowledgeEntry.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.resolveLinksFromContent(content, workspaceId);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].link.target).toBe("Non-existent Page");
|
||||
expect(result[0].entryId).toBeNull();
|
||||
});
|
||||
|
||||
it("should return empty array for content with no wiki links", async () => {
|
||||
const content = "This content has no wiki links.";
|
||||
|
||||
const result = await service.resolveLinksFromContent(content, workspaceId);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockPrismaService.knowledgeEntry.findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle content with display text syntax", async () => {
|
||||
const content = "Read the [[typescript-guide|TS Guide]] first.";
|
||||
|
||||
mockPrismaService.knowledgeEntry.findFirst.mockResolvedValueOnce(null);
|
||||
mockPrismaService.knowledgeEntry.findUnique.mockResolvedValueOnce({
|
||||
id: "entry-1",
|
||||
});
|
||||
|
||||
const result = await service.resolveLinksFromContent(content, workspaceId);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].link.target).toBe("typescript-guide");
|
||||
expect(result[0].link.displayText).toBe("TS Guide");
|
||||
expect(result[0].entryId).toBe("entry-1");
|
||||
});
|
||||
|
||||
it("should preserve link position information", async () => {
|
||||
const content = "Start [[Page One]] middle [[Page Two]] end.";
|
||||
|
||||
mockPrismaService.knowledgeEntry.findFirst
|
||||
.mockResolvedValueOnce({ id: "entry-1" })
|
||||
.mockResolvedValueOnce({ id: "entry-2" });
|
||||
|
||||
const result = await service.resolveLinksFromContent(content, workspaceId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].link.start).toBe(6);
|
||||
expect(result[0].link.end).toBe(18);
|
||||
expect(result[1].link.start).toBe(26);
|
||||
expect(result[1].link.end).toBe(38);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getBacklinks", () => {
|
||||
it("should return all entries that link to the target entry", async () => {
|
||||
const targetEntryId = "entry-target";
|
||||
const mockBacklinks = [
|
||||
{
|
||||
id: "link-1",
|
||||
sourceId: "entry-1",
|
||||
targetId: targetEntryId,
|
||||
linkText: "Target Page",
|
||||
displayText: "Target Page",
|
||||
positionStart: 10,
|
||||
positionEnd: 25,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
source: {
|
||||
id: "entry-1",
|
||||
title: "TypeScript Guide",
|
||||
slug: "typescript-guide",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "link-2",
|
||||
sourceId: "entry-2",
|
||||
targetId: targetEntryId,
|
||||
linkText: "Target Page",
|
||||
displayText: "See Target",
|
||||
positionStart: 50,
|
||||
positionEnd: 70,
|
||||
resolved: true,
|
||||
context: null,
|
||||
createdAt: new Date(),
|
||||
source: {
|
||||
id: "entry-2",
|
||||
title: "React Hooks",
|
||||
slug: "react-hooks",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce(
|
||||
mockBacklinks
|
||||
);
|
||||
|
||||
const result = await service.getBacklinks(targetEntryId);
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toEqual({
|
||||
sourceId: "entry-1",
|
||||
sourceTitle: "TypeScript Guide",
|
||||
sourceSlug: "typescript-guide",
|
||||
linkText: "Target Page",
|
||||
displayText: "Target Page",
|
||||
});
|
||||
expect(result[1]).toEqual({
|
||||
sourceId: "entry-2",
|
||||
sourceTitle: "React Hooks",
|
||||
sourceSlug: "react-hooks",
|
||||
linkText: "Target Page",
|
||||
displayText: "See Target",
|
||||
});
|
||||
|
||||
expect(mockPrismaService.knowledgeLink.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
targetId: targetEntryId,
|
||||
resolved: true,
|
||||
},
|
||||
include: {
|
||||
source: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty array when no backlinks exist", async () => {
|
||||
mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
const result = await service.getBacklinks("entry-with-no-backlinks");
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("should only return resolved backlinks", async () => {
|
||||
const targetEntryId = "entry-target";
|
||||
|
||||
mockPrismaService.knowledgeLink.findMany.mockResolvedValueOnce([]);
|
||||
|
||||
await service.getBacklinks(targetEntryId);
|
||||
|
||||
expect(mockPrismaService.knowledgeLink.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
resolved: true,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user